Python

Что такое метаклассы в Python?

Spread the love

Как обычно проходит собеседования на позицию разработчика Python? Обычно одним из первых вопросов будет просьба рассказать о типа данных (или составных типах данных) в Python. Потом через несколько других общих вопросов разговор обязательно перейдет к теме дескрипторов и метаклассов в Python. И хотя это такие вещи которые в реальной практике редко когда приходится использовать, каждый разработчик должен иметь хотя бы общее представление о них. Поэтому в этой статье я хочу немного рассказать о метаклассах.

В большинстве языков классы — это просто фрагменты кода, описывающие, как создать объект. В общем случае это верно и для Python:

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

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

Но в Python классы — это так же еще и нечто большее. В Python все является объектами в том числе и классы. Как только вы используете ключевое слово class, Python выполняет его и создает OBJECT.

Давай те сейчас создадим в памяти объект с именем «ObjectCreator».

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

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

Но все же это объект, а значит:

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

Например:

>>> print(ObjectCreator) # вы можете отобразить класс, потому что это объект
<class '__main__.ObjectCreator'>
>>> def echo(o):
...       print(o)
...
>>> echo(ObjectCreator) # вы можете передать класс в качестве параметра
<class '__main__.ObjectCreator'>
>>> print(hasattr(ObjectCreator, 'new_attribute'))
False
>>> ObjectCreator.new_attribute = 'foo' # вы можете добавлять атрибуты в класс
>>> print(hasattr(ObjectCreator, 'new_attribute'))
True
>>> print(ObjectCreator.new_attribute)
foo
>>> ObjectCreatorMirror = ObjectCreator # вы можете присвоить класс переменной
>>> print(ObjectCreatorMirror.new_attribute)
foo
>>> print(ObjectCreatorMirror())
<__main__.ObjectCreator object at 0x8997b4c>

В общем в Python вы можете делать с классом все что можно делать с объектом. В том числе создавать класс на лету.

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

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

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

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

Но это не так динамично, так как вам все равно придется писать весь класс самостоятельно. Поскольку классы являются объектами, они должны быть чем-то порождены. Когда вы используете ключевое слово class, Python создает этот объект автоматически. Но, как и в большинстве случаев в Python, он позволяет делать это и вручную. Помните функции type? Старая добрая функция, которая позволяет узнать, к какому типу относится объект:

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

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

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

type(name, bases, attrs)

Где:

  • name: имя класса
  • bases: кортеж родительского класса (для экземпляра, может быть пустым)
  • attrs: словарь, содержащий имена и значения атрибутов

Таким образом

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

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

>>> MyShinyClass = type('MyShinyClass', (), {}) # returns a class object
>>> 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 унаследован от Foo
True

В конце концов, вы захотите добавить методы в свой класс. Просто определите функцию с соответствующей подписью и назначьте ее как атрибут.

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

И вы можете добавить еще больше методов после динамического создания класса, точно так же, как можно добавить методы к обычно создаваемому объекту класса.

>>> def echo_bar_more(self):
...       print('yet another method')
...
>>> FooChild.echo_bar_more = echo_bar_more
>>> hasattr(FooChild, 'echo_bar_more')
True

Вы видите, к чему мы идем: в Python классы — это объекты, и вы можете создавать классы на лету, динамически. Это то, что Python делает, когда вы используете ключевое слово class, и делает это с помощью метакласса.

Что такое метаклассы

Метаклассы — это «материал», который создает классы.

Вы определяете классы для создания объектов, верно? Но мы узнали, что классы Python — это объекты. Что ж, метаклассы создают эти объекты. Это классы классов, и их можно изобразить так:

MyClass = MetaClass()
my_object = MyClass()

Мы уже обсудили, что этот type позволяет делать что-то вроде этого:

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

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

Теперь вы можете спросить, почему type пишется с маленькой буквы, а не Type?

Что ж, я думаю, это вопрос согласованности с str, классом, который создает строковые объекты, и int с классом, который создает целочисленные объекты. type — это просто класс, который создает объекты class.

Вы можете убедиться в этом, проверив атрибут __class__.

Все является объектом в Python. Сюда входят целые числа, строки, функции и классы. Все они объекты. И все они созданы из класса:

>>> 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__?

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

Итак, метакласс — это просто материал, который создает объекты class. Если хотите, можете назвать это «фабрикой классов». type — это встроенный метакласс, который использует Python, но, конечно, и вы можете создать свой собственный метакласс.

Атрибут __metaclass__

В Python 2 вы можете добавить атрибут __metaclass__ при описание класса:

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

Если вы это сделаете, Python будет использовать этот метакласс для создания класса Foo.

Сначала вы пишете class Foo(object), но объект класса Foo еще не создан в памяти. Python будет искать __metaclass__ в определении класса. Если он его найдет, он будет использовать его для создания класса объекта Foo. Если не найдет, он будет использовать type для создания класса. Прочтите это несколько раз.

Когда вы это сделаете:

class Foo(Bar):
    pass

Python делает следующее:

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

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

Если Python не может найти __metaclass__, он будет искать __metaclass__ на уровне MODULE и пытаться сделать то же самое (но только для классов, которые ничего не наследуют, в основном классов старого стиля).

Затем, если он вообще не может найти какой-либо __metaclass__, он будет использовать собственный метакласс Bar (первого родителя) (который может быть type по умолчанию) для создания объекта класса.

Примечание: атрибут __metaclass__ не будет унаследован, а метакласс родительского элемента (Bar .__ class__) будет. Если Bar использовал атрибут __metaclass__, который создал Bar с помощью type () (а не type .__ new __ ()), подклассы не унаследуют это поведение.

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

Ответ — то, что может создать класс.

А что можно создать класс? type или что-либо, что его подклассы использует для этого.

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

В Python 3 был изменен синтаксис для установки метакласса:

class Foo(object, metaclass=something):
    ...

то есть атрибут __metaclass__ больше не используется в качестве аргумента ключевого слова в списке базовых классов.

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

class Foo(object, metaclass=something, kwarg1=value1, kwarg2=value2):

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

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

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

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

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

К счастью, __metaclass__ на самом деле может быть любым вызываемым, он не обязательно должен быть формальным классом (то есть , что-то с ‘class’ в его имени не обязательно должно быть классом).

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

# the metaclass will automatically get passed the same argument
# that you usually pass to `type`
def upper_attr(future_class_name, future_class_parents, future_class_attrs):
    """
      Return a class object, with the list of its attribute turned
      into uppercase.
    """
    # pick up any attribute that doesn't start with '__' and uppercase it
    uppercase_attrs = {
        attr if attr.startswith("__") else attr.upper(): v
        for attr, v in future_class_attrs.items()
    }

    # let `type` do the class creation
    return type(future_class_name, future_class_parents, uppercase_attrs)

__metaclass__ = upper_attr # this will affect all classes in the module

class Foo(): # global __metaclass__ won't work with "object" though
    # but we can define __metaclass__ here instead to affect only this class
    # and this will work with "object" children
    bar = 'bip'

Давайте проверим:

>>> hasattr(Foo, 'bar')
False
>>> hasattr(Foo, 'BAR')
True
>>> Foo.BAR
'bip

Теперь сделаем то же самое, но с использованием реального класса для метакласса:

# remember that `type` is actually a class like `str` and `int`
# so you can inherit from it
class UpperAttrMetaclass(type):
    # __new__ is the method called before __init__
    # it's the method that creates the object and returns it
    # while __init__ just initializes the object passed as parameter
    # you rarely use __new__, except when you want to control how the object
    # is created.
    # here the created object is the class, and we want to customize it
    # so we override __new__
    # you can do some stuff in __init__ too if you wish
    # some advanced use involves overriding __call__ as well, but we won't
    # see this
    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attrs):
        uppercase_attrs = {
            attr if attr.startswith("__") else attr.upper(): v
            for attr, v in future_class_attrs.items()
        }
        return type(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
            for attr, v in attrs.items()
        }
        return type(clsname, bases, uppercase_attrs)

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

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

class UpperAttrMetaclass(type):
    def __new__(cls, clsname, bases, attrs):
        uppercase_attrs = {
            attr if attr.startswith("__") else attr.upper(): v
            for attr, v in attrs.items()
        }
        return type.__new__(cls, clsname, bases, uppercase_attrs)

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

class UpperAttrMetaclass(type):
    def __new__(cls, clsname, bases, attrs):
        uppercase_attrs = {
            attr if attr.startswith("__") else attr.upper(): v
            for attr, v in attrs.items()
        }
        return super(UpperAttrMetaclass, cls).__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=default):
        ...

Ну собственно вот и все. Больше нечего сказать о метаклассах.

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

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

  • перехватывают создание класса
  • изменяют класс
  • возвращают измененный класс

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

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

Для этого есть несколько причин:

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

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

А теперь большой вопрос. Зачем вам использовать какую-то непонятную функцию, подверженную ошибкам?

Ну, обычно вам это не нужно:

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

Python Guru Tim Peters

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

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

Но если вы используете это так:

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

Этот код не вернет объект IntegerField. Он вернет int и даже может взять его прямо из базы данных.

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

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

Еще хочу указать здесь ссылку на статью о вариантах использование метаклассов: Когда использовать метаклассы в Python: 5 интересных вариантов использования

Заключение

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

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

В Python все является объектом, и все они являются экземплярами классов или экземплярами метаклассов.

За исключением type.

type на самом деле является отдельным метаклассом. И это не то, что можно было бы воспроизвести на чистом Python, это делается путем небольшого мошенничества на уровне реализации.

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

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

Но в 98% случаев вам вообще не нужно менять класс.

Статья написана на основе следующего топика на StackOverflow: https://stackoverflow.com/questions/100003/what-are-metaclasses-in-python

Была ли вам полезна эта статья?
[15 / 4.7]

Spread the love
DenSP

View Comments

  • Класная статья, сам не знал что так можно:
    MyClass = type('MyClass', (), {})

Recent Posts

Vue 3.4 Новая механика v-model компонента

Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование​ v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…

11 месяцев ago

Анонс Vue 3.4

Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…

11 месяцев ago

Как принудительно пере-отобразить (re-render) компонент Vue

Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…

2 года ago

Проблемы с установкой сертификата на nginix

Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…

2 года ago

Введение в JavaScript Temporal API

Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…

2 года ago

Когда и как выбирать между медиа запросами и контейнерными запросами

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

2 года ago