Как создать класс без инструкции class

Время на прочтение10 мин

Количество просмотров203K

Как сказал один из пользователей StackOverflow, «using SO is like doing lookups with a hashtable instead of a linked list». Мы снова обращаемся к этому замечательному ресурсу, на котором попадаются чрезвычайно подробные и понятные ответы на самые различные вопросы.

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

Классы как объекты

Перед тем, как изучать метаклассы, надо хорошо разобраться с классами, а классы в Питоне — вещь весьма специфическая (основаны на идеях из языка Smalltalk).

В большинстве языков класс это просто кусок кода, описывающий, как создать объект. В целом это верно и для Питона:

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

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

Но в Питоне класс это нечто большее — классы также являются объектами.

Как только используется ключевое слово class, Питон исполняет команду и создаёт объект. Инструкция

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

создаст в памяти объект с именем ObjectCreator.

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

Тем не менее, это объект, а потому:

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

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

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

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

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

Однако это не очень-то динамично, поскольку по-прежнему нужно самому писать весь класс целиком.

Поскольку классы являются объектами, они должны генерироваться чем-нибудь.

Когда используется ключевое слово class, Питон создаёт этот объект автоматически. Но как и большинство вещей в Питоне, есть способ сделать это вручную.

Помните функцию type? Старая-добрая функция, которая позволяет определить тип объекта:

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

На самом деле, у функции type есть совершенно иное применение: она также может создавать классы на ходу. type принимает на вход описание класса и созвращает класс.

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

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

  type(<имя класса>, 
       <кортеж родительских классов>, # для наследования, может быть пустым
       <словарь, содержащий атрибуты и их значения>)

Например,

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

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

  >>> MyShinyClass = type('MyShinyClass', (), {}) # возвращает объект-класс
  >>> print MyShinyClass
  <class '__main__.MyShinyClass'>
  >>> print MyShinyClass() # создаёт экземпляр класса
  <__main__.MyShinyClass object at 0x8997cec>

Возможно, вы заметили, что мы используем «MyShinyClass» и как имя класса, и как имя для переменной, содержащей ссылку на класс. Они могут быть различны, но зачем усложнять?

type принимает словарь, определяющий атрибуты класса:

>>> class Foo(object):
...       bar = True

можно переписать как

  >>> Foo = type('Foo', (), {'bar':True})

и использовать как обычный класс

  >>> print Foo
  <class '__main__.Foo'>
  >>> print Foo.bar
  True
  >>> f = Foo()
  >>> print f
  <__main__.Foo object at 0x8a9b84c>
  >>> print f.bar
  True

Конечно, можно от него наследовать:

  >>>   class FooChild(Foo):
  ...         pass

превратится в

  >>> FooChild = type('FooChild', (Foo,), {})
  >>> print FooChild
  <class '__main__.FooChild'>
  >>> print FooChild.bar # bar is inherited from Foo
  True

В какой-то момент вам захочется добавить методов вашему классу. Для этого просто определите функцию с нужной сигнатурой и присвойте её в качестве атрибута:

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

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

Это именно то, что Питон делает, когда используется ключевое слово class, и делает он это с помощью метаклассов.

Что такое метакласс (наконец)

Метакласс это «штука», которая создаёт классы.

Мы создаём класс для того, чтобы создавать объекты, так? А классы являются объектами. Метакласс это то, что создаёт эти самые объекты. Они являются классами классов, можно представить это себе следующим образом:

  MyClass = MetaClass()
  MyObject = MyClass()

Мы уже видели, что type позволяет делать что-то в таком духе:

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

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

Естественный вопрос: с чего это он его имя пишется в нижнем регистре, а не Type?

Я полагаю, это просто для соответствия str, классу для создания объектов-строк, и int, классу для создания объектов-целых чисел. type это просто класс для создания объектов-классов.

Это легко проверить с помощью атрибута __class__:

В питоне всё (вообще всё!) является объектами. В том числе числа, строки, функции и классы — они все являются объектами и все были созданы из класса:

  >>> age = 35
  >>> age.__class__
  <type 'int'>
  >>> name = 'bob'
  >>> name.__class__
  <type 'str'>
  >>> def foo(): pass
  >>> foo.__class__
  <type 'function'>
  >>> class Bar(object): pass
  >>> b = Bar()
  >>> b.__class__
  <class '__main__.Bar'>

А какой же __class__ у каждого __class__?

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

Итак, метакласс это просто штука, создающая объекты-классы.

Если хотите, можно называть его «фабрикой классов»

type это встроенный метакласс, который использует Питон, но вы, конечно, можете создать свой.

Атрибут __metaclass__

При написании класса можно добавить атрибут __metaclass__:

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

В таком случае Питон будет использовать указанный метакласс при создании класса Foo.

Осторожно, тут есть тонкость!

Хоть вы и пишете class Foo(object), объект-класс пока ещё не создаётся в памяти.

Питон будет искать __metaclass__ в определении класса. Если он его найдёт, то использует для создания класса Foo. Если же нет, то будет использовать type.

То есть когда вы пишете

class Foo(Bar):
  pass

Питон делает следующее:

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

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

Если Питон не находит __metaclass__, он ищет __metaclass__ в родительском классе Bar и попробует сделать то же самое.

Если же __metaclass__ не находится ни в одном из родителей, Питон будет искать __metaclass__ на уровне модуля.

И если он не может найти вообще ни одного __metaclass__, он использует type для создания объекта-класса.

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

Ответ: что-нибудь, что может создавать классы.

А что создаёт классы? type или любой его подкласс, а также всё, что использует их.

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

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

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

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

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

К счастью, __metaclass__ может быть любым вызываемым объектом, не обязательно формальным классом (я знаю, что-то со словом «класс» в названии не обязано быть классом, что за ерунда? Однако это полезно).

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

# метаклассу автоматически придёт на вход те же аргументы,
# которые обычно используются в `type`
def upper_attr(future_class_name, future_class_parents, future_class_attr):
  """
    Возвращает объект-класс, имена атрибутов которого
    переведены в верхний регистр
  """

  # берём любой атрибут, не начинающийся с '__'
  attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
  # переводим их в верхний регистр
  uppercase_attr = dict((name.upper(), value) for name, value in attrs)

  # создаём класс с помощью `type`
  return type(future_class_name, future_class_parents, uppercase_attr)

__metaclass__ = upper_attr # это сработает для всех классов в модуле

class Foo(object): 
  # или можно определить __metaclass__ здесь, чтобы сработало только для этого класса
  bar = 'bip'

print hasattr(Foo, 'bar')
# Out: False
print hasattr(Foo, 'BAR')
# Out: True

f = Foo()
print f.BAR
# Out: 'bip'

А теперь то же самое, только используя настояший класс:

# помним, что `type` это на само деле класс, как `str` и `int`,
# так что от него можно наследовать
class UpperAttrMetaclass(type): 
    # Метод __new__ вызывается перед __init__
    # Этот метод создаёт обхект и возвращает его,
    # в то время как __init__ просто инициализирует объект, переданный в качестве аргумента.
    # Обычно вы не используете __new__, если только не хотите проконтролировать,
    # как объект создаётся
    # В данном случае созданный объект это класс, и мы хотим его настроить,
    # поэтому мы перегружаем __new__.
    # Можно также сделать что-нибудь в __init__, если хочется.
    # В некоторых более продвинутых случаях также перегружается __call__,
    # но этого мы сейчас не увидим.
    def __new__(upperattr_metaclass, future_class_name, 
                future_class_parents, future_class_attr):

        attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)

        return type(future_class_name, future_class_parents, uppercase_attr)

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

class UpperAttrMetaclass(type): 

    def __new__(upperattr_metaclass, future_class_name, 
                future_class_parents, future_class_attr):

        attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)

        # используем метод type.__new__
        # базовое ООП, никакой магии
        return type.__new__(upperattr_metaclass, future_class_name, 
                            future_class_parents, uppercase_attr)

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

Конечно, имена, которые я тут использовал, такие длинные для ясности, но как и self, есть соглашение об именовании всех этих аргументов. Так что реальный метакласс выгляит как-нибудь так:

class UpperAttrMetaclass(type): 

    def __new__(cls, name, bases, dct):

        attrs = ((name, value) for name, value in dct.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)

        return type.__new__(cls, name, bases, uppercase_attr)

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

class UpperAttrMetaclass(type): 

    def __new__(cls, name, bases, dct):

        attrs = ((name, value) for name, value in dct.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)

        return super(UpperAttrMetaclass, cls).__new__(cls, name, bases, uppercase_attr)

Вот и всё. О метаклассах больше ничего и не сказать.

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

Действительно, метаклассы особенно полезны для всякой «чёрной магии», а, следовательно, сложных штук. Но сами по себе они просты:

  • перехватить создание класса
  • изменить класс
  • вернуть модифицированный

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

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

Тому есть несколько причин:

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

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

Наконец, главный вопрос. С чего кому-то использовать какую-то непонятную (и способствующую ошибкам) фичу?

Ну, обычно и не надо использовать:

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

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

Она позволяет написать что-то в таком духе:

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

Однако если вы выполните следующий код:

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

вы получите не IntegerField, а int, причём значение может быть получено прямо из базы данных.

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

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

Напоследок

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

На самом деле, классы это тоже экземпляры. Экземпляры метаклассов.

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

Всё что угодно является объектом в Питоне: экземпляром класса или экземпляром метакласса.

Кроме type.

type является собственным метаклассом. Это нельзя воспроизвести на чистом Питоне и делается небольшим читерством на уровне реализации.

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

  • руками
  • декораторы классов

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

Но в 99% случаев вам вообще не нужно изменять классы :-)

Метаклассы

Python: Погружаясь в классы

Python — язык программирования, в котором все является объектом, включая классы и функции. Но иногда стандартных возможностей классов недостаточно, и возникает необходимость более тонкого управления поведением классов. Здесь-то и появляется проблема: как добавить дополнительную функциональность или контроль над процессом создания и поведением классов?

Когда мы говорим об объектах в Python, мы часто думаем о простых типах данных или о сложных структурах, таких как классы. Однако в Python даже классы являются объектами, и эти объекты-классы создаются с использованием метаклассов, в основном метакласса type.

В языке Python решением этой проблемы являются метаклассы. Они позволяют воздействовать на процесс создания и поведение классов.

В этом уроке мы рассмотрим, что такое метаклассы, как их создать и применять для решения задач, связанных с управлением классами, а также возможные сценарии их использования.

Метакласс — это «класс классов». Как класс определяет поведение объекта, метакласс определяет поведение класса. По умолчанию в Python все классы создаются из встроенного метакласса type.

Самый простой способ использования метаклассов — это их непосредственное использование при создании классов. Мы можем напрямую использовать type как метакласс для создания классов:

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

Это создаст новый класс MyClass без родителей кроме базового класса object и без методов или атрибутов.

В этом примере мы используем встроенный метакласс type для создания нового класса MyClass. Первый аргумент type — это строка с именем класса, второй — кортеж с родительскими классами. В нашем случае он пуст, что означает, что у класса нет родителей кроме базового класса object. А третий — словарь с атрибутами и методами класса. Он тоже пустой.

Но что если мы хотим добавить методы или атрибуты в класс? Мы можем сделать это, передав их в виде словаря третьим аргументом в type:

def my_method(self):
    print("Hello, World!")


MyClass = type("MyClass", (), {"my_method": my_method})

obj = MyClass()
obj.my_method()  # Hello, World!

Здесь мы добавляем метод my_method в класс MyClass. Мы определяем функцию my_method и затем включаем ее в словарь атрибутов и методов класса, который передаем в type как третий аргумент.

Но что, если нам нужно добавить специфическое поведение при создании класса? Рассмотрим, как создавать собственные метаклассы.

Мы можем создать собственные метаклассы, наследуясь от type. Это позволяет переопределить методы метакласса:

class MyMeta(type):
    def __init__(cls, name, bases, attrs):
        print(f"Создание класса {name}")
        super().__init__(name, bases, attrs)


class MyClass(metaclass=MyMeta):
    pass  # Создание класса MyClass

В этом примере мы создаем собственный метакласс MyMeta, который выводит сообщение при создании класса. Метод __init__ в метаклассе принимает четыре аргумента:

  • cls — это новый класс, который создается
  • name — имя класса
  • bases — родительские классы
  • attrs — словарь с атрибутами и методами класса

Мы переопределяем __init__ для добавления собственного поведения, но также вызываем __init__ родительского класса, чтобы убедиться, что класс создается правильно.

Затем создаем класс MyClass с MyMeta в качестве его метакласса. Когда мы делаем это, наш метакласс автоматически вызывается для создания MyClass, и мы видим сообщение «Создание класса MyClass».

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

Метаклассы используются редко из-за их сложности, но они могут быть чрезвычайно мощными. Один из наиболее известных примеров использования метаклассов — это фреймворк Django. Django использует метаклассы для создания моделей базы данных. Это позволяет разработчикам определять структуры своих баз данных с помощью простого Python-кода, и метаклассы автоматически преобразуют эти определения в SQL-запросы для создания таблиц.

Для создания модели в Django, разработчики определяют классы, которые наследуются от django.db.models.Model. Но что интересно, когда вы определяете поля модели, например, CharField или IntegerField, вы на самом деле не создаете экземпляры этих полей. Вместо этого, Django использует метаклассы для преобразования этих определений полей в столбцы базы данных:

from django.db import models


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

Здесь Person — это модель, которая будет преобразована в таблицу базы данных с полями name и age.

Когда вы определяете такой класс, Django автоматически использует метакласс, который проходит по вашим определениям полей и создает соответствующие столбцы в базе данных. Так, CharField будет преобразован в столбец типа VARCHAR в базе данных, а IntegerField — в столбец типа INT.

Давайте рассмотрим как это работает. Модель Model Django имеет метакласс под названием ModelBase. Когда вы определяете класс, такой как Person, ModelBase автоматически анализирует все его атрибуты. Он определяет, какие из этих атрибутов являются определениями полей, и затем создает структуру для таблицы базы данных.

Метакласс ModelBase также обеспечивает дополнительные функции, такие как создание методов для запросов к базе данных и работу с наследованием моделей.

Таким образом в Django разработчики могут определить структуры данных с помощью понятного Python-кода, и Django автоматически преобразует это в SQL и обеспечивает интерфейс для работы с данными.

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно


  • 130 курсов, 2000+ часов теории

  • 1000 практических заданий в браузере

  • 360 000 студентов

Наши выпускники работают в компаниях:

Метаклассы – это классы, экземпляры которых являются классами. Давайте поговорим о специфике языка Python и его функционале.

метаклассы

Чтобы создать свой собственный метакласс в Python, нужно воспользоваться подклассом type, стандартным метаклассом в Python. Чаще всего метаклассы используются в роли виртуального конструктора. Чтобы создать экземпляр класса, нужно сначала вызвать этот самый класс. Точно так же делает и Python: для создания нового класса вызывает метакласс. Метаклассы определяются с помощью базовых классов в атрибуте __metaclass__. При создании класса допускается использование методов __init__ и __new__. С их помощью можно пользоваться дополнительными функциями. Во время выполнения оператора class генерируется пространство имен, которое будет содержать атрибуты будущего класса. Затем, для непосредственного создания, вызывается метакласс с именем и атрибутами.

Пример:

def make_hook(f):
    """Decorator to turn 'foo' method into '__foo__'"""
    f.is_hook = 1
    return f

class MyType(type):
    def __new__(mcls, name, bases, attrs):

        if name.startswith('None'):
            return None

        # Go over attributes and see if they should be renamed.
        newattrs = {}
        for attrname, attrvalue in attrs.iteritems():
            if getattr(attrvalue, 'is_hook', 0):
                newattrs['__%s__' % attrname] = attrvalue
            else:
                newattrs[attrname] = attrvalue

        return super(MyType, mcls).__new__(mcls, name, bases, newattrs)

    def __init__(self, name, bases, attrs):
        super(MyType, self).__init__(name, bases, attrs)

        # classregistry.register(self, self.interfaces)
        print "Would register class %s now." % self

    def __add__(self, other):
        class AutoClass(self, other):
            pass
        return AutoClass
        # Alternatively, to autogenerate the classname as well as the class:
        # return type(self.__name__ + other.__name__, (self, other), {})

    def unregister(self):
        # classregistry.unregister(self)
        print "Would unregister class %s now." % self

class MyObject:
    __metaclass__ = MyType


class NoneSample(MyObject):
    pass

# Will print "NoneType None"
print type(NoneSample), repr(NoneSample)

class Example(MyObject):
    def __init__(self, value):
        self.value = value
    @make_hook
    def add(self, other):
        return self.__class__(self.value + other.value)

# Will unregister the class
Example.unregister()

inst = Example(10)
# Will fail with an AttributeError
#inst.unregister()

print inst + inst
class Sibling(MyObject):
    pass

ExampleSibling = Example + Sibling
# ExampleSibling is now a subclass of both Example and Sibling (with no
# content of its own) although it will believe it's called 'AutoClass'
print ExampleSibling
print ExampleSibling.__mro__

Классы как объекты

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

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

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

Но есть один нюанс. Классы в Python это объекты. Когда выполняется оператор class, Python создает в памяти объект с именем ObjectCreator.

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

Объект способен сам создавать экземпляры, так что это класс. А объект вот почему:

  • его можно назначить в качестве переменной
  • копируется
  • есть возможность добавить к нему атрибуты
  • передается в роли параметра функции
    >>> print(ObjectCreator) # you can print a class because it's an object
    <class '__main__.ObjectCreator'>
    >>> def echo(o):
    ...       print(o)
    ...
    >>> echo(ObjectCreator) # you can pass a class as a parameter
    <class '__main__.ObjectCreator'>
    >>> print(hasattr(ObjectCreator, 'new_attribute'))
    False
    >>> ObjectCreator.new_attribute = 'foo' # you can add attributes to a class
    >>> print(hasattr(ObjectCreator, 'new_attribute'))
    True
    >>> print(ObjectCreator.new_attribute)
    foo
    >>> ObjectCreatorMirror = ObjectCreator # you can assign a class to a variable
    >>> 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 # return the class, not an instance
...     else:
...         class Bar(object):
...             pass
...         return Bar
...
>>> MyClass = choose_class('foo')
>>> print(MyClass) # the function returns a class, not an instance
<class '__main__.Foo'>
>>> print(MyClass()) # you can create an object from this class
<__main__.Foo object at 0x89c6d4c>

Но это не слишком-то динамично, так как все равно придется прописывать весь класс самостоятельно.

Исходя из того, что классы являются объектами, можно сделать вывод, что они должны чем-то генерироваться. Во время выполнения оператора class, 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 работает следующим образом:

type(name of the class,
     tuple of the parent class (for inheritance, can be empty),
     dictionary containing attributes names and values)

Например:

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

Можно создать вручную:

>>> MyShinyClass = type('MyShinyClass', (), {}) # returns a class object
>>> print(MyShinyClass)
<class '__main__.MyShinyClass'>
>>> print(MyShinyClass()) # create an instance with the class
<__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')
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', (), {})

Скорее всего, вы задаетесь вопросом: почему имя функции пишется с маленькой буквы?

Скорее всего, это вопрос соответствия со str – классом, который отвечает за создание строк, и int – классом, создающим целочисленные объекты. type – это просто класс, создающий объекты класса. Проверить можно с помощью атрибута __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'>

Можно сделать вывод, что метакласс создает объекты класса. Это можно назвать «фабрикой классов». type – встроенный метакласс, который использует Python. Также можно создать свой собственный метакласс.

Атрибут __metaclass__

При написании класса можно добавить атрибут __metaclass__:

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

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

СТОИТ ПОМНИТЬ!

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

Если написать:

class Foo(Bar):
    pass

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

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

Если Python вдруг не сможет найти __metaclass__, он будет искать этот атрибут на уровне модуля и после этого повторит процедуру. В случае если он вообще не может найти какой-либо __metaclass__, Python использует собственный метакласс type, чтобы создать объект класса.

Теперь вопрос: что можно добавить в __metaclass__?
Ответ: что угодно, что может создавать классы.

А что может создать класс? type или его подклассы, а также всё, что его использует.

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

Основной целью метакласса является автоматическое изменение класса во время его создания. Обычно это делается для API, когда нужно создать классы, соответствующие текущему контексту. Например, вы решили, что все классы в модуле должны иметь свои атрибуты, и они должны быть записаны в верхнем регистре. Чтобы решить эту задачу, можно задать __metaclass__ на уровне модуля.

Таким образом, нужно просто сообщить метаклассу, что все атрибуты должны быть в верхнем регистре. __metaclass__ действительно может быть любым вызываемым объектом, он не обязательно должен быть формальным классом. Итак, начнем с простого примера, с использованием функции.

# 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_attr):
    """
      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_attr = {}
    for name, val in future_class_attr.items():
        if not name.startswith('__'):
            uppercase_attr[name.upper()] = val
        else:
            uppercase_attr[name] = val

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

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

print(hasattr(Foo, 'bar'))
# Out: False
print(hasattr(Foo, 'BAR'))
# Out: True

f = Foo()
print(f.BAR)
# Out: '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_attr):

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return type(future_class_name, future_class_parents, uppercase_attr)

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

class UpperAttrMetaclass(type):

    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attr):

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        # reuse the type.__new__ method
        # this is basic OOP, nothing magic in there
        return type.__new__(upperattr_metaclass, future_class_name,
                            future_class_parents, uppercase_attr)

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

class UpperAttrMetaclass(type):

    def __new__(cls, clsname, bases, dct):

        uppercase_attr = {}
        for name, val in dct.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return type.__new__(cls, clsname, bases, uppercase_attr)

Используя метод super, можно сделать код более “чистым”:

class UpperAttrMetaclass(type):

    def __new__(cls, clsname, bases, dct):

        uppercase_attr = {}
        for name, val in dct.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return super(UpperAttrMetaclass, cls).__new__(cls, clsname, bases, uppercase_attr)

Вот и все. О метаклассах больше рассказать нечего.

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

  • перехватить создание класса
  • изменить класс
  • вернуть измененный класс

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

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

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

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

«Метаклассы – это магия, о которой 99% пользователей не стоит даже задумываться. Если вам интересно, нужны ли они вам – тогда точно нет. Люди, которым они на самом деле нужны, знают, зачем, и что с ними делать.»

~ Гуру Python Tim Peters

В основном метаклассы используются для создания 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 и даже может взять его непосредственно из базы данных.

Последнее слово

Во-первых, классы – это объекты, создающие экземпляры. Классы сами являются экземплярами метаклассов.

>&gt;> class Foo(object): pass
>>> id(Foo)
142630324

В Python все является объектами. Все они являются либо экземплярами классов, либо экземплярами метаклассов. За исключением type. type – сам себе метакласс. Его невозможно создать в чистом Python, это можно сделать только при помощи небольшого читерства.

Во-вторых, метаклассы сложны. Если вам не нужны сложные изменения класса, метаклассы использовать не стоит. Просто изменить класс можно двумя способами:

  • Руками
  • Декораторами класса

В 99% случаев лучше использовать эти методы, а в 98% изменения класса вообще не нужны.

Оригинал

Материалы по теме:

  • Самые эффективные ресурсы и материалы для изучения Python.
  • Изучение Python: ТОП-10 вопросов разной направленности.

Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Metaclasses in Python

The term metaprogramming refers to the potential for a program to have knowledge of or manipulate itself. Python supports a form of metaprogramming for classes called metaclasses.

Metaclasses are an esoteric OOP concept, lurking behind virtually all Python code. You are using them whether you are aware of it or not. For the most part, you don’t need to be aware of it. Most Python programmers rarely, if ever, have to think about metaclasses.

When the need arises, however, Python provides a capability that not all object-oriented languages support: you can get under the hood and define custom metaclasses. The use of custom metaclasses is somewhat controversial, as suggested by the following quote from Tim Peters, the Python guru who authored the Zen of Python:

“Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t (the people who actually need them know with certainty that they need them, and don’t need an explanation about why).”

Tim Peters

There are Pythonistas (as Python aficionados are known) who believe that you should never use custom metaclasses. That might be going a bit far, but it is probably true that custom metaclasses mostly aren’t necessary. If it isn’t pretty obvious that a problem calls for them, then it will probably be cleaner and more readable if solved in a simpler way.

Still, understanding Python metaclasses is worthwhile, because it leads to a better understanding of the internals of Python classes in general. You never know: you may one day find yourself in one of those situations where you just know that a custom metaclass is what you want.

Old-Style vs. New-Style Classes

In the Python realm, a class can be one of two varieties. No official terminology has been decided on, so they are informally referred to as old-style and new-style classes.

Old-Style Classes

With old-style classes, class and type are not quite the same thing. An instance of an old-style class is always implemented from a single built-in type called instance. If obj is an instance of an old-style class, obj.__class__ designates the class, but type(obj) is always instance. The following example is taken from Python 2.7:

New-Style Classes

New-style classes unify the concepts of class and type. If obj is an instance of a new-style class, type(obj) is the same as obj.__class__:

Type and Class

In Python 3, all classes are new-style classes. Thus, in Python 3 it is reasonable to refer to an object’s type and its class interchangeably.

Remember that, in Python, everything is an object. Classes are objects as well. As a result, a class must have a type. What is the type of a class?

Consider the following:

The type of x is class Foo, as you would expect. But the type of Foo, the class itself, is type. In general, the type of any new-style class is type.

The type of the built-in classes you are familiar with is also type:

For that matter, the type of type is type as well (yes, really):

type is a metaclass, of which classes are instances. Just as an ordinary object is an instance of a class, any new-style class in Python, and thus any class in Python 3, is an instance of the type metaclass.

In the above case:

  • x is an instance of class Foo.
  • Foo is an instance of the type metaclass.
  • type is also an instance of the type metaclass, so it is an instance of itself.

Defining a Class Dynamically

The built-in type() function, when passed one argument, returns the type of an object. For new-style classes, that is generally the same as the object’s __class__ attribute:

You can also call type() with three arguments—type(<name>, <bases>, <dct>):

  • <name> specifies the class name. This becomes the __name__ attribute of the class.
  • <bases> specifies a tuple of the base classes from which the class inherits. This becomes the __bases__ attribute of the class.
  • <dct> specifies a namespace dictionary containing definitions for the class body. This becomes the __dict__ attribute of the class.

Calling type() in this manner creates a new instance of the type metaclass. In other words, it dynamically creates a new class.

In each of the following examples, the top snippet defines a class dynamically with type(), while the snippet below it defines the class the usual way, with the class statement. In each case, the two snippets are functionally equivalent.

Example 1

In this first example, the <bases> and <dct> arguments passed to type() are both empty. No inheritance from any parent class is specified, and nothing is initially placed in the namespace dictionary. This is the simplest class definition possible:

Example 2

Here, <bases> is a tuple with a single element Foo, specifying the parent class that Bar inherits from. An attribute, attr, is initially placed into the namespace dictionary:

Example 3

This time, <bases> is again empty. Two objects are placed into the namespace dictionary via the <dct> argument. The first is an attribute named attr and the second a function named attr_val, which becomes a method of the defined class:

Example 4

Only very simple functions can be defined with lambda in Python. In the following example, a slightly more complex function is defined externally then assigned to attr_val in the namespace dictionary via the name f:

Consider again this well-worn example:

The expression Foo() creates a new instance of class Foo. When the interpreter encounters Foo(), the following occurs:

  • The __call__() method of Foo’s parent class is called. Since Foo is a standard new-style class, its parent class is the type metaclass, so type’s __call__() method is invoked.

  • That __call__() method in turn invokes the following:

    • __new__()
    • __init__()

If Foo does not define __new__() and __init__(), default methods are inherited from Foo’s ancestry. But if Foo does define these methods, they override those from the ancestry, which allows for customized behavior when instantiating Foo.

In the following, a custom method called new() is defined and assigned as the __new__() method for Foo:

This modifies the instantiation behavior of class Foo: each time an instance of Foo is created, by default it is initialized with an attribute called attr, which has a value of 100. (Code like this would more usually appear in the __init__() method and not typically in __new__(). This example is contrived for demonstration purposes.)

Now, as has already been reiterated, classes are objects too. Suppose you wanted to similarly customize instantiation behavior when creating a class like Foo. If you were to follow the pattern above, you’d again define a custom method and assign it as the __new__() method for the class of which Foo is an instance. Foo is an instance of the type metaclass, so the code looks something like this:

Except, as you can see, you can’t reassign the __new__() method of the type metaclass. Python doesn’t allow it.

This is probably just as well. type is the metaclass from which all new-style classes are derived. You really shouldn’t be mucking around with it anyway. But then what recourse is there, if you want to customize instantiation of a class?

One possible solution is a custom metaclass. Essentially, instead of mucking around with the type metaclass, you can define your own metaclass, which derives from type, and then you can muck around with that instead.

The first step is to define a metaclass that derives from type, as follows:

The definition header class Meta(type): specifies that Meta derives from type. Since type is a metaclass, that makes Meta a metaclass as well.

Note that a custom __new__() method has been defined for Meta. It wasn’t possible to do that to the type metaclass directly. The __new__() method does the following:

  • Delegates via super() to the __new__() method of the parent metaclass (type) to actually create a new class
  • Assigns the custom attribute attr to the class, with a value of 100
  • Returns the newly created class

Now the other half of the voodoo: Define a new class Foo and specify that its metaclass is the custom metaclass Meta, rather than the standard metaclass type. This is done using the metaclass keyword in the class definition as follows:

Voila! Foo has picked up the attr attribute automatically from the Meta metaclass. Of course, any other classes you define similarly will do likewise:

In the same way that a class functions as a template for the creation of objects, a metaclass functions as a template for the creation of classes. Metaclasses are sometimes referred to as class factories.

Compare the following two examples:

Object Factory:

Class Factory:

Is This Really Necessary?

As simple as the above class factory example is, it is the essence of how metaclasses work. They allow customization of class instantiation.

Still, this is a lot of fuss just to bestow the custom attribute attr on each newly created class. Do you really need a metaclass just for that?

In Python, there are at least a couple other ways in which effectively the same thing can be accomplished:

Simple Inheritance:

Class Decorator:

Conclusion

As Tim Peters suggests, metaclasses can easily veer into the realm of being a “solution in search of a problem.” It isn’t typically necessary to create custom metaclasses. If the problem at hand can be solved in a simpler way, it probably should be. Still, it is beneficial to understand metaclasses so that you understand Python classes in general and can recognize when a metaclass really is the appropriate tool to use.

Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Metaclasses in Python


3

ответов

Похожие вопросы

Обсуждают сегодня

Господа, а что сейчас вообще с рынком труда на делфи происходит? Какова ситуация?


Rꙮman Yankꙮvsky


29

А вообще, что может смущать в самой Julia — бы сказал, что нет единого стандартного подхода по многим моментам, поэтому многое выглядит как «хаки» и произвол. Короче говоря, с…


Viktor G.


2

30500 за редактор? )


Владимир


47

а через ESC-код ?


Alexey Kulakov


29

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


notme


18

У меня есть функция где происходит это:
write_bit(buffer, 1);
write_bit(buffer, 0);
write_bit(buffer, 1);
write_bit(buffer, 1);
write_bit(buffer, 1);
w…


~


14

Добрый день!
Скажите пожалуйста, а какие программы вы бы рекомендовали написать для того, чтобы научиться управлять памятью?
Можно написать динамический массив, можно связный …


Филипп


7

Недавно Google Project Zero нашёл багу в SQLite с помощью LLM, о чём достаточно было шумно в определённых интернетах, которые сопровождались рассказами, что скоро всех «ибешни…


Alex Sherbakov


5

Ребят в СИ можно реализовать ООП?


Николай


33

https://github.com/erlang/otp/blob/OTP-27.1/lib/kernel/src/logger_h_common.erl#L174
https://github.com/erlang/otp/blob/OTP-27.1/lib/kernel/src/logger_olp.erl#L76

15 лет назад…


Maksim Lapshin


20

Понравилась статья? Поделить с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
  • Атлант холодильник фулл ноу фрост инструкция
  • Профлосин инструкция по применению капсулы отзывы взрослым
  • Антенна мицар bas 5341 подключение инструкция по применению
  • Мультиварка polaris pmc 0531ad инструкция
  • Золетил 100 инструкция по применению в ветеринарии