Блоґ одного кібера

Історія хвороби контуженого інформаційним вибухом

Метакласи для чайників

with 5 comments

Якщо ви чайник, то вам метакласи непотрібні, і навіть не бажані. В 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

Особливо цікаво почути коментарі тих, хто справді придумав куди б ці метакласи застосувати.

Written by bunyk

14 Вересня, 2012 at 00:30

Опубліковано в Кодерство

Tagged with

Відповідей: 5

Subscribe to comments with RSS.

  1. Мені подобається як це реалізовано в Lua – у вигляді метатаблиць, які можна повторно використовувати, причепити на льоту то будь-якого об\’єкту і взагалі передавати навколо як звичайні таблиці.

    Alex Y

    14 Вересня, 2012 at 07:37

    • Ну в Python всі простори імен, і атрибути в тому числі теж словники. Тільки класи тут це не дані, а код конструктора. Тобто не просто список атрибутів, а код який заповнює простір імен атрибутів.

      Зміна класу на льоту виявляється теж можлива, алгоритму MRO це по барабану, аби клас був. Ось наприклад такий Кіт-Пес:

      class Dog(object):
          def voice(self):
              print 'woof! ' * 3
      
      class Cat(object):
          def voice(self):
              print 'meow!'
      
      
      x = Cat()
      x.voice()
      x.__class__=Dog
      x.voice()
      

      meow!
      woof! woof! woof!

      bunyk

      14 Вересня, 2012 at 14:38

  2. Колись, в далекому 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


Залишити коментар

Цей сайт використовує Akismet для зменшення спаму. Дізнайтеся, як обробляються ваші дані коментарів.