Разработчик
0,0
рейтинг
15 июля 2013 в 13:00

Разработка → Руководство по магическим методам в Питоне перевод tutorial

Это перевод 1.17 версии руководства от Rafe Kettler.


Содержание


  1. Вступление
  2. Конструирование и инициализация
  3. Переопределение операторов на произвольных классах
  4. Представление своих классов
  5. Контроль доступа к атрибутам
  6. Создание произвольных последовательностей
  7. Отражение
  8. Вызываемые объекты
  9. Менеджеры контекста
  10. Абстрактные базовые классы
  11. Построение дескрипторов
  12. Копирование
  13. Использование модуля pickle на своих объектах
  14. Заключение
  15. Приложение 1: Как вызывать магические методы
  16. Приложение 2: Изменения в Питоне 3


Вступление


Что такое магические методы? Они всё в объектно-ориентированном Питоне. Это специальные методы, с помощью которых вы можете добавить в ваши классы «магию». Они всегда обрамлены двумя нижними подчеркиваниями (например, __init__ или __lt__). Ещё, они не так хорошо документированны, как хотелось бы. Все магические методы описаны в документации, но весьма беспорядочно и почти безо всякой организации. Поэтому, чтобы исправить то, что я воспринимаю как недостаток документации Питона, я собираюсь предоставить больше информации о магических методах, написанной на понятном языке и обильно снабжённой примерами. Надеюсь, это руководство вам понравится. Используйте его как обучающий материал, памятку или полное описание. Я просто постарался как можно понятнее описать магические методы.


Конструирование и инициализация.


Всем известен самый базовый магический метод, __init__. С его помощью мы можем инициализировать объект. Однако, когда я пишу x = SomeClass(), __init__ не самое первое, что вызывается. На самом деле, экземпляр объекта создаёт метод __new__, а затем аргументы передаются в инициализатор. На другом конце жизненного цикла объекта находится метод __del__. Давайте подробнее рассмотрим эти три магических метода:

  • __new__(cls, [...)
    Это первый метод, который будет вызван при инициализации объекта. Он принимает в качестве параметров класс и потом любые другие аргументы, которые будут переданы в __init__. __new__ используется весьма редко, но иногда бывает полезен, в частности, когда класс наследуется от неизменяемого (immutable) типа, такого как кортеж (tuple) или строка. Я не намерен очень детально останавливаться на __new__, так как он не то чтобы очень часто нужен, но этот метод очень хорошо и детально описан в документации.

  • __init__(self, [...)
    Инициализатор класса. Ему передаётся всё, с чем был вызван первоначальный конструктор (так, например, если мы вызываем x = SomeClass(10, 'foo'), __init__ получит 10 и 'foo' в качестве аргументов. __init__ почти повсеместно используется при определении классов.

  • __del__(self)
    Если __new__ и __init__ образуют конструктор объекта, __del__ это его деструктор. Он не определяет поведение для выражения del x (поэтому этот код не эквивалентен x.__del__()). Скорее, он определяет поведение объекта в то время, когда объект попадает в сборщик мусора. Это может быть довольно удобно для объектов, которые могут требовать дополнительных чисток во время удаления, таких как сокеты или файловыве объекты. Однако, нужно быть осторожным, так как нет гарантии, что __del__ будет вызван, если объект продолжает жить, когда интерпретатор завершает работу. Поэтому __del__ не может служить заменой для хороших программистских практик (всегда завершать соединение, если закончил с ним работать и тому подобное). Фактически, из-за отсутствия гарантии вызова, __del__ не должен использоваться почти никогда; используйте его с осторожностью!

    Замечание от переводчика: svetlov отмечает, что здесь автор ошибается, на самом деле __del__ всегда вызывается по завершении работы интерпретатора.


Соединим всё вместе, вот пример __init__ и __del__ в действии:

from os.path import join

class FileObject:
    '''Обёртка для файлового объекта, чтобы быть уверенным в том, что файл будет закрыт при удалении.'''

    def __init__(self, filepath='~', filename='sample.txt'):
        # открыть файл filename в filepath в режиме чтения и записи
        self.file = open(join(filepath, filename), 'r+')

    def __del__(self):
        self.file.close()
        del self.file


Переопределение операторов на произвольных классах


Одно из больших преимуществ использования магических методов в Питоне то, что они предоставляют простой способ заставить объекты вести себя по подобию встроенных типов. Это означает, что вы можете избежать унылого, нелогичного и нестандартного поведения базовых операторов. В некоторых языках обычное явление писать как-нибудь так:

if instance.equals(other_instance):
    # do something

Вы, конечно, можете поступать так же и в Питоне, но это добавляет путаницы и ненужной многословности. Разные библиотеки могут по разному называть одни и те же операции, заставляя использующего их программиста совершать больше действий, чем необходимо. Используя силу магических методов, мы можем определить нужный метод (__eq__, в этом случае), и так точно выразить, что мы имели в виду:

if instance == other_instance:
    #do something

Это одна из сильных сторон магических методов. Подавляющее большинство из них позволяют определить, что будут делать стандартные операторы, так что мы можем использовать операторы на своих классах так, как будто они встроенные типы.


Магические методы сравнения


В Питоне уйма магических методов, созданных для определения интуитивного сравнения между объектами используя операторы, а не неуклюжие методы. Кроме того, они предоставляют способ переопределить поведение Питона по-умолчанию для сравнения объектов (по ссылке). Вот список этих методов и что они делают:

  • __cmp__(self, other)
    Самый базовый из методов сравнения. Он, в действительности, определяет поведение для всех операторов сравнения (>, ==, !=, итд.), но не всегда так, как вам это нужно (например, если эквивалентность двух экземпляров определяется по одному критерию, а то что один больше другого по какому-нибудь другому). __cmp__ должен вернуть отрицательное число, если self < other, ноль, если self == other, и положительное число в случае self > other. Но, обычно, лучше определить каждое сравнение, которое вам нужно, чем определять их всех в __cmp__. Но __cmp__ может быть хорошим способом избежать повторений и увеличить ясность, когда все необходимые сравнения оперерируют одним критерием.

  • __eq__(self, other)
    Определяет поведение оператора равенства, ==.

  • __ne__(self, other)
    Определяет поведение оператора неравенства, !=.

  • __lt__(self, other)
    Определяет поведение оператора меньше, <.

  • __gt__(self, other)
    Определяет поведение оператора больше, >.

  • __le__(self, other)
    Определяет поведение оператора меньше или равно, <=.

  • __ge__(self, other)
    Определяет поведение оператора больше или равно, >=.


Для примера расммотрим класс, описывающий слово. Мы можем сравнивать слова лексиграфически (по алфавиту), что является дефолтным поведением при сравнении строк, но можем захотеть использовать при сравнении какой-нибудь другой критерий, такой, как длина или количество слогов. В этом примере мы будем сравнивать по длине. Вот реализация:

class Word(str):
    '''Класс для слов, определяющий сравнение по длине слов.'''

    def __new__(cls, word):
        # Мы должны использовать __new__, так как тип str неизменяемый
        # и мы должны инициализировать его раньше (при создании)
        if ' ' in word:
            print "Value contains spaces. Truncating to first space."
            word = word[:word.index(' ')] # Теперь Word это все символы до первого пробела
        return str.__new__(cls, word)

    def __gt__(self, other):
        return len(self) > len(other)
    def __lt__(self, other):
        return len(self) < len(other)
    def __ge__(self, other):
        return len(self) >= len(other)
    def __le__(self, other):
        return len(self) <= len(other)

Теперь мы можем создать два Word (при помощи Word('foo') и Word('bar')) и сравнить их по длине. Заметьте, что мы не определяли __eq__ и __ne__, так как это приведёт к странному поведению (например, Word('foo') == Word('bar') будет расцениваться как истина). В этом нет смысла при тестировании на эквивалентность, основанную на длине, поэтому мы оставляем стандартную проверку на эквивалентность от str.
Сейчас, кажется, удачное время упомянуть, что вы не должны определять каждый из магических методов сравнения, чтобы полностью охватить все сравнения. Стандартная библиотека любезно предоставляет нам класс-декторатор в модуле functools, который и определит все сравнивающие методы, от вас достаточно определить только __eq__ и ещё один (__gt__, __lt__ и т.п.) Эта возможность доступна начиная с 2.7 версии Питона, но если это вас устраивает, вы сэкономите кучу времени и усилий. Для того, чтобы задействовать её, поместите @total_ordering над вашим определением класса.


Числовые магические методы


Точно так же, как вы можете определить, каким образом ваши объекты будут сравниваться операторами сравнения, вы можете определить их поведение для числовых операторов. Приготовтесь, друзья, их много. Для лучшей организации, я разбил числовые магические методы на 5 категорий: унарные операторы, обычные арифметические операторы, отражённые арифметические операторы (подробности позже), составные присваивания и преобразования типов.


Унарные операторы и функции


Унарные операторы и функции имеют только один операнд — отрицание, абсолютное значение, и так далее.

  • __pos__(self)
    Определяет поведение для унарного плюса (+some_object)

  • __neg__(self)
    Определяет поведение для отрицания(-some_object)

  • __abs__(self)
    Определяет поведение для встроенной функции abs().

  • __invert__(self)
    Определяет поведение для инвертирования оператором ~. Для объяснения что он делает смотри статью в Википедии о бинарных операторах.

  • __round__(self, n)
    Определяет поведение для встроенной функции round(). n это число знаков после запятой, до которого округлить.

  • __floor__(self)
    Определяет поведение для math.floor(), то есть, округления до ближайшего меньшего целого.

  • __ceil__(self)
    Определяет поведение для math.ceil(), то есть, округления до ближайшего большего целого.

  • __trunc__(self)
    Определяет поведение для math.trunc(), то есть, обрезания до целого.


Обычные арифметические операторы


Теперь рассмотрим обычные бинарные операторы (и ещё пару функций): +, -, * и похожие. Они, по большей части, отлично сами себя описывают.

  • __add__(self, other)
    Сложение.

  • __sub__(self, other)
    Вычитание.

  • __mul__(self, other)
    Умножение.

  • __floordiv__(self, other)
    Целочисленное деление, оператор //.

  • __div__(self, other)
    Деление, оператор /.

  • __truediv__(self, other)
    Правильное деление. Заметьте, что это работает только когда используется from __future__ import division.

  • __mod__(self, other)
    Остаток от деления, оператор %.

  • __divmod__(self, other)
    Определяет поведение для встроенной функции divmod().

  • __pow__
    Возведение в степень, оператор **.

  • __lshift__(self, other)
    Двоичный сдвиг влево, оператор <<.

  • __rshift__(self, other)
    Двоичный сдвиг вправо, оператор >>.

  • __and__(self, other)
    Двоичное И, оператор &.

  • __or__(self, other)
    Двоичное ИЛИ, оператор |.

  • __xor__(self, other)
    Двоичный xor, оператор ^.


Отражённые арифметические операторы


Помните как я сказал, что собираюсь остановиться на отражённой арифметике подробнее? Вы могли подумать, что это какая-то большая, страшная и непонятная концепция. На самом деле всё очень просто. Вот пример:

some_object + other

Это «обычное» сложение. Единственное, чем отличается эквивалентное отражённое выражение, это порядок слагаемых:

other + some_object

Таким образом, все эти магические методы делают то же самое, что и их обычные версии, за исключением выполнения операции с other в качестве первого операнда и self в качестве второго. В большинстве случаев, результат отражённой операции такой же, как её обычный эквивалент, поэтому при определении __radd__ вы можете ограничиться вызовом __add__ да и всё. Заметьте, что объект слева от оператора (other в примере) не должен иметь обычной неотражённой версии этого метода. В нашем примере, some_object.__radd__ будет вызван только если в other не определён __add__.

  • __radd__(self, other)
    Отражённое сложение.

  • __rsub__(self, other)
    Отражённое вычитание.

  • __rmul__(self, other)
    Отражённое умножение.

  • __rfloordiv__(self, other)
    Отражённое целочисленное деление, оператор //.

  • __rdiv__(self, other)
    Отражённое деление, оператор /.

  • __rtruediv__(self, other)
    Отражённое правильное деление. Заметьте, что работает только когда используется from __future__ import division.

  • __rmod__(self, other)
    Отражённый остаток от деления, оператор %.

  • __rdivmod__(self, other)
    Определяет поведение для встроенной функции divmod(), когда вызывается divmod(other, self).

  • __rpow__
    Отражённое возведение в степерь, оператор **.

  • __rlshift__(self, other)
    Отражённый двоичный сдвиг влево, оператор <<.

  • __rrshift__(self, other)
    Отражённый двоичный сдвиг вправо, оператор >>.

  • __rand__(self, other)
    Отражённое двоичное И, оператор &.

  • __ror__(self, other)
    Отражённое двоичное ИЛИ, оператор |.

  • __rxor__(self, other)
    Отражённый двоичный xor, оператор ^.


Составное присваивание


В Питоне широко представлены и магические методы для составного присваивания. Вы скорее всего уже знакомы с составным присваиванием, это комбинация «обычного» оператора и присваивания. Если всё ещё непонятно, вот пример:

x = 5
x += 1 # другими словами x = x + 1

Каждый из этих методов должен возвращать значение, которое будет присвоено переменной слева (например, для a += b, __iadd__ должен вернуть a + b, что будет присвоено a). Вот список:

  • __iadd__(self, other)
    Сложение с присваиванием.

  • __isub__(self, other)
    Вычитание с присваиванием.

  • __imul__(self, other)
    Умножение с присваиванием.

  • __ifloordiv__(self, other)
    Целочисленное деление с присваиванием, оператор //=.

  • __idiv__(self, other)
    Деление с присваиванием, оператор /=.

  • __itruediv__(self, other)
    Правильное деление с присваиванием. Заметьте, что работает только если используется from __future__ import division.

  • __imod_(self, other)
    Остаток от деления с присваиванием, оператор %=.

  • __ipow__
    Возведение в степерь с присваиванием, оператор **=.

  • __ilshift__(self, other)
    Двоичный сдвиг влево с присваиванием, оператор <<=.

  • __irshift__(self, other)
    Двоичный сдвиг вправо с присваиванием, оператор >>=.

  • __iand__(self, other)
    Двоичное И с присваиванием, оператор &=.

  • __ior__(self, other)
    Двоичное ИЛИ с присваиванием, оператор |=.

  • __ixor__(self, other)
    Двоичный xor с присваиванием, оператор ^=.


Магические методы преобразования типов


Кроме того, в Питоне множество магических методов, предназначенных для определния поведения для встроенных функций преобразования типов, таких как float(). Вот они все:

  • __int__(self)
    Преобразование типа в int.

  • __long__(self)
    Преобразование типа в long.

  • __float__(self)
    Преобразование типа в float.

  • __complex__(self)
    Преобразование типа в комплексное число.

  • __oct__(self)
    Преобразование типа в восьмеричное число.

  • __hex__(self)
    Преобразование типа в шестнадцатиричное число.

  • __index__(self)
    Преобразование типа к int, когда объект используется в срезах (выражения вида [start:stop:step]). Если вы определяете свой числовый тип, который может использоваться как индекс списка, вы должны определить __index__.

  • __trunc__(self)
    Вызывается при math.trunc(self). Должен вернуть своё значение, обрезанное до целочисленного типа (обычно long).

  • __coerce__(self, other)
    Метод для реализации арифметики с операндами разных типов. __coerce__ должен вернуть None если преобразование типов невозможно. Если преобразование возможно, он должен вернуть пару (кортеж из 2-х элементов) из self и other, преобразованные к одному типу.


Представление своих классов


Часто бывает полезно представление класса в виде строки. В Питоне существует несколько методов, которые вы можете определить для настройки поведения встроенных функций при представлении вашего класса.

  • __str__(self)
    Определяет поведение функции str(), вызванной для экземпляра вашего класса.

  • __repr__(self)
    Определяет поведение функции repr(), вызыванной для экземпляра вашего класса. Главное отличие от str() в целевой аудитории. repr() больше предназначен для машинно-ориентированного вывода (более того, это часто должен быть валидный код на Питоне), а str() предназначен для чтения людьми.

  • __unicode__(self)
    Определяет поведение функции unicode(), вызыванной для экземпляра вашего класса. unicode() похож на str(), но возвращает строку в юникоде. Будте осторожны: если клиент вызывает str() на экземпляре вашего класса, а вы определили только __unicode__(), то это не будет работать. Постарайтесь всегда определять __str__() для случая, когда кто-то не имеет такой роскоши как юникод.

  • __format__(self, formatstr)
    Определяет поведение, когда экземпляр вашего класса используется в форматировании строк нового стиля. Например, "Hello, {0:abc}!".format(a) приведёт к вызову a.__format__("abc"). Это может быть полезно для определения ваших собственных числовых или строковых типов, которым вы можете захотеть предоставить какие-нибудь специальные опции форматирования.

  • __hash__(self)
    Определяет поведение функции hash(), вызыванной для экземпляра вашего класса. Метод должен возвращать целочисленное значение, которое будет использоваться для быстрого сравнения ключей в словарях. Заметьте, что в таком случае обычно нужно определять и __eq__ тоже. Руководствуйтесь следующим правилом: a == b подразумевает hash(a) == hash(b).

  • __nonzero__(self)
    Определяет поведение функции bool(), вызванной для экземпляра вашего класса. Должна вернуть True или False, в зависимости от того, когда вы считаете экземпляр соответствующим True или False.

  • __dir__(self)
    Определяет поведение функции dir(), вызванной на экземпляре вашего класса. Этот метод должен возвращать пользователю список атрибутов. Обычно, определение __dir__ не требуется, но может быть жизненно важно для интерактивного использования вашего класса, если вы переопределили __getattr__ или __getattribute__ (с которыми вы встретитесь в следующей части), или каким-либо другим образом динамически создаёте атрибуты.

  • __sizeof__(self)
    Определяет поведение функции sys.getsizeof(), вызыванной на экземпляре вашего класса. Метод должен вернуть размер вашего объекта в байтах. Он главным образом полезен для классов, определённых в расширениях на C, но всё-равно полезно о нём знать.


Мы почти закончили со скучной (и лишённой примеров) частью руководства по магическим методам. Теперь, когда мы рассмотрели самые базовые магические методы, пришло время перейти к более продвинутому материалу.


Контроль доступа к атрибутам


Многие люди, пришедшие в Питон из других языков, жалуются на отсутствие настоящей инкапсуляции для классов (например, нет способа определить приватные атрибуты с публичными методами доступа). Это не совсем правда: просто многие вещи, связанные с инкапсуляцией, Питон реализует через «магию», а не явными модификаторами для методов и полей. Смотрите:

  • __getattr__(self, name)
    Вы можете определить поведение для случая, когда пользователь пытается обратиться к атрибуту, который не существует (совсем или пока ещё). Это может быть полезным для перехвата и перенаправления частых опечаток, предупреждения об использовании устаревших атрибутов (вы можете всё-равно вычислить и вернуть этот атрибут, если хотите), или хитро возвращать AttributeError, когда это вам нужно. Правда, этот метод вызывается только когда пытаются получить доступ к несуществующему атрибуту, поэтому это не очень хорошее решение для инкапсуляции.

  • __setattr__(self, name, value)
    В отличии от __getattr__, __setattr__ решение для инкапсуляции. Этот метод позволяет вам определить поведение для присвоения значения атрибуту, независимо от того существует атрибут или нет. То есть, вы можете определить любые правила для любых изменений значения атрибутов. Впрочем, вы должны быть осторожны с тем, как использовать __setattr__, смотрите пример нехорошего случая в конце этого списка.

  • __delattr__
    Это то же, что и __setattr__, но для удаления атрибутов, вместо установки значений. Здесь требуются те же меры предосторожности, что и в __setattr__ чтобы избежать бесконечной рекурсии (вызов del self.name в определении __delattr__ вызовет бесконечную рекурсию).

  • __getattribute__(self, name)
    __getattribute__ выглядит к месту среди своих коллег __setattr__ и __delattr__, но я бы не рекомендовал вам его использовать. __getattribute__ может использоваться только с классами нового типа (в новых версиях Питона все классы нового типа, а в старых версиях вы можете получить такой класс унаследовавшись от object). Этот метод позволяет вам определить поведение для каждого случая доступа к атрибутам (а не только к несуществующим, как __getattr__(self, name)). Он страдает от таких же проблем с бесконечной рекурсией, как и его коллеги (на этот раз вы можете вызывать __getattribute__ у базового класса, чтобы их предотвратить). Он, так же, главным образом устраняет необходимость в __getattr__, который в случае реализации __getattribute__ может быть вызван только явным образом или в случае генерации исключения AttributeError. Вы конечно можете использовать этот метод (в конце концов, это ваш выбор), но я бы не рекомендовал, потому что случаев, когда он действительно полезен очень мало (намного реже нужно переопределять поведение при получении, а не при установке значения) и реализовать его без возможных ошибок очень сложно.


Вы можете запросто получить проблему при определении любого метогда, управляющего доступом к атрибутам. Рассмотрим пример:

def __setattr__(self, name, value):
    self.name = value
    # это рекурсия, так как всякий раз, когда любому атрибуту присваивается значение,
    # вызывается  __setattr__().
    # тоесть, на самом деле это равнозначно self.__setattr__('name', value). 
    # Так как метод вызывает сам себя, рекурсия продолжится бесконечно, пока всё не упадёт

def __setattr__(self, name, value):
    self.__dict__[name] = value # присваивание в словарь переменных класса
    # дальше определение произвольного поведения

Ещё раз, мощь магических методов в Питоне невероятна, а с большой силой приходит и большая ответственность. Важно знать, как правильно использовать магические методы, ничего не ломая.

Итак, что мы узнали об управлении доступом к атрибутам? Их не нужно использовать легкомысленно. На самом деле, они имеют склонность к чрезмерной мощи и нелогичности. Причина, по которой они всё-таки существуют, в удволетворении определённого желания: Питон склонен не запрещать плохие штуки полностью, а только усложнять их использование. Свобода первостепенна, поэтому вы на самом деле можете делать всё, что хотите. Вот пример использования методов контроля доступа (заметьте, что мы используем super, так как не все классы имеют атрибут __dict__):

class AccessCounter(object):
    '''Класс, содержащий атрибут value и реализующий счётчик доступа к нему.
    Счётчик увеличивается каждый раз, когда меняется value.'''

    def __init__(self, val):
        super(AccessCounter, self).__setattr__('counter', 0)
        super(AccessCounter, self).__setattr__('value', val)

    def __setattr__(self, name, value):
        if name == 'value':
            super(AccessCounter, self).__setattr__('counter', self.counter + 1)
        # Не будем делать здесь никаких условий.
        # Если вы хотите предотвратить изменение других атрибутов,
        # выбросьте исключение AttributeError(name)
        super(AccessCounter, self).__setattr__(name, value)

    def __delattr__(self, name):
        if name == 'value':
            super(AccessCounter, self).__setattr__('counter', self.counter + 1)
        super(AccessCounter, self).__delattr__(name)]


Создание произвольных последовательностей


В Питоне существует множество способов заставить ваши классы вести себя как встроенные последовательности (словари, кортежи, списки, строки и так далее). Это, безусловно, мои любимые магические методы, из-за до абсурда высокой степени контроля, которую они дают и той магии, от которой с экземплярами ваших классов вдруг начинает прекрасно работать целое множество глобальных функций. Но, до того как мы перейдём ко всяким хорошим вещам, мы должны знать о протоколах.


Протоколы


Теперь, когда речь зашла о создании собственных последовательностей в Питоне, пришло время поговорить о протоколах. Протоколы немного похожи на интерфейсы в других языках тем, что они предоставляют набор методов, которые вы должны реализовать. Однако, в Питоне протоколы абсолютно ни к чему не обязывают и не требуют обязательно реализовать какое-либо объявление. Наверное, они больше похожи на руководящие указания.

Почему мы заговорили о протоколах? Потому, что реализация произвольных контейнерных типов в Питоне влечёт за собой использование некоторых из них. Во-первых, протокол для определения неизменяемых контейнеров: чтобы создать неизменяемый контейнер, вы должны только определить __len__ и __getitem__ (продробнее о них дальше). Протокол изменяемого контейнера требует того же, что и неизменяемого контейнера, плюс __setitem__ и __delitem__. И, наконец, если вы хотите, чтобы ваши объекты можно было перебирать итерацией, вы должны определить __iter__, который возвращает итератор. Этот итератор должен соответствовать протоколу итератора, который требует методов __iter__(возвращает самого себя) и next.


Магия контейнеров


Без дальнейшего промедления, вот магические методы, используемые контейнерами:

  • __len__(self)
    Возвращает количество элементов в контейнере. Часть протоколов для изменяемого и неизменяемого контейнеров.

  • __getitem__(self, key)
    Определяет поведение при доступе к элементу, используя синтаксис self[key]. Тоже относится и к протоколу изменяемых и к протоколу неизменяемых контейнеров. Должен выбрасывать соответствующие исключения: TypeError если неправильный тип ключа и KeyError если ключу не соответствует никакого значения.

  • __setitem__(self, key, value)
    Определяет поведение при присваивании значения элементу, используя синтаксис self[nkey] = value. Часть протокола изменяемого контейнера. Опять же, вы должны выбрасывать KeyError и TypeError в соответсвующих случаях.

  • __delitem__(self, key)
    Определяет поведение при удалении элемента (то есть del self[key]). Это часть только протокола для изменяемого контейнера. Вы должны выбрасывать соответствующее исключение, если ключ некорректен.

  • __iter__(self)
    Должен вернуть итератор для контейнера. Итераторы возвращаются в множестве ситуаций, главным образом для встроенной функции iter() и в случае перебора элементов контейнера выражением for x in container:. Итераторы сами по себе объекты и они тоже должны определять метод __iter__, который возвращает self.

  • __reversed__(self)
    Вызывается чтобы определить поведения для встроенной функции reversed(). Должен вернуть обратную версию последовательности. Реализуйте метод только если класс упорядоченный, как список или кортеж.

  • __contains__(self, item)
    __contains__ предназначен для проверки принадлежности элемента с помощью in и not in. Вы спросите, почему же это не часть протокола последовательности? Потому что когда __contains__ не определён, Питон просто перебирает всю последовательность элемент за элементом и возвращает True если находит нужный.

  • __missing__(self, key)
    __missing__ используется при наследовании от dict. Определяет поведение для для каждого случая, когда пытаются получить элемент по несуществующему ключу (так, например, если у меня есть словарь d и я пишу d["george"] когда "george" не является ключом в словаре, вызывается d.__missing__("george")).


Пример


Для примера, давайте посмотрим на список, который реализует некоторые функциональные конструкции, которые вы могли встретить в других языках (Хаскеле, например).

class FunctionalList:
    '''Класс-обёртка над списком с добавлением некоторой функциональной магии: head,
    tail, init, last, drop, take.'''

    def __init__(self, values=None):
        if values is None:
            self.values = []
        else:
            self.values = values

    def __len__(self):
        return len(self.values)

    def __getitem__(self, key):
        # если значение или тип ключа некорректны, list выбросит исключение
        return self.values[key]

    def __setitem__(self, key, value):
        self.values[key] = value

    def __delitem__(self, key):
        del self.values[key]

    def __iter__(self):
        return iter(self.values)

    def __reversed__(self):
        return FunctionalList(reversed(self.values))

    def append(self, value):
        self.values.append(value)
    def head(self):
        # получить первый элемент
        return self.values[0]
    def tail(self):
        # получить все элементы после первого
        return self.values[1:]
    def init(self):
        # получить все элементы кроме последнего
        return self.values[:-1]
    def last(self):
        # получить последний элемент
        return self.values[-1]
    def drop(self, n):
        # все элементы кроме первых n
        return self.values[n:]
    def take(self, n):
        # первые n элементов
        return self.values[:n]

Теперь у вас есть полезный (относительно) пример реализации своей собственной последовательности. Существуют, конечно, и куда более практичные реализации произвольных последовательностей, но большое их число уже реализовано в стандартной библиотеке (с батарейками в комплекте, да?), такие как Counter, OrderedDict, NamedTuple.


Отражение


Вы можете контролировать и отражение, использующее встроенные функции isinstance() и issubclass(), определив некоторые магические методы. Вот они:

  • __instancecheck__(self, instance)
    Проверяет, является ли экземлпяр членом вашего класса (isinstance(instance, class), например.

  • __subclasscheck__(self, subclass)
    Проверяет, является наследуется ли класс от вашего класса (issubclass(subclass, class)).


Может показаться, что вариантов полезного использования этих магических методов немного и, возможно, это на самом деле так. Я не хочу тратить слишком много времени на магические методы отражения, не особо они и важные, но они отражают кое-что важное об объектно-ориентированном программировании в Питоне и о Питоне вообще: почти всегда существует простой способ что-либо сделать, даже если надобность в этом «что-либо» возникает очень редко. Эти магические методы могут не выглядеть полезными, но если они вам когда-нибудь понадобятся, вы будете рады вспомнить, что они есть (и для этого вы читаете настоящее руководство!).


Вызываемые объекты


Как вы наверное уже знаете, в Питоне функции являются объектами первого класса. Это означает, что они могут быть переданы в функции или методы так же, как любые другие объекты. Это невероятно мощная особенность.

Специальный магический метод позволяет экземплярам вашего класса вести себя так, как будто они функции, тоесть вы сможете «вызывать» их, передавать их в функции, которые принимают функции в качестве аргументов и так далее. Это другая удобная особенность, которая делает программирование на Питоне таким приятным.

  • __call__(self, [args...])
    Позволяет любому экземпляру вашего класса быть вызванным как-будто он функция. Главным образом это означает, что x() означает то же, что и x.__call__(). Заметьте, __call__ принимает произвольное число аргументов; то есть, вы можете определить __call__ так же как любую другую функцию, принимающую столько аргументов, сколько вам нужно.


__call__, в частности, может быть полезен в классах, чьи экземпляры часто изменяют своё состояние. «Вызвать» экземпляр может быть интуитивно понятным и элегантным способом изменить состояние объекта. Примером может быть класс, представляющий положение некоторого объекта на плоскости:

class Entity:
    '''Класс, описывающий объект на плоскости. "Вызываемый", чтобы обновить позицию объекта.'''

    def __init__(self, size, x, y):
        self.x, self.y = x, y
        self.size = size

    def __call__(self, x, y):
        '''Изменить положение объекта.'''
        self.x, self.y = x, y

    # чик...


Менеджеры контекста


В Питоне 2.5 было представлено новое ключевое слово вместе с новым способом повторно использовать код, ключевое слово with. Концепция менеджеров контекста не являлась новой для Питона (она была реализована раньше как часть библиотеки), но в PEP 343 достигла статуса языковой конструкции. Вы могли уже видеть выражения с with:

with open('foo.txt') as bar:
    # выполнение каких-нибудь действий с bar

Менеджеры контекста позволяют выполнить какие-то действия для настройки или очистки, когда создание объекта обёрнуто в оператор with. Поведение менеджера контекста определяется двумя магическими методами:

  • __enter__(self)
    Определяет, что должен сделать менеджер контекста в начале блока, созданного оператором with. Заметьте, что возвращаемое __enter__ значение и есть то значение, с которым производится работа внутри with.

  • __exit__(self, exception_type, exception_value, traceback)
    Определяет действия менеджера контекста после того, как блок будет выполнен (или прерван во время работы). Может использоваться для контроллирования исключений, чистки, любых действий которые должны быть выполнены незамедлительно после блока внутри with. Если блок выполнен успешно, exception_type, exception_value, и traceback будут установлены в None. В другом случае вы сами выбираете, перехватывать ли исключение или предоставить это пользователю; если вы решили перехватить исключение, убедитесь, что __exit__ возвращает True после того как всё сказано и сделано. Если вы не хотите, чтобы исключение было перехвачено менеджером контекста, просто позвольте ему случиться.


__enter__ и __exit__ могут быть полезны для специфичных классов с хорошо описанным и распространённым поведением для их настройки и очистки ресурсов. Вы можете использовать эти методы и для создания общих менеджеров контекста для разных объектов. Вот пример:

class Closer:
    '''Менеджер контекста для автоматического закрытия объекта вызовом метода close 
    в with-выражении.'''

    def __init__(self, obj):
        self.obj = obj

    def __enter__(self):
        return self.obj # привязка к активному объекту with-блока

    def __exit__(self, exception_type, exception_val, trace):
        try:
           self.obj.close()
        except AttributeError: # у объекта нет метода close
           print 'Not closable.'
           return True # исключение перехвачено

Пример использования Closer с FTP-соединением (сокет, имеющий метод close):

>>> from magicmethods import Closer
>>> from ftplib import FTP
>>> with Closer(FTP('ftp.somesite.com')) as conn:
...     conn.dir()
...
# output omitted for brevity
>>> conn.dir()
# long AttributeError message, can't use a connection that's closed
>>> with Closer(int(5)) as i:
...     i += 1
...
Not closable.
>>> i
6

Видите, как наша обёртка изящно управляется и с правильными и с неподходящими объектами. В этом сила менеджеров контекста и магических методов. Заметьте, что стандартная библиотека Питона включает модуль contextlib, который включает в себя contextlib.closing() — менеджер контекста, который делает приблизительно то же (без какой-либо обработки случая, когда объект не имеет метода close()).


Абстрактные базовые классы


Смотри http://docs.python.org/2/library/abc.html.


Построение дескрипторов


Дескрипторы это такие классы, с помощью которых можно добавить свою логику к событиям доступа (получение, изменение, удаление) к атрибутам других объектов. Дескрипторы не подразумевается использовать сами по себе; скорее, предполагается, что ими будут владеть какие-нибудь связанные с ними классы. Дескрипторы могут быть полезны для построения объектно-ориентированных баз данных или классов, чьи атрибуты зависят друг от друга. В частности, дескрипторы полезны при представлении атрибутов в нескольких системах исчисления или каких-либо вычисляемых атрибутов (как расстояние от начальной точки до представленной атрибутом точки на сетке).

Чтобы класс стал дескриптором, он должен реализовать по крайней мере один метод из __get__, __set__ или __delete__. Давайте рассмотрим эти магические методы:

  • __get__(self, instance, instance_class)
    Определяет поведение при возвращении значения из дескриптора. instance это объект, для чьего атрибута-дескриптора вызывается метод. owner это тип (класс) объекта.

  • __set__(self, instance, value)
    Определяет поведение при изменении значения из дескриптора. instance это объект, для чьего атрибута-дескриптора вызывается метод. value это значение для установки в дескриптор.

  • __delete__(self, instance)
    Определяет поведение для удаления значения из дескриптора. instance это объект, владеющий дескриптором.


Теперь пример полезного использования дескрипторов: преобразование единиц измерения.

class Meter(object):
    '''Дескриптор для метра.'''

    def __init__(self, value=0.0):
        self.value = float(value)
    def __get__(self, instance, owner):
        return self.value
    def __set__(self, instance, value):
        self.value = float(value)

class Foot(object):
    '''Дескриптор для фута.'''

    def __get__(self, instance, owner):
        return instance.meter * 3.2808
    def __set__(self, instance, value):
        instance.meter = float(value) / 3.2808

class Distance(object):
    '''Класс, описывающий расстояние, содержит два дескриптора для футов и
    метров.'''
    meter = Meter()
    foot = Foot()


Копирование


В Питоне оператор присваивания не копирует объекты, а только добавляет ещё одну ссылку. Но для коллекций, содержащих изменяемые элементы, иногда необходимо полноценное копирование, чтобы можно было менять элементы одной последовательности, не затрагивая другую. Здесь в игру и вступает copy. К счастью, модули в Питоне не обладают разумом, поэтому мы можем не беспокоиться что они вдруг начнут бесконтрольно копировать сами себя и вскоре линуксовые роботы заполонят всю планету, но мы должны сказать Питону как правильно копировать.

  • __copy__(self)
    Определяет поведение copy.copy() для экземпляра вашего класса. copy.copy() возвращает поверхностную копию вашего объекта — это означает, что хоть сам объект и создан заново, все его данные ссылаются на данные оригинального объекта. И при изменении данных нового объекта, изменения будут происходить и в оригинальном.

  • __deepcopy__(self, memodict={})
    Определяет поведение copy.deepcopy() для экземпляров вашего класса. copy.deepcopy() возвращает глубокую копию вашего объекта — копируются и объект и его данные. memodict это кэш предыдущих скопированных объектов, он предназначен для оптимизации копирования и предотвращения бесконечной рекурсии, когда копируются рекурсивные структуры данных. Когда вы хотите полностью скопировать какой-нибудь конкретный атрибут, вызовите на нём copy.deepcopy() с первым параметром memodict.


Когда использовать эти магические методы? Как обычно — в любом случае, когда вам необходимо больше, чем стандартное поведение. Например, если вы пытаетесь скопировать объект, который содержит кэш как словарь (возможно, очень большой словарь), то может быть вам и не нужно копировать весь кэш, а обойтись всего одним в общей памяти объектов.


Использование модуля pickle на своих объектах


Pickle это модуль для сериализации структур данных Питона и он может быть невероятно полезен, когда вам нужно сохранить состояние какого-либо объекта и восстановить его позже (обычно, в целях кэширования). Кроме того, это ещё и отличный источник переживаний и путаницы.

Сериализация настолько важна, что кроме своего модуля (pickle) имеет и свой собственный протокол и свои магические методы. Но для начала о том, как сериализовать с помощью pickle уже существующие типы данных (спокойно пропускайте, если вы уже знаете).


Вкратце про сериализацию


Давайте погрузимся в сериализацию. Допустим, у вас есть словарь, который вы хотите сохранить и восстановить позже. Вы должны записать его содержимое в файл, тщательно убедившись, что пишете с правильным синтаксисом, потом восстановить его, или выполнив exec(), или прочитав файл. Но это в лучшем случае рискованно: если вы храните важные данные в тексте, он может быть повреждён или изменён множеством способов, с целью обрушить вашу программу или, вообще, запустить какой-нибудь опасный код на вашем компьютере. Лучше использовать pickle:

import pickle

data = {'foo': [1, 2, 3],
        'bar': ('Hello', 'world!'),
        'baz': True}
jar = open('data.pkl', 'wb')
pickle.dump(data, jar) # записать сериализованные данные в jar
jar.close()

И вот, спустя несколько часов, нам снова нужен наш словарь:

import pickle

pkl_file = open('data.pkl', 'rb') # открываем
data = pickle.load(pkl_file) # сохраняем в переменную
print data
pkl_file.close()

Что произошло? Точно то, что и ожидалось. data как-будто всегда тут и была.

Теперь, немного об осторожности: pickle не идеален. Его файлы легко испортить случайно или преднамеренно. Pickle, может быть, безопаснее чем текстовые файлы, но он всё ещё может использоваться для запуска вредоносного кода. Кроме того, он несовместим между разными версиями Питона, поэтому если вы будете распространять свои объекты с помощью pickle, не ожидайте что все люди смогут их использовать. Тем не менее, модуль может быть мощным инструментом для кэширования и других распространённых задач с сериализацией.


Сериализация собственных объектов.


Модуль pickle не только для встроенных типов. Он может использоваться с каждым классом, реализующим его протокол. Этот протокол содержит четыре необязательных метода, позволяющих настроить то, как pickle будет с ними обращаться (есть некоторые различия для расширений на C, но это за рамками нашего руководства):

  • __getinitargs__(self)
    Если вы хотите, чтобы после десериализации вашего класса был вызыван __init__, вы можете определить __getinitargs__, который должен вернуть кортеж аргументов, который будет отправлен в __init__. Заметьте, что этот метод работает только с классами старого стиля.

  • __getnewargs__(self)
    Для классов нового стиля вы можете определить, какие параметры будут переданы в __new__ во время десериализации. Этот метод так же должен вернуть кортеж аргументов, которые будут отправлены в __new__.

  • __getstate__(self)
    Вместо стандартного атрибута __dict__, где хранятся атрибуты класса, вы можете вернуть произвольные данные для сериализации. Эти данные будут переданы в __setstate__ во время десериализации.

  • __setstate__(self, state)
    Если во время десериализации определён __setstate__, то данные объекта будут переданы сюда, вместо того чтобы просто записать всё в __dict__. Это парный метод для __getstate__: когда оба определены, вы можете представлять состояние вашего объекта так, как вы только захотите.

  • __reduce__(self)
    Если вы определили свой тип (с помощью Python's C API), вы должны сообщить Питону как его сериализовать, если вы хотите, чтобы он его сериализовал. __reduce__() вызывается когда сериализуется объект, в котором этот метод был определён. Он должен вернуть или строку, содержащую имя глобальной переменной, содержимое которой сериализуется как обычно, или кортеж. Кортеж может содержать от 2 до 5 элементов: вызываемый объект, который будет вызван, чтобы создать десериализованный объект, кортеж аргументов для этого вызываемого объекта, данные, которые будут переданы в __setstate__ (опционально), итератор списка элементов для сериализации (опционально) и итератор словаря элементов для сериализации (опционально).

  • __reduce_ex__(self, protocol)
    Иногда полезно знать версию протокола, реализуя __reduce__. И этого можно добиться, реализовав вместо него __reduce_ex__. Если __reduce_ex__ реализован, то предпочтение при вызове отдаётся ему (вы всё-равно должны реализовать __reduce__ для обратной совместимости).


Пример


Для примера опишем грифельную доску (Slate), которая запоминает что и когда было на ней записано. Впрочем, конкретно эта доска становится чистой каждый раз, когда она сериализуется: текущее значение не сохраняется.

import time

class Slate:
    '''Класс, хранящий строку и лог изменений. И забывающий своё значение после 
    сериализации.'''

    def __init__(self, value):
        self.value = value
        self.last_change = time.asctime()
        self.history = {}

    def change(self, new_value):
        # Изменить значение. Зафиксировать последнее значение в истории. 
        self.history[self.last_change] = self.value
        self.value = new_value
        self.last_change = time.asctime()

    def print_changes(self):
        print 'Changelog for Slate object:'
        for k, v in self.history.items():
            print '%s\t %s' % (k, v)

    def __getstate__(self):
        # Намеренно не возвращаем self.value or self.last_change.
        # Мы хотим "чистую доску" после десериализации.
        return self.history

    def __setstate__(self, state):
        self.history = state
        self.value, self.last_change = None, None


Заключение


Цель этого руководства донести что-нибудь до каждого, кто его читает, независимо от его опыта в Питоне или объектно-ориентированном программировании. Если вы новичок в Питоне, вы получили ценные знания об основах написания функциональных, элегантных и простых для использования классов. Если вы программист среднего уровня, то вы, возможно, нашли несколько новых приятных идей и стратегий, несколько хороших способов уменьшить количество кода, написанного вами и вашими клиентами. Если же вы Питонист-эксперт, то вы обновили некоторые свои, возможно, подзабытые знания, а может и нашли парочку новых трюков. Независимо от вашего уровня, я надеюсь, что это путешествие через специальные питоновские методы было поистине магическим (не смог удержаться).


Дополнение 1: Как вызывать магические методы


Некоторые из магических методов напрямую связаны со встроенными функциями; в этом случае совершенно очевидно как их вызывать. Однако, так бывает не всегда. Это дополнение посвящено тому, чтобы раскрыть неочевидный синтаксис, приводящий к вызову магических методов.
Магический метод Когда он вызывается (пример) Объяснение
__new__(cls [,...]) instance = MyClass(arg1, arg2) __new__ вызывается при создании экземпляра
__init__(self [,...]) instance = MyClass(arg1, arg2) __init__ вызывается при создании экземпляра
__cmp__(self, other) self == other, self > other, etc. Вызывается для любого сравнения
__pos__(self) +self Унарный знак плюса
__neg__(self) -self Унарный знак минуса
__invert__(self) ~self Побитовая инверсия
__index__(self) x[self] Преобразование, когда объект используется как индекс
__nonzero__(self) bool(self), if self: Булевое значение объекта
__getattr__(self, name) self.name # name не определено Пытаются получить несуществующий атрибут
__setattr__(self, name, val) self.name = val Присвоение любому атрибуту
__delattr__(self, name) del self.name Удаление атрибута
__getattribute__(self, name) self.name Получить любой атрибут
__getitem__(self, key) self[key] Получение элемента через индекс
__setitem__(self, key, val) self[key] = val Присвоение элементу через индекс
__delitem__(self, key) del self[key] Удаление элемента через индекс
__iter__(self) for x in self Итерация
__contains__(self, value) value in self, value not in self Проверка принадлежности с помощью in
__call__(self [,...]) self(args) «Вызов» экземпляра
__enter__(self) with self as x: with оператор менеджеров контекста
__exit__(self, exc, val, trace) with self as x: with оператор менеджеров контекста
__getstate__(self) pickle.dump(pkl_file, self) Сериализация
__setstate__(self) data = pickle.load(pkl_file) Сериализация


Надеюсь, эта таблица избавит вас от любых вопросов о том, что за синтаксис вызова магических методов.


Дополнение 2: Изменения в Питоне 3


Опишем несколько главных случаев, когда Питон 3 отличается от 2.x в терминах его объектной модели:

  • Так как в Питоне 3 различий между строкой и юникодом больше нет, __unicode__ исчез, а появился __bytes__ (который ведёт себя так же как __str__ и __unicode__ в 2.7) для новых встроенных функций построения байтовых массивов.
  • Так как деление в Питоне 3 теперь по-умолчанию «правильное деление», __div__ больше нет.
  • __coerce__ больше нет, из-за избыточности и странного поведения.
  • __cmp__ больше нет, из-за избыточности.
  • __nonzero__ было переименовано в __bool__.
  • next у итераторов был переименован в __next__.


Перевод: Rafe Kettler
Сергей Головин @begezavr
карма
37,6
рейтинг 0,0
Разработчик
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (57)

  • –11
    мне очень нравится питон синтаксисом, но именование ______________name_____________ выглядит просто вырвиглазно.

    хотя, наверное, для питонщиков, это стало привычкой.
    • +10
      Ну, во-первых, подчёркивания там максимум по 2 с каждой стороны, что вполне читаемо. А во-вторых, сравните это с кучей ключевых слов и специального синтаксиса типа private, operator, Constructor() и т.д. в других языках ;)
      • –6
        Читаемо, не спорю. Но не красиво. Вырывается из стиля :-)

        class, def, return, except, print — присуствуют и смотрятся ведь красиво. Так что нет ничего страшного в ключевых словах, кроме их количества.

        Давайте пофантазируем.
        from os.path import join
        
        class FileObject:
            def __init__(self, filepath='~', filename='sample.txt'):
                self.file = open(join(filepath, filename), 'r+')
        
            def __del__(self):
                self.file.close()
                del self.file
        
        например в
        from os.path import join
        
        class FileObject:
            def init.core(self, filepath = '~', filename = 'sample.txt'):
                self.file = open(join(filepath, filename), 'r+')
        
            def del.core(self):
                self.file.close()
                del self.file
        


        согласитесь, выглядет по-гламурнее? :-) (надеюсь в существующем питоне нет возможнсти def parent.child(args))
        • +10
          Пара минусов del.core/init.core по сравнению с __del__/__init__:

          1) del.core читается «удалить ядро», init.core — «инициализировать ядро». В __del__ таких лишних смыслов нет.
          2) в синтаксис языка добавляется новая сущность — точка в объявлении метода. Это усложнение, которое, к тому же, ко всяким неоднозначностям ведет:

          class Foo:
              def core(self):
                  print('Foo')
          
          class FileObject:
              def del.core(self):
                  print('FileObject')
          
              def del(self):
                  return Foo()
          


          и теперь вызываем file_object.del.core() — что будет напечатано?

          __магические__ методы решают вполне осязаемые практические проблемы простым (и imho красивым) способом, зачем что-то усложнять? Переименование __next__ в next.core никаких практических проблем не решает.
          • +2
            согласен с вашими аргументами, ниже ответил anjensan.
        • +2
          Ваш вариат не работает. Вызвать x.__init__(x) можно, а вот x.init.core(x) — уже нет, нету способа отличить это от 2х независимых вызовов.

          На самом деле, это достаточно распространенная практика во многих языках — использовать знак(и) подчеркивания для обозначения приватных или «магических» вещей. Могли бы конечно выделять специальные методы только с одной стороны (было бы __init, __del и т.д.). Но это, на мой взгляд, менее красиво, да и выделение с одной стороны уже используется для name mangling.

          Попробуйте предложить рабочую альтертаниву такому решению. Уверен, что однозначного «победителя» не найдется.
          • –2
            Что ж, у вас железные аргументы. Асло, "__" мне не нравится и в других языках. Исключая бинарные операции, программирование символами псевдографики — это истинное брэйнфакство. Архаизм. Имхоу. От этого не уйдешь, потому что удобно, но "__" в именах — злоупотребление, мне кажется.

            На счет рабочих вариантов — не приходит на ум ничего оригинального.
            Mожет быть x.magic.init(x) и def magic.init(x):. «magic» — неймспейс для доступа к магии.
            • +2
              Исключая бинарные операции, программирование символами псевдографики — это истинное брэйнфакство.


              * — вместо «всё»
              import com.package.*
              [] — для обращения к элементам массива
              items[5]
              = — для обозначения присваивания:
              x = 1
              ну и т.д.

              Что касается конкретно символов подчёркивания в имени, чем они хуже точки или CamelCase? Это вопрос привычки, и когда привыкаете, понимаете, что это действительно удобно, и вы уже не хотите ничего другого.
            • +1
              Уж что-что, а Питон это просто оазис красоты, вкуса, стиля и изящества среди языков, протоколов, систем разметки и т. д., созданных людьми с психикой, навсегда травмированной консольными интерфейсами.
    • +6
      Вырвиглазность повышает читаемость кода и позволяет улучшать язык обратно-совместимым способом.

      Если чуть подробнее, этот стиль наименования:

      1. позволяет вводить в язык новые магические методы без риска сломать старый код. Программисты на питоне в своем коде названия __name__ не используют (по соглашению, да и сложно так назвать что-то случайно), а значит новый магический метод ничего не сломает. Без подобного соглашения нужно быть телепатом, чтоб писать код, который не сломается в будущих версиях интерпретатора. Ср., например, с toJSON в javascript — когда в браузерах поддержка этого метода появилась, куча кода перестала работать.

      2. при чтении кода сразу видно, что метод магический, и вызываться он, скорее всего, будет неявно. Вызовы остальных методов обычно можно грепом найти. Кстати, название магического метода часто подсказывает, как он может вызываться: если метод называется __foo__, то во многих случаях он будет вызван с помощью функции foo(obj). Например, функции getattr, setattr, len, iter, next, reversed, copy, deepcopy, hash вызывают соответствующие __методы__.
  • +2
    Бывают и менее очевидные вызовы магических методов. Например, сложение вызывается не только для оператора +, но и для встроенной функции sum.
    class N():
        def __init__(self, n):
            self.n = n
    
        def __add__(self, other):
            return N(self.n + other.n)
    
        def __repr__(self):
            return '<N: %s>' % self.n
    
    print sum([N(1), N(2), N(3)], N(0))
    

    Выводит <N: 6>
    • +1
      Или вот ещё __nonzero__. Вызывается не только когда явно укажешь bool(object), но и просто if obj:
      В таком случае bool() применяется к объекту «автоматически».
      • 0
        Добавил в табличку ваш пример с if. Но, на самом деле, в обоих ваших примерах было бы странно ожидать от Питона какого-нибудь другого поведения, мне кажется :)
        • 0
          Согласен, это логично. Ещё пара примеров (сами в голову приходят, уж извините :о))
          all(iterable) и any(iterable) тоже вызывают bool неявным образом.
          С одной стороны, об этом тоже можно догадаться внимательно посмотрев в документацию, а с другой, лучше помнить заранее, чтобы не писать своих хитроумных алгоритмов для проверки истинности «всех», или «хотя бы одного» объекта из списка, когда достаточно определить один простой магический метод и пользоваться встроенными возможностями.
  • +2
    Ну все, можно смело положить в избранное. Материал обязательно пригодится, когда надо срочно что-то заставить работать по-другомупереопределить
  • +2
    Огромнейшее спасибо. Однозначно в избранное. Я бы даже сделал в виде pdf.
  • +3
    Можно добавить еще про новый (с 3 версии питона) магический метод получения следующего значения у итераторов, .__next__(), который принят на вооружение взамен старому .next(). При этом, правильный метод в зависимости от версии питона, отлично вызывается встроенной функцией next().
    см. PEP 3114
    • 0
      Спасибо, добавил.
  • 0
    __hash__ используется не только для поиска по словарю, но и для проверки вхождения в множество (set()). Хотя, некоторые представляют себе множество словарём без значений… Технически это может и имеет смысл, но как-то совсем не интуитивно.
    • 0
      __hash__ это просто дуаль встроенной функции hash, а уж кто и как ее использует — дело второе.
      • 0
        Нет. Значение, возвращаемое __hash__ для экземпляра класса, является значением, используемым для получения хэша объекта.

        class A(object):
            def __hash__(self):
                return 10
        
        a = A()
        


        Вызов hash(a) вызывает __hash__ у объекта a.

        >>> class A(object):
        ...     def __hash__(self):
        ...             print "i'm called, you are wrong"
        ...             return 10
        ... 
        >>> a = A()
        >>> v = {a: 10}
        i'm called, you are wrong
        
        • 0
          5 раз перечитал ваш комментарий, не понял, почему «нет». Если бы стандартные словари и множества были реализованы на самом Питоне, а не внутри интерпретатора, там бы использовалась ф-я hash. Любой может сделать свой контейнер и использовать там ф-ю hash, но это не повод писать "__hash__ используется не только для поиска по словарю и для проверки вхождения в множество, но и вот в этом моем контейнере".
          • 0
            1) Контейнер не мой, это просто словарь.
            2) Так она и используется — но функция hash всего-лишь вызывает __hash__ у объекта. Это не дуаль потому, что hash — это syntactic sugar, а __hash__ *на самом деле* вычисляет значения хэша.
            • +1
              > __hash__ *на самом деле* вычисляет значения хэша
              __hash__ вычисляет значение, из которого вычисляется хэш.
              • 0
                __hash__ вычисляет *целое* из которого вычисляется хэш. docs.python.org/2/reference/datamodel.html#object.__hash__. По факту можно использовать и это целое, но реализация CPython хочет хэш в виде числа 32 или 64 битного.
      • 0
        set — это встроенная функция и узнать, что она тоже использует hash можно либо добравшись до исходников на C, либо потеряв некоторое время, пытаясь понять, почему даже при определение __eq__ равные объекты внутри множества не считаются равными. Поэтому я посчитал полезным добавить это замечание.
      • +1
        Не совсем.

        >>> class X(object):
        ...     def __hash__(self):
        ...         return 111111111111111111111111111111111111111111111
        ...         
        >>> x = X()
        >>> hash(x)
        891475464178347604
        

        • 0
          Слишком длинное число, хэш должен помещаться в целое. Он берёт hash от hash'а.
  • +2
    Про __del__ неправильно написано. Метод будет вызван если интертретатор завершает работу.
    Другое дело что он не вызовется сборщиком мусора (объекты с __del__ сборщик мусора до версии 3.4 не умеет обрабатывать вообще).
    __del__ работает только если произошел decref, а не garbage collection.
    • +1
      Хм, в документации написано:

      «It is not guaranteed that __del__() methods are called for objects that still exist when the interpreter exits.»

      Неужели я неправильно перевёл такое, вроде бы несложное предложение?
      • 0
        Документация не совсем точна. Следующий параграф Note более внятно описывает что именно происходит
        • 0
          Но и там я так и не нашёл утверждения, что вызов __del__ гарантирован :(

          Сейчас потестил и всё говорит за то, что вы правы и __del__ на самом деле всегда вызывается по завершению интерпретатора. Добавлю замечание.
      • 0
        Скорее всего, это относится к общим гарантиям языка, которые не зависят от реализации. Например, это будет справедливо в Jython, IronPython, или другой реализации, не использующей подсчет ссылок, и основанной на стороннем GC, не гарантирующим финализацию объектов при выходе процесса. Как написано в самом начале этой же страницы документации:

        «Objects are never explicitly destroyed; however, when they become unreachable they may be garbage-collected. An implementation is allowed to postpone garbage collection or omit it altogether — it is a matter of implementation quality how garbage collection is implemented, as long as no objects are collected that are still reachable.»
    • 0
      Однако, следует пояснить, что это справедливо только для объектов с циклическими зависимостями.
      Объекты, у которых есть метод __del__, но без циклических ссылок, удаляются сразу после удаления последней ссылки на него. Метод __del__ у них вызывается сразу.
  • +2
    > На самом деле, экземпляр объекта создаёт метод __new__ и затем передаёт аргументы в инициализатор.
    Немного не так. __new__ не передает аргументы в __init__. __init__ вызывается интерпретатором, если __new__ вернул объект того класса. Причем, если вы вернули объект, уже созданный до этого и сохраненный где-то (например, это singleton), то __init__ все равно будет вызван.
    • 0
      Спасибо за комментарий, действительно не совсем правильно получилось. Изменил предложение на:
      На самом деле, экземпляр объекта создаёт метод __new__, а затем аргументы передаются в инициализатор.
      • 0
        Я не уверен, что можно называть __new__ методом, поскольку объекта в этот момент ещё не существует. Это скорее classmethod.
  • –2
    У меня возникло ощущение, что писать ОО-код на питоне будет порядком мозговзрывательнее, чем, например, на том же С++ (с нормальной IDE с подсказками)
    • +2
      Вы не правы, модель в питоне очень проста, не обязательно использовать все перечисленные изыски, большинство из них для особых ценителей-извращенцев.
    • 0
      Как человек достаточно хорошо знающий и С++ и Python, могу сказать на Python гораздо менее мозговзрывательнее, чем на Це++. Просто с непривычки да, а как втянешься, переписать все на питон останавливает только то, что это все таки транслятор со всеми вытекающими.
    • 0
      Питон в самом деле превосходит Цэ++ по читабельности, но Вы правы в одном — отсутствие типизации в самом деле создает некоторые трудности с подсказками в IDE
  • +5
    Для полной картины не хватает еще и вещей вроде __dict__, __doc__, __ slots__ и т.д. (хоть это и не методы, но статья бы стала почти идеальной)).
    Ну, и еще не хватает __prepare__.
    • –1
      Тогда уж можно начать описывать вообще все magic method's, в том числе NumPy'вые.
  • +2
    Статья тупо перевод мануала, было бы клёво если бы показали больше примеров зачем это все нужно. Тот же синглетон на __new__.
  • 0
    Если в первом примере под тильдой в filepath='~' понимается домашняя директория текущего пользователя, то это так не работает — будет попытка открыть файл в директории ./~/, т.к. Python не разворачивает шелл-подстановки (и спасибо ему). Чтобы работало, код нужно написать следующим образом:

        def __init__(self, filepath = '~', filename = 'sample.txt'):
            # открыть файл filename в filepath в режиме чтения и записи
            self.file = open(os.path.expanduser(os.path.join(filepath, filename)), 'r+')
    


    Более того, опасно использовать __del__ так, как это было продемонстрировано, так как __del__ вызывается даже для тех экземпляров класса, для которых не было завершено конструирование. Таким образом, если создать FileObject с неправильным именем файла, open() выбросит IOError, и self.file не будет присвоен — но к нему обратится __del__ и получит вдобавок еще и AttributeError, и последующая деинициализация выполнена не будет, лишая __del__ того смысла, который в него вкладывал автор. Будьте осторожнее.
  • 0
    В Python 3 больше нет метода __cmp__() и функции cmp(), и рекомендуется использовать __lt__/__gt__/__le__/__ge__/__eq__/__ne__ исключительно. Если нет необходимости определять все операторы сравнения, можно определить один оператор сравнения (например, __eq__) и один оператор упорядочения (например, __lt__), и применить к объявлению класса декоратор @functools.total_ordering — он достроит остальные функции на основании имеющихся.
    • –1
      В статье есть всё из того, что вы написали.
  • 0
    Про __metaclass__ нет упоминания, и это, как мне кажется, большое упущение. На хабре даже есть подробная статья, посвященная этой теме: Метаклассы в Python.
    • +1
      Про __dict__, __name__, __doc__ тоже ничего нет, потому что статья про магические методы, а не про всю объектную модель Питона вообще. А метаклассы это вообще отдельная тема.
      • –1
        Электронный текст имеет право быть семантическим, не так ли? Если затронули тему магических методов, то стоит упомянуть и о магических свойствах.

        Отдельные темы оформляются отдельными ссылками (на соответствующие статьи).

        Не кажется ли вам, что неправильно давать материал в отрыве от контекста? То есть без возможности продолжить изыскания на смежные темы. Уважение к читателю — важная составляющая работы любого автора.
        • +1
          Я всего лишь перевёл статью. А продолжить изыскания всегда можно в гугле.
  • 0
    Зря так часто специализированные методы называют «магическими». В них нет никакой магии, в них — специализация.

    Любителям магии и пошалить: смотрите занятие от Бизли с Pycon2013 US, где разбирается работа метаклассов и дескрипторов.
    • 0
      Магическими их называют потому-что обычно на прямую их не вызывают и определить их назначение в коде можно только обладая сакральными знаниями :о)
      • +1
        Ага, тогда, пожалуй, любой метод, который, скажем, обычно вызывают не напрямую, сказочным образом становится магическим ;)
        Неее, тут явно не обошлось без неутоленного в детстве желания волшебства.
  • +3
    Не верьте этому:

    Pickle, может быть, безопаснее чем текстовые файлы,

    — не безопаснее =/

    но он всё ещё может использоваться для запуска вредоносного кода

    Может и будет использоваться, если сериализованные объекты будут приходить из недоверенного источника:

    >>> import pickle, base64
    >>> pickle.loads(base64.b64decode("Y29zCnN5c3RlbQooUydjYWxjLmV4ZScKdFIu"))
    




    • –1
      Всё же немного безопаснее, т.к. при использовании бинарного формата квалификация злоумышленника должна быть выше, чем при обычном текстовом. Хотя, лучше конечно свой Pickler делать, недопускающий подобных трюков.
      • +2
        Есть быстрый и безопасный cerealizer.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.