Якщо ви чайник, то вам метакласи непотрібні, і навіть не бажані. В 99% випадків звичайно можна обійтись без них. Але їх варто знати хоча б для того щоб перестати бути чайником.
Це важко, якщо неможливо знайти застосування. Коли я на одній “п’янці” спитав для чого потрібні метакласи Костя сказав мені що напкриклад для того щоб обчислити len()
від класу. Ну, а я як людина яка про аналіз вимог не просто чула, а й робила доповідь на 4 курсі, логічно запитав “Для чого потрібно обчислювати len()
від класу?”. Відповідь невідома. Та ми тут зібрались не для того щоб виясняти для чого це потрібно, а щоб розібратись що це таке взагалі. Тому давайте напишемо клас, len()
повертає нам кількість його екземплярів. Просто для розваги.
В Python – все об’єкт. Кожен об’єкт має тип, який можна отримати функцією type()
. Наприклад:
>>> type(1)
<type 'int'>
>>> type(None)
<type 'NoneType'>
>>> import sys
>>> type(sys)
<type 'module'>
>>> type(type(1))
<type 'type'>
Цікаво що в Python3, ці типи вже є класами:
>>> type(1)
<class 'int'>
>>> type(None)
<class 'NoneType'>
>>> import sys
>>> type(sys)
<class 'module'>
>>> type(type(1))
<class 'type'>
Це трохи збиває з пантелику, але насправді класи і типи це одне й те саме. В старіших версія Python були якісь відмінності, там старі класи це класи, а нові класи – це типи:
>>> class Old:
... pass
...
>>> class New(object):
... pass
...
>>> type(Old)
<type 'classobj'>
>>> type(New)
<type 'type'>
Але зараз прийнято писати все новими класами і забути про відмінності.
Ах. Ви бачили що ми можемо отримати тип типу? Тип типу це те ж саме що й клас класу, тобто метаклас. В Python все – об’єкти, класи в тому числі. Об’єкти – екземпляри класів, класи – екземпляри метакласів.
Для класів в Python також характерне те, що вони всі – об’єкти яких можна викликати, і їх виклик створює нам новий екземпляр. Наприклад отримати рядок можна викликавши клас str()
:
>>> str(1)
'1'
>>> type('')(1)
'1'
А можна викликати той клас що нам повертає type('')
, і теж отримати рядок. Це той самий клас.
Тепер, класи можна створювати так само як і інші об’єкти, викликом конструктора. Тільки цього разу метакласу. Як називається цей конструктор? Ну, ми можемо й не знати, а використати функцію type()
:
>>> type(type(1))('')
<type 'str'>
>>> type(type(1)) == type
True
Хах, виявляється сама type()
і є класом класів. Тільки як з її допомогою сконструювати клас не маючи його екземплярів? Бо ж зазвичай спочатку є клас, а потім вже його екземпляри. Давайте добре почитаємо документацію. type()
з трьома параметрами – конструктор. І він дозволяє нам створювати типи прямо в виразах, на льоту. Прямо як лямбда-функція функції на льоту. Лямбда-клас, ага.
Що ми передаємо в конструктор? name
– рядок з ім’ям того що створюємо, bases
– кортеж класів від яких наш буде успадковувати атрибути і словник власних атрибутів.
Таким чином наступні два способи створення класу еквівалентні:
>>> class X(object):
... a = 1
...
>>> X = type('X', (object,), dict(a=1))
Тепер варто задати собі питання “Чи існують метакласи окрім type()
?”. І на щастя відповідь – так, існують. Ми можемо наприклад пронаслідуватись від type()
. Давайте так і зробимо і почнемо писати наш клас, довжина якого – кількість екземплярів.
class CountedInstancesMeta(type):
def __len__(self):
return self._len
Отак. Це клас що наслідується від type
, і описує магічний метод __len__
, який буде викликатись коли від екземпляра спробують обчислити len()
. Сам екзепляр буде передаватись в self
, як і в звичайних класах, ну а довжину ми сховаємо в атрибуті.
Ну що, давайте тепер збацаємо клас певної довжини?
>>> ExampleClass = CountedInstancesMeta('ExampleClass', (object, ), {'_len': 42})
>>> ExampleClass()
<__main__.ExampleClass object at 0x8eaaa0c>
>>> len(ExampleClass)
42
>>> type(ExampleClass)
<class '__main__.CountedInstancesMeta'>
Бачимо що довжина працює, ми її задали як 42. Також типом нашого класу є не type()
, а наш метаклас.
Але як зробити клас, довжина якого – кількість екземплярів? Тут “лямбда” формою створення буде незручно оперувати, тому використаємо інший синтаксис.
class CountedInstances(metaclass=CountedInstancesMeta):
__metaclass__ = CountedInstancesMeta
_len = 0
def __init__(self):
self.__class__._len += 1
def __del__(self):
self.__class__._len -= 1
Код який ви бачите вище – на Python3. Для того щоб задати метаклас використовується синтаксис подібний до задання стандартних значень аргументів функції. В Python2 використовувався магічний атрибут __metaclass__
, який Python3 успішно ігнорує.
Далі теж просто, достатньо лише знати різницю між атрибутом класу, та атрибутом екземпляра. При створенні нового екземпляра ми збільшуємо значення атрибуту класу, при його видаленні – зменшуємо. Щоб добратись до атрибуту класу, треба мати клас. Щоб добратись до класу – взяти тип екземпляра, або його атрибут __class__
.
Тест:
print(len(CountedInstances)) # 0
x = CountedInstances()
print(len(CountedInstances)) # 1
y = CountedInstances()
print(len(CountedInstances)) # 2
y = x
print(len(CountedInstances)) # 1
del x
print(len(CountedInstances)) # 1
del y
print(len(CountedInstances)) # 0
Особливо цікаво почути коментарі тих, хто справді придумав куди б ці метакласи застосувати.
Filed under:
Кодерство Tagged:
Python