Метакласи для чайників
Якщо ви чайник, то вам метакласи непотрібні, і навіть не бажані. В 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
Особливо цікаво почути коментарі тих, хто справді придумав куди б ці метакласи застосувати.
Мені подобається як це реалізовано в Lua – у вигляді метатаблиць, які можна повторно використовувати, причепити на льоту то будь-якого об\’єкту і взагалі передавати навколо як звичайні таблиці.
Alex Y
14 Вересня, 2012 at 07:37
Ну в Python всі простори імен, і атрибути в тому числі теж словники. Тільки класи тут це не дані, а код конструктора. Тобто не просто список атрибутів, а код який заповнює простір імен атрибутів.
Зміна класу на льоту виявляється теж можлива, алгоритму MRO це по барабану, аби клас був. Ось наприклад такий Кіт-Пес:
meow!
woof! woof! woof!
bunyk
14 Вересня, 2012 at 14:38
Колись, в далекому 2008 році на конференції “Exception” в ході майстер-класу було озвучено кілька задач, які ефективно вирішуються метакласами(http://itblog.org.ua/post/64). І хоча в своїй роботі я й досі не мав потреби в метакласах, проте дещо, реалізоване на них таки можу підказати.
* метакласи використовуються в Django для моделей (http://www.slideshare.net/scorpion032/python-meta-classes-and-how-django-uses-them);
* використання метакласів виправдане в веденні структурованих логів; тут метакласи виграють за рахунок можливості “слідкування” за класами-нащадками (http://stackoverflow.com/a/392255/435302).
Jolly Roger
14 Вересня, 2012 at 09:42
Дякую за посилання.
bunyk
14 Вересня, 2012 at 14:39
http://koder-ua.blogspot.com/2011/12/blog-post.html
koder
28 Вересня, 2012 at 19:49