__toc__
Іноді в цій книжці ви бачили приклади "спеціальних методів" - деяких "магічних методів" які Python викликає коли ви використовуєте спеціальний синтаксис. Використовуючи ці спеціальні методи ваші класи можуть поводитись як словники, як функції, як ітератори, чи навіть як числа. Цей додаток слугує одночасно і як довідник по тих методах які ми вже розглядали, і як короткий вступ до більш езотеричних.
Базові Якщо ви читали вступ до класів, ви вже знайомі з найбільш поширеним магічним методом - __init__(). Більшість класів які я писав потребували якоїсь ініціалізації. Також є кілька базових магічних методів які особливо корисні при зневадженні ваших класів.
Ви хочете... Тому ви пишете... І Python виконує... Ініціалізувати екземпляр x = MyClass() x.__init__()
Метод __init__() викликається після створення екземпляра. Якщо ви хочете конролювати сам процес створення екземпляра, використайте метод __new__().
Ви хочете... Тому ви пишете... І Python виконує... "офіційне" представлення у вигляді рядка repr(x) x.__repr__()
За домовленістю, __repr__() повинен повертати рядок, який є правильним виразом Python.
Ви хочете... Тому ви пишете... І Python виконує... "неофіційне" представлення у вигляді рядка str(x) x.__str__()
Метод __str__() також викликається коли ви виконуєте print(x).
Ви хочете... Тому ви пишете... І Python виконує... "неофіційне" представлення у вигляді масиву байт bytes(x) x.__bytes__()
А цей магічний метод з’явився в Python 3 разом з типом bytes.
Ви хочете... Тому ви пишете... І Python виконує... значення як форматований рядок format(x, format_spec) x.__format__(format_spec)
За замовчуванням, format_spec повинен відповідати мінімові специфікації формату. decimal.py в стандартній бібліотеці мови Python надає власний метод __format__().
Класи що поводяться як ітератори У розділі про ітератори ви читали про те як побудувати ітератор з нуля за допомогою методів __iter__() та __next__().
Ви хочете... Тому ви пишете... І Python виконує... Ітеруватись через послідовність iter(seq) seq.__iter__()
Метод __iter__ викликається щоразу як ви створюєте новий ітератор. Це гарне місце для того, щоб ініціалізувати ітератор з початковими значеннями.
Ви хочете... Тому ви пишете... І Python виконує... отримати наступне значення з ітератора next(seq) seq.__next__()
Метод __next__() викликається щоразу як ви отримуєте наступне значення з ітератора.
Ви хочете... Тому ви пишете... І Python виконує... Створити обернений ітератор reversed(seq) seq.__reversed__()
Метод __reversed__ не дуже часто зустрічається. Він приймає існуючу послідовність та повертає ітератор, який видає її елементи в зворотньому порядку, від останнього до першого.
Як ви бачили в розділі про ітератори, цикл for може працювати над ітератором. В такому циклі:
for x in seq: print(x)
Python 3 викличе seq.__iter__() щоб створити ітератор, потім метод __next__() щоб отримати кожне із значень. Коли метод __next__() згенерує виняток StopIteration, цикл for елегантно завершиться.
Обчислювані атрибути
Ви хочете... Тому ви пишете... І Python виконує... Отримати обчислений атрибут (безумовно) x.my_attr x.__getattribute__('my_attr')
Якщо ваш клас описує метод __getattribute__() Python викликатиме його при кожному звертанні до будь-якого атрибуту чи методу (окрім спеціальних методів, тому що це може створити негарний нескінченний цикл).
Ви хочете... Тому ви пишете... І Python виконує... Отримати обчислений атрибут (якщо інших не знайдеться) x.my_attr x.__getattr__('my_attr')
Якщо клас описує метод __getattr__(), Python викличе його лише після пошуку атрибута в усіх нормальних місцях. Якщо екземпляр x описує атрибут color, x.color не викличе x.__getattr__('color'); він просто поверне вже задане значення x.color.
Ви хочете... Тому ви пишете... І Python виконує... Задати значення атрибута x.my_attr = value x.__setattr__('my_attr', value)
Метод __setattr__ викликається щоразу як ви пробуєте задати значення атрибута.
Ви хочете... Тому ви пишете... І Python виконує... Видалити атрибут del x.my_attr x.__delattr__('my_attr')
Метод __delattr__() викликається щоразу як ви видаляєте атрибут.
Ви хочете... Тому ви пишете... І Python виконує... Перелічити всі атрибути та методи dir(x) x.__dir__()
Метод __dir__() корисний коли ви описуєте методи __getattr__() чи __getattribute__(). Зазвичай, виклик dir(x) дає перелік лише звичайних атрибутів та методів. Якщо ваш метод __getattr__() працює з атрибутом color динамічно, dir(x) не перелічуватиме color серед доступних атрибутів. Перезадання методу __dir__() дозволяє вам додати color до списку доступних атрибутів, що буде корисно для інших людей, які схочуть користуватись вашим класом не вникаючи в його нутрощі.
Різниця між методами __getattr__() та __getattribute__() тонка але важлива. Я можу пояснити її двома прикладами:
class Dynamo: def __getattr__(self, key): if key == 'color': return 'PapayaWhip' else: raise AttributeError
Ім’я атрибуту передається в метод __getattr__() як рядок. Якщо ім’я - 'color', метод повертає значення. (В даному випадку це жорстко заданий рядок, але ви зазвичайн зробите певні обчислення та повернете результат.)
Якщо ім’я атрибуту невідоме, метод __getattr__() повинен згенерувати виняток AttributeError, інакше ваш код працюватиме неправильно. Технічно, якщо метод не генерує виняток, чи явно повертає якесь значення, він повертає None. Це означає що всі атрибути які не задані явно матимуть значення None, а це майже напевне не те що вам потрібно.
>>> dyn = Dynamo() >>> dyn.color 'PapayaWhip'
Екземпляр dyn не має атрибуту з назвою color, тому викликається метод __getattr__() який обчислює необхідне значення атрибуту.
>>> dyn.color = 'LemonChiffon' >>> dyn.color 'LemonChiffon'
Після явного задання dyn.color, метод __getattr__() вже не викликається щоб надати значення dyn.color, тому що dyn.color вже заданий в екземплярі.
А метод __getattribute__() - навпаки, абсолютний та безумовний.
class SuperDynamo: def __getattribute__(self, key): if key == 'color': return 'PapayaWhip' else: raise AttributeError
>>> dyn = SuperDynamo() >>> dyn.color 'PapayaWhip'
Метод __getattribute__() викликається щоб повернути значення dyn.color.
>>> dyn.color = 'LemonChiffon' >>> dyn.color 'PapayaWhip'
І навіть після того як ми явно задали значення dyn.color, метод __getattribute__() все ще викликається щоб надати значення для dyn.color. Якщо присутній, метод __getattribute__() викликається безумовно для кожного атрибута, навіть якщо ми явно задали його після створення екземпляра.
Потрібно бути особливо обережним з методом __getattribute__(), тому що він також викликається коли Python шукає методи вашого класу.
class Rastan: def __getattribute__(self, key): raise AttributeError def swim(self): pass
Цей клас описує метод __getattribute__(), який завжди генерує виняток AttributeError. Жоден пошук атрибуту чи методу не буде успішним.
>>> hero = Rastan() >>> hero.swim() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 3, in __getattribute__ AttributeError
Коли ви викликаєте hero.swim(), Python шукає метод swim() в класі Rastan. Цей пошук йде крізь метод __getattribute__(), тому що всі атрибути та методи шукаються з його допомогою. В нашому випадку __getattribute__() генерує виняток AttributeError тому пошук метода не вдається, і його виклик відповідно теж.
Класи що поводяться як функції Можна зробити так щоб екземпляр класу можна було викликати як функцію - описавши метод __call__
Ви хочете... Тому ви пишете... І Python виконує... викликати об’єкт як функцію my_instance() my_instance.__call__()
Модуль zipfile використовує це щоб описати клас що може розшифрувати зашифрований zip-файл переданим йому паролем. Алгоритм шифрування в файлах zip потребує зберігати стан протягом розшифрування. Опис розшифровщика як класу дозволяє зберігати стан в екземплярах цього класу. Стан ініціалізується в методі __init__() і змінюється протягом розшифровки. Але так як такий клас можна викликати як функцію, можна передавати його екземпляр першим аргументом в функцію map(), наприклад так як в наступному прикладі:
# шматочок файла zipfile.py class _ZipDecrypter: . . . def __init__(self, pwd): self.key0 = 305419896 self.key1 = 591751049 self.key2 = 878082192 for p in pwd: self._UpdateKeys(p)
Клас _ZipDecryptor зберігає стан в формі трьох ключів, які пізніше оновлюються в методі _UpdateKeys() (тут не показаний).
def __call__(self, c): assert isinstance(c, int) k = self.key2 | 2 c = c ^ (((k * (k^1)) >> 8) & 255) self._UpdateKeys(c) return c
Клас описує метод __call__(), який дозволяє викликати екземпляри класу як функції. В нашому випадку метод __call__() розшифровує один байт zip-файлу, після чого оновлює ключі способом що залежить від розшифрованого байта.
zd = _ZipDecrypter(pwd) bytes = zef_file.read(12)
zd - екземпляр класу _ZipDecryptor. Змінна pwd передається в метод __init__(), де вона зберігається та використовується для першого оновлення ключів.
h = list(map(zd, bytes[0:12]))
Маючи перших 12 байт zip-файла, ми розшифровуємо їх map-лячи байти на zd, що дає ефект дванадцятиразового викликання zd, що викликає метод __call__() 12 разів, що оновлює внутрішній стан та повертає результуючий байт 12 разів.
Класи що поводяться як множини Якщо ваш клас працює як контейнер для множини значень, тобто, якщо є сенс перевіряти чи "містить" клас значення, тоді напевне варто описувати наступні спеціальні методи і змусити їх працювати як множину.
Ви хочете... Тому ви пишете... І Python виконує... дізнатись кількість елементів len(s) s.__len__() знати чи містить клас певне значення x in s s.__contains__(x)
Модуль cgi використовує ці методи в класі FieldStorage, який представляє всі поля форми, чи параметри запиту які були відправлені динамічній веб-сторінці.
# Скрипт який відповідає на запит http://example.com/search?q=cgi import cgi fs = cgi.FieldStorage() if 'q' in fs: do_search()
Як тільки ви створите екземпляр класу cgi.FieldStorage, ви можете використати оператор in щоб перевірити чи певний параметр був переданий в рядку запиту.
# Витяг з cgi.py який пояснює як це працює class FieldStorage: . . . def __contains__(self, key): if self.list is None: raise TypeError('not indexable') return any(item.name == key for item in self.list)
Метод __contains__() - це магія яка приводить все це в дію. Коли ви пишете if 'q' in fs, Python шукає в об’єкті fs метод __contains__(), який описаний в cgi.py. Значення 'q' передається в цей метод як аргумент key.
А функція any(), приймає генераторний вираз та повертає True якщо генератор повертає хоча б одне істинне значення. Вона досить розумна щоб одразу після цього зупинитись.
def __len__(self): return len(self.keys())
Цей же клас FieldStorage також підтримує повернення своєї довжини, тому ви можете написати len(fs), що викличе метод __len__() для FieldStorage і поверне кількість параметрів запиту які були знайдені. Метод self.keys() перевіряє чи self.list is None, тому метод __len__ не повинен проводити цю перевірку на помилки повторно.
Класи що поводяться як словники Трішки розширюючи інформацію попереднього параграфа, ви можете описувати класи що відповідають не тільки на оператор in та функцію len(), а й працюють як повноцінні словники, повертаючи значення за ключами.
Ви хочете... Тому ви пишете... І Python виконує... отримати значення за ключем x[key] x.__getitem__(key) записати значення за ключем x[key] = value x.__setitem__(key, value) видалити значення за ключем del x[key] x.__delitem__(key) надати значення за замовчуванням для неіснуючих ключів x[nonexistent_key] x.__missing__(nonexistent_key)
Клас FieldStorage з модуля cgi також описує ці спеціальні методи, що означає, що ви можете робити речі на зразок наступних:
# Скрипт який відповідає на запит http://example.com/search?q=cgi import cgi fs = cgi.FieldStorage() if 'q' in fs: do_search(fs['q'])