Метаклассы в 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
Уже понятно, к чему я клоню: в Питоне классы являются объектами и можно создавать классы на ходу.
Это именно то, что Питон делает, когда используется ключевое слово
, и делает он это с помощью метаклассов.
Метакласс это «штука», которая создаёт классы.
Мы создаём класс для того, чтобы создавать объекты, так? А классы являются объектами. Метакласс это то, что создаёт эти самые объекты. Они являются классами классов, можно представить это себе следующим образом:
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__
:
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% случаев вам вообще не нужно изменять классы 🙂
Метаклассы в Python / Хабр
Привет, Хабр! У нас продолжается распродажа в честь черной пятницы. Там вы найдете много занимательных книг.
Возможен вопрос: а что такое метакласс? Если коротко, метакласс относится к классу точно как класс к объекту.
Метаклассы – не самый популярный аспект языка Python; не сказать, что о них воспоминают в каждой беседе. Тем не менее, они используется в весьма многих статусных проектах: в частности, Django ORM[2], стандартная библиотека абстрактных базовых классов (ABC)[3] и реализации Protocol Buffers [4].
Это сложная фича, позволяющая программисту приспособить под задачу некоторые самые базовые механизмы языка. Именно по причине такой гибкости открываются и возможности для злоупотреблений – но нас это уже не удивляет. С большими возможностями приходит большая ответственность.
Данная тема обычно не затрагивается в различных руководствах и вводных материалах по языку, поскольку считается «продвинутой» — но и с ней надо с чего-то начинать. Я немного поискал в онлайне и в качестве наилучшего введения в тему нашел соответствующий вопрос на StackOverflow и ответы на него [1].
Поехали. Все примеры кода приведены на Python 3.6 – на момент написания статьи это новейшая версия.
Первый контакт
Мы уже кое-что успели обсудить, но пока еще не видели, что представляет собой метакласс. Скоро разберемся с этим, но пока следите за моим рассказом. Начнем с чего-нибудь простого: создадим объект.
>>> o = object() >>> print(type(o)) <class 'object'>
Мы создали новый object
и сохранили ссылку на него в переменной o
.
Тип o
– это object
.
Мы также можем объявить и наш собственный класс:
>>> class A: ... pass ... >>> a = A() >>> print(type(a)) <class '__main__.A'>
Теперь у нас две плохо названные переменные a
и o
, и мы можем проверить, в самом ли деле они относятся к соответствующим классам:
>>> isinstance(o, object) True >>> isinstance(a, A) True >>> isinstance(a, object) True >>> issubclass(A, object) True
Выше заметна одна интересная вещь: объект a
также относится к типу object
. Ситуация такова, поскольку класс A
является подклассом object
(все классы, определяемые пользователем, наследуют от object
).
Еще одна интересная вещь – во многих контекстах мы можем взаимозаменяемо применять переменные a
и A
. Для таких функций как print
невелика разница, какую переменную мы ей выдадим, a
или A
– оба вызова «что-то» выведут на экран.
Давайте поподробнее рассмотрим класс B
, который мы только что определили:
>>> class B: ... def __call__(self): ... return 5 ... >>> b = B() >>> print(b) <__main__.B object at 0x1032a5a58> >>> print(B) <class '__main__.B'> >>> b.value = 6 >>> print(b.value) 6 >>> B.value = 7 >>> print(B.value) 7 >>> print(b()) 5 >>> print(B()) <__main__.B object at 0x1032a58d0>
Как видим, b
и B
во многих отношениях действуют похоже. Можно даже сделать выражение с вызовом функции, в котором использовались бы обе переменные, просто возвращены в данном случае будут разные вещи: b
возвращает 5, как и указано в определении класса, тогда как B
создает новый экземпляр класса.
Это сходство – не случайность, а намеренно спроектированная черта языка. В Python классы являются сущностями первой категории[5] (ведут себя как все нормальные объекты).
Более того, если классы – как объекты, то у них обязательно должен быть собственный тип:
>>> print(type(object)) <class 'type'> >>> print(type(A)) <class 'type'> >>> isinstance(object, type) True >>> isinstance(A, type) True >>> isinstance(A, object) True >>> issubclass(type, object) True
Оказывается, что и object
, и A
относятся к классу type
– type
это «метакласс, задаваемый по умолчанию «. Все остальные метаклассы должны наследовать от него. Возможно, на данном этапе вас уже немного путает, что класс имеет имя type
, но в то же время это и функция, возвращающая тип сообщаемого объекта (семантика у type
будет совершенно разной в зависимости от того, сколько аргументов вы ему сообщите – 1 или 3). В таком виде его сохраняют по историческим причинам.
Как object
, так и A
также являются экземплярами object
– в конечном итоге, все они объекты. Каков же в таком случае тип type
, могли бы вы спросить?
>>> print(type(type)) <class 'type'> >>> isinstance(type, type) True
Оказывается, никакого двойного дна здесь нет, поскольку type
относится к собственному типу.
Весь фокус, заключающийся в метаклассах: мы создали A
, подкласс object
, так, чтобы новый экземпляр a
относился к типу A
и, следовательно, object
. Таким же образом можно создать подкласс от type
под названием Meta
. Впоследствии мы можем использовать его как тип для новых классов; они будут экземплярами обоих типов: type
и Meta
.
Рассмотрим это на практике:
class Meta(type): def __init__(cls, name, bases, namespace): super(Meta, cls).__init__(name, bases, namespace) print("Creating new class: {}".format(cls)) def __call__(cls): new_instance = super(Meta, cls). __call__() print("Class {} new instance: {}".format(cls, new_instance)) return new_instance
Это наш первый метакласс. Мы могли бы сделать его определение еще более минималистичным, но хотели сделать, чтобы в итоге он делал хотя бы что-нибудь полезное.
Он переопределяет магический метод
__init__
, чтобы на экран выводилось сообщение всякий раз, когда создается новый экземплярMeta
.Он переопределяет магический метод
call
, чтобы выводилось сообщение · всякий раз, когда пользователь применяет синтаксис вызова функций к экземпляру – пишетvariable()
.
Оказывается, что в Python создание экземпляра класса имеет ту же форму, что и вызов функции. Если у нас есть функция f
, то, чтобы вызвать ее, мы пишем f()
. Если у нас есть класс A
, то мы пишем A()
для создания нового экземпляра. Соответственно, мы используем хук __call__
.
Все-таки, метакласс сам по себе не так интересен. Интересное начинается, лишь когда мы создаем экземпляр метакласса. Давайте это и сделаем:
>>> class C(metaclass=Meta): ... pass ... Creating new class: <class '__main__.C'> >>> c = C() Class <class '__main__.C'> new instance: <__main__.C object at 0x10e99ae48> >>> print(c) <__main__.C object at 0x10e99ae48>
Действительно, наш метакласс работает как задумано: выводит сообщения, когда в жизненном цикле класса происходят определенные события. В данном случае важно понимать, что мы работаем сразу с тремя разными уровнями абстракции — метаклассом, классом и экземпляром.
Когда мы пишем class C(metaclass=Meta)
, мы создаем C
, представляющий собой экземпляр Meta
— вызывается Meta.init
, и выводится сообщение. На следующем шаге мы вызываем C()
для создания нового экземпляра класса C
, и на этот раз выполняется Meta. __call__
. На последнем шаге мы вывели на экран c
, вызывая C.__str__
, который, в свою очередь, разрешается в заданную по умолчанию реализацию, определенную в базовом классе object
.
Сейчас можем посмотреть все типы наших переменных:
>>> print(type(C)) <class '__main__.Meta'> >>> isinstance(C, Meta) True >>> isinstance(C, type) True >>> issubclass(Meta, type) True >>> print(type(c)) <class '__main__.C'> >>> isinstance(c, C) True >>> isinstance(c, object) True >>> issubclass(C, object) True
Выше я попытался сделать мягкое введение в тему метаклассов и, надеюсь, вы уже представляете, что это такое, и как ими можно пользоваться. Но, на мой взгляд, этот текст ничего бы не стоил без нескольких практических примеров. К ним и перейдем.
Полезный пример: синглтон
В этом разделе мы напишем совсем маленькую библиотеку, в которой будет малость метаклассов. Мы реализуем «эскиз» для паттерна проектирования синглтон [6] – это класс, который может иметь всего один экземпляр.
Честно говоря, его можно было бы реализовать и без всякого использования метаклассов, просто переопределив метод __new__
в базовом классе, так, чтобы он вернул ранее запомненный экземпляр:
class SingletonBase: instance = None def __new__(cls, *args, **kwargs): if cls.instance is None: cls.instance = super().__new__(cls, *args, **kwargs) return cls.instance
Вот и все. Любой подкласс, наследующий от SingletonBase
, теперь проявляет поведение синглтона.
Рассмотрим, каков он в действии:
>>> class A(SingletonBase): ... pass ... >>> class B(A): ... pass ... >>> print(A()) <__main__.A object at 0x10c8d8710> >>> print(A()) <__main__.A object at 0x10c8d8710> >>> print(B()) <__main__.A object at 0x10c8d8710>
Тот подход, который мы здесь используем, вроде бы работает – при каждой попытке создать экземпляр возвращается тот же самый объект. Но есть и такое поведение, которое может показаться нам неожиданным: при попытке создать экземпляр класса B
мы получаем в ответ тот же самый экземпляр A
, что и раньше.
Эту проблему можно решить, и не прибегая никоим образом к метаклассам, но решение с ними просто очевидное – так почему бы ими не воспользоваться?
У нас будет такой класс SingletonBaseMeta
, чтобы каждый его подкласс при создании инициализировал поле instance
со значением None
.
Вот что получается:
class SingletonMeta(type): def __init__(cls, name, bases, namespace): super().__init__(name, bases, namespace) cls.instance = None def __call__(cls, *args, **kwargs): if cls.instance is None: cls.instance = super().__call__(*args, **kwargs) return cls.instance class SingletonBaseMeta(metaclass=SingletonMeta): pass
Можем попробовать, а работает ли этот подход:
>>> class A(SingletonBaseMeta): . .. pass ... >>> class B(A): ... pass ... >>> print(A()) <__main__.A object at 0x1101f6358> >>> print(A()) <__main__.A object at 0x1101f6358> >>> print(B()) <__main__.B object at 0x1101f6eb8>
Поздравляем, по-видимому наша библиотека-синглтон работает именно так, как и планировалось!
На правах опытных проектировщиков библиотеки с метаклассами, давайте замахнемся на что-нибудь посложнее.
Полезный пример: упрощенное ORM
Как упоминалось выше, с паттерном синглтон можно красиво разобраться, слегка воспользовавшись метаклассами, но острой необходимости в них нет. Большинство реальных проектов, в которых метаклассы действительно используются – это те или иные вариации на тему ORM[7].
В качестве упражнения построим подобный пример, но сильно упрощенный. Это будет уровень сериализации/десериализации между классами Python и JSON.
Вот как должен выглядеть интерфейс, который мы хотим получить (смоделирован на Django ORM/SQLAlchemy):
class User(ORMBase): """ Пользователь в нашей системе """ id = IntField(initial_value=0, maximum_value=2**32) name = StringField(maximum_length=200) surname = StringField(maximum_length=200) height = IntField(maximum_value=300) year_born = IntField(maximum_value=2017)
Мы хотим иметь возможность определять классы и их поля вместе с типами. Для этого нам пригодилась бы возможность сериализовать наш класс в JSON:
>>> u = User() >>> u.name = "Guido" >>> u.surname = "van Rossum" >>> print("User ID={}".format(u.id)) User ID=0 >>> print("User JSON={}".format(u.to_json())) User JSON={"id": 0, "name": "Guido", "surname": "van Rossum", "height": null, "year_born": null}
И десериализовать его:
>>> w = User('{"id": 5, "name": "John", "surname": "Smith", "height": 185, "year_born": 1989}') >>> print("User ID={}".format(w.id)) User ID=5 >>> print("User NAME={}".format(w.name)) User NAME=John
Для всего вышеприведенного нам не так уж и нужны метаклассы, так что давайте реализуем одну «изюминку» — добавим валидацию.
>>> w.name = 5 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "simple-orm.py", line 96, in __setattr__ raise AttributeError('Invalid value "{}" for field "{}"'. format(value, key)) AttributeError: Invalid value "5" for field "name" >>> w.middle_name = "Stephen" Traceback (most recent call last): File "<stdin>", line 1, in <module> File "simple-orm.py", line 98, in __setattr__ raise AttributeError('Unknown field "{}"'.format(key)) AttributeError: Unknown field "middle_name" >>> w.year_born = 3000 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "simple-orm.py", line 96, in __setattr__ raise AttributeError('Invalid value "{}" for field "{}"'.format(value, key)) AttributeError: Invalid value "3000" for field "year_born"
Напоминание о конструкторе типов
Прежде чем перейти к реализации ORM-библиотеки, я должен напомнить еще об одной вещи, конструкторе типов type
. Я упоминал его лишь вскользь, но это важная тема, которую требуется развернуть.
Вспомните эпизод из предыдущего раздела, когда мы определяли метод __init__
для нашего первого метакласса:
class Meta(type): def __init__(cls, name, bases, namespace):
Откуда же взялись эти три аргумента name
, bases
и namespace
? Это параметры конструктора типов. Три этих значения полностью описывают класс, создаваемый в данный момент.
name – просто имя класса в формате строки
bases – кортеж базовых классов, может быть пустым
namespace – словарь всех полей, определенных внутри класса. Сюда идут все методы и переменные класса.
Вот и все, что здесь есть. На самом деле, можно было бы и не определять класс при помощи общего синтаксиса, а вызвать конструктор type
напрямую:
class A: X = 5 def f(self): print("Class A {}".format(self)) def f(self): print("Class B {}".format(self)) B = type("B", (), {'X': 6, 'f': f})
В этом коде мы определили два почти идентичных класса, A
и B
.
У них отличаются значения, присвоенные переменной класса X
, и выводятся на экран разные значения при вызове метода f
. Но на этом все – фундаментальных отличий нет, и оба принципа определения классов эквивалентны. Фактически, интерпретатор Python преобразует первый из описанных здесь механизмов во второй.
>>> print(A) <class '__main__.A'> >>> print(B) <class '__main__.B'> >>> print(A.X) 5 >>> print(B.X) 6 >>> a = A() >>> b = B() >>> a.f() Class A <__main__.A object at 0x1023432b0> >>> b.f() Class B <__main__.B object at 0x1023431d0>
Именно на этом этапе определение собственного метакласса позволяет вам влиять на события. Можно перехватывать параметры, передаваемые конструктору type
, изменять их и создавать собственный класс таким образом, как вам угодно.
Упрощенное ORM – грамотная программа
Мы уже знаем, чего хотим – написать библиотеку, удовлетворяющую требованиям указанного интерфейса. Мы также знаем, что будем решать эту задачу при помощи метаклассов.
Далее я приведу реализацию в стиле грамотного программирования. Код из этого раздела можно загрузить в интерпретатор Python и запустить.
Мы будем использовать всего один пакет – для синтаксического разбора/сериализации JSON:
import json
Далее определим базовый класс для всех наших полей. Он устроен весьма просто, как и большинство других отдельных частей данной библиотеки. В нем есть реализация-заглушка для валидационной функции и пустое начальное значение.
class Field: """ Базовый класс для всех полей. Каждому полю должно быть присвоено начальное значение """ def __init__(self, initial_value=None): self.initial_value = initial_value def validate(self, value): """ Проверить, является ли это значение допустимым для данного поля """ return True
Для простоты я реализую всего два подкласса Field
: IntField
и StringField
. При необходимости можно добавить и другие.
class StringField(Field): """ Строковое поле. Опционально в нем можно проверять длину строки """ def __init__(self, initial_value=None, maximum_length=None): super(). __init__(initial_value) self.maximum_length = maximum_length def validate(self, value): """ Проверить, является ли это значение допустимым для данного поля """ if super().validate(value): return (value is None) or (isinstance(value, str) and self._validate_length(value)) else: return False def _validate_length(self, value): """ Проверить, имеет ли строка верную длину """ return (self.maximum_length is None) or (len(value) <= self.maximum_length) class IntField(Field): """ Целочисленное поле. Опционально можно проверять, является ли записанное в нем число целым""" def __init__(self, initial_value=None, maximum_value=None): super().__init__(initial_value) self.maximum_value = maximum_value def validate(self, value): """ Проверить, является ли это значение допустимым для данного поля """ if super().validate(value): return (value is None) or (isinstance(value, int) and self. _validate_value(value)) else: return False def _validate_value(self, value): """ Проверить, относится ли целое число к желаемому дмапазону """ return (self.maximum_value is None) or (value <= self.maximum_value)
Если не считать перенаправления initial_value
конструктору базового класса, этот код состоит в основном из процедур валидации. Опять же, не сложно добавить в него другие подобные акты валидации, но я хотел показать вам простейшую возможную модель в качестве доказательства концепции.
В StringField
мы хотим проверить, относится ли значение к правильному типу – str
, и является ли длина строки меньшей или равной максимальному значению (если такое значение определено). В поле IntField
мы проверяем, является ли значение целым числом, и является ли оно меньшим или равным, чем сообщенное максимальное значение.
Важно отметить: мы допускаем, чтобы значения в полях были равны None
. В качестве интересного упражнения предлагаю читателю реализовать обязательные поля, в которых не допускается значение None
.
Следующий фрагмент кода – это наш метакласс:
class ORMMeta(type): """ Метакласс для нашего собственного ORM """ def __new__(self, name, bases, namespace): fields = { name: field for name, field in namespace.items() if isinstance(field, Field) } new_namespace = namespace.copy() # Удалить поля, относящиеся к переменным класса for name in fields.keys(): del new_namespace[name] new_namespace['_fields'] = fields return super().__new__(self, name, bases, new_namespace)
Наш метакласс совсем не кажется сложным. В нем одна функция, и единственное его назначение – собрать все экземпляры Field
в новую переменную класса, которая называется _fields
. Все экземпляры полей также удаляются из словаря класса.
Единственная вещь, для которой нам нужен наш метакласс – чтобы он подключался в момент, когда создается наш класс, брал все определения полей и сохранял их все в одном месте.
Собственно, большая часть фактической работы выполняется в базовом классе нашей библиотеки:
class ORMBase(metaclass=ORMMeta): """ Пользовательский интерфейс для базового класса """ def __init__(self, json_input=None): for name, field in self._fields.items(): setattr(self, name, field.initial_value) # Если предоставляется JSON, то мы разберем его if json_input is not None: json_value = json.loads(json_input) if not isinstance(json_value, dict): raise RuntimeError("Supplied JSON must be a dictionary") for key, value in json_value.items(): setattr(self, key, value) def __setattr__(self, key, value): """ Установщик магического метода """ if key in self. _fields: if self._fields[key].validate(value): super().__setattr__(key, value) else: raise AttributeError('Invalid value "{}" for field "{}"'.format(value, key)) else: raise AttributeError('Unknown field "{}"'.format(key)) def to_json(self): """ Преобразовать заданный объект в JSON """ new_dictionary = {} for name in self._fields.keys(): new_dictionary[name] = getattr(self, name) return json.dumps(new_dictionary)
У класса ORMBase
три метода, и у каждого из них своя конкретная задача:
__init__
— первым делом, установить все поля в начальные значения. Затем, если в качестве параметра передается документ в формате JSON, разобрать его и присвоить значения, полученные в процессе считывания, полям нашей модели.__setattr__
— Это магический метод, вызываемый всякий раз, когда кто-нибудь пытается присвоить значение атрибуту класса. Когда кто-нибудь записываетobject.attribute = value
, вызывается методobject.__setattr__("attribute", value)
. Переопределив этот метод, мы можем изменить поведение, заданное по умолчанию, в данном случае – при помощи инъекции валидационного кода.to_json
– простейший из всех методов в классе. Просто принимает все значения полей и сериализует их в документ JSON.
Вот и вся реализация – наша библиотека готова. Можете сами убедиться, что она работает как положено, и менять ее, если считаете, что она должна работать иначе.
>>> User('{"name": 5}') Traceback (most recent call last): File "/usr/local/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2881, in run_code exec(code_obj, self.user_global_ns, self.user_ns) File "<ipython-input-1-76a1a93378fc>", line 1, in <module> User('{"name": 5}') File "/Users/jrx/repos/metaclass-playground/simple-orm.py", line 86, in __init__ setattr(self, key, value) File "/Users/jrx/repos/metaclass-playground/simple-orm. py", line 94, in __setattr__ raise AttributeError('Invalid value "{}" for field "{}"'.format(value, key)) AttributeError: Invalid value "5" for field "name"
Заключительные замечания
Весь код к этому посту можно скачать в репозитории на GitHub [8].
Надеюсь, эта статья вам понравилась и подсказала вам какие-то идеи. Метаклассы могут казаться немного непонятными и не всегда полезными. Однако, они определенно позволяют собирать элегантные библиотеки и интерфейсы, если уметь метаклассами пользоваться.
Подробнее о том, как метаклассы используются в реальной жизни, можно почитать в статье [9].
Источники
[1] http://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python
[2] https://docs.djangoproject.com/en/1.10/topics/db/
[3] https://docs.python.org/3/library/abc.html#abc.ABCMeta
[4] https://developers.google.com/protocol-buffers/docs/pythontutorial
[5] https://ru. wikipedia.org/wiki/Объект_первого_класса
[6] https://ru.wikipedia.org/wiki/Одиночка_(шаблон_проектирования)
[7] https://ru.wikipedia.org/wiki/ORM
[8] https://github.com/MillionIntegrals/metaclass-playground
[9] http://eli.thegreenplace.net/2011/08/14/python-metaclasses-by-example
Метаклассы Python – настоящий Python
Термин метапрограммирование относится к возможности программы получать знания о себе или манипулировать ими. Python поддерживает форму метапрограммирования для классов, называемую метаклассами .
Метаклассы — это эзотерическая концепция ООП, скрывающаяся практически за всем кодом Python. Вы используете их, знаете ли вы об этом или нет. По большей части вам не нужно знать об этом. Большинству программистов Python редко, если вообще когда-либо, приходится думать о метаклассах.
Однако, когда возникает необходимость, Python предоставляет возможность, которую поддерживают не все объектно-ориентированные языки: вы можете заглянуть внутрь и определить собственные метаклассы. Использование пользовательских метаклассов несколько спорно, о чем свидетельствует следующая цитата Тима Питерса, гуру Python, автора Zen of Python:
.«Метаклассы — это более глубокая магия, чем 99% пользователей должны беспокоиться. Если вы задаетесь вопросом, нужны ли они вам, значит, они вам не нужны (люди, которым они действительно нужны, точно знают, что они им нужны, и не нуждаются в объяснении, почему)».
— Тим Питерс
Есть сторонники Python (так называют поклонников Python), которые считают, что вы никогда не должны использовать пользовательские метаклассы. Это может зайти слишком далеко, но, вероятно, это правда, что пользовательские метаклассы в большинстве случаев не нужны. Если не совсем очевидно, что проблема требует их, то, вероятно, она будет чище и читабельнее, если будет решена более простым способом.
Тем не менее, понимание метаклассов Python полезно, потому что оно приводит к лучшему пониманию внутреннего устройства классов Python в целом. Вы никогда не знаете: однажды вы можете оказаться в одной из тех ситуаций, когда вы просто знаете, что пользовательский метакласс — это то, что вам нужно.
Классы старого и нового стиля
В области Python класс может быть одним из двух вариантов. Официальная терминология не определена, поэтому они неофициально называются классами старого и нового стиля.
Удалить рекламу
Старые классы
В классах старого стиля класс и тип не совсем одно и то же. Экземпляр класса в старом стиле всегда реализуется из одного встроенного типа с именем instance
. Если obj
является экземпляром класса старого стиля, obj.__class__
обозначает класс, но type(obj)
всегда instance
. Следующий пример взят из Python 2.7:
>>>
>>> класс Foo: ... проходить ... >>> х = Фу() >>> х.__класс__ <класс __main__. Foo по адресу 0x000000000535CC48> >>> тип(х) <тип 'экземпляр'>
Классы нового стиля
Классы нового стиля объединяют понятия класса и типа. Если obj
является экземпляром класса нового стиля, type(obj)
совпадает с obj.__class__
:
>>>
>>> класс Foo: ... проходить >>> объект = Фу() >>> объект__класс__ <класс '__main__.Foo'> >>> тип(объект) <класс '__main__.Foo'> >>> obj.__class__ это тип (obj) Истинный
>>>
>>> n = 5 >>> d = {'х': 1, 'у': 2} >>> класс Foo: ... проходить ... >>> х = Фу() >>> для obj в (n, d, x): ... print(type(obj) is obj.__class__) ... Истинный Истинный Истинный
Тип и класс
В Python 3 все классы являются классами нового стиля. Таким образом, в Python 3 разумно ссылаться на тип объекта и его класс взаимозаменяемо.
Примечание: В Python 2 классы по умолчанию имеют старый стиль. До Python 2.2 классы нового стиля вообще не поддерживались. Начиная с Python 2.2, их можно создавать, но они должны быть явно объявлены как новые.
Помните, что в Python все является объектом. Классы также являются объектами. В результате класс должен иметь тип. Что такое тип класса?
Рассмотрим следующее:
>>>
>>> класс Foo: ... проходить ... >>> х = Фу() >>> тип(х) <класс '__main__.Foo'> >>> тип(фу) <класс 'тип'>
Тип x
— это класс Foo
, как и следовало ожидать. Но тип Foo
, сам класс, это тип
. В общем, тип любого класса нового стиля — тип
.
Тип встроенных классов, с которыми вы знакомы, тоже тип
:
>>>
>>> для t в int, float, dict, list, tuple: ... печать (тип (т)) ... <класс 'тип'> <класс 'тип'> <класс 'тип'> <класс 'тип'> <класс 'тип'>
Если уж на то пошло, тип тип
тоже тип
(да, действительно):
>>>
>>> тип(тип) <класс 'тип'>
тип
— это метакласс, экземплярами которого являются классы. Подобно тому, как обычный объект является экземпляром класса, любой класс нового стиля в Python и, следовательно, любой класс в Python 3 является экземпляром класса.0034 тип метакласс.
В приведенном выше случае:
-
x
является экземпляром классаFoo
. -
Foo
является экземпляром метаклассатипа
. -
тип
также является экземпляром метаклассатипа
, поэтому он является экземпляром самого себя.
Удаление рекламы
Динамическое определение класса
Встроенная функция type()
при передаче одного аргумента возвращает тип объекта. Для классов нового стиля это обычно то же самое, что и 9 объектов.0034 __класс__ атрибут:
>>>
>>> тип(3) <класс 'целое число'> >>> type(['foo', 'bar', 'baz']) <класс 'список'> >>> т = (1, 2, 3, 4, 5) >>> тип(т) <класс 'кортеж'> >>> класс Foo: . .. проходить ... >>> тип(фу()) <класс '__main__.Foo'>
Вы также можете вызвать type()
с тремя аргументами — type(
:
-
<имя>
указывает имя класса. Это становится атрибутом__name__
класса. -
__bases__
класса. -
__dict__
класса.
Звонок type()
таким образом создает новый экземпляр метакласса типа
. Другими словами, он динамически создает новый класс.
В каждом из следующих примеров верхний фрагмент определяет класс динамически с помощью type()
, а фрагмент под ним определяет класс обычным способом с помощью оператора class
. В каждом случае два фрагмента функционально эквивалентны.
Пример 1
В этом первом примере
и
аргументы, переданные в type()
, оба пусты. Наследование от какого-либо родительского класса не указано, и изначально ничего не помещается в словарь пространства имен. Это самое простое определение класса:
>>>
>>> Foo = тип('Foo', (), {}) >>> х = Фу() >>> х Объект <__main__.Foo по адресу 0x04CFAD50>
>>>
>>> класс Foo: ... проходить ... >>> х = Фу() >>> х Объект <__main__.Foo по адресу 0x0370AD50>
Пример 2
Здесь
— это кортеж с одним элементом Foo
, определяющий родительский класс, от которого наследуется Bar
. Атрибут attr
изначально помещается в словарь пространства имен:
>>>
>>> Bar = type('Bar', (Foo,), dict(attr=100)) >>> х = бар() >>> x. attr 100 >>> х.__класс__ <класс '__main__.Bar'> >>> x.__class__.__bases__ (<класс '__main__.Foo'>,)
>>>
>>> класс Bar(Foo): ... атрибут = 100 ... >>> х = бар() >>> x.attr 100 >>> х.__класс__ <класс '__main__.Bar'> >>> x.__class__.__bases__ (<класс '__main__.Foo'>,)
Пример 3
На этот раз
снова пусто. Два объекта помещаются в словарь пространства имен с помощью аргумента
. Первый — это атрибут с именем attr
, а второй — функция с именем 9.0034 attr_val , который становится методом определенного класса:
>>>
>>> Foo = тип( ... 'Фу', ... (), ... { ... 'атрибут': 100, ... 'attr_val': лямбда x : x.attr ... } ... ) >>> х = Фу() >>> x.attr 100 >>> x.attr_val() 100
>>>
>>> класс Foo: ... атрибут = 100 ... def attr_val(self): ... вернуть self.attr ... >>> х = Фу() >>> x.attr 100 >>> x.attr_val() 100
Пример 4
Только очень простые функции могут быть определены с помощью lambda
в Python. В следующем примере несколько более сложная функция определяется извне, а затем назначается attr_val
в словаре пространства имен с помощью имени f
:
>>>
>>> определение f(obj): ... print('attr =', obj.attr) ... >>> Фу = тип( ... 'Фу', ... (), ... { ... 'атрибут': 100, ... 'attr_val': f ... } ... ) >>> х = Фу() >>> x.attr 100 >>> x.attr_val() атрибут = 100
>>>
>>> определение f(obj): ... print('attr =', obj.attr) ... >>> класс Foo: ... атрибут = 100 ... attr_val = f ... >>> х = Фу() >>> x.attr 100 >>> x.attr_val() атрибут = 100
Удалить рекламу
Пользовательские метаклассы
Рассмотрим еще раз этот заезженный пример:
>>>
>>> класс Foo: . .. проходить ... >>> f = Фу()
Выражение Foo()
создает новый экземпляр класса Foo
. Когда интерпретатор встречает Foo()
, происходит следующее:
Вызывается метод
__call__()
родительского классаFoo
. ПосколькуFoo
является стандартным классом нового стиля, его родительский класс является метаклассомтипа
, поэтому вызывается метод__call__()
типаЭтот метод
__call__()
, в свою очередь, вызывает следующее:-
__new__()
-
__init__()
-
Если Foo
не определяет __new__()
и __init__()
, методы по умолчанию наследуются от предков Foo
. Но если Foo
действительно определяет эти методы, они переопределяют методы из предков, что позволяет настроить поведение при создании экземпляра Foo
.
В дальнейшем пользовательский метод с именем new()
определяется и назначается как метод __new__()
для Foo
:
>>>
>>> по умолчанию новый (cls): ... x = объект.__new__(cls) ... х.аттр = 100 ... вернуть х ... >>> Foo.__new__ = новый >>> f = Фу() >>> f.attr 100 >>> г = Фу() >>> g.attr 100
Это изменяет поведение экземпляра класса Foo
: каждый раз, когда создается экземпляр Foo
, по умолчанию он инициализируется атрибутом с именем attr
, который имеет значение 100
. (Подобный код обычно появляется в методе __init__()
, а не в методе __new__()
. Этот пример придуман для демонстрационных целей.)
Теперь, как уже было сказано, классы тоже являются объектами. Предположим, вы хотели аналогичным образом настроить поведение инстанцирования при создании класса вроде Foo
. Если бы вы следовали приведенному выше шаблону, вы бы снова определили пользовательский метод и назначили его в качестве 9. 0034 __new__() метод, экземпляром которого является Foo
класса. Foo
является экземпляром метакласса типа
, поэтому код выглядит примерно так:
>>>
# Спойлер: это не работает! >>> определение нового (cls): ... x = тип.__new__(cls) ... х.аттр = 100 ... вернуть х ... >>> type.__new__ = новый Traceback (последний последний вызов): Файл "", строка 1, в type.__new__ = новый TypeError: невозможно установить атрибуты встроенного/расширенного типа «тип»
За исключением того, что, как видите, вы не можете переназначить метод __new__()
метакласса типа
. Питон не позволяет.
Наверное, это даже к лучшему. тип
— это метакласс, из которого происходят все классы нового стиля. Вы действительно не должны возиться с этим в любом случае. Но тогда какой выход есть, если вы хотите настроить создание экземпляра класса?
Одним из возможных решений является собственный метакласс. По сути, вместо того, чтобы возиться с тип
метакласс, вы можете определить свой собственный метакласс, который является производным от типа
, а затем вместо этого вы можете возиться с ним.
Первым шагом является определение метакласса, производного от типа
, следующим образом:
>>>
>>> класс Мета(тип): ... def __new__(cls, name, bases, dct): ... x = super().__new__(cls, name, bases, dct) ... х.аттр = 100 ... вернуть х ...
Заголовок определения class Meta(type):
указывает, что Meta
происходит от типа
. Поскольку тип
является метаклассом, это делает Meta
также метаклассом.
Обратите внимание, что для Meta
был определен пользовательский метод __new__()
. Невозможно было сделать это напрямую с метаклассом типа
. Метод __new__()
делает следующее:
- Делегаты через
super()
в__new__()
метод родительского метакласса (тип
) для фактического создания нового класса - Присваивает классу пользовательский атрибут
attr
со значением100
- Возвращает только что созданный класс
Теперь другая половина шаманства: Определите новый класс Foo
и укажите, что его метакласс является пользовательским метаклассом Meta
, а не стандартным метаклассом типа
. Это делается с помощью метакласса 9.0035 ключевое слово в определении класса следующим образом:
>>>
>>> класс Foo (метакласс = мета): ... проходить ... >>> Foo.attr 100
Вуаля! Foo
автоматически выбрал атрибут attr
из метакласса Meta
. Конечно, любые другие классы, которые вы определяете аналогичным образом, будут вести себя так же:
>>>
>>> класс Бар (метакласс = мета): ... проходить ... >>> класс Qux(метакласс=мета): ... проходить ... >>> Bar.attr, Qux.attr (100, 100)
Точно так же, как класс функционирует как шаблон для создания объектов, метакласс функционирует как шаблон для создания классов. Метаклассы иногда называют фабриками классов.
Сравните следующие два примера:
Фабрика объектов:
>>>
>>> класс Foo: . .. защита __init__(я): ... self.attr = 100 ... >>> х = Фу() >>> x.attr 100 >>> у = Фу() >>> y.attr 100 >>> г = Фу() >>> z.attr 100
Класс Фабрика:
>>>
>>> класс Мета(тип): ... защита __init__( ... cls, имя, базы, dct ... ): ... клс.аттр = 100 ... >>> класс X (метакласс = мета): ... проходить ... >>> X.attr 100 >>> класс Y (метакласс = мета): ... проходить ... >>> Y.attr 100 >>> класс Z (метакласс = мета): ... проходить ... >>> Z.attr 100
Удалить рекламу
Это действительно необходимо?
Каким бы простым ни был приведенный выше пример фабрики классов, в нем суть работы метаклассов. Они позволяют настраивать экземпляр класса.
Тем не менее, присваивать настраиваемый атрибут attr
каждому вновь созданному классу слишком много. Вам действительно нужен метакласс только для этого?
В Python есть по крайней мере пара других способов, которыми можно эффективно выполнить то же самое:
Простое наследование:
>>>
>>> класс База: . .. атрибут = 100 ... >>> класс X (базовый): ... проходить ... >>> класс Y (базовый): ... проходить ... >>> класс Z (базовый): ... проходить ... >>> X.attr 100 >>> Y.attr 100 >>> Z.attr 100
Декоратор класса:
>>>
>>> определитель декоратора (cls): ... класс NewClass (cls): ... атрибут = 100 ... вернуть новый класс ... >>> @декоратор ... класс Х: ... проходить ... >>> @декоратор ... класс Y: ... проходить ... >>> @декоратор ... класс Z: ... проходить ... >>> X.attr 100 >>> Y.attr 100 >>> Z.attr 100
Заключение
Как предполагает Тим Питерс, метаклассов могут легко превратиться в «решение в поисках проблемы». Обычно нет необходимости создавать собственные метаклассы. Если проблема под рукой может быть решена более простым способом, вероятно, так и должно быть. Тем не менее полезно понимать метаклассы, чтобы понимать классы Python в целом и понимать, когда метакласс действительно является подходящим инструментом для использования.
oop — Что такое метаклассы в Python?
Прежде чем разбираться в метаклассах, вам необходимо изучить Python. И у Python очень своеобразное представление о том, что такое классы, заимствованное из языка Smalltalk.
В большинстве языков классы — это просто фрагменты кода, описывающие, как создать объект. Это верно и для Python:
>>> class ObjectCreator(object): ... проходить ... >>> мой_объект = ObjectCreator() >>> печать (мой_объект) <__main__.ОбъектCreator с адресом 0x8974f2c>
Но классы в Python — это нечто большее. Классы тоже объекты.
Да, объекты.
Как только вы используете ключевое слово class
, Python выполняет его и создает
объект . Инструкция
>>> class ObjectCreator(object): ... проходить ...
создает в памяти объект с именем ObjectCreator
.
Этот объект (класс) сам по себе способен создавать объекты (экземпляры), вот почему это 9 класс0004 .
Но все же это объект, а значит:
- можно присвоить переменной
- можно скопировать
- вы можете добавить к нему атрибуты
- вы можете передать его как параметр функции
например:
>>> print(ObjectCreator) # вы можете распечатать класс, потому что это объект <класс '__main__.ObjectCreator'> >>> защитное эхо(о): ... печать (о) ... >>> echo(ObjectCreator) # вы можете передать класс в качестве параметра <класс '__main__.ObjectCreator'> >>> print(hasattr(ObjectCreator, 'new_attribute')) ЛОЖЬ >>> ObjectCreator.new_attribute = 'foo' # вы можете добавлять атрибуты к классу >>> print(hasattr(ObjectCreator, 'new_attribute')) Истинный >>> print(ObjectCreator.new_attribute) фу >>> ObjectCreatorMirror = ObjectCreator # вы можете присвоить класс переменной >>> print(ObjectCreatorMirror.new_attribute) фу >>> print(ObjectCreatorMirror()) <__main__.ОбъектCreator с адресом 0x8997b4c>
Поскольку классы являются объектами, их можно создавать на лету, как и любой другой объект.
Во-первых, вы можете создать класс в функции, используя класс
:
>>> def Choose_class(name): ... если имя == 'foo': ... класс Foo (объект): ... проходить ... return Foo # вернуть класс, а не экземпляр ... еще: ... класс Бар (объект): ... проходить ... вернуть Бар ... >>> МойКласс = select_class('foo') >>> print(MyClass) # функция возвращает класс, а не экземпляр <класс '__main__.Foo'> >>> print(MyClass()) # вы можете создать объект из этого класса <__main__.Foo объект в 0x89c6d4c>
Но это не так динамично, так как вам все равно придется писать весь класс самостоятельно.
Поскольку классы являются объектами, они должны быть чем-то созданы.
Когда вы используете ключевое слово class
, Python создает этот объект автоматически. Но, как
с большинством вещей в Python это дает вам возможность сделать это вручную.
Помните функцию типа
? Старая добрая функция, которая позволяет узнать, что
введите объект:
>>> print(type(1)) <тип 'целое число'> >>> напечатать(тип("1")) <тип 'строка'> >>> печать (тип (создатель объектов)) <тип 'тип'> >>> print(type(ObjectCreator())) <класс '__main__. ObjectCreator'>
Ну, тип
имеет еще и совсем другую способность: он может создавать классы на лету. тип
может принимать в качестве параметров описание класса,
и вернуть класс.
(Я знаю, это глупо, что одна и та же функция может иметь два совершенно разных использования в зависимости от параметров, которые вы ей передаете. Это проблема из-за обратного совместимость в Python)
тип
работает следующим образом:
тип(имя, базы, атрибуты)
Где:
-
имя
: имя класса -
базы
: кортеж родительского класса (для наследования может быть пустым) -
attrs
: словарь, содержащий имена и значения атрибутов
например:
>>> класс MyShinyClass(объект): ... проходить
можно создать вручную следующим образом:
>>> MyShinyClass = type('MyShinyClass', (), {}) # возвращает объект класса >>> печать (MyShinyClass) <класс '__main__. MyShinyClass'> >>> print(MyShinyClass()) # создать экземпляр с классом Объект <__main__.MyShinyClass по адресу 0x8997cec>
Вы заметите, что мы используем MyShinyClass
в качестве имени класса.
и как переменная для хранения ссылки на класс. Они могут быть разными,
но нет причин все усложнять.
тип
принимает словарь для определения атрибутов класса. Итак:
>>> класс Foo(объект): ... бар = Истина
Можно перевести как:
>>> Foo = type('Foo', (), {'bar':True})
И используется как обычный класс:
>>> печать(Фу) <класс '__main__.Foo'> >>> печать (Foo.bar) Истинный >>> f = Фу() >>> напечатать(ф) Объект <__main__.Foo по адресу 0x8a9b84c> >>> print(f.bar) Истинный
И, конечно же, от него можно наследоваться, так что:
>>> class FooChild(Foo): ... проходить
будет:
>>> FooChild = type('FooChild', (Foo,), {}) >>> печать (FooChild) <класс '__main__.FooChild'> >>> print(FooChild. bar) # бар наследуется от Foo Истинный
Со временем вы захотите добавить методы в свой класс. Просто определите функцию с соответствующей подписью и назначьте его как атрибут.
>>> def echo_bar(self): ... распечатать (self.bar) ... >>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar}) >>> hasattr(Foo, 'echo_bar') ЛОЖЬ >>> hasattr(FooChild, 'echo_bar') Истинный >>> my_foo = FooChild() >>> my_foo.echo_bar() Истинный
И вы можете добавить еще больше методов после динамического создания класса, точно так же, как добавление методов к обычно созданному объекту класса.
>>> def echo_bar_more(self): ... print('еще один метод') ... >>> FooChild.echo_bar_more = echo_bar_more >>> hasattr(FooChild, 'echo_bar_more') Истинный
Вы видите, к чему мы идем: в Python классы являются объектами, и вы можете динамически создавать классы на лету.
Вот что делает Python, когда вы используете ключевое слово class
, и делает это с помощью метакласса.
Метаклассы — это «материал», который создает классы.
Вы определяете классы для создания объектов, верно?
Но мы узнали, что классы Python — это объекты.
Метаклассы - это то, что создает эти объекты. Это классы классов, вы можете изобразить их так:
MyClass = MetaClass() мой_объект = МойКласс()
Вы видели, что тип
позволяет вам сделать что-то вроде этого:
MyClass = type('MyClass', (), {})
Это потому, что функция типа
на самом деле является метаклассом. тип
является
метакласс Python использует для создания всех классов за кулисами.
Теперь вы удивляетесь "почему, черт возьми, это написано строчными буквами, а не Наберите
?"
Ну, я думаю, это вопрос согласованности с str
, классом, который создает
строковые объекты и int
класс, создающий целочисленные объекты. тип
есть
просто класс, который создает объекты класса.
Это можно увидеть, проверив атрибут __class__
.
Все, и я имею в виду все, является объектом в Python. Это включает целые числа, строки, функции и классы. Все они являются объектами. И все они имеют создан из класса:
>>> age = 35 >>> возраст.__класс__ <тип 'целое число'> >>> имя = 'боб' >>> имя.__класс__ <тип 'строка'> >>> def foo(): пройти >>> foo.__class__ <тип 'функция'> >>> класс Bar(объект): пройти >>> б = бар() >>> б.__класс__ <класс '__main__.Bar'>
Что такое __class__
любого __class__
?
>>> возраст.__класс__.__класс__ <тип 'тип'> >>> имя.__класс__.__класс__ <тип 'тип'> >>> foo.__class__.__class__ <тип 'тип'> >>> б.__класс__.__класс__ <тип 'тип'>
Итак, метакласс — это просто материал, который создает объекты класса.
Вы можете назвать это «фабрикой классов», если хотите.
тип
— это встроенный метакласс, который использует Python, но, конечно, вы можете создать свой
собственный метакласс.
В Python 2 вы можете добавить атрибут __metaclass__
при написании класса (см. следующий раздел о синтаксисе Python 3):
class Foo(object): __metaclass__ = что-то... [...]
Если вы это сделаете, Python будет использовать метакласс для создания класса Foo
.
Осторожно, это сложно.
Сначала вы пишете класс Foo(object)
, но объект класса Foo
не создается
в памяти еще.
Python будет искать __metaclass__
в определении класса. Если найдет,
он будет использовать его для создания класса объектов Foo
. Если это не так, он будет использовать введите
, чтобы создать класс.
Прочтите это несколько раз.
Когда вы делаете:
класс Foo(Bar): проходить
Python делает следующее:
Есть ли атрибут __metaclass__
в Foo
?
Если да, создайте в памяти объект класса (я сказал объект класса, оставайтесь со мной здесь) с именем Foo
, используя то, что находится в __metaclass__
.
Если Python не может найти __metaclass__
, он будет искать __metaclass__
на уровне МОДУЛЯ и попытается сделать то же самое (но только для классов, которые ничего не наследуют, в основном классы старого стиля).
Затем, если он вообще не может найти __metaclass__
, он будет использовать собственный метакласс Bar
(первый родитель) (который может быть типом по умолчанию ) для создания объекта класса.
Будьте осторожны, чтобы атрибут __metaclass__
не был унаследован, метакласс родителя ( Bar.__class__
) будет унаследован. Если Bar
использовал атрибут __metaclass__
, который создал Bar
с type()
(а не type.__new__()
), подклассы не наследуют это поведение.
Теперь большой вопрос, что вы можете поместить в __metaclass__
?
Ответ: что-то, что может создать класс.
А что может создать класс? введите
или что-либо, что является подклассом или использует его.
Синтаксис для установки метакласса был изменен в Python 3:
class Foo(object, metaclass=something): ...
т. е. атрибут __metaclass__
больше не используется, вместо него в списке базовых классов используется аргумент ключевого слова.
Однако поведение метаклассов в основном остается прежним.
Одна вещь, добавленная к метаклассам в Python 3, заключается в том, что вы также можете передавать атрибуты в качестве аргументов ключевого слова в метакласс, например:
класс Foo(объект, метакласс=что-то, kwarg1=значение1, kwarg2=значение2): ...
Прочтите раздел ниже, чтобы узнать, как Python справляется с этим.
Основное назначение метакласса — автоматическое изменение класса, когда он создан.
Обычно вы делаете это для API, где вы хотите создать классы, соответствующие текущий контекст.
Представьте себе глупый пример, когда вы решаете, что все классы в вашем модуле
должны иметь свои атрибуты, написанные в верхнем регистре. Есть несколько способов
сделать это, но один из способов установить __metaclass__
на уровне модуля.
Таким образом, все классы этого модуля будут созданы с использованием этого метакласса, и нам просто нужно сказать метаклассу перевести все атрибуты в верхний регистр.
К счастью, __metaclass__
на самом деле может быть любым вызываемым, он не обязательно должен быть
формальный класс (я знаю, что-то с «классом» в его имени не должно быть
класс, пойди разберись… но это полезно).
Итак, мы начнем с простого примера, используя функцию.
# метакласс автоматически получит тот же аргумент # который вы обычно передаете в `type` def upper_attr (future_class_name, future_class_parents, future_class_attrs): """ Вернуть объект класса со списком его атрибутов, перевернутым в верхний регистр. """ # выберите любой атрибут, который не начинается с '__', и запишите его в верхнем регистре uppercase_attrs = { attr if attr. startswith("__") else attr.upper(): v для attr, v в future_class_attrs.items() } # пусть `type` создаст класс возвращаемый тип (future_class_name, future_class_parents, uppercase_attrs) __metaclass__ = upper_attr # это повлияет на все классы в модуле class Foo(): # global __metaclass__ не будет работать с "object" # но мы можем определить здесь __metaclass__, чтобы воздействовать только на этот класс # и это будет работать с дочерними "объектами" бар = 'бип'
Проверим:
>>> hasattr(Foo, 'bar') ЛОЖЬ >>> hasattr(Фу, 'БАР') Истинный >>> Фу.БАР бип
Теперь давайте сделаем то же самое, но используя реальный класс для метакласса:
# помните, что `type` на самом деле является классом, таким как `str` и `int` # так что вы можете наследовать от него класс UpperAttrMetaclass (тип): # __new__ - это метод, вызываемый перед __init__ # это метод, который создает объект и возвращает его # в то время как __init__ просто инициализирует объект, переданный в качестве параметра # вы редко используете __new__, за исключением случаев, когда вы хотите контролировать, как объект # создано. # здесь созданный объект является классом, и мы хотим его настроить # поэтому мы переопределяем __new__ # вы можете сделать кое-что и в __init__, если хотите # некоторые расширенные варианты использования также включают переопределение __call__, но мы не будем # посмотри это def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attrs): uppercase_attrs = { attr if attr.startswith("__") else attr.upper(): v для attr, v в future_class_attrs.items() } возвращаемый тип (future_class_name, future_class_parents, uppercase_attrs)
Давайте перепишем приведенное выше, но с более короткими и реалистичными именами переменных, теперь, когда мы знаем, что они означают:
class UpperAttrMetaclass(type): def __new__(cls, clsname, bases, attrs): uppercase_attrs = { attr if attr.startswith("__") else attr.upper(): v для attr, v в attrs. items() } возвращаемый тип (clsname, bases, uppercase_attrs)
Возможно, вы заметили дополнительный аргумент cls
. Есть
ничего особенного в этом нет : __new__
всегда получает класс, в котором он определен, в качестве первого параметра. Так же, как у вас есть self
для обычных методов, которые получают экземпляр в качестве первого параметра или определяющий класс для методов класса.
Но это неправильный ООП. Мы вызываем типа
напрямую, и мы не переопределяем и не вызываем родительский __new__
. Давайте сделаем это вместо этого:
class UpperAttrMetaclass(type): def __new__(cls, clsname, bases, attrs): uppercase_attrs = { attr if attr.startswith("__") else attr.upper(): v для attr, v в attrs.items() } возвращаемый тип.__new__(cls, clsname, bases, uppercase_attrs)
Мы можем сделать его еще чище, используя super
, что упростит наследование (потому что да, вы можете иметь метаклассы, наследование от метаклассов, наследование от типа):
class UpperAttrMetaclass(type): def __new__(cls, clsname, bases, attrs): uppercase_attrs = { attr if attr. startswith("__") else attr.upper(): v для attr, v в attrs.items() } # Python 2 требует передачи аргументов в super: вернуть super(UpperAttrMetaclass, cls).__new__( cls, clsname, bases, uppercase_attrs) # Python 3 может использовать super() без аргументов, который выводит их: return super().__new__(cls, clsname, bases, uppercase_attrs)
А, и в Python 3, если вы делаете этот вызов с ключевыми словами, например:
class Foo(object, metaclass=MyMetaclass, kwarg1=value1): ...
Это переводится в метакласс, чтобы использовать его:
class MyMetaclass(type): def __new__(cls, clsname, bases, dct, kwargs1=по умолчанию): ...
Вот и все. В метаклассах больше ничего нет.
Причина сложности кода с использованием метаклассов не в том, что
метаклассов, это потому, что вы обычно используете метаклассы, чтобы делать извращенные вещи
полагаться на самоанализ, манипулировать наследованием, такими переменными, как __dict__
и т. д.
Действительно, метаклассы особенно полезны для черной магии, и поэтому сложные вещи. Но сами по себе они просты:
- перехватить создание класса
- изменить класс
- вернуть измененный класс
Поскольку __metaclass__
может принимать любой вызываемый объект, зачем вам использовать класс
так как это явно сложнее?
Для этого есть несколько причин:
- Намерение ясно. Когда вы читаете
UpperAttrMetaclass(type)
, вы знаете что будет дальше - Вы можете использовать ООП. Метакласс может наследовать от метакласса, переопределять родительские методы. Метаклассы могут даже использовать метаклассы.
- Подклассы класса будут экземплярами его метакласса, если вы указали класс метакласса, но не функцию метакласса.
- Вы можете лучше структурировать свой код. Вы никогда не используете метаклассы для чего-то тривиального, как в приведенном выше примере. Обычно это что-то сложное. Возможность сделать несколько методов и сгруппировать их в один класс очень полезна для облегчения чтения кода.
- Можно подключить
__new__
,__init__
и__call__
. Что позволит вам делать разные вещи, даже если обычно вы можете делать все это в__new__
, некоторым людям удобнее использовать__init__
. - Это называется метаклассы, черт возьми! Это должно что-то означать!
Теперь большой вопрос. Зачем вам использовать какую-то непонятную функцию, подверженную ошибкам?
Обычно нет:
Метаклассы — это более глубокая магия, 99% пользователей никогда не должны беспокоиться об этом. Если вы задаетесь вопросом, нужны ли они вам, вы не (люди, которые на самом деле нужно, чтобы они знали с уверенностью, что они им нужны и не нужны пояснение почему).
Гуру Python Тим Питерс
Основным вариантом использования метакласса является создание API. Типичным примером этого является Django ORM. Это позволяет вам определить что-то вроде этого:
class Person(models.Model): имя = модели.CharField(max_length=30) возраст = модели.IntegerField()
Но если вы сделаете так:
человек = Человек(имя='боб', возраст='35') печать(человек.возраст)
Он не вернет объект IntegerField
. Он вернет int
и даже может взять его непосредственно из базы данных.
Это возможно, потому что модели. Модель
определяет __metaclass__
и
он использует некоторую магию, которая превратит человека в
, который вы только что определили с помощью простых операторов.
в сложный хук к полю базы данных.
Django делает что-то сложное простым, предоставляя простой API и используя метаклассы, воссоздавая код из этого API для выполнения реальной работы за кулисами.
Во-первых, вы знаете, что классы — это объекты, которые могут создавать экземпляры.