Как работать с метаклассами в Python
Метаклассы являются одной из продвинутых особенностей Python, которые позволяют определять правила и поведение классов. В этой статье мы рассмотрим основы работы с метаклассами и как они могут быть полезны в разработке.
Что такое метаклассы?
Метаклассы – это классы, которые создают классы. В Python, классы сами являются объектами, и метаклассы отвечают за их создание. Они определяют правила и поведение, которыми должны обладать классы, создаваемые с их помощью.
Создание метаклассов
Для создания метакласса, нужно унаследоваться от базового метакласса type и переопределить его методы __new__ и/или __init__ . Вот пример простого метакласса:
class SimpleMeta(type): def __new__(cls, name, bases, dct): print(f"Создание нового класса ") return super().__new__(cls, name, bases, dct) class MyClass(metaclass=SimpleMeta): pass
Здесь мы создали метакласс SimpleMeta , который выводит сообщение при создании нового класса. Затем мы определили класс MyClass , используя SimpleMeta в качестве метакласса.
Python-разработчик: новая работа через 9 месяцев
Получится, даже если у вас нет опыта в IT
Применение метаклассов
Метаклассы могут быть полезны для решения различных задач, включая валидацию атрибутов класса, автоматическую регистрацию классов и других. Вот несколько примеров:
Автоматическая регистрация классов
Метаклассы могут использоваться для автоматической регистрации классов в определенной структуре данных, например, в словаре.
class RegistryMeta(type): registry = <> def __new__(cls, name, bases, dct): new_class = super().__new__(cls, name, bases, dct) cls.registry[name] = new_class return new_class class Base(metaclass=RegistryMeta): pass class Derived1(Base): pass class Derived2(Base): pass print(RegistryMeta.registry)
В этом примере, метакласс RegistryMeta регистрирует все создаваемые классы в своем словаре registry . Затем мы создаем базовый класс Base и два производных класса. После выполнения кода, словарь registry будет содержать все созданные классы.
Валидация атрибутов класса
Метаклассы могут использоваться для проверки атрибутов класса на соответствие определенным правилам.
class ValidationMeta(type): def __new__(cls, name, bases, dct): if "attribute" not in dct: raise ValueError("Класс должен содержать атрибут 'attribute'") return super().__new__(cls, name, bases, dct) class ValidClass(metaclass=ValidationMeta): attribute = 42 class InvalidClass(metaclass=ValidationMeta): pass
Здесь метакласс ValidationMeta проверяет наличие атрибута attribute в создаваемом классе. Если он отсутствует, возникает ошибка ValueError .
Заключение
Метаклассы – это мощный инструмент Python, который позволяет определять правила и поведение классов. Они могут быть полезны для решения различных задач, таких как валидация атрибутов, автоматическая регистрация классов и других. Однако следует быть осторожным при их использовании, так как они могут значительно усложнить код и сделать его менее понятным для других разработчиков.
Не забудьте посетить нашу рекомендуемую онлайн школу, которая учит специалистов в сфере «Python-разработка»!
Классы как объекты
Прежде чем разбираться в метаклассах, нужно пройти мастер-классы по Python. Python имеет очень своеобразное представление о том, что такое классы, заимствованное из языка Smalltalk.
В большинстве языков класс — это просто фрагменты кода, которые описывают, как создать объект. Это отчасти верно и в Python:
>>> class ObjectCreator(object): . pass . >>> my_object = ObjectCreator() >>> print(my_object)
Но в Python классы это нечто большее. Классы это тоже объекты.
Как только вы используете ключевое слово class , Python выполняет его и создает объект. Данные создаст в памяти объект с именем «ObjectCreator».
>>> class ObjectCreator(object): . pass .
Этот объект (класс) сам по себе может создавать объекты (экземпляры), и поэтому он является классом.
Но все же это объект, а значит
- Вы можете присвоить его переменной
- Вы можете скопировать его
- Вы можете добавить к нему атрибуты
- Вы можете передать его как параметр функции
>>> print(ObjectCreator) # Вы можете распечатать класс, потому что это объект >>> def echo(o): . print(o) . >>> echo(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
Динамическое создание классов
Поскольку классы являются объектами, вы можете создавать их на лету, как и любой объект.
Во-первых, вы можете создать класс в функции, используя 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) # функция возвращает класс, а не экземпляр >>> print(MyClass()) # вы можете из этого создать объект
Но это не так динамично, так как вам все равно придется писать весь класс самостоятельно.
Поскольку классы являются объектами, они должны быть созданы чем-то.
Когда вы используете ключевое слово class , Python создает этот объект автоматически. Но, как и в большинстве случаев в Python, он позволяет делать это вручную.
Помните функцию type ? Старая добрая функция, которая позволяет узнать, к какому типу относится объект:
>>> print(type(1)) >>> print(type("1")) >>> print(type(ObjectCreator)) >>> print(type(ObjectCreator()))
Что ж, у type есть совершенно другие возможности, он также может создавать классы на лету. type может принимать описание класса как параметры и возвращать класс
(Я знаю, глупо, что одна и та же функция может иметь два совершенно разных использования в зависимости от параметров, которые вы ей передаете. Это проблема из-за обратной совместимости в Python)
type работает следующим образом:
type(name, bases, attrs)
- name : название класса
- base : кортеж родительских классов для наследования (может быть пустым)
- attrs : словарь, содержащий имена и значения атрибутов
>>> class MyShinyClass(object): . pass
можно создать вручную следующим образом:
>>> MyShinyClass = type('MyShinyClass', (), <>) # returns a class object >>> print(MyShinyClass) >>> print(MyShinyClass()) # создать экземпляр из класса
Вы заметите, что мы используем «MyShinyClass» как имя класса и как переменную для хранения ссылки на класс. Они могут быть разными, но нет причин усложнять ситуацию.
type принимает словарь для определения атрибутов класса
>>> class Foo(object): . bar = True
Можно превратить в:
>>> Foo = type('Foo', (), )
И использовать как обычный класс:
>>> print(Foo) >>> print(Foo.bar) True >>> f = Foo() >>> print(f) >>> print(f.bar) True
И, конечно, вы можете наследовать от него:
>>> class FooChild(Foo): . pass
>>> FooChild = type('FooChild', (Foo,), <>) >>> print(FooChild) >>> print(FooChild.bar) # bar унаследован от Foo True
В конце концов, вы захотите добавить методы в свой класс. Просто определите функцию с соответствующей сигнатурой и назначьте ее как атрибут.
>>> def echo_bar(self): . print(self.bar) . >>> FooChild = type('FooChild', (Foo,), ) >>> 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 ?
Что ж, я думаю, это вопрос согласованности с str , классом, который создает строковые объекты, и int с классом, который создает целочисленные объекты. type — это просто класс, который создает объекты класса.
Вы увидите это, проверив атрибут __class__ .
Все, абсолютно все, в Python является объектом. Сюда входят целые числа, строки, функции и классы. Все они объекты. И все они созданы из класса:
>>> age = 35 >>> age.__class__ >>> name = 'bob' >>> name.__class__ >>> def foo(): pass >>> foo.__class__ >>> class Bar(object): pass >>> b = Bar() >>> b.__class__
Теперь, что такое __class__ у любого __class__ ?
>>> age.__class__.__class__ >>> name.__class__.__class__ >>> foo.__class__.__class__ >>> b.__class__.__class__
Итак, метакласс — это просто материал, который создает объекты класса.
Если хотите, можете назвать это «фабрикой классов».
type — это встроенный метакласс, который использует Python, но, конечно, вы можете создать свой собственный метакласс.
Атрибут metaclass
Метаклассы в Python 2
В Python 2 вы можете добавить атрибут __metaclass__ при написании класса (синтаксис Python 3 см. В следующем разделе):
class Foo(object): __metaclass__ = something. # Что-нибудь [. ]
Если вы это сделаете, Python будет использовать метакласс для создания класса Foo .
Осторожно, это сложно.
Сначала вы пишете class Foo(object) , но объект класса Foo еще не создан в памяти.
Python будет искать __metaclass__ в определении класса. Если он его найдет, он будет использовать его для создания класса объекта Foo . Если это не так, он будет использовать type для создания класса.
Прочтите это несколько раз
Когда вы сделаете это:
class Foo(Bar): pass
Python делает следующее:
Есть ли в Foo атрибут __metaclass__ ?
Если да, создайте в памяти объект класса с именем 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): .
Прочтите раздел ниже, чтобы узнать, как Python справляется с этим.
Пользовательские метаклассы
Основная цель метакласса, автоматически изменять класс при его создании.
Обычно вы делаете это для API, где хотите создавать классы, соответствующие текущему контексту.
Представьте себе глупый пример, в котором вы решили, что атрибуты всех классов в вашем модуле должны быть написаны в верхнем регистре. Есть несколько способов сделать это, но один из них — установить __metaclass__ на уровне модуля.
Таким образом, все классы этого модуля будут созданы с использованием этого метакласса, и нам просто нужно указать метаклассу перевести все атрибуты в верхний регистр.
К счастью, __metaclass__ на самом деле может быть любым вызываемым объектом, он не обязательно должен быть формальным классом (я знаю, что что-то с «class» в его имени не обязательно должно быть классом, поймите… это полезно).
Итак, мы начнем с простого примера, используя функцию.
# метаклассу автоматически передаются те же аргументы # которые вы обычно передаете в `type` def upper_attr(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() ># пусть `type` создаст класс return type(future_class_name, future_class_parents, uppercase_attrs) __metaclass__ = upper_attr # это повлияет на все классы в модуле class Foo(): # __metaclass__ не будет работать с «объектом» # но мы можем определить здесь __metaclass__, чтобы воздействовать только на этот класс # и это будет работать с дочерними элементами «объекта» bar = 'bip'
>>> hasattr(Foo, 'bar') False >>> hasattr(Foo, 'BAR') True >>> Foo.BAR '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_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 , который упростит наследование (потому что, у вас могут быть метаклассы которые наследуются от метаклассов):
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)
Вот и все. Больше в метаклассах больше ничего нет.
Причина сложности кода с использованием метаклассов заключается не в метаклассах, а в том, что вы обычно используете метаклассы для выполнения извращенных вещей, основанных на интроспекции, манипулировании наследованием, переменными, такими как __dict__ и т.д.
В самом деле, метаклассы особенно полезны для черной магии и, следовательно, для сложных вещей. Но сами по себе они просты:
- Перехватить создание класса
- Изменить класс
- Вернуть измененный класс
Зачем для метаклассов использовать классы, а не функций?
Поскольку metaclass может принимать любые вызываемые объекты, зачем использовать класс, если он явно более сложен?
Для этого есть несколько причин:
- Ясные намерение. Когда вы читаете 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()
Но если вы сделаете это:
person = Person(name='bob', age='35') print(person.age)
Он не вернет объект IntegerField . Он вернет int и даже может взять его прямо из базы данных.
Это возможно, потому что models.Model определяет __metaclass__ и использует некоторую магию, которая превратит Person , которую вы только что определили с помощью простых операторов, в сложный крючок для поля базы данных.
Django делает что-то сложное простым, предоставляя простой API и используя метаклассы, воссоздавая код из этого API, чтобы делать реальную работу за кулисами.
Последнее слово
Во-первых, вы знаете, что классы — это объекты, которые могут создавать экземпляры.
На самом деле классы сами по себе являются экземплярами метаклассов.
>>> class Foo(object): pass >>> id(Foo) 142630324
В Python все является объектом, и все они являются экземплярами классов или экземплярами метаклассов.
type на самом деле является отдельным метаклассом. Это не то, что вы могли бы воспроизвести на чистом Python, это делается путем небольшого мошенничества на уровне реализации.
Во-вторых, метаклассы сложны. Возможно, вы не захотите использовать их для очень простых изменений класса. Вы можете менять классы, используя два разных методы:
- monkey patching
- Декораторы классов
В 99% случаев, когда вам нужно изменить класс, вам лучше использовать их.
Но в 98% случаев вам вообще не нужно менять класс.
Что такое метаклассы в python

МЕРОПРИЯТИЯ
YADRO X MOSCOW USER GROUP C++ MEETUP
Комментарии
Популярные По порядку
Не удалось загрузить комментарии.
ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ
ООП на Python: концепции, принципы и примеры реализации
Программирование на Python допускает различные методологии, но в его основе лежит объектный подход, поэтому работать в стиле ООП на Python очень просто.
3 самых важных сферы применения Python: возможности языка
Существует множество областей применения Python, но в некоторых он особенно хорош. Разбираемся, что же можно делать на этом ЯП.
Программирование на Python: от новичка до профессионала
Пошаговая инструкция для всех, кто хочет изучить программирование на Python (или программирование вообще), но не знает, куда сделать первый шаг.
Метаклассы в 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
Но в Питоне класс это нечто большее — классы также являются объектами.
Как только используется ключевое слово 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 # функция возвращает класс, а не экземпляр >>> print MyClass() # можно создать экземпляр этого класса
Однако это не очень-то динамично, поскольку по-прежнему нужно самому писать весь класс целиком.
Поскольку классы являются объектами, они должны генерироваться чем-нибудь.
Когда используется ключевое слово class , Питон создаёт этот объект автоматически. Но как и большинство вещей в Питоне, есть способ сделать это вручную.
Помните функцию type ? Старая-добрая функция, которая позволяет определить тип объекта:
>>> print type(1) >>> print type("1") >>> print type(ObjectCreator) >>> print type(ObjectCreator())
На самом деле, у функции type есть совершенно иное применение: она также может создавать классы на ходу. type принимает на вход описание класса и созвращает класс.
(Я знаю, это по-дурацки, что одна и та же функция может использоваться для двух совершенно разных вещей в зависимости от передаваемых аргументов. Так сделано для обратной совместимости)
type работает следующим образом:
type(, , # для наследования, может быть пустым )
>>> class MyShinyClass(object): . pass
может быть создан вручную следующим образом:
>>> MyShinyClass = type('MyShinyClass', (), <>) # возвращает объект-класс >>> print MyShinyClass >>> print MyShinyClass() # создаёт экземпляр класса
Возможно, вы заметили, что мы используем «MyShinyClass» и как имя класса, и как имя для переменной, содержащей ссылку на класс. Они могут быть различны, но зачем усложнять?
type принимает словарь, определяющий атрибуты класса:
>>> class Foo(object): . bar = True
можно переписать как
>>> Foo = type('Foo', (), )
и использовать как обычный класс
>>> print Foo >>> print Foo.bar True >>> f = Foo() >>> print f >>> print f.bar True
Конечно, можно от него наследовать:
>>> class FooChild(Foo): . pass
превратится в
>>> FooChild = type('FooChild', (Foo,), <>) >>> print FooChild >>> print FooChild.bar # bar is inherited from Foo True
В какой-то момент вам захочется добавить методов вашему классу. Для этого просто определите функцию с нужной сигнатурой и присвойте её в качестве атрибута:
>>> def echo_bar(self): . print self.bar . >>> FooChild = type('FooChild', (Foo,), ) >>> 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__ >>> name = 'bob' >>> name.__class__ >>> def foo(): pass >>> foo.__class__ >>> class Bar(object): pass >>> b = Bar() >>> b.__class__
А какой же __class__ у каждого __class__ ?
>>> a.__class__.__class__ >>> age.__class__.__class__ >>> foo.__class__.__class__ >>> b.__class__.__class__
Итак, метакласс это просто штука, создающая объекты-классы.
Если хотите, можно называть его «фабрикой классов»
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 является собственным метаклассом. Это нельзя воспроизвести на чистом Питоне и делается небольшим читерством на уровне реализации.
- руками
- декораторы классов
Но в 99% случаев вам вообще не нужно изменять классы 🙂
