Классы
Класс связывает данные с операциями, определёнными над ними, и вместо разрозненных переменных и обрабатывающих их функций получается один объект, отвечающий за своё состояние. От Java и C++ Python отличается тем, что не скрывает механику: ссылка на экземпляр передаётся методу явным первым аргументом self, а не появляется в теле сама собой.
ООП в Python не является обязательным. Модуль, составленный из десятка функций, представляет собой полноценную программу, и оборачивать её в класс только ради класса не требуется. Классы применяются тогда, когда у данных появляется состояние, которое необходимо поддерживать согласованным, или когда одну и ту же операцию требуется выполнять по-разному для объектов разного вида, обрабатываемых единым кодом.
Базовые понятия
Определение класса
Метод __init__ вызывается сразу после создания объекта и задаёт начальное состояние; всё, записанное в self, становится атрибутом конкретного экземпляра, которым и пользуются остальные методы.
class Counter:
"""I count. That is all."""
def __init__(self, initial=0): # конструктор
self.value = initial # запись атрибута
def increment(self):
self.value += 1
def get(self):
return self.value # чтение атрибута
# Использование
c = Counter(42)
c.increment()
print(c.get()) # 43
Название «конструктор», закрепившееся за __init__, строго говоря неверно, поскольку объект к этому моменту уже создан. Создаёт его метод __new__, а __init__ только заполняет заготовку, полученную от него. Разница проявляется при наследовании от неизменяемого типа наподобие int или tuple, у которого изменять уже нечего, и вся работа переносится в __new__.
Специфика Python
Первым аргументом любого метода следует self, то есть экземпляр, на котором метод вызван, причём указывать его приходится вручную, в соответствии с правилом «явное лучше неявного».
Модификаторов доступа наподобие private и protected в Python нет, вместо них действует соглашение об именовании.
public_attributeсоставляет часть публичного интерфейса, и его использование допустимо;_internal_attributeявляется внутренним, извне к нему не обращаются;__private_attributeозначает то же самое, но с дополнительной защитой от случайного переопределения, выполненного в подклассе.
Только за двойным подчёркиванием стоит механизм, встроенный в язык. Интерпретатор переписывает имя, помеченное таким образом, добавляя к нему имя класса, вследствие чего __private внутри класса Counter превращается в _Counter__private. Механизм предназначен для предотвращения коллизий, а не для сокрытия данных, так что базовый класс и подкласс, объявив по атрибуту __cache, не столкнутся. Доступ к «приватному» атрибуту при этом сохраняется через полное имя.
Атрибуты классов и экземпляров
Атрибуты в Python бывают двух видов, и путаница между ними приводит к трудноуловимым ошибкам.
Атрибуты экземпляра
Атрибут экземпляра появляется в результате присваивания, выполненного к self, и принадлежит только этому объекту. Создавать его не обязательно в __init__, поскольку присвоить атрибут можно и извне, уже созданному объекту.
class Noop:
def __init__(self):
self.some_attribute = 42
noop = Noop()
noop.other_attribute = 100500 # динамическое добавление
Эта свобода следует из того, что атрибуты хранятся в обычном словаре, и у неё есть оборотная сторона: опечатка в имени не вызовет ошибки. Строка noop.som_attribute = 0 без предупреждения создаст новый атрибут, а старый останется прежним.
Атрибуты класса
Атрибут, объявленный в теле класса, принадлежит самому классу и является одним на все его экземпляры. Это подходит для общих сущностей: настроек, констант, реестра созданных объектов.
class Counter:
all_counters = [] # атрибут класса
def __init__(self, initial=0):
Counter.all_counters.append(self)
self.value = initial
# Также можно добавлять атрибуты после определения
Counter.some_other_attribute = 42
Ловушка та же, что с изменяемыми аргументами по умолчанию: список all_counters создаётся один раз, при чтении определения класса. Если бы он был задуман как список, заведённый у каждого счётчика отдельно, все экземпляры разделяли бы один. Здесь общий список необходим, однако при объявлении изменяемого атрибута класса следует проверять, должен ли он быть общим.
Словарь атрибутов
Все атрибуты экземпляра хранятся в обычном словаре __dict__, благодаря которому и работает динамическое добавление. В него можно заглянуть, просмотреть поля, имеющиеся у объекта, и записывать напрямую.
noop = Noop()
noop.some_attribute = 42
print(noop.__dict__) # {'some_attribute': 42}
print(vars(noop)) # альтернативный способ
# Динамическое управление атрибутами
noop.__dict__["dynamic_attr"] = "value"
Встроенная vars() с аргументом возвращает __dict__ объекта, а без аргументов — словарь текущего локального пространства имён. Прямая запись в __dict__ требуется редко, но оказывается полезной при имени атрибута, вычисляемом во время выполнения.
__slots__ для оптимизации
За гибкость приходится платить памятью, так как словарь, заведённый у каждого экземпляра, на объекте с двумя полями занимает больше самих полей. Если объектов миллионы, по одному на событие, зарегистрированное в эксперименте, плата становится ощутимой.
__slots__ фиксирует набор допустимых атрибутов заранее, благодаря чему интерпретатор обходится без словаря и размещает значения по ячейкам, отведённым при создании объекта.
class Noop:
__slots__ = ["some_attribute"] # фиксирует набор атрибутов
def __init__(self):
self.some_attribute = 42
# Экономит память, но запрещает добавление новых атрибутов
На объекте с парой полей это экономит около 40 % памяти: 96 байт на экземпляр без слотов против 56 со слотами. Скорость доступа не меняется: измерение, приведённое в главе про оптимизацию, показывает, что разница находится в пределах погрешности. Взамен теряется динамика: добавить атрибут, отсутствующий в списке, не удастся, будет получено AttributeError, и перестают работать слабые ссылки, если __weakref__ не указан в слотах явно. Начинать с __slots__ нецелесообразно; его добавляют тогда, когда профилировщик покажет, что память расходуется именно здесь.
Методы
Связанные и несвязанные методы
Метод представляет собой функцию, хранящуюся в атрибуте класса, и получить её можно двумя путями. Через класс получается обычная функция, требующая экземпляр, переданный явно. Через экземпляр получается связанный метод, в котором объект уже подставлен в self, и передавать его повторно не требуется.
class SomeClass:
def do_something(self):
print("Doing something.")
# Несвязанный метод
method = SomeClass.do_something
instance = SomeClass()
method(instance) # нужно явно передать экземпляр
# Связанный метод
bound_method = instance.do_something
bound_method() # self уже привязан
Связанный метод представляет собой полноценный объект: его помещают в переменную, передают в map или регистрируют как обработчик события, а ссылку на свой экземпляр он хранит в себе. В Python 2 обращение через класс давало особый тип, проверявший тип первого аргумента и не пропускавший посторонние объекты; в Python 3 это обычная функция.
Свойства (Properties)
Публичный атрибут иногда необходимо защитить проверкой или вычислять при обращении. В языках со строгой инкапсуляцией на этот случай заранее пишутся геттеры и сеттеры для всех полей. В Python атрибут остаётся атрибутом, а когда потребуется, за ним размещаются методы без изменения кода, использующего его.
Это обеспечивает декоратор @property: метод, помещённый под ним, вызывается при чтении, метод под @имя.setter при записи, метод под @имя.deleter при удалении. Извне всё выглядит как обычное присваивание.
class BigDataModel:
def __init__(self):
self._params = []
@property
def params(self):
return self._params
@params.setter
def params(self, new_params):
assert all(p > 0 for p in new_params)
self._params = new_params
@params.deleter
def params(self):
del self._params
model = BigDataModel()
model.params = [0.1, 0.5, 0.4]
print(model.params) # [0.1, 0.5, 0.4]
Присваивание model.params = [...] выглядит как запись в обычное поле, но проходит через assert в сеттере. Интерфейс не меняется, а поведение за ним может меняться произвольно. Внутренне свойства построены на дескрипторах, рассматриваемых в конце главы.
Наследование
Базовое наследование
Подкласс получает всё, объявленное у родителя, и добавляет своё. Если метода нет у подкласса, интерпретатор поднимается по цепочке наследования и ищет его выше. OtherCounter не определяет __init__, поэтому при создании работает родительский.
class Counter:
def __init__(self, initial=0):
self.value = initial
class OtherCounter(Counter):
def get(self):
return self.value
oc = OtherCounter() # вызывает Counter.__init__
print(oc.get()) # вызывает OtherCounter.get
Родителей, в отличие от многих языков, может быть несколько, и чем сложнее иерархия, тем труднее определить, чей метод, найденный по цепочке, будет вызван.
Перегрузка методов и super()
Чаще подкласс не заменяет метод родителя целиком, а надстраивает над ним своё и передаёт управление выше, а ссылку на реализацию, унаследованную от родителя, предоставляет super().
class Counter:
all_counters = []
def __init__(self, initial=0):
self.__class__.all_counters.append(self)
self.value = initial
def increment(self):
self.value += 1
def get(self):
return self.value
class OtherCounter(Counter):
def __init__(self, initial=0):
self.initial = initial
super().__init__(initial) # вызов родительского конструктора
Запись Counter.__init__(self, initial) вместо super() является распространённой ошибкой, и разница проявится при множественном наследовании. super() обращается не к родителю, а к следующему классу в порядке разрешения методов, зависящем от того, как класс использовался в дальнейшем. Имя родителя, указанное явно, эту цепочку разрывает.
Множественное наследование
Порядок, в котором интерпретатор перебирает классы в поисках метода, называется MRO, то есть method resolution order, и вычисленный список доступен у любого класса.
class A:
def f(self):
print("A.f")
class B:
def f(self):
print("B.f")
class C(A, B):
pass
print(C.mro()) # порядок разрешения методов
C().f() # A.f (согласно MRO)
Здесь f находится в A, поскольку A стоит первым в списке родителей, перечисленных в скобках. Порядок, выстраиваемый здесь, вычисляет алгоритм C3, заимствованный из языка Dylan, и он обеспечивает три гарантии. Подкласс всегда следует раньше своих родителей; порядок, в котором родители перечислены, сохраняется; каждый класс иерархии встречается в списке единожды. Если требования противоречат друг другу, Python откажется создавать класс.
Классы-примеси
Примесью (mixin) называют класс, не предназначенный для самостоятельного использования и не описывающий сущность, а только добавляющий к чужому поведению одну черту, будь то блокировка, журналирование или кеширование. Ниже примесь оборачивает операции счётчика в блокировку, захватываемую на время вызова.
import threading
class ThreadSafeMixin:
def __init__(self, *args, **kwargs):
self._lock = threading.Lock()
super().__init__(*args, **kwargs)
def get_lock(self):
return self._lock
def increment(self):
with self.get_lock():
super().increment()
def get(self):
with self.get_lock():
return super().get()
class ThreadSafeCounter(ThreadSafeMixin, Counter):
pass
Всё это работает благодаря super() и порядку разрешения методов. Примесь стоит перед Counter в списке родителей, поэтому её increment перехватывает вызов первым, а super().increment(), расположенный внутри, передаётся дальше по цепочке, в настоящий счётчик. ThreadSafeMixin не наследуется от Counter и ничего о нём не знает: следующий класс в MRO определяется в момент, когда собирается ThreadSafeCounter. Аналогичным образом устроена значительная часть Django.
Декораторы классов
Декоратор применяется не только к функциям: класс также является объектом, и его можно пропустить через функцию, возвращающую нечто другое. Классическим примером служит синглтон, где класс подменяется функцией, создающей экземпляр при первом вызове и в дальнейшем возвращающей тот же самый.
import functools
def singleton(cls):
instance = None
@functools.wraps(cls)
def inner(*args, **kwargs):
nonlocal instance
if instance is None:
instance = cls(*args, **kwargs)
return instance
return inner
@singleton
class Noop:
"I do nothing at all."
print(id(Noop()) == id(Noop())) # True
Декораторы классов появились в Python 3.0 и потеснили метаклассы: многое, ради чего раньше писался метакласс, теперь выполняется одной функцией. Наиболее известным примером является @dataclass, добавляющий классу __init__, __repr__ и __eq__ по полям, объявленным в теле.
Магические методы
Магическими (или дандер-) методами называют методы, чьи имена окружены двойными подчёркиваниями. Напрямую их не вызывают, поскольку за каждым стоит синтаксическая конструкция или встроенная функция. Определение нужного метода позволяет объекту вести себя как встроенный, то есть складываться, сравниваться, печататься, попадать в множество.
Управление атрибутами
Первая группа перехватывает работу с атрибутами. __getattr__ вызывается только тогда, когда атрибут не найден обычным путём, и в нём подставляется значение вместо AttributeError. __setattr__ и __delattr__, напротив, перехватывают любую запись и любое удаление.
class Noop:
def __getattr__(self, name):
# Вызывается при доступе к несуществующему атрибуту
return f"Attribute {name} doesn't exist"
def __setattr__(self, name, value):
# Вызывается при установке любого атрибута
super().__setattr__(name, value)
def __delattr__(self, name):
# Вызывается при удалении атрибута
super().__delattr__(name)
noop = Noop()
print(noop.non_existent) # "Attribute non_existent doesn't exist"
Обращение к super() внутри __setattr__ обязательно: self.name = value вызовет метод рекурсивно. По той же причине не рекомендуется переопределять __getattribute__, перехватывающий доступ ко всем атрибутам, а не только к отсутствующим.
Операторы сравнения
Вторая группа отвечает за сравнение, и методов в ней шесть, но записывать все шесть не требуется: декоратор @functools.total_ordering достроит остальные по __eq__ и любому одному из методов, задающих упорядочивание.
import functools
@functools.total_ordering
class Counter:
def __init__(self, value):
self.value = value
def __eq__(self, other):
return self.value == other.value
def __lt__(self, other):
return self.value < other.value
c1, c2 = Counter(1), Counter(2)
print(c1 < c2) # True
print(c1 >= c2) # False (автоматически из __lt__ и __eq__)
Определение __eq__ отключает хеширование: объект не попадёт ни в множество, ни в ключи словаря. Причина та же, что у изменяемых коллекций: равные объекты обязаны иметь равные хеши, а вычислять их самостоятельно Python не берётся. Если объект должен оставаться хешируемым, __hash__ необходимо определить явно.
Строковое представление
Третья группа отвечает за текстовое представление. __repr__ предназначен для программиста: он должен быть однозначным и в идеале выглядеть как выражение, воссоздающее объект. __str__ предназначен для человека, просматривающего вывод программы. Если __str__ не определён, Python использует __repr__, поэтому начинают именно с него.
class Counter:
def __init__(self, initial=0):
self.value = initial
def __repr__(self):
return f"Counter({self.value})"
def __str__(self):
return f"Counted to {self.value}"
def __format__(self, format_spec):
return self.value.__format__(format_spec)
c = Counter(42)
print(repr(c)) # Counter(42)
print(str(c)) # Counted to 42
print(f"{c:b}") # 101010 (бинарное представление)
Третий метод, __format__, отвечает за подстановку в f-строку со спецификатором, а здесь он передаёт полученный спецификатор дальше в целое число, поэтому {c:b} печатает значение счётчика в двоичном виде.
__repr__ имеет смысл определять у любого класса, существующего дольше пары строк, иначе при отладке объекты печатаются как <__main__.Counter object at 0x7f3a…>.
Другие полезные магические методы
__call__ делает экземпляр вызываемым, и на этом основаны декораторы, реализованные классами, и объекты, хранящие настройки между вызовами. __bool__ задаёт, что объект означает в условии if; без него Python обращается к __len__, и объект нулевой длины оказывается ложным, а объект без обоих методов истинен всегда. __hash__ определяет, как объект попадает в множества и словари.
class Identity:
def __call__(self, x):
# Позволяет вызывать экземпляры как функции
return x
def __bool__(self):
# Определяет поведение в булевом контексте
return True
def __hash__(self):
# Используется для хеширования в словарях и множествах
return hash(id(self))
identity = Identity()
print(identity(42)) # 42
__call__ стирает границу между функцией и объектом: вызов identity(42) выглядит как обычный, хотя слева стоит экземпляр класса. Поэтому всюду, где документация требует передать функцию, подходит любой объект с __call__.
Дескрипторы
Свойство решает задачу для одного атрибута одного класса, а проверка на неотрицательность требуется десяткам полей в разных классах. Дескриптор выносит логику доступа в отдельный класс, откуда её можно переиспользовать.
Базовый дескриптор
Дескриптор представляет собой класс с определёнными __get__, __set__ или __delete__. Экземпляр такого класса, помещённый в атрибут класса, с этого момента перехватывает любое обращение к атрибуту.
class NonNegative:
def __get__(self, instance, owner):
# magically_get_value
pass
def __set__(self, instance, value):
assert value >= 0, "non-negative value required"
# magically_set_value
def __delete__(self, instance):
# magically_delete_value
pass
class VerySafe:
x = NonNegative()
y = NonNegative()
very_safe = VerySafe()
very_safe.x = 42 # OK
very_safe.x = -42 # AssertionError
Проверка написана один раз, защищённых ею полей два, и добавление третьего требует одной строки.
Через дескрипторы в Python реализовано почти всё, связанное с атрибутами: и @property, и @staticmethod, и @classmethod, и сама привязка методов к экземпляру. Знание протокола напрямую требуется нечасто, но он объясняет, почему функция, хранящаяся в классе, при обращении через экземпляр оказывается связанным методом.
Хранение данных в дескрипторах
Хранить значение в самом дескрипторе нельзя: он один на весь класс, а экземпляров много, и значения, записанные ими, затирали бы друг друга; правильным местом является словарь самого экземпляра.
class Proxy:
def __init__(self, label):
self.label = label
def __get__(self, instance, owner):
return instance.__dict__[self.label]
def __set__(self, instance, value):
instance.__dict__[self.label] = value
def __delete__(self, instance):
del instance.__dict__[self.label]
class Something:
attr = Proxy("attr")
some = Something()
some.attr = 42
print(some.attr) # 42
У каждого экземпляра значение своё, и существует оно столько же, сколько сам экземпляр. Второй вариант, общий словарь «экземпляр → значение», заведённый внутри дескриптора, хуже по двум причинам: он требует, чтобы объекты были хешируемыми, и удерживает на них ссылки, не позволяя сборщику мусора их удалить.
Имя поля приходится дублировать в Proxy("attr"); начиная с Python 3.6 от этого избавляет метод __set_name__, вызываемый интерпретатором при создании класса и сообщающий дескриптору имя, под которым тот записан.
Встроенные дескрипторы
Три реализации ниже сокращены, но верно передают устройство, заложенное в интерпретатор.
# @property реализован через дескрипторы
class property:
def __init__(self, get=None, set=None, delete=None):
self._get = get
self._set = set
self._delete = delete
def __get__(self, instance, owner):
if self._get is None:
raise AttributeError("unreadable attribute")
return self._get(instance)
# @staticmethod и @classmethod тоже используют дескрипторы
class staticmethod:
def __init__(self, method):
self.__method = method
def __get__(self, instance, owner):
return self.__method
class classmethod:
def __init__(self, method):
self.__method = method
def __get__(self, instance, owner):
if owner is None:
owner = type(instance)
return self.__method.__get__(owner, type(owner))
Вся разница между @staticmethod и @classmethod заключается в их __get__. Статический метод возвращает исходную функцию без привязки, поэтому self в нём отсутствует. Метод класса привязывает функцию к классу, и первым аргументом приходит сам класс. Аналогично, только к экземпляру, привязываются обычные методы.
Метаклассы
Поскольку класс является объектом, у него также есть тип. Типом обычного класса служит type, а метаклассом называют класс, унаследованный от type, экземплярами которого оказываются другие классы. Если обычный класс описывает, как устроены его объекты, то метакласс описывает, как устроены сами классы.
Требуются они редко: почти всё, ради чего раньше писались метаклассы, сегодня выполняется декоратором, применённым к классу, или методом __init_subclass__. Однако на метаклассах построены ORM и системы валидации данных, где поля, объявленные в классе, описываются декларативно.
Базовый метакласс
Метакласс перехватывает создание класса, а его метод __new__ получает всё, из чего класс будет собран, то есть имя, кортеж родителей и словарь, составленный из тела класса, и может изменить полученное до создания класса.
from collections import OrderedDict
class Meta(type):
def __new__(metacls, name, bases, clsdict):
print(f"Creating class {name}")
cls = super().__new__(metacls, name, bases, clsdict)
return cls
@classmethod
def __prepare__(metacls, name, bases):
# Может вернуть нестандартный mapping для clsdict
return OrderedDict()
class Something(metaclass=Meta):
attr = "foo"
other_attr = "bar"
Второй метод, __prepare__, выбирает, в чём накапливать тело класса, пока оно читается. Возвращённый им словарь и попадёт затем в clsdict, а значит, подставив сюда собственный тип отображения, можно запомнить порядок объявления атрибутов или проверять на месте каждое выполненное присваивание. Так и работают ORM, где порядок полей определяет порядок колонок, созданных в таблице. Начиная с версии 3.7 тело класса и без __prepare__ накапливается в обычном dict, сохраняющем порядок вставки, так что OrderedDict здесь требуется не ради порядка, а ради move_to_end и прочих возможностей, отсутствующих у простого словаря.
Создание классов через type()
Оператор class является удобной записью вызова type с тремя аргументами: именем, родителями и словарём атрибутов.
# Эквивалентно class Something: attr = 42
name, bases, attrs = "Something", (), {"attr": 42}
Something = type(name, bases, attrs)
some = Something()
print(some.attr) # 42
Модуль abc для абстрактных базовых классов
Абстрактный базовый класс описывает интерфейс, не реализуя его, и перечисляет методы, обязательные для наследника. Пока хотя бы один из них не реализован, класс считается абстрактным, и создать его экземпляр не удастся, причём ошибка возникнет сразу, а не при первом обращении к недостающему методу.
import abc
class Iterable(metaclass=abc.ABCMeta):
@abc.abstractmethod
def __iter__(self):
pass
class Something(Iterable):
pass
# Something() # TypeError: Can't instantiate abstract class
Отдельной конструкции interface, как в Java, в Python нет: интерфейсы описываются обычными классами с заданным метаклассом ABCMeta. Проще всего наследоваться от готового abc.ABC, выполняющего то же самое.
Модуль collections.abc
Абстрактные классы для самих коллекций уже написаны и находятся в collections.abc, и это те же Container, Iterable, Sequence и Mapping, рассмотренные в главе про коллекции. Наследование даёт две выгоды: интерпретатор сначала проверит, что реализовано всё обязательное, а затем сам добавит остальное. Реализация у MutableMapping пяти методов даёт готовые get, pop, setdefault, update, items, keys и values.
from collections import deque
from collections.abc import MutableMapping
class MemorizingDict(MutableMapping):
def __init__(self, *args, **kwargs):
self._data = dict(*args, **kwargs)
self._history = deque(maxlen=10)
def __getitem__(self, key):
return self._data[key]
def __setitem__(self, key, value):
self._history.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)
def get_history(self):
return self._history
Написано пять обязательных методов и один собственный, а в результате получается полноценный словарь, запоминающий десять последних записанных ключей. Он пройдёт isinstance(d, MutableMapping) и будет принят любой библиотекой, ожидающей отображение.
Заключение
Большая часть этого инструментария в повседневной практике не потребуется: метаклассы за студенческую практику, скорее всего, не встретятся ни разу, дескрипторы встретятся в лучшем случае однажды. Рассматриваются они потому, что почти всё, кажущееся в Python магией, оказывается одним из этих механизмов.
В повседневной работе достаточно немногого:
- Класс необходим там, где есть состояние, которое требуется поддерживать согласованным. Если состояния нет, достаточно функции.
- Атрибут остаётся атрибутом; свойство добавляется тогда, когда за ним потребовалась логика.
__repr__определяется сразу.- В переопределённых методах родитель вызывается через
super(), а не по имени класса. - Прежде чем строить иерархию наследования, целесообразно проверить, не решается ли задача композицией: объект, содержащий другой объект, почти всегда проще объекта, унаследованного от него.
Язык одинаково принимает и модуль из десяти функций, и иерархию классов; выбор определяется задачей, а не привычкой, сформированной в других языках.