Как сказал один из пользователей StackOverflow, «using SO is like doing lookups with a hashtable instead of a linked list». Мы снова обращаемся к этому замечательному ресурсу, на котором попадаются чрезвычайно подробные и понятные ответы на самые различные вопросы.
В этот раз мы обсудим, что такое метаклассы, как, где и зачем их использовать, а также почему обычно этого делать не стоит.
Перед тем, как изучать метаклассы, надо хорошо разобраться с классами, а классы в Питоне — вещь весьма специфическая (основаны на идеях из языка Smalltalk).
В большинстве языков класс это просто кусок кода, описывающий, как создать объект. В целом это верно и для Питона:
Но в Питоне класс это нечто большее — классы также являются объектами.
Как только используется ключевое слово
создаст в памяти объект с именем
Этот объект (класс) сам может создавать объекты (экземпляры), поэтому он и является классом.
Тем не менее, это объект, а потому:
Так как классы являются объектами, их можно создавать на ходу, как и любой объект.
Например, можно создать класс в функции, используя ключевое слово
Однако это не очень-то динамично, поскольку по-прежнему нужно самому писать весь класс целиком.
Поскольку классы являются объектами, они должны генерироваться чем-нибудь.
Когда используется ключевое слово
Помните функцию
На самом деле, у функции
(Я знаю, это по-дурацки, что одна и та же функция может использоваться для двух совершенно разных вещей в зависимости от передаваемых аргументов. Так сделано для обратной совместимости)
Например,
может быть создан вручную следующим образом:
Возможно, вы заметили, что мы используем «MyShinyClass» и как имя класса, и как имя для переменной, содержащей ссылку на класс. Они могут быть различны, но зачем усложнять?
можно переписать как
и использовать как обычный класс
Конечно, можно от него наследовать:
превратится в
В какой-то момент вам захочется добавить методов вашему классу. Для этого просто определите функцию с нужной сигнатурой и присвойте её в качестве атрибута:
Уже понятно, к чему я клоню: в Питоне классы являются объектами и можно создавать классы на ходу.
Это именно то, что Питон делает, когда используется ключевое слово
Метакласс это «штука», которая создаёт классы.
Мы создаём класс для того, чтобы создавать объекты, так? А классы являются объектами. Метакласс это то, что создаёт эти самые объекты. Они являются классами классов, можно представить это себе следующим образом:
Мы уже видели, что
Это потому что функция
Естественный вопрос: с чего это он его имя пишется в нижнем регистре, а не
Я полагаю, это просто для соответствия
Это легко проверить с помощью атрибута
В питоне всё (вообще всё!) является объектами. В том числе числа, строки, функции и классы — они все являются объектами и все были созданы из класса:
А какой же
Итак, метакласс это просто штука, создающая объекты-классы.
Если хотите, можно называть его «фабрикой классов»
Атрибут
При написании класса можно добавить атрибут
В таком случае Питон будет использовать указанный метакласс при создании класса
Осторожно, тут есть тонкость!
Хоть вы и пишете
Питон будет искать
То есть когда вы пишете
Питон делает следующее:
Есть ли у класса
Если да, создаёт в памяти объект-класс с именем
Если Питон не находит
Если же
И если он не может найти вообще ни одного
Теперь важный вопрос: что можно положить в
Ответ: что-нибудь, что может создавать классы.
А что создаёт классы?
Основная цель метаклассов — автоматически изменять класс в момент создания.
Обычно это делает для API, когда хочется создавать классы в соответсвии с текущим контекстом.
Представим глупый пример: вы решили, что у всех классов в вашем модуле имена атрибутов должны быть записать в верхнем регистре. Есть несколько способов это сделать, но один из них — задать
В таком случае все классы этого модуля будут создаваться с использованием указанного меакласса, а нам остаётся только заставить метакласса переводить имена всех атрибутов в верхний регистр.
К счастью,
Так что мы начнём с простого примера, используя функцию.
А теперь то же самое, только используя настояший класс:
Но это не совсем ООП. Мы напрямую вызываем
Вы, возможно, заметили дополнительный аргумент
Конечно, имена, которые я тут использовал, такие длинные для ясности, но как и
Можно сделать даже лучше, использовав
Вот и всё. О метаклассах больше ничего и не сказать.
Причина сложности кода, использующего метаклассы, не в самих метаклассах. Она в том, что обычно метаклассы используются для всяких изощрённых вещей, основанных на интроспекции, манипуляцией наследованием, переменными вроде
Действительно, метаклассы особенно полезны для всякой «чёрной магии», а, следовательно, сложных штук. Но сами по себе они просты:
Поскольку
Тому есть несколько причин:
Наконец, главный вопрос. С чего кому-то использовать какую-то непонятную (и способствующую ошибкам) фичу?
Ну, обычно и не надо использовать:
Основное применение метаклассов это создание API. Типичный пример — Django ORM.
Она позволяет написать что-то в таком духе:
Однако если вы выполните следующий код:
вы получите не
Это возможно, потому что
Django делает что-то сложное выглядящим простым, выставляя наружу простое API и используя метаклассы, воссоздающие код из API и незаметно делающие всю работу.
ВО-первых, вы узнали, что классы это объекты, которые могут создавать экземпляры.
На самом деле, классы это тоже экземпляры. Экземпляры метаклассов.
Всё что угодно является объектом в Питоне: экземпляром класса или экземпляром метакласса.
Кроме
Во-вторых, метаклассы сложны. Вам не нужно использовать их для простого изменения классов. Это можно делать двумя разными способами:
В 99% случаев, когда вам нужно изменить класс, лучше использовать эти два.
Но в 99% случаев вам вообще не нужно изменять классы :-)
В этот раз мы обсудим, что такое метаклассы, как, где и зачем их использовать, а также почему обычно этого делать не стоит.
Классы как объекты
Перед тем, как изучать метаклассы, надо хорошо разобраться с классами, а классы в Питоне — вещь весьма специфическая (основаны на идеях из языка 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% случаев вам вообще не нужно изменять классы :-)