В этот раз мы обсудим, что такое метаклассы, как, где и зачем их использовать, а также почему обычно этого делать не стоит.
Классы как объекты
Перед тем, как изучать метаклассы, надо хорошо разобраться с классами, а классы в Питоне — вещь весьма специфическая (основаны на идеях из языка 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% случаев вам вообще не нужно изменять классы :-)

