Пориньте у Python 3/Імена магічних методів

Моя професія - бути правим, коли всі інші помиляються.
Бернард Шоу

Іноді в цій книжці ви бачили приклади "спеціальних методів" - деяких "магічних методів" які 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__(), вам напевне варто також описати метод __setattr__(), та скоординувати їх роботу так аби стежити за значеннями атрибутів. Інакше, будь-які атрибути які ви задаєте після створення екземпляру пропадуть в чорній дірі.

Потрібно бути особливо обережним з методом __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'])

Об’єкт fs є екземпляром cgi.FieldStorage, але ви все одно можете отримати значення виразу на зразок fs['q'].

# Шматок з cgi.py який показує як це працює
class FieldStorage:
.
.
.
    def __getitem__(self, key):
        if self.list is None:
            raise TypeError('not indexable')
        found = []
        for item in self.list:
            if item.name == key: found.append(item)
        if not found:
            raise KeyError(key)
        if len(found) == 1:
            return found[0]
        else:
            return found

fs['q'] задіює метод __getitem__() передаючи йому 'q' в параметр key. Після чого він шукає у власному списку параметрів запиту (self.list) елемент, .name якого відповідає даному ключу.

Класи що поводяться як числа ред.

Використовуючи відповідні спеціальні методи, ви можете описати свої власні класи що поводяться як числа. Тобто, ви можете додавати їх, віднімати та виконувати інші математичні операції. Таким чином реалізовані дроби - клас Fraction містить реалізацію цих методів, і тому ми можемо робити речі на зразок таких:

>>> from fractions import Fraction
>>> x = Fraction(1, 3)
>>> x / 3
Fraction(1, 9)

Далі вичерпний список спеціальних методів які потрібні щоб створити числоподібний клас.

Ви хочете... Тому ви пишете... І Python виконує...
додавання
x + y
x.__add__(y)
віднімання
x - y
x.__sub__(y)
множення
x * y
x.__mul__(y)
ділення
x / y
x.__truediv__(y)
цілочисельне ділення
x // y
x.__floordiv__(y)
остачу від ділення
x % y
x.__mod__(y)
цілочисельне ділення з остачею
divmod(x, y)
x.__divmod__(y)
піднесення до степеня
x ** y
x.__pow__(y)
побітовий зсув вліво
x << y
x.__lshift__(y)
побітовий зсув право
x >> y
x.__rshift__(y)
побітове "і"
x & y
x.__and__(y)
побітове виключне "або"
x ^ y
x.__xor__(y)
побітове або
x | y
x.__or__(y)

Це все чудово і добре, якщо x - екземпляр класу що реалізує ці методи. Але що якщо він не реалізує жодного? Чи гірше, що якщо він їх реалізує, але не може обробляти певні види аргументів? Наприклад:

>>> from fractions import Fraction
>>> x = Fraction(1, 3)
>>> 1 / x
Fraction(3, 1)

Це як в попередньому прикладі, коли ми брали Fraction та ділили його на ціле. Цей випадок був прямолінійним, x / 3 викликає x.__truediv__(3), а метод __truediv__() класу Fraction обробляє всю математику. Але цілі не знають як робити арифметичні операції з дробами. То чому цей приклад працює?

Існує інший набір спеціальних арифметичних методів, з оберненими операндами. Маючи арифметичну операцію яка бере два операнди (наприклад x / y), існує два способи її здійснити:

  1. Попросити x поділити себе на y, або
  2. Попросити y поділити на себе x

Множина спеціальних методів вище обирає перший підхід: маючи x / y, вони описують те як x може поділити себе на y. Наступний набір спеціальних методів обирає інший підхід: вони надають спосіб за допомогою якого y може сказати "Я знаю як бути знаменником і поділити на себе x".

Ви хочете... Тому ви пишете... І Python виконує...
додавання
x + y
y.__radd__(x)
віднімання
x - y
y.__rsub__(x)
множення
x * y
y.__rmul__(x)
ділення
x / y
y.__rtruediv__(x)
цілочисельне ділення
x // y
y.__rfloordiv__(x)
остачу від ділення
x % y
y.__rmod__(x)
цілочисельне ділення з остачею
divmod(x, y)
y.__rdivmod__(x)
піднесення до степеня
x ** y
y.__rpow__(x)
побітовий зсув вліво
x << y
y.__rlshift__(x)
побітовий зсув право
x >> y
y.__rrshift__(x)
побітове "і"
x & y
y.__rand__(x)
побітове виключне "або"
x ^ y
y.__rxor__(x)
побітове або
x | y
y.__ror__(x)

Але зачекайте, є ще! Якщо ви використовуєте оператори з присвоєнням ("in-place operator" - оператор на місці), такі як x /= 3, існує ще більше спеціальних методів які ви можете описати.

Ви хочете... Тому ви пишете... І Python виконує...
додавання з присвоєнням
x += y
x.__iadd__(y)
віднімання з присвоєнням
x -= y
x.__isub__(y)
множення з присвоєнням
x *= y
x.__imul__(y)
ділення з присвоєнням
x /= y
x.__itruediv__(y)
цілочисельне ділення з присвоєнням
x //= y
x.__ifloordiv__(y)
остача від ділення з присвоєнням
x %= y
x.__imod__(y)
піднесення до степеня з присвоєнням
x **= y
x.__ipow__(y)
побітовий зсув вліво з присвоєнням
x <<= y
x.__ilshift__(y)
побітовий зсув право з присвоєнням
x >>= y
x.__irshift__(y)
побітове "і" з присвоєнням
x &= y
x.__iand__(y)
побітове виключне "або" з присвоєнням
x ^= y
x.__ixor__(y)
побітове або з присвоєнням
x |= y
x.__ior__(y)

Зауваження: у більшості випадків перевантажувати оператори з присвоєнням не є необхідним. Якщо ви не задасте метод для певної операції з присвоєнням, Python спробує інші методи. Наприклад, щоб виконати вираз x /= y, Python спробує:

  1. Спробувати викликати x.__itruediv__(y). Якщо цей метод описаний та повертає будь-яке значення окрім NotImplemented, то на цьому все.
  2. Спробувати викликати x.__truediv__(y). Якщо цей метод описаний та повертає будь-яке значення окрім NotImplemented, то старе значення x відкидається та замінюється повернутим значенням, так ніби ми написали x = x / y.
  3. Спробувати викликати y.__rtruediv__(x). Якщо цей метод описаний та повертає будь-яке значення окрім NotImplemented, то старе значення x відкидається та замінюється повернутим значенням.

Тому ви справді потребуєте задавати методи для операторів з присвоєнням на зразок __itruediv__(), якщо ви хочете зробити спеціальні оптимізації для таких операторів. Інакше Python переформулює ці оператори в звичайні з наступним присвоєнням.

Існує також кілька "унарних" математичних операцій, які можна виконувати на числоподібних об’єктах.

Ви хочете... Тому ви пишете... І Python виконує...
від’ємне число
-x
x.__neg__()
додатнє число
+x
x.__pos__()
побітову інверсію ("не")
~x
x.__invert__()
комплексне число
complex(x)
x.__complex__()
ціле число
int(x)
x.__int__()
число з плаваючою крапкою
float(x)
x.__float__()
число округлене до найближчого цілого
round(x)
x.__round__()
число округлене до найближчого значення з n значущими цифрами
round(x, n)
x.__round__(n)
найменше ціле >= x
math.ceil(x)
x.__ceil__()
найбільше ціле <= x
math.floor(x)
x.__round__()
обрізати x до найближчого цілого в сторону 0
math.trunc(x)
x.__trunc__()
використати як індекс списку (PEP 357)
a_list[x]
a_list[x.__index__()]

Класи які можна порівнювати ред.

Я виокремив цей параграф від попереднього, тому що порівняння не є строгою компетенцією чисел. Багато типів даних можна порівнювати - рядки, списки, навіть словники. Якщо ви створюєте власний клас і є сенс в порівнянні ваших об’єктів з іншими, ви можете використати наступні спеціальні методи для реалізації порівнянь.

Ви хочете... Тому ви пишете... І Python виконує...
рівність
x == y
x.__eq__(y)
нерівність
x != y
x.__ne__(y)
менше
x < y
x.__lt__(y)
менше рівно
x <= y
x.__le__(y)
більше
x > y
x.__gt__(y)
більше рівно
x >= y
x.__ge__(y)
значення істинності в булевому контексті
if x:
x.__bool__()

Якщо ви опишете метод __lt__() але не __gt__(), Python використає метод __lt__() з переставленими аргументами. Щоправда, Python не комбінуватиме методи. Наприклад, якщо ви опишете методи __lt__() та __eq__() та спробуєте перевірити чи x <= y, Python не викликатиме __lt__() та __eq__() один за одним. Він лише викличе метод __le__().

Класи які можна серіалізувати ред.

Python підтримує серіалізацію та розсеріалізацію довільних об’єктів. (Також цей процес називають pickling та unpickling відповідно). Це може бути корисним для зберігання стану в файл, та відновлення його пізніше. Всі cтандартні типи даних вже підтримують серіалізацію. Якщо ви створюєте власний клас, та хочете його серіалізувати, прочитайте про протокол pickle щоб побачити коли і як викликаються наступні спеціальні методи.

Ви хочете... Тому ви пишете... І Python виконує...
копію власного об’єкта
copy.copy(x)
x.__copy__()
"глибоку" копію власного об’єкта
copy.deepcopy(x)
x.__deepcopy__()
отримати стан об’єкта перед серіалізацією*
pickle.dump(x, file)
x.__getstate__()
серіалізувати об’єкт*
pickle.dump(x, file)
x.__reduce__()
серіалізувати об’єкт (новий протокол pickle)*
pickle.dump(x, file, protocol_version)
x.__reduce_ex__(protocol_version)
контролювати створення об’єкта під час розсеріалізації*
x = pickle.load(file)
x.__getnewargs__()
відновити стан об’єкта після розсеріалізації*
x = pickle.load(file)
x.__setstate__()
* Щоб відтворити серіалізований об’єкт, Python повинен створити новий об’єкт який виглядає як серіалізований, після чого встановити значення всіх атрибутів нового об’єкта. Метод __getnewargs__() контролює процес створення об’єкта, а метод __setstate__() - процес відновлення значень атрибутів.

Класи що можуть використовуватись в блоці with ред.

Блок with утворює контекст виконання; ви входите в контекст при виконанні оператора with і виходите з нього після виконання останнього оператора з блоку.

Ви хочете... Тому ви пишете... І Python виконує...
зробити певну дію при вході в блок with
with x:
x.__enter__()
зробити певну дію при виході з блоку with
with x:
x.__exit__(exc_type, exc_value, traceback)

Ось як це працює з файлами:

# витяг з io.py:
def _checkClosed(self, msg=None):
    '''Internal: raise an ValueError if file is closed
    '''
    if self.closed:
        raise ValueError('I/O operation on closed file.'
                         if msg is None else msg)

def __enter__(self):
    '''Context management protocol.  Returns self.'''
    self._checkClosed()                                
    return self

Файловий об’єкт описує як метод __enter__() так і метод __exit__(). Метод __enter__() перевіряє чи файл є відкритим, і якщо ні, метод _checkClosed() кидає виняток.

Метод __enter__() завжди повинен повертати self - це об’єкт який блок with використовує для доступу до атрибутів об’єкта.

def __exit__(self, *args):
    '''Context management protocol.  Calls close()'''
    self.close()

Після виходу з блоку with файловий об’єкт автоматично закривається, завдяки коду в методі __exit__().

Метод __exit__() виконується завжди, навіть якщо всередині блоку with трапився виняток. Інформація про цей виняток передається в параметри __exit__(). Щоб дізнатись деталі дивіться документацію.

Щоб дізнатись більше про керування контекстом читайте розділи Автоматичне закривання файлів та Перенаправлення стандартного потоку виводу

Зовсім езотеричні штуки ред.

Якщо ви знаєте що робите, ви можете отримати майже повний контроль над тим як класи порівнюються, як визначаються їх атрибути, та які види класів вважаються підкласами вашого.


Ви хочете... Тому ви пишете... І Python виконує...
конструктор класу
x = MyClass()
x.__new__()
деструктор класу*
del x
x.__del__()
задати певну множину атрибутів
x.__slots__()
задати власне значення хешу
hash(x)
x.__hash__()
отримати значення властивості
x.color
type(x).__dict__['color'].__get__(x, type(x))
встановити значення властивості
x.color = 'PapayaWhip'
type(x).__dict__['color'].__set__(x, 'PapayaWhip')
видалити властивість
del x.color
type(x).__dict__['color'].__del__(x)
встановити чи є об’єкт екземпляром класу
isinstance(x, MyClass)
MyClass.__instancecheck__(x)
встановити чи є клас підкласом класу
issubclass(C, MyClass)
MyClass.__subclasscheck__(C)
встановити чи є клас підкласом абстрактного базового класу
issubclass(C, MyABC)
MyABC.__subclasshook__(C)
* Точний момент коли Python викликає метод __del__ надзвичайно складно визначити. Щоб повністю це зрозуміти, потрібно знати як Python зберігає об’єкти в пам’яті. Ось гарна стаття про збирання сміття в Python та деструктори класів. Також ви повинні прочитати про слабкі посилання, модуль weakref та, для певності, модуль gc.

Для подальшого читання ред.

Модулі згадані в цьому додатку:

Інше неважке читання:

Пакування бібліотек · Що читати далі?