Классы в Python

Классы и экземпляры, методы и свойства, наследование, магические методы, дескрипторы и метаклассы

Вячеслав Федоров

Лекция 6

Разработка и применение ПО

в физических исследованиях

class

Содержание

От объявления класса до метаклассов

01

Классы и экземпляры

02

Методы и свойства

03

Наследование

04

Магические методы

05

Дескрипторы

06

Метаклассы

Цель лекции

Класс как модель объекта: состояние, его согласованность, единый интерфейс

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

Классы и экземпляры

Объявление класса, создание объекта, атрибуты, __dict__ и __slots__

01

Классы и экземпляры

Методы и свойства

Наследование

Магические методы

Дескрипторы

Метаклассы

Объявление

класса

class Particle:
    """Материальная точка."""
 
    def __init__(self, mass, v=0.0):
        self.mass = mass
        self.v = v
 
    def kinetic_energy(self):
        return self.mass * self.v ** 2 / 2
 
p = Particle(2.0, v=3.0)
p.kinetic_energy()       # 9.0

ЭлементНазначение
class Particle:создание объекта класса и связывание имени
__init__начальное состояние экземпляра
selfэкземпляр; явный первый параметр
p.massатрибут экземпляра в p.__dict__
Particle(...)создание экземпляра вызовом класса

Пояснение

Методы — функции в теле класса; self — обычный параметр, имя которого закреплено соглашением.

Создание

экземпляра

Particle(2.0, v=3.0)

__new__(Particle, 2.0, v=3.0)

создание пустого объекта

obj.__init__(2.0, v=3.0)

заполнение атрибутов

p — готовый экземпляр

class PositiveFloat(float):
    def __new__(cls, value):
        if value <= 0:
            raise ValueError("нужно > 0")
        return super().__new__(cls, value)
 
PositiveFloat(2.5) + 1   # 3.5
PositiveFloat(-1)        # ValueError

Пояснение

__init__ называют конструктором условно: объект уже создан методом __new__. У неизменяемых типов значение задаётся в __new__.

Соглашения

об именах

ЗаписьСмыслМеханизм
massпубличный атрибут—
_cacheвнутренний атрибутсоглашение без проверки языком
__tagзащита от совпадения имён в подклассахискажение: _Particle__tag
__init__специальный методбез искажения: __ с обеих сторон

Пояснение

Модификаторов private и protected нет: доступ ограничивается соглашением, а не языком.

Важно

Искажение имён не скрывает данные: p._Particle__tag по-прежнему доступен.

Демонстрация:

класс и экземпляр

01

Класс Particle с методом и атрибутом __tag

02

Создание экземпляра и вызов метода

03

Атрибуты в p.__dict__: имя _Particle__tag

04

Обращение к __tag: AttributeError

05

Тип экземпляра и тип класса

06

Новый атрибут после создания

Что наблюдается

__tag хранится под искажённым именем _Particle__tag; атрибуты экземпляра — записи словаря, и новый добавляется присваиванием.

root@lab — запись со стенда

Атрибуты

класса и экземпляра

class Detector:
    unit = "keV"          # атрибут класса
    hits = []             # общий список
 
    def __init__(self, name):
        self.name = name  # атрибут экземпляра

Важно

Изменяемый атрибут класса общий для всех экземпляров: a.hits.append(...) меняет и b.hits.

Пояснение

Присваивание a.unit = "MeV" создаёт атрибут экземпляра и заслоняет атрибут класса только у a.

a.unit

a.__dict__

1 — атрибуты экземпляра

Detector.__dict__

2 — атрибуты класса

базовые классы

3 — в порядке MRO

Демонстрация:

атрибуты класса

01

Класс с атрибутами класса unit и hits

02

Общий список hits у двух детекторов

03

Заслонение атрибута класса атрибутом экземпляра

04

Атрибут класса после del

05

setattr и getattr с именем-строкой

06

Опечатка в имени: лишний атрибут

Что наблюдается

a.hits и b.hits — один список; присваивание a.unit меняет только a; запись b.treshold без ошибки добавила лишний атрибут.

root@lab — запись со стенда

Словарь

атрибутов

det = Detector("A")
cfg = {"gain": 2.5, "threshold": 0.1}
for name, value in cfg.items():
    setattr(det, name, value)
 
getattr(det, "gain")          # 2.5
getattr(det, "offset", 0.0)   # по умолчанию
hasattr(det, "threshold")     # True
vars(det)
# {'name': 'A', 'gain': 2.5, 'threshold': 0.1}

ФункцияДействие
getattr(obj, name, default)чтение по имени-строке
setattr(obj, name, value)запись
hasattr(obj, name)проверка наличия
delattr(obj, name)удаление
vars(obj)словарь атрибутов obj.__dict__

Важно

Опечатка в присваивании не вызывает ошибки: det.treshold = 0.2 создаёт новый атрибут.

__slots__

и память

class Event:
    __slots__ = ("time", "energy")
 
    def __init__(self, time, energy):
        self.time = time
        self.energy = energy
 
e = Event(0.1, 511.0)
e.charge = -1      # AttributeError

Важно

Без __dict__ нет новых атрибутов и слабых ссылок, если '__weakref__' не указан в слотах.

Объект с двумя полями в списке, 3.12Память
обычный класс88 байт
после обращения к __dict__152 байта
__slots__56 байт

Для физика

Для миллионов объектов-событий применяется __slots__, для массивов чисел — NumPy: событие из двух float64 занимает 16 байт.

Демонстрация:

__slots__

01

Два класса: со словарём и со __slots__

02

Замер памяти через tracemalloc

03

Память на объект: 88 и 56 байт

04

Новый атрибут у объекта со слотами

05

Наличие __dict__ у двух объектов

Что наблюдается

На объект в списке приходится 56 байт со __slots__ против 88 у обычного; новых атрибутов он не принимает.

root@lab — запись со стенда

Методы и свойства

Связанные методы, classmethod и staticmethod, свойства, классы данных

02

Классы и экземпляры

Методы и свойства

Наследование

Магические методы

Дескрипторы

Метаклассы

Методы

и связывание

p = Particle(2.0, v=3.0)
 
f = Particle.kinetic_energy   # функция
f(p)                          # 9.0
 
m = p.kinetic_energy          # связанный метод
m.__self__ is p, m.__func__ is f   # (True, True)
m()                           # 9.0
 
energies = map(Particle.kinetic_energy, beam)

Пояснение

И функция, и связанный метод — объекты: их передают в map, в sorted(key=...) и в обработчики событий.

p.kinetic_energy

функция в Particle.__dict__

__get__(p, Particle)

связанный метод

__self__ = p
__func__ = функция

classmethod

и staticmethod

class Particle:
    def __init__(self, mass, v=0.0):
        self.mass, self.v = mass, v
 
    @classmethod
    def from_momentum(cls, mass, p):
        return cls(mass, v=p / mass)  # v << c
 
    @staticmethod
    def gamma(v, c=299_792_458.0):
        return 1 / (1 - (v / c) ** 2) ** 0.5
 
class Electron(Particle): ...
Electron.from_momentum(9.1e-31, 2.7e-24)  # Electron

ВидПервый аргументПрименение
обычный методself — экземплярработа с состоянием
@classmethodcls — классальтернативные конструкторы
@staticmethodнетфункция, относящаяся к классу

Пояснение

cls — фактический класс вызова: Electron.from_momentum создаёт Electron, а не Particle.

Свойства

property

K_B = 1.380649e-23   # Дж/К
 
class Gas:
    def __init__(self, temperature):
        self.temperature = temperature
 
    @property
    def temperature(self):
        return self._t
 
    @temperature.setter
    def temperature(self, value):
        if value < 0:
            raise ValueError("T < 0 K")
        self._t = value
 
    @property
    def kT(self):                    # только чтение
        return K_B * self._t

СредствоВычислениеЗначение
атрибут—obj.__dict__
propertyпри каждом чтенииобычно в _поле
cached_property (3.8)при первом чтенииobj.__dict__

Пояснение

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

Демонстрация:

свойства

01

Класс Gas со свойством temperature и проверкой

02

Чтение температуры и вычисляемого kT

03

Отрицательная температура: ValueError

04

Запись в свойство без сеттера: AttributeError

05

Словарь экземпляра: только _t

Что наблюдается

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

root@lab — запись со стенда

Классы

данных

from dataclasses import dataclass, field
 
@dataclass(frozen=True, slots=True)
class Measurement:
    value: float
    error: float = 0.0
    unit: str = "м"
 
@dataclass
class Run:
    number: int
    events: list[int] = field(default_factory=list)

ПараметрЧто даёт
по умолчанию__init__, __repr__, __eq__ по полям
frozen=Trueнеизменяемость и __hash__
order=Trueсравнения <, <=, >, >= по полям
slots=True (3.10)без словаря и новых атрибутов
field(default_factory=list)свой список у каждого объекта

Для физика

Результат измерения, параметры прогона, конфигурация детектора — естественные классы данных.

Демонстрация:

классы данных

01

Класс данных Measurement с frozen=True

02

repr и сравнение по полям

03

Присваивание полю: FrozenInstanceError

04

Множество из равных измерений

05

Список событий через default_factory

06

Список по умолчанию: ValueError

Что наблюдается

repr и сравнение — по полям; frozen запрещает присваивание; список по умолчанию — только через default_factory.

root@lab — запись со стенда

Наследование

super и MRO, примеси, композиция, абстрактные классы и протоколы

03

Классы и экземпляры

Методы и свойства

Наследование

Магические методы

Дескрипторы

Метаклассы

Наследование

и super

class Particle:
    def __init__(self, mass, charge=0.0):
        self.mass, self.charge = mass, charge
 
    def describe(self):
        return f"m={self.mass:.3e} кг"
 
class Electron(Particle):
    def __init__(self):
        super().__init__(9.109e-31, -1.602e-19)
 
    def describe(self):
        return "электрон, " + super().describe()

Пояснение

super() обращается к следующему классу в MRO, а не к явно названному родителю.

e.describe()

Electron

describe найден

Particle

super().describe()

object

конец цепочки

Множественное

наследование и MRO

A

B(A)

C(A)

D(B, C)

D.__mro__: D → B → C → A → object

class A: ...
class B(A): ...
class C(A): ...
class D(B, C): ...
 
D.__mro__      # (D, B, C, A, object)
 
class Bad(A, B): ...
# TypeError: Cannot create a consistent
# method resolution order (MRO)

Пояснение

Алгоритм C3: подкласс раньше родителей, порядок родителей сохраняется, каждый класс — один раз.

Демонстрация:

порядок MRO

01

Ромб: A, B(A), C(A), D(B, C)

02

Цепочка super(): D, B, C, A

03

Порядок D.__mro__

04

Противоречивый порядок: TypeError

Что наблюдается

super().hello() в B передал управление C, а не A: следующий класс берётся из MRO класса D, и A выполняется один раз.

root@lab — запись со стенда

Классы-примеси

class ReprMixin:
    def __repr__(self):
        items = vars(self).items()
        args = ", ".join(f"{k}={v!r}" for k, v in items)
        name = type(self).__name__
        return f"{name}({args})"
 
class AsDictMixin:
    def as_dict(self):
        return dict(vars(self))
 
class Detector(ReprMixin, AsDictMixin):
    def __init__(self, name, gain):
        self.name, self.gain = name, gain
 
Detector("A", 2.5)   # Detector(name='A', gain=2.5)

01

Одна черта

представление, сериализация, журналирование, блокировка

02

Минимум состояния

__init__ примеси передаёт аргументы дальше через super()

03

Перед базовым классом

class X(Mixin, Base): метод примеси вызывается первым

04

Примеры

socketserver.ThreadingMixIn, примеси представлений Django

Композиция

вместо наследования

class Channel:
    def __init__(self, number, gain):
        self.number, self.gain = number, gain
 
class Detector:
    def __init__(self, channels):
        self.channels = list(channels)
 
    def total_gain(self):
        return sum(c.gain for c in self.channels)

ОтношениеПримерСредство
«является»электрон — частицанаследование
«содержит»детектор содержит каналыкомпозиция
«умеет»объект сохраняется в файлпримесь или протокол

Важно

Признак неудачного наследования: подкласс отключает унаследованные методы или нарушает гарантии родителя.

Абстрактные

базовые классы

from abc import ABC, abstractmethod
 
class Integrator(ABC):
    @abstractmethod
    def step(self, f, t, y, h): ...
 
    def run(self, f, y0, t0, t1, n):
        h, t, y = (t1 - t0) / n, t0, y0
        for _ in range(n):
            y, t = self.step(f, t, y, h), t + h
        return y
 
class Euler(Integrator):
    def step(self, f, t, y, h):
        return y + h * f(t, y)

01

Обязательные методы без реализации

abstractmethod помечает методы наследника

02

Ошибка при создании экземпляра

TypeError при Integrator(), а не при вызове step

03

Общий алгоритм в базе

run один для всех схем интегрирования

04

Регистрация

register(cls): виртуальный подкласс для isinstance, без проверки методов

Для физика

Схемы Эйлера, Рунге — Кутты и Верле (для q'' = a(q)) — подклассы одного интерфейса: сравнение сводится к замене объекта.

collections.abc

и протоколы

from collections.abc import MutableMapping
 
class History(MutableMapping):
    def __init__(self):
        self._data, self.log = {}, []
    def __getitem__(self, key):
        return self._data[key]
    def __setitem__(self, key, value):
        self.log.append(key)
        self._data[key] = value
    def __delitem__(self, key):
        del self._data[key]
    def __iter__(self):
        return iter(self._data)
    def __len__(self):
        return len(self._data)

Пять методов → get, pop, setdefault, update, keys, items, values, in, ==

Пояснение

isinstance(h, MutableMapping) — True: объект примут библиотеки, ожидающие отображение.

Важно

Наследование от dict ненадёжно: dict.update и конструктор не вызывают переопределённый __setitem__.

Демонстрация:

абстрактный класс

01

Абстрактный Integrator с общим методом run

02

Создание экземпляра абстрактного класса: TypeError

03

Подкласс Euler с методом step

04

Решение y' = -y на [0, 1]: y(1) и exp(-1)

05

Проверка протоколов collections.abc

Что наблюдается

Экземпляр абстрактного класса не создаётся; Euler, реализовав один step, получил run и дал 0,36770 при точном значении 0,36788.

root@lab — запись со стенда

Магические методы

Модель данных, арифметика, сравнение, протоколы, вызов, атрибуты

04

Классы и экземпляры

Методы и свойства

Наследование

Магические методы

Дескрипторы

Метаклассы

Модель

данных

ЗаписьВызов
len(x)x.__len__()
x + yx.__add__(y), иначе y.__radd__(x)
x == y, x < yx.__eq__(y), x.__lt__(y)
x[i], x[i] = vx.__getitem__(i), x.__setitem__(i, v)
for v in xiter(x) → x.__iter__()
x(...)x.__call__(...)
repr(x), str(x), f'{x:.2f}'x.__repr__(), x.__str__(), x.__format__('.2f')
if xx.__bool__(), иначе x.__len__()
hash(x)x.__hash__()

Важно

Специальные методы ищутся у типа, а не у экземпляра: присваивание obj.__len__ = ... не меняет len(obj).

Строковое

представление

class Measured:
    def __init__(self, value, error):
        self.value, self.error = value, error
 
    def __repr__(self):
        return f"Measured({self.value!r}, {self.error!r})"
 
    def __str__(self):
        return f"{self.value} ± {self.error}"
 
    def __format__(self, spec):
        return f"{self.value:{spec}} ± {self.error:{spec}}"

ВызовРезультат
repr(m)Measured(9.81, 0.02)
str(m), print(m)9.81 ± 0.02
f'{m:.3f}'9.810 ± 0.020
без __repr__<__main__.Measured object at 0x…>

Пояснение

__repr__ — для программиста, однозначно и по возможности как выражение; __str__ — для человека и по умолчанию вызывает __repr__.

Арифметика

и погрешности

class Measured:
    ...
    def __add__(self, other):
        if not isinstance(other, Measured):
            return NotImplemented
        e = math.hypot(self.error, other.error)
        return Measured(self.value + other.value, e)
 
    def __mul__(self, k):
        if not isinstance(k, (int, float)):
            return NotImplemented
        return Measured(self.value * k, abs(k) * self.error)
 
    __rmul__ = __mul__

Для физика

Для независимых величин погрешность суммы — в квадратуре: σ² = σa² + σb².

a + b

a.__add__(b)

результат, если не NotImplemented

NotImplemented

b.__radd__(a)

отражённый метод правого операнда

NotImplemented

TypeError

Демонстрация:

величина с погрешностью

01

Класс Measured: repr, формат, сложение, умножение

02

Сумма: погрешности в квадратуре

03

Умножение на число слева и справа

04

Формат с заданной точностью

05

print без __str__: вызов __repr__

06

Сложение с числом: TypeError

Что наблюдается

Погрешности 0,3 и 0,4 дали для суммы 0,5; 2 * a работает через __rmul__, а a + 1 после двух NotImplemented завершается TypeError.

root@lab — запись со стенда

Сравнение

и хеширование

import functools
 
@functools.total_ordering
class Energy:
    def __init__(self, e):
        self.e = e
    def __eq__(self, other):
        if not isinstance(other, Energy):
            return NotImplemented
        return self.e == other.e
    def __lt__(self, other):
        if not isinstance(other, Energy):
            return NotImplemented
        return self.e < other.e
    def __hash__(self):
        return hash(self.e)

Определено в классеhash(obj)
ничегопо идентичности объекта
__eq__нет: __hash__ = None
__eq__ и __hash__по полям сравнения
dataclass(frozen=True)по полям

Важно

Равные объекты обязаны иметь равные хеши; объект, изменившийся после попадания в множество, поиск в нём его не находит.

Демонстрация:

сравнение и хеш

01

Класс Version только с __eq__

02

__hash__ = None и множество: TypeError

03

Energy с total_ordering и NotImplemented

04

Сортировка и достроенное >=

05

Множество из Energy(1) и Energy(1.0)

06

Сравнение с None: False

Что наблюдается

__eq__ без __hash__ отключил хеш; у Energy равные значения дают один элемент множества, а сравнение с None — False.

root@lab — запись со стенда

Протоколы

коллекций

class Spectrum:
    def __init__(self, counts):
        self._counts = list(counts)
 
    def __len__(self):
        return len(self._counts)
 
    def __getitem__(self, i):
        return self._counts[i]
 
s = Spectrum([3, 5, 8, 2])
len(s), s[2], s[1:3]   # (4, 8, [5, 8])
list(s), 8 in s        # ([3, 5, 8, 2], True)

МетодДаёт
__len__len(s), истинность
__getitem__s[i], срезы, for, in
__len__ и __getitem__reversed(s)
__iter__итератор для for
__contains__быстрая проверка in
__setitem__, __delitem__s[i] = v, del s[i]

Пояснение

С __len__ и __getitem__ объект уже перебирается в цикле; полный набор методов последовательности даёт collections.abc.Sequence.

Вызываемые

объекты

import math
 
class Gaussian:
    def __init__(self, mu, sigma):
        self.mu, self.sigma = mu, sigma
 
    def __call__(self, x):
        z = (x - self.mu) / self.sigma
        return math.exp(-z * z / 2)
 
g = Gaussian(0.0, 2.0)
g(2.0)               # 0.6065306597126334
quad(g, -10, 10)     # (5.0132..., ...)

Модель с параметрамиГде параметры
замыканиев ячейках __closure__, не в атрибутах
partialпараметры в args и keywords
объект с __call__параметры в атрибутах, repr, другие методы

Для физика

Объект с __call__ передаётся в интегратор как функция, а его параметры видны в атрибутах.

Доступ

к атрибутам

class Settings:
    def __init__(self, **values):
        object.__setattr__(self, "_values", values)
 
    def __getattr__(self, name):
        values = self.__dict__.get("_values", {})
        if name in values:
            return values[name]
        raise AttributeError(name)
 
    def __setattr__(self, name, value):
        raise AttributeError("только для чтения")
 
    def __delattr__(self, name):
        raise AttributeError("только для чтения")
 
cfg = Settings(gain=2.5, threshold=0.1)
cfg.gain                 # 2.5
cfg.gain = 3             # AttributeError

МетодКогда вызывается
__getattr__(name)после AttributeError обычного поиска
__getattribute__(name)при любом чтении
__setattr__(name, value)при любой записи
__delattr__(name)при любом удалении

Важно

Для неизвестного имени __getattr__ возбуждает AttributeError: на этом основаны hasattr, getattr с умолчанием, copy и pickle.

Демонстрация:

протоколы и __call__

01

Spectrum с __len__ и __getitem__

02

len, индекс и срез

03

Перебор, in и reversed без __iter__

04

len при методе, присвоенном экземпляру

05

Gaussian с __call__

06

Вызов объекта и сумма Римана

Что наблюдается

Двух методов достаточно для len, срезов, for, in и reversed; len ищет __len__ у класса, а Gaussian вызывается как функция.

root@lab — запись со стенда

Дескрипторы

Протокол, проверка значений, дескрипторы внутри языка

05

Классы и экземпляры

Методы и свойства

Наследование

Магические методы

Дескрипторы

Метаклассы

Протокол

дескрипторов

МетодКогда вызывается
__get__(self, obj, objtype)чтение obj.attr
__set__(self, obj, value)запись obj.attr = value
__delete__(self, obj)del obj.attr
__set_name__(self, owner, name)при создании класса (3.6)

Пояснение

Дескриптор работает, только будучи атрибутом класса; дескриптор данных определяет __set__ или __delete__.

obj.x

дескриптор данных в классе

1 — приоритет над словарём

obj.__dict__['x']

2 — атрибут экземпляра

дескриптор не данных, атрибут класса

3 — например, метод

__getattr__(obj, 'x')

4 — иначе AttributeError

Дескриптор

с проверкой

class Positive:
    def __set_name__(self, owner, name):
        self.public, self.name = name, "_" + name
 
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.name)
 
    def __set__(self, obj, value):
        if value <= 0:
            raise ValueError(f"{self.public}: ожидается > 0")
        setattr(obj, self.name, value)
 
class Particle:
    mass = Positive()
    lifetime = Positive()

01

Одна проверка — много полей

масса, время жизни, температура

02

Значение — в экземпляре

obj._mass, а не в дескрипторе, общем для класса

03

Имя из __set_name__

без повторения Positive('mass') в аргументе

04

obj is None

при обращении через класс — сам дескриптор

Дескрипторы

внутри языка

Объект в классеВидobj.attr даёт
функцияне данныхсвязанный метод
propertyдескриптор данныхвызов геттера
classmethodне данныхметод, связанный с классом
staticmethodне данныхисходная функция
слот из __slots__дескриптор данныхзначение из ячейки объекта

Пояснение

Метод становится связанным, потому что функция — дескриптор: её __get__ возвращает связанный метод. __set__ у функции нет, поэтому атрибут экземпляра её заслоняет.

class C:
    def f(self): ...
 
c = C()
C.__dict__["f"].__get__(c, C)
# <bound method C.f of <C object ...>>
c.f = 42
c.f      # 42: атрибут заслонил функцию
 
p = Particle()
p.mass = 0.1057
p.__dict__["mass"] = -5
p.mass   # 0.1057: приоритет у дескриптора

Демонстрация:

дескриптор

01

Дескриптор Positive с __set_name__

02

Particle с двумя полями Positive

03

Значения в vars(p): _mass, _lifetime

04

Отрицательное время жизни: ValueError

05

Запись в __dict__ при дескрипторе данных

06

Функция как дескриптор: связанный метод

Что наблюдается

Значения хранятся в экземпляре под именами из __set_name__; дескриптор данных имеет приоритет над словарём, а __get__ функции дал связанный метод.

root@lab — запись со стенда

Метаклассы

Класс как объект, создание класса, __init_subclass__, метаклассы

06

Классы и экземпляры

Методы и свойства

Наследование

Магические методы

Дескрипторы

Метаклассы

Класс

как объект

p

экземпляр

type(p)

Particle

класс

type(Particle)

type

метакласс; type(type) is type

type(p)          # <class '__main__.Particle'>
type(Particle)   # <class 'type'>
type(type)       # <class 'type'>
 
Point = type("Point", (), {"dim": 2})
Point.dim        # 2

Пояснение

Инструкция class выполняет тело в отдельном пространстве имён и вызывает метакласс: type(name, bases, namespace).

Создание

класса

01

Выбор метакласса

наиболее производный из metaclass= и метаклассов баз, иначе type

02

__prepare__(name, bases)

словарь для пространства имён тела

03

Выполнение тела класса

заполнение словаря именами методов и атрибутов

04

Вызов метакласса

metaclass(name, bases, ns): __new__, затем __init__

05

__set_name__ и __init_subclass__

вызов внутри type.__new__

06

Декораторы класса

применение к готовому классу, затем связывание имени

Пояснение

Каждый шаг — точка вмешательства: от простого декоратора до метакласса.

__init_subclass__

и декораторы

class Integrator:
    registry = {}
 
    def __init_subclass__(cls, /, name=None, **kwargs):
        super().__init_subclass__(**kwargs)
        if name is not None:
            Integrator.registry[name] = cls
 
class Euler(Integrator, name="euler"): ...
class RK4(Integrator, name="rk4"): ...
class ImplicitEuler(Euler): ...   # без имени
 
method = Integrator.registry[config["method"]]()

ЗадачаСредство
реестр подклассов, проверка их атрибутов__init_subclass__ (3.6)
дописывание методов готовому классудекоратор класса: @dataclass, @total_ordering
изменение пространства имён или создания классаметакласс

Пояснение

Сначала — самое простое средство: метакласс нужен редко.

Метаклассы

на практике

class Meta(type):
    def __new__(mcls, name, bases, ns):
        print("создаётся", name, list(ns))
        return super().__new__(mcls, name, bases, ns)
 
class Detector(metaclass=Meta):
    gain = 2.5
# создаётся Detector ['__module__', '__qualname__', 'gain']
 
class Tracker(Detector): ...
# создаётся Tracker ['__module__', '__qualname__']

ГдеМетаклассЧто делает
abcABCMetaабстрактные методы, register
enumEnumTypeчлены перечисления: Spin(-1)
DjangoModelBaseполя модели → столбцы таблицы
pydanticModelMetaclassпроверка полей по аннотациям

Важно

Метакласс наследуется всеми подклассами; несовместимые метаклассы у родителей — TypeError: metaclass conflict.

Демонстрация:

метаклассы

01

Типы экземпляра, класса и type

02

Класс, созданный вызовом type

03

Метакласс, печатающий пространство имён

04

Метакласс подкласса

05

Реестр через __init_subclass__

06

Enum: метакласс EnumType

Что наблюдается

Тип класса — type; метакласс получил пространство имён Detector и был вызван для Tracker без явного указания; __init_subclass__ заполнил реестр.

root@lab — запись со стенда

Выбор

средства

ЗадачаСредство
запись с именованными полями@dataclass
проверка или вычисление одного атрибутаproperty
одна проверка для многих атрибутовдескриптор
общий интерфейс и алгоритмabc.ABC, typing.Protocol
поведение встроенного типамагические методы
реестр и проверка подклассов__init_subclass__
изменение самого создания классаметакласс

Пояснение

Сверху вниз растут возможности и сложность: выбирается первое средство, решающее задачу.

Итоги

лекции

01

Класс связывает состояние с операциями; атрибуты экземпляра — в __dict__, атрибуты класса общие для всех.

02

Метод связывается с экземпляром при обращении; classmethod и staticmethod меняют привязку, property добавляет логику.

03

Атрибуты ищутся по MRO, super() обращается к следующему классу; композиция часто проще иерархии.

04

Магические методы включают объект в операторы; возврат NotImplemented передаёт операцию другому операнду.

05

На дескрипторах построены методы, свойства и слоты; дескриптор данных имеет приоритет над словарём.

06

Класс — экземпляр метакласса type; __init_subclass__ и декораторы классов заменяют большинство метаклассов.

Упражнения для самопроверки

01

Класс Vector3 с __add__, __sub__, __mul__ и __rmul__ для умножения на число, __abs__, __eq__ и __repr__; проверить 2 * v и v * 2

02

Класс данных Measurement(frozen=True) со сложением, погрешностью в квадратуре и форматом через __format__

03

Дескриптор InRange(lo, hi) для температуры и давления класса Gas с понятным сообщением об ошибке

04

Абстрактный Integrator и подклассы Euler и RK4; сравнить погрешность на y' = -y при n = 10, 100, 1000

05

Реестр интеграторов через __init_subclass__ и выбор метода по строке из файла конфигурации

Глава книги: «Классы» · упражнения для самопроверки, без сдачи

Источники

1

Книга курса: глава «Классы»

phys-dev.github.io/soft-dev-book

2

Лебедев С. Классы 1. – Computer Science Center

youtube.com/watch?v=drsO-1RJlnE

3

Лебедев С. Классы 2. – Computer Science Center

youtube.com/watch?v=mTp2pLEoDaI

4

The Python Tutorial: Classes

docs.python.org/3/tutorial/classes.html

5

Data model — справочник языка Python

docs.python.org/3/reference/datamodel.html

6

Descriptor Guide — документация Python

docs.python.org/3/howto/descriptor.html

7

PEP 557, 487, 3119, 3129, 544 — классы данных, хуки подклассов, ABC, декораторы классов, протоколы

peps.python.org

8

Ramalho L. Fluent Python. – 2nd ed. – O'Reilly, 2022

Спасибо за внимание

Вопросы

phys-dev.github.io/soft-dev-book

self