Метаклассы в Python

в 16:09, , рубрики: python

Как сказал один из пользователей StackOverflow, «using SO is like doing lookups with a hashtable instead of a linked list». Мы снова обращаемся к этому замечательному ресурсу, на котором попадаются чрезвычайно подробные и понятные ответы на самые различные вопросы.

В этот раз мы обсудим, что такое метаклассы, как, где и зачем их использовать, а также почему обычно этого делать не стоит :-)

Классы как объекты

Перед тем, как изучать метаклассы, надо хорошо разобраться с классами, а классы в Питоне — вещь весьма специфическая (основаны на идеях из языка Smalltalk).

В большинстве языков класс это просто кусок кода, описывающий, как создать объект. В целом это верно и для Питона:

  >>> class ObjectCreator(object):
  ...       pass
  ... 

  >>> my_object = ObjectCreator()
  >>> print my_object
  <__main__.ObjectCreator object at 0x8974f2c>

Но в Питоне класс это нечто большее — классы также являются объектами.

Как только используется ключевое слово class, Питон исполняет команду и создаёт объект. Инструкция

  >>> class ObjectCreator(object):
  ...       pass
  ...

создаст в памяти объект с именем ObjectCreator.

Этот объект (класс) сам может создавать объекты (экземпляры), поэтому он и является классом.

Тем не менее, это объект, а потому:

  • его можно присвоить переменной,
  • его можно скопировать,
  • можно добавить к нему атрибут,
  • его можно передать функции в качестве аргумента,

Динамическое создание классов

Так как классы являются объектами, их можно создавать на ходу, как и любой объект.

Например, можно создать класс в функции, используя ключевое слово class:

  >>> def choose_class(name):
  ...     if name == 'foo':
  ...         class Foo(object):
  ...             pass
  ...         return Foo # возвращает класс, а не экземпляр
  ...     else:
  ...         class Bar(object):
  ...             pass
  ...         return Bar
  ...     
  >>> MyClass = choose_class('foo') 
  >>> print MyClass # функция возвращает класс, а не экземпляр
  <class '__main__.Foo'>
  >>> print MyClass() # можно создать экземпляр этого класса
  <__main__.Foo object at 0x89c6d4c>

Однако это не очень-то динамично, поскольку по-прежнему нужно самому писать весь класс целиком.

Поскольку классы являются обхектами, они должны генерироваться чем-нибудь.

Когда используется ключевое слово class, Питон создаёт этот объект автоматически. Но как и большинство вещей в Питоне, есть способ сделать это вручную.

Помните функцию type? Старая-добрая функция, которая позволяет определитьтип объекта:

>>> print type(1)
<type 'int'>
>>> print type("1")
<type 'str'>
>>> print type(ObjectCreator)
<type 'type'>
>>> print type(ObjectCreator())
<class '__main__.ObjectCreator'>

На самом деле, у функции type есть совершенно иное применение: она также может создавать классы на ходу. type принимает на вход описание класса и созвращает класс.

(Я знаю, это по-дурацки, что одна и та же функция может использоваться для двух совершенно разных вещей в зависимости от передаваемых аргументов. Так сделано для обратной совместимости)

type работает следующим образом:

  type(<имя класса>, 
       <кортеж родительских классов>, # для наследования, может быть пустым
       <словарь, содержащий атрибуты и их значения>)

Например,

>>> class MyShinyClass(object):
...       pass

может быть создан вручную следующим образом:

  >>> MyShinyClass = type('MyShinyClass', (), {}) # возвращает объект-класс
  >>> print MyShinyClass
  <class '__main__.MyShinyClass'>
  >>> print MyShinyClass() # создаёт экземпляр класса
  <__main__.MyShinyClass object at 0x8997cec>

Возможно, вы заметили, что мы используем «MyShinyClass» и как имя класса, и как имя для переменной, содержащей ссылку на класс. Они могут быть различны, но зачем усложнять?

type принимает словарь, определяющий атрибуты класса:

>>> class Foo(object):
...       bar = True

можно переписать как

  >>> Foo = type('Foo', (), {'bar':True})

и использовать как обычный класс

  >>> print Foo
  <class '__main__.Foo'>
  >>> print Foo.bar
  True
  >>> f = Foo()
  >>> print f
  <__main__.Foo object at 0x8a9b84c>
  >>> print f.bar
  True

Конечно, можно от него наследовать:

  >>>   class FooChild(Foo):
  ...         pass

превратится в

  >>> FooChild = type('FooChild', (Foo,), {})
  >>> print FooChild
  <class '__main__.FooChild'>
  >>> print FooChild.bar # bar is inherited from Foo
  True

В какой-то момент вам захочется добавить методов вашему классу. Для этого просто определите функцию с нужной сигнатурой и присвоёте её в качестве атрибута:

>>> def echo_bar(self):
...       print self.bar
... 
>>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})
>>> hasattr(Foo, 'echo_bar')
>>> hasattr(FooChild, 'echo_bar')
True
>>> my_foo = FooChild()
>>> my_foo.echo_bar()
True

Уже понятно, к чему я клоню: в Питоне классы являются объектами и можно создавать классы на ходу.

Это именно то, что Питон делает, когда используется ключевое слово class, и делает он это с помощью метаклассов.

Что такое метакласс (наконец)

Метакласс это «штука», которая создаёт классы.

Мы создаём класс для того, чтобы создавать объекты, так? А классы являются объектами. Метакласс это то, что создаёт эти самые объекты. Они являются классами классов, можно представить это себе следующим образом:

  MyClass = MetaClass()
  MyObject = MyClass()

Мы уже видели, что type позволяет делать что-то в таком духе:

  MyClass = type('MyClass', (), {})

Это потому что функция type на самом деле является метаклассом. type это метакласс, который Питон внутренне использует для создания всех классов.

Естественный вопрос: с чего это он его имя пишется в нижнем регистре, а не Type?

Я полагаю, это просто для соответствия str, классу для создания объектов-строк, и int, классу для создания объектов-целых чисел. type это просто класс для создания объектов-классов.

Это легко проверить с помощью атрибута __class__:

В питоне всё (вообще всё!) является объектами. В том числе числа, строки, функции и классы — они все являются объектами и все были созданы из класса:

  >>> age = 35
  >>> age.__class__
  <type 'int'>
  >>> name = 'bob'
  >>> name.__class__
  <type 'str'>
  >>> def foo(): pass
  >>> foo.__class__
  <type 'function'>
  >>> class Bar(object): pass
  >>> b = Bar()
  >>> b.__class__
  <class '__main__.Bar'>

А какой же __class__ у каждого __class__?

  >>> a.__class__.__class__
  <type 'type'>
  >>> age.__class__.__class__
  <type 'type'>
  >>> foo.__class__.__class__
  <type 'type'>
  >>> b.__class__.__class__
  <type 'type'>

Итак, метакласс это просто штука, создающая объекты-классы.

Если хотите, можно называть его «фабрикой классов»

type это встроенный метакласс, который использует Питон, но вы, конечно, можете создать свой.

Атрибут __metaclass__

При написании класса можно добавить атрибут __metaclass__:

class Foo(object):
  __metaclass__ = something...
  [...]

В таком случае Питон будет использовать указанный метакласс при создании класса Foo.

Осторожно, тут есть тонкость!

Хоть вы и пишете class Foo(object), объект-класс пока ещё не создаётся в памяти.

Питон будет искать __metaclass__ в определении класса. Если он его найдёт, то использует для создания класса Foo. Если же нет, то будет использовать type.

То есть когда вы пишете

class Foo(Bar):
  pass

Питон делает следующее:

Есть ли у класса Foo атрибут __metaclass__?

Если да, создаёт в памяти объект-класс с именем Foo, используя то, что указано в __metaclass__.

Если Питон не находит __metaclass__, он ищет __metaclass__ в родительском классе Bar и попробует сделать то же самое.

Если же __metaclass__ не находится ни в одном из родителей, Питон будет искать __metaclass__ на уровне модуля.

И если он не может найти вообще ни одного __metaclass__, он использует type для создания объекта-класса.

Теперь важный вопрос: что можно положить в __metaclass__?

Ответ: что-нибудь, что может создавать классы.

А что создаёт классы? type или любой его подкласс, а также всё, что использует их.

Пользовательские метаклассы

Основная цель метаклассов — автоматически изменять класс в момент создания.

Обычно это делает для API, когда хочется создавать классы в соответсвии с текущим контекстом.

Представим глупый пример: вы решили, что у всех классов в вашем модуле имена атрибутов должны быть записать в верхнем регистре. Есть несколько способов это сделать, но один из них — задать __metaclass__ на уровне модуля.

В таком случае все классы этого модуля будут создаваться с использованием указанного меакласса, а нам остаётся только заставить метакласса переводить имена всех атрибутов в верхний регистр.

К счастью, __metaclass__ может быть любым вызываемым объектом, не обязательно формальным классом (я знаю, что-то со словом «класс» в названии не обязано быть классом, что за ерунда? Однако это полезно).

Так что мы начнём с простого примера, используя функцию.

# метаклассу автоматически придёт на вход те же аргументы,
# которые обычно используются в `type`
def upper_attr(future_class_name, future_class_parents, future_class_attr):
  """
    Возвращает объект-класс, имена атрибутов которого
    переведены в верхний регистр
  """

  # берём любой атрибут, не начинающийся с '__'
  attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
  # переводим их в верхний регистр
  uppercase_attr = dict((name.upper(), value) for name, value in attrs)

  # создаём класс с помощью `type`
  return type(future_class_name, future_class_parents, uppercase_attr)

__metaclass__ = upper_attr # это сработает для всех классов в модуле

class Foo(object): 
  # или можно определить __metaclass__ здесь, чтобы сработало только для этого класса
  bar = 'bip'

print hasattr(Foo, 'bar')
# Out: False
print hasattr(Foo, 'BAR')
# Out: True

f = Foo()
print f.BAR
# Out: 'bip'

А теперь то же самое, только используя настояший класс:

# помним, что `type` это на само деле класс, как `str` и `int`,
# так что от него можно наследовать
class UpperAttrMetaclass(type): 
    # Метод __new__ вызывается перед __init__
    # Этот метод создаёт обхект и возвращает его,
    # в то время как __init__ просто инициализирует объект, переданный в качестве аргумента.
    # Обычно вы не используете __new__, если только не хотите проконтролировать,
    # как объект создаётся
    # В данном случае созданный объект это класс, и мы хотим его настроить,
    # поэтому мы перегружаем __new__.
    # Модно также сделать что-нибудь в __init__, если хочется.
    # В некоторых более продвинутых случаях также перегружается __call__,
    # но этого мы сейчас не увидим.
    def __new__(upperattr_metaclass, future_class_name, 
                future_class_parents, future_class_attr):

        attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)

        return type(future_class_name, future_class_parents, uppercase_attr)

Но это не совсем ООП. Мы напрямую вызываем type и не перегружаем вызов __new__ родителя. Давайте сделаем это:

class UpperAttrMetaclass(type): 

    def __new__(upperattr_metaclass, future_class_name, 
                future_class_parents, future_class_attr):

        attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)

        # используем метод type.__new__
        # базовое ООП, никакой магии
        return type.__new__(upperattr_metaclass, future_class_name, 
                            future_class_parents, uppercase_attr)

Вы, возможно, заметили дополнительный аргумент upperattr_metaclass. Ничего особого в нём нет: метод всегда получает первым аргументом текущий экземпляр. Точно так же, как вы используете self в обычным методах.

Конечно, имена, которые я тут использовал, такие длинные для ясности, но как и self, есть соглашение об именовании всех этих аргументов. Так что реальный метакласс выгляит как-нибудь так:

class UpperAttrMetaclass(type): 

    def __new__(cls, name, bases, dct):

        attrs = ((name, value) for name, value in dct.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)

        return type.__new__(cls, name, bases, uppercase_attr)

Можно сделать даже лучше, использовав super, который вызовет наследование (поскольку, конечно, можно создать метакласс, унаследованный от метакласса, унаследованного от type):

class UpperAttrMetaclass(type): 

    def __new__(cls, name, bases, dct):

        attrs = ((name, value) for name, value in dct.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)

        return super(UpperAttrMetaclass, cls).__new__(cls, name, bases, uppercase_attr)

Вот и всё. О метаклассах больше ничего и не сказать.

Причина сложности кода, использующего метаклассы, не в самих метаклассах. Она в том, что обычно метаклассы используются для всяких изощрённых вещей, основанных на интроспекции, манипуляцией наследованием, переменными вроде __dict__ и тому подобном.

Действительно, метаклассы особенно полезны для всякой «чёрной магии», а, следовательно, сложных штук. Но сами по себе они просты:

  • перехватить создание класса
  • изменить класс
  • вернуть модифицированный

Зачем использовать метаклассы вместо функций?

Поскольку __metaclass__ принимает любой вызываемый объект, с чего бы вдруг использовать класс, если это очевидно сложнее?

Тому есть несколько причин:

  • Назначение яснее. Когда вы видите UpperAttrMetaclass(type), вы сразу знаете, что дальше будет.
  • Можно использовать ООП. Метаклассы могту наследоваться от метаклассов, перегружая родитальские методы.
  • Лучше структурированный код. Вы не будете использовать метаклассы для таких простых вещей, как в примере выше. Обычно это что-то сложное. Возможность создать несколько методов и сгруппировать их в одном классе очень полезна, чтобы сделать код более удобным для чтения.
  • Можно использовать __new__, __init__ и __call__. Конечно, обычно можно всё сделать в __new__, но некоторым комфортнее использовать __init__
  • Они называются метаклассами, чёрт возьми! Это должно что-то значить!

Зачем вообще использовать метаклассы?

Наконец, главный вопрос. С чего кому-то использовать какую-то непонятную (и способствующую ошибкам) фичу?

Ну, обычно и не надо использовать:

Метаклассы это глубокая магия, о которой 99% пользователей даже не нужно задумываться. Если вы думаете, нужно ли вам их использовать — вам не нужно (люди, которым они реально нужны, точно знают, зачем они им, и не нуждаются в объяснениях, почему).
~ Гуру Питона Тим Питерс

Основное применение метаклассов это создание API. Типичный пример — Django ORM.

Она позволяет написать что-то в таком духе:

  class Person(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()

Однако если вы выполните следующий код:

  guy = Person(name='bob', age='35')
  print guy.age

вы получите не IntegerField, а int, причём значение может быть получено прямо из базы данных.

Это возможно, потому что models.Model определяет __metaclass__, который сотворит некую магию и превратит класс Person, который мы только что определили простым выражением в сложную привязку к базе данных.

Django делает что-то сложное выглядящим простым, выставляя наружу простое API и используя метаклассы, воссоздающие код из API и незаметно делающие всю работу.

Напоследок

ВО-первых, вы узнали, что классы это объекты, которые могут создавать экземпляры.

На самом деле, классы это тоже экземпляры. Экземпляры метаклассов.

  >>> class Foo(object): pass
  >>> id(Foo)
  142630324

Всё что угодно является объектом в Питоне: экземпляром класса или экземпляром метакласса.

Кроме type.

type является собственным метаклассом. Это нельзя воспроизвести на чистом Питоне и делается небольшим читерством на уровне реализации.

Во-вторых, метаклассы сложны. Вам не нужно использовать их для простого изменения классов. Это можно делать двумя разными способами:

  • руками
  • декораторы классов

В 99% случаев, когда вам нужно изменить класс, лучше использовать эти два.

Но в 99% случаев вам вообще не нужно изменять классы :-)

Автор: qrazydraqon

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js