Как сказал один из пользователей 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