Пролог

Если вы не поймете некоторые вещи в этом прологе, то не расстраивайтесь. Я постараюсь объяснить последовательно, от простого к сложному. Однако для полного понимания этого текста надо знать как основы синтаксиса языка Python, так и ООП в Python.

Задумывались ли вы, почему функция isinstance(int, object) возвращает True? Объяснение, что все является объектом, которое можно часто услышать, хоть и является правдой, но не дает ответа на вопрос и на самом деле есть профанация, потому что не дает настоящего понимания, а создает только его видимость. Ведь совсем непонятно, почему int (как и любой другой стандартный класс) является непременно экземпляром базового класса. Да, он является подклассом, но почему именно экземпляром? Ответ будет дан по ходу изложения.

Что такое объект?

(Кто понимает это, может смело пропустить эту главу. Она сделана для линейности повествования.)

Для тех, кто изучал Python и не пытался осознать, фраза: «В Python все есть объект!» не несет никакого смысла. Чтобы понять, что это значит, надо прежде всего знать, что такое объект. Объект - это сущность, которая имеет состояние и поведение. Состояние объекта определяется его полями, а поведение определяется его методами. Функция dir позволяет нам увидеть атрибуты объекта.

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

>>> class MyClass:
    	n = 3
    
>>> MyClass.n
3
>>> instance = MyClass()
>>> instance.m = 5
>>> instance.m
5
>>> def greet():
        print('hi')
    
>>> instance.f = greet
>>> instance.f()
hi

Приведу некоторые примеры объектов в Python:

Встроенные типы данных:

  • Целые числа: 1, -100

  • Вещественные числа: 3.14, -5.2

  • Строки: "Hello", "World"

  • Списки: [1, 2, 3], ["a", "b", "c"]

  • Кортежи: (1, 2, 3), ("a", "b", "c")

  • Множества: {1, 2, 3}, {"a", "b", "c"}

  • Словари: {"name": "John", "age": 30}

Объекты, созданные пользователем:

  • Функции: def my_function(): ...

  • Классы: class MyClass: ...

  • Экземпляры классов: my_object = MyClass()

Стандартные библиотечные объекты:

  • Файлы: open("myfile.txt", "r")

  • Модули: import math

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

Итак, в Python все является объектом: числа, строки, булевы значения, списки, словари и т.д. Все это объекты в языке Python. Эти объекты созданы, образованы от соответствующих классов или типов данных: int, str, bool, list, dict и т.д. Понятия класс и тип данных, по сути, являются синонимами в Python. Но классы тоже являются объектами. То есть класс объект для создания объектов. Поэтому есть что-то, что может создавать классы. И это метакласс. Метакласс тоже объект (и, в свою очередь, класс). Его нельзя создать другим метаклассом. Он находится на вершине.

Здесь можно провести следующую аналогию: Есть какое-то оборудование: инструменты, машины, станки – в общем средства для производства материальных благ. Но есть то, что их производит – средства для производства средств производства – машиностроение, станкостроение или тяжелая промышленность. Конкретное значение, например, целое число 3 – материальное благо, класс – средство для производства этих материальных благ, а метаклассы –средства для производства средств производства.

Метакласс это то, что может создавать классы. Можно сказать, что метакласс создает объект-класс по созданию объектов-экземпляров класса. В Python метаклассом является объект type. Это также одноименная функция для определения типа объекта. Но функция type имеет другое применение: она также может создавать классы динамически. То есть одна и та же функция может использоваться для двух совершенно разных вещей в зависимости от передаваемых аргументов. Семантика у type будет совершенно разной в зависимости от того, сколько аргументов вы ему сообщите 1 или 3. Это сделано по историческим причинам, для сохранения обратной совместимости. type принимает на вход описание класса (имя класса, кортеж родительских классов и словарь с атрибутами) и возвращает динамически новый класс, новый тип данных.

type(obj) – определить тип

type(name, bases, dict) – создать тип

Один аргумент:

obj : Объект, тип которого требуется определить.

Три аргумента:

name : Имя для создаваемого типа (становится атрибутом __name__);

bases : Кортеж с родительскими классами (становится атрибутом __bases__);

dict : Словарь, который будет являться пространством имён для тела класса (становится атрибутом __dict__).

На самом деле все стандартные типы данных порождены метаклассом type.

>>> type(3) # Целое число порождено классом целых чисел
<class 'int'>
>>>
>>> type(int) # Класс целых чисел, в свою очередь, порожден метаклассом type
<class 'type'>
>>>
>>> class A: pass # Пользовательские классы также создаются этим метаклассом
>>> type(A)
<class 'type'>
>>>
>>> type(type) # И даже метакласс type как бы порождает сам себя
<class 'type'>

То, что type является метаклассом для самого себя, есть уловка на уровне реализации. Он является встроенным метаклассом в ядре языка и не создается каким-либо другим классом или метаклассом.

Метакласс: инструмент динамического создания классов

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

Вместо декларативного создания классов с помощью оператора class, который создает класс статически. Динамическое создание классов позволяет определять поведение и функциональность классов во время выполнения программы, а не только на этапе компиляции.

Как создать простейший класс с помощью метакласса

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

>>> class MyClass:
        pass
>>> MyClass()
<main.MyClass object at 0x00000139620C3C40>

Python выполняет следующее:

>>> MyClass = type('MyClass', (), {})
>>> MyClass()
<main.MyClass object at 0x00000139620C3D60>

Здесь type создает новый класс, который называется MyClass. Аргументы в () на самом деле представляют собой кортеж базовых классов, от которых наследуется MyClass. В данном случае, поскольку MyClass не имеет явно указанного родительского класса, он наследуется от object по умолчанию. Третий аргумент {} - это словарь, содержащий атрибуты класса MyClass (как поля, так и методы).

Рассмотрим более сложный пример.

Создание класса с помощью метакласса более сложный случай. Сравнение с классическим созданием

Посмотрим на создание класса с помощью type, у которого будет родитель и атрибуты. Но для начала приведу аналог того, как это делается обычно.

Классическое создание класса
Классическое создание класса
Динамическое создание класса, с помощью метакласса
Динамическое создание класса, с помощью метакласса

Мы создали класс ChildClass сначала обычным способом, а затем так, как Python делает это под капотом.

У класса ChildClass есть родитель ParentClass, также он имеет одно поле класса species и два метода __init__ и greet.

В функцию type первым аргументов указываем имя класса ChildClass, далее указываем кортеж базовых классов (классов, от которых наследуется наш класс) это будет ParentClass и object (но object указывается автоматически) и последний аргумент словарь с атрибутами.  Первый атрибут spicies = 'Homo sapiens' (прописывается имя атрибута в виде строки и через доветочие указывается его значение). Далее надо оговориться:

Когда мы динамически создаем класс, то в него можно добавлять и методы. Методы – это тоже атрибуты. Атрибуты, которые ссылаются на объект функцию. Это возможно сделать двумя способами. Для первого способа надо предварительно создать функцию в программе. Во втором с помощью lambda-функций.

Добавим два метода __init__ и greet. Для метода __init__ вместо ссылки на реальную функцию укажем lambda-функцию. Для второго метода отдельно зададим функцию в программе и укажем имя метода и через двоеточие передадим ссылку на функцию.

Сравниваем выводы: классы родители одинаковые, доступ к полю класса species есть, можно создать экземпляр класса и передать аргументы в конструктор, через ссылку child можно обратиться к полю species и методу greet и получить значение и вывод.

То есть с помощью метакласса type динамически в программе создается новый класс ChildClass, который по структуре в точности повторяет первое (классическое) определение. type это метакласс, который Python внутренне использует для создания всех классов.

Некоторая справка.

В Python есть две функции isinstance и issubclass.

Функция isinstance()

Функция issubclass()

Функция isinstance() в Python проверяет, является ли объект экземпляром указанного класса или его подкласса (обратите внимание). Она принимает два аргумента:

* Объект, который нужно проверить.

* Класс или кортеж классов, с которыми нужно сравнить объект.

Функция isinstance() возвращает True, если объект является экземпляром указанного класса или его подкласса, и False в противном случае.

Функция issubclass() в Python проверяет, является ли класс подклассом указанного класса или эквивалентен (обратите внимание) ему. Она принимает два аргумента:

* Класс, который нужно проверить.

* Класс или кортеж классов, с которыми нужно сравнить проверяемый класс.

Функция issubclass() возвращает True, если проверяемый класс является подклассом указанного класса или эквивалентен ему, и False в противном случае.

type.mro

Порядок разрешения методов (Method Resolution Order — MRO)

Демонстрация работы __mro__
Демонстрация работы __mro__

Содержит кортеж с родительскими типами, выстроенными в порядке разрешения методов.

MRO - это список классов, который определяет порядок, в котором Python ищет атрибуты и методы в иерархии классов. MRO используется несколькими методами и операторами в Python, включая issubclass(), isinstance(), а также при поиске атрибутов и методов объекта.

Перечислю несколько утверждений, которые разберем дальше:

  1. В Python все классы, явно или неявно, наследуются от базового класса object. Это означает, что все классы в Python, включая стандартные встроенные типы, такие как int, float и str, наследуют методы и атрибуты от класса object.

  2. Все встроенные (стандартные) типы создаются с помощью метакласса type (и даже object).

  3. Так как метакласс type является объектом и также классом то он тоже должен наследоваться от object.

  4. Любое конкретное значение является экземпляром класса object.

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

Объяснение и доказательство первого пункта:

>>> issubclass(int, object) # Возвращает True так как любой встроенный класс наследуется от object
True
>>> class A: pass # Следующая проверка возвращает True так как любой пользовательский класс автоматически будет подклассом object
>>> issubclass(A, object)
True

Вопросы для закрепления:

  • Что выведет issubclass(int, int)?

  • Что выведет issubclass(object, object)?

  • Что выведет issubclass(type, type)?

>>> issubclass(int, int)
True
>>> issubclass(object, object)
True
>>> issubclass(type, type)
True

Результаты должны быть понятны так как я просил обратить внимание на то, что функция issubclass() проверяет, является ли класс подклассом указанного класса или эквивалентен ему. Здесь аналогия с множествами: любое множество является подмножеством самого себя.

Объяснение и доказательство второго пункта:

>>> isinstance(int, type) # Это корректный для нас результат так как в начале я показывал это
True
>>> isinstance(object, type) # И даже класс стоящий на вершине иерархии object будет экземпляром метакласса type. Возвращает True так как object есть класс, а значит порожден метаклассом.
True

Еще немного вопросов, ответ на второй вопрос будет дан после доказательства утверждения номер три:

  • Что выведет isinstance(3, int)?

  • Что выведет isinstance(int, object)?

  • Что выведет isinstance(bool, int)?

>>> isinstance(3, int) # Этот результат довольно очевиден. Целые числа создаются с помощью класса int
True
>>> isinstance(int, object) # Вывод этой проверки не понятен
True
>>> isinstance(bool, int) # bool является подклассом, а не экземпляром int
False

Вторая проверка вводит в ступор. Неужели базовый object является метаклассом, который порожадет все типы данных? Но в самом начале я показывал, что встроенные типы порождаются type. Противоречие?

Подумайте над следующими вопросами перед объяснением третьего утверждения, чтобы лучше прочувствовать их:

  • Что выведет isinstance(type, object)?

  • Что выведет isinstance(type, type)?

  • Что выведет isinstance(object, object)?

Объяснение и доказательство третьего пункта (самого интересного потому что даст ответ на вопрос из пролога):

>>> issubclass(type, object) # Возвращает True так как type также является классом и должен, как и все классы, наследоваться от object.
True
>>> isinstance(type, object) # А isinstance проверяет все подклассы, а раз у object подкласс type, то будет выполнена следующая проверка.
True
>>> isinstance(type, type) # type как бы порождает сам себя (я уже об этом говорил). Это сделано специально, чтобы не существовала бесконечно метаклассов друг над другом.
True
>>> isinstance(object, object) # Также как и во второй проверке isinstance проверяет все подклассы, а раз у object подкласс type, то будет сделана последняя проверка из пункта два. И проверка возвращает True так как object является экземпляром от type.
True

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

Теперь, наконец, ответ на вопрос из пролога, который вы теперь можете дать сами.

Но перед этим вопрос на понимание:

  • Что выведет issubclass(object, type)?

>>> issubclass(object, type) # Возвращает False потому что object является суперклассом для всех остальных классов.
False

Почему же isinstance(int, object) возвращает True?

Может возникнуть заблуждение, что если A является экземпляром B (то есть isinstance(A, B) равно True), то можно создать экземпляры A с помощью B. Однако это не всегда верно.

То есть неправда то, что если int является экземпляром класса object (здесь говорится именно что isinstance(int, object) есть True и это может сильно запутать потому что на самом деле это не так, но из-за того, что метакласс type является классом и наследуется от object, а isinstance проверяет подклассы, проверка возвращает True), то значит можно создать int через объект object. Подобно тому как object создается через type. Но при этом isinstance(type, object) тоже True, а на самом деле False. Хотя если дословно переводить название функции (является экземпляром/есть экземпляр) может показаться, что object есть метакласс над самым "верхним" метаклассом type. Это происходит из описанного выше запутанного наследования в Python.

Логика примерно такая: если int порождает объекты целого типа (например 3), то isinstance(3, int) возвращает True, если что-то порождает int (это метакласс type), то isinstance(int, type) возвращает True, но object не является метаклассом. Так почему isinstance(int, object) возвращает True, ведь object не создает int, а просто наследует.

Эта запутанность возникает если не знать про то, что type подкласс object (об этом не говорит переменная __mro__ когда вызывается для класса int, потому что как классы они на одной ступени, но как экземпляр int ниже type). То есть не учитывается существование промежуточного метакласса type. Поэтому я составил следующую иерархию для лучшего понимания. Не уверен, что вторую иерархию можно так изобразить. Но чтобы усвоить, что я имею в виду, лучше взглянуть.

Когда рассматриваем классы (кто кого наследует):

object
|
+--> type, int, … (все собственные и встроенные типы данных)

Я для простоты написал все типы данных, понятно, что можно выделить больше ступеней, например, int наследует bool, но для понимания достаточно этого

Когда рассматриваем экземпляры (кто кого инстанцирует):

type
|
+--> Метаклассы (не обязательная ступень)
   |
   +--> int, str, … (все собственные и встроенные типы данных)
      |
      + --> объекты целого типа, строки, …

При этом будет ошибкой как в наследовании сказать, что type порождает объекты целого типа (если проводить аналогию с классами, то object родительский класс для любого подкласса, даже если он имеет посредников). Поэтому эта схема довольно условная. isinstance(3, type) есть False так как у type нет потомков классов (он может наследовать только метаклассы) и isinstance просто не найдет класс int, который и создает объекты целого типа.

И, как говорится, проницательный читатель может спросить: Не возникает ли парадокса в том, как type может наследоваться от object, если он еще не создан? А чтобы type мог его создать, ему надо наследоваться от object (object создается классом type, а type, в свою очередь, наследуется от object).

Чуть подробнее и немного занудно об этом парадоксе (кто понял вопрос выше может пропустить):

object создается метаклассом type: type является встроенным метаклассом, используемым для создания новых классов в Python. Когда создается новый класс, метакласс type используется для его создания.

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

Объяснение данного парадокса

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

Объект type является встроенным в ядре языка и не создается каким-либо другим метаклассом и присутствует в интерпретаторе Python с самого начала. Это означает, что он не создается во время выполнения программы. Класс object создается с помощью функции type() во время инициализации интерпретатора Python.

Порядок событий следующий:

  1. Интерпретатор Python запускается.

  2. Интерпретатор Python создает класс object с помощью функции type().

  3. Класс type определяется как потомок класса object.

  4. Все остальные классы в Python создаются с помощью функции type(), которая теперь является потомком класса object.

Может быть класс object также существует в ядре. И то, что type наследуется от object, а object создается type, делается на уровне реализации специально.

Ну и объяснение четвертого пункта:

>>> isinstance(3, int) # Возвращает True так как целое число 3 создается классом int.
True
>>> isinstance(3, object) # Возвращает True так как потомком object является класс int, который в свою очередь создает конкретные значения целого типа (как видно выше).
True

Думаю, тут все понятно и без объяснений.

Создание пользовательских метаклассов

Именно потому что метаклассы также, в свою очередь, являются классами, они имеют магические методы унаследованные от object.

Методы new и init у метакласса

Метод __new__ метакласса

Метод __init__ метакласса

Метод __new__ метакласса вызывается при создании нового класса. Он используется для создания нового экземпляра класса и возвращает его. Магический метод new вызывается непосредственно перед созданием класса.

Аргументы:

cls: Сам метакласс.

name: Имя нового класса.

bases: Кортеж базовых классов нового класса.

attrs: Словарь атрибутов нового класса.

Метод __init__ метакласса вызывается после создания нового класса, но до его инициализации. Он используется для инициализации класса и выполнения любой необходимой настройки для нового класса. Конструктор срабатывает сразу же после создания объекта (класса).

Аргументы:

cls: Сам класс.

name: Имя нового класса.

bases: Кортеж базовых классов нового класса.

attrs: Словарь атрибутов нового класса.

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

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

Первый способ

Создание метакласса с помощью функции
Создание метакласса с помощью функции

Первый способ реализуется используя функцию. Она принимает три параметра: имя класса, список базовых классов и словарь атрибутов класса.

Функция возвращает то, что вернет функция type. Этому метаклассу type мы передаем все эти три параметра. Еще у нас должен добавляться атрибут description. Вот так вот можно описать простейший метакласс с одним атрибутом. И далее его применяем. Пишем наш класс и в круглых скобках указываем специальный параметр metaclass и передаем ему ссылку на нужный метакласс.

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

Заметьте, что метод в этом классе мы определили привычным способом, а не отдельно в программе как раньше, это преимущество использования собственных метаклассов. Все атрибуты внутри класса автоматически попадают в словарь attrs. И затем когда вызыватся метакласс type с этим словарем, то в нем уже присутствует этот метод.

Как выполняется создание класса

То есть когда отрабатывает функция my_metaclass интерпретатор языка Python автоматически передает имя (MyClass), кортеж из базовых классов и словарь атрибутов, которые мы или явно (description), или неявно (в самом классе метод my_method) передали в словарь attrs. И затем формируется новый класс MyClass.

Второй способ

Второй способ реализуется с помощью классов. Объявим метакласс MyMetaclass и так как это метакласс, то он должен наследоваться от метакласса type.

Создание метакласса с помощью класса
Создание метакласса с помощью класса

Здесь добавлять атрибуты и методы можно либо через конструктор, либо для более тонкой настройки лучше переопределять магический метод __new__. В этом примере атрибут добавляется через магический метод new и еще есть дополнительная функциональность (по сравнению с примером выше), которая получается за счет вывода информации как из __new__ так и из __init__. Здесь можно использовать всю мощь ООП, чего нельзя сделать в функции. Поэтому на практике чаще всего поступают именно так.

Разница через что добавлять атрибуты, только в том, что когда переопределяется магический метод __new__, то атрибуты добавляются через словарь attrs (потому что класс еще не создан). А в __init__ работаем через ссылку на класс для непосредственного создания атрибутов класса.

Как выполняется создание класса

Когда будет отрабатывать строчка с определением класса MyClass, то вызовется класс MyMetaclass, в коллекцию attrs будут переданы все атрибуты, что присутствуют внутри класса MyClass (метод my_method), в base будет передан кортеж из базовых классов, в name будет передано имя класса MyClass. В new в cls будет передана ссылка на сам метакласс. В __init__ в параметр cls будет передана ссылка на уже созданный класс MyClass. И три остальные параметры как и раньше. Все это Python делает за нас автоматически и в этом огромное преимущество перед просто использованием объекта type.

Способы изменения поведения класса

Управление поведением классов и экземпляров в Python

Существует несколько механизмов управления поведением классов и экземпляров в Python: декораторы, наследование и метаклассы. Каждый из них выполняет уникальную роль.

Метаклассы: проникновение в код до объявления класса

Метаклассы выделяются своим особым статусом. Они позволяют вмешиваться в процесс создания класса еще до его объявления. В отличие от декораторов и наследования, метаклассы не работают с уже существующими классами.

Декораторы классов: настройка после объявления

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

Наследование: уточнение поведения во время выполнения

Наследование предоставляет доступ к атрибутам и методам родительского класса. Оно уточняет поведение новых классов и экземпляров во время выполнения, предоставляя возможность прямого вызова или использования метода super для доступа к функциональности родительского класса.

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

Зачем нужны метаклассы. Примеры

Далее я привожу области применения, но не показываю на примерах. Конкретные примеры можно найти в этой статье.

Django ORM

Singleton

ABCMeta

• Область применения: Создание и настраивание моделей базы данных.

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

• Зачем: Метаклассы упрощают работу с базами данных в Django, предоставляя автоматизированный и настраиваемый способ создания и управления моделями.

• Область применения: Создание классов, которые имеют только один экземпляр.

• Как: Метаклассы можно использовать для перехвата создания нового экземпляра класса и возврата существующего экземпляра вместо этого.

• Зачем: Синглтоны полезны для объектов конфигурации, кэшей или других объектов, которые должны существовать только в одном экземпляре.

• Область применения: Создание абстрактных базовых классов (ABC) и проверка их реализации подклассами.

• Как: Метакласс ABCMeta предоставляет функциональность ABC в Python. Он проверяет, реализованы ли абстрактные методы в подклассах, и поднимает исключение, если это не так.

• Зачем: ABCMeta помогает гарантировать, что подклассы реализуют необходимый интерфейс, обеспечивая согласованность и полноту в иерархии классов.

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

Комментарии (14)


  1. RichardMerlock
    29.04.2024 11:41

    Экземпляр класса - это же инстанс, ну когда память под класс выделили, так? А без экземпляра класс просто тип бестелесный, да?


    1. kale
      29.04.2024 11:41

      Полагаю что нет. Как минимум, есть инстанс типа класса


    1. danyakr Автор
      29.04.2024 11:41

      Да, экземпляр класса - это инстанс этого класса. Что вы подразумеваете под бестелестностью? То, что у него нет ни полей, ни методов? Это не так. Все классы наследуются как минимум от object и обладают его атрибутами. Самое простое это магический метод __init__, а из полей можно привести в пример переменную __doc__. А экземпляры класса никак не влияют на его атрибуты.


      1. RichardMerlock
        29.04.2024 11:41
        +1

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


        1. kale
          29.04.2024 11:41

          Нет. В питоне у класса есть инстанс, по сути обычный объект с незначительными нюансами. Как бы статик методы на это намекают.


        1. danyakr Автор
          29.04.2024 11:41
          +1

          А про какой вы язык говорите? Если объяснять как дело обстоит в питоне, то можно привести следующий код:

          >>> class MyClass:
              	class_attribute = "Hello"
          
              	@classmethod
              	def class_method(cls):
              		print("This is a class method")
          
              	def instance_method(self):
              		print("This is an instance method")
          
          		
          >>> MyClass.class_attribute
          'Hello'
          >>> MyClass.class_method()
          This is a class method
          >>> MyClass.instance_method(object)
          This is an instance method

          У нас нет экземпляра, но мы можем получить доступ как к полям, так и методам класса. И даже к методам экземпляра (на это указывает параметр self), однако надо передать явно ссылку, потому что когда метод экземпляра вызывает от экземпляра, то в него автоматически (неявно) передается ссылка на этот объект. Здесь, для работы функции, ссылка не важна и передаем ее просто для того, чтобы у нас не поднялось исключение.


  1. passiboom1991
    29.04.2024 11:41

    И насколько это эффективно?


    1. danyakr Автор
      29.04.2024 11:41

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


  1. ARechitsky
    29.04.2024 11:41
    +3

    Может возникнуть заблуждение, что если класс A является экземпляром класса B (то есть isinstance(A, B) равно True), то можно создать экземпляры класса A с помощью класса B. Однако это не всегда верно.

    Это вообще не верно

    Класс А является экземпляром класса B. Это значит что B - метакласс. Класс int - экземпляр класса type. Очевидно что 3 и 7 нельзя создать с помощью класса type.

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

    Это заблуждение очень просто развеять на бытовом примере

    d = Dog()
    isinstance(d, Animal) == True

    Очевидно что d нельзя создать с помощью только класса Animal, это его наследник.

    Когда рассматриваем экземпляры (кто кого инстанцирует)

    isinstance не отвечает на вопрос "кто кого инстанцирует"(!), он отвечает на вопрос "есть ли между первым аргументом и вторым is-a relationship", "может ли А быть интерпретировано как экземпляр класса B". И это снимает существенную часть вопросов - int это класс (тип), экземпляр класса type, а тот - наследник object'а. Очевидно, int можно, например, подставить в функцию которая принимает object.


    1. danyakr Автор
      29.04.2024 11:41

      Да, вы правы, здесь вообще лучше убрать упоминание о классах. Имелось в виду, что если функция isinstance возвращает True, то это еще не говорит о том, что первый объект экземпляр второго.

      Очевидно что d нельзя создать с помощью только класса Animal, это его наследник.

      Вы, верно, ошиблись. d не наследник Animal. Dog наследник Animal как я понял. И тут вы показали пример, о котором говорю и я. isinstance возвращает True, но второй аргумент не порождает первый.

      isinstance не отвечает на вопрос "кто кого инстанцирует"(!)

      В статье не было так написано. Я наоборот писал следующее:

      Хотя если дословно переводить название функции (является экземпляром/есть экземпляр) ... При этом будет ошибкой как в наследовании сказать, что type порождает объекты целого типа ...

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

      Не понял это:

      он отвечает на вопрос ... "может ли А быть интерпретировано как экземпляр класса B" .

      И что вы действительно думаете, что int может интерпретироваться как экземпляр object? int наследник object и не более.


  1. Andrey_Solomatin
    29.04.2024 11:41
    +1

    Метакласс в двух словах: хук для создания класса. Другими словами возможность создать класс программно, а не декларативно.


  1. danilovmy
    29.04.2024 11:41
    +1

    Спасибо за поднятие темы еще раз в этом месяце. Могу отметить, что к пониманию, что такое метаклассы, так и не подобрались. И только в комментариях @Andrey_Solomatin наконец-то дал максимально близкое объяснение. Поблагодарил его отдельно.

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

    MyMetaclass = type('MyMetaClass', (type,), {})

    Управлять поведением класса и экземплярами класса можно несколькими способами: через декоратор, через наследование, через метакласс. Так в чем же разница?

    • Метакласс - официальный способ вклиниться в код ДО объявления класса. Это в статье отмечено, но не подчеркнуто, а ведь это единственное существенное отличие.

    • Декоратор класса: может донастраивать класс ПОСЛЕ объявления, поскольку получает на вход уже объявленный класс.

    • Родительский класс - уточняет поведение класса или экземпляров в runtime, предоставляя доступ к атрибутам и методам как напрямую так и через вызов super.

    Именно хук beforeCreateClass и является тем важным инструментом метакласса, ради чего нужны метаклассы. И вот именно этот хук используется в ABC, хотя лучше бы использовать Protocol. Не реализовал метод протокола - лови Exception ДО создания класса на старте а не в runtime. Но это тоже шито белыми нитками из-за возможности объявления классов в runtime. Именно поэтому я категорически против Protocols/ABC, поскольку они способны добавить более сложные ошибки в код вместо упрощения.

    Причина использования Metaclass в Django аналогична. На объявлении класса модели происходит внесение модели в реестр моделей (django.apps.apps.all_models). Последнее можно было бы решить более стандартно, например, через сигналы, но что сделано - то сделано.

    В остальном по статье - утверждение "в Python все является объектом" намного глубже, чем может показаться на первый взгляд, и желаю автору добраться до настоящего понимания этого высказывания. Для этого, вероятно, стоит посмотреть повнимательнее внутрь cpython/Objects/typeobject.c


  1. danyakr Автор
    29.04.2024 11:41
    +1

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


    1. danilovmy
      29.04.2024 11:41

      да, пожалуйста. Посмотри ещё соседнюю статью про метаклассы, там тоже интересно и обсуждение активное.