Как создаётся объект
Point(1, 2) вызывает два метода по очереди:
Point.__new__(Point, 1, 2)— создаёт и возвращает экземпляр;obj.__init__(1, 2)— инициализирует его и ничего не возвращает.
Если __new__ вернул объект не своего класса, __init__ не вызывается. Переопределять __new__ нужно редко:
- подклассы неизменяемых типов — значение
int,str,tupleзадаётся при создании, в__init__его уже не поменять; - кэши и синглтоны — вернуть уже существующий экземпляр;
- метаклассы.
class Celsius(float):
def __new__(cls, value):
if value < -273.15:
raise ValueError("ниже абсолютного нуля")
return super().__new__(cls, value)
__eq__ и __hash__
Хешируемый объект имеет хеш, неизменный за всё время жизни. Правило: равные объекты обязаны иметь равные хеши — иначе словарь и множество перестанут их находить.
Если класс переопределяет __eq__ и не определяет __hash__, Python ставит __hash__ = None — экземпляры становятся нехешируемыми. Хеш считают по тем же полям, что и равенство, и только по неизменяемым:
def __eq__(self, other):
return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
Проще всего — @dataclass(frozen=True): он сгенерирует согласованную пару.
__getattribute__ и __getattr__
__getattribute__вызывается при каждом обращении к атрибуту — это сам механизм поиска. Переопределять его опасно: любоеself.xвнутри снова вызовет его же — бесконечная рекурсия. Обращаются черезobject.__getattribute__(self, name).__getattr__вызывается только если атрибут не найден обычным путём. Для прокси, обёрток и ленивых атрибутов используют его; для неизвестных имён он должен бросатьAttributeError.
class Proxy:
def __init__(self, target):
self._target = target
def __getattr__(self, name):
return getattr(self._target, name)
Дескрипторы
Дескриптор — объект в атрибуте класса, у которого есть __get__ и, возможно, __set__ и __delete__. При обращении obj.attr Python вызывает эти методы вместо простого чтения. На дескрипторах построены property, обычные методы (функция — дескриптор, который привязывает self), staticmethod, classmethod и __slots__.
class Positive:
def __set_name__(self, owner, name):
self.name = "_" + name
def __get__(self, obj, owner):
return getattr(obj, self.name)
def __set__(self, obj, value):
if value <= 0:
raise ValueError("нужно положительное число")
setattr(obj, self.name, value)
class Order:
count = Positive()
Дескриптор с __set__ (data descriptor) важнее словаря экземпляра, а без него уступает словарю — поэтому метод можно перекрыть атрибутом экземпляра. Так устроены поля ORM в Django и SQLAlchemy.
__slots__
__slots__ = ("x", "y") заменяет у экземпляров словарь __dict__ фиксированными ячейками. Объект занимает в несколько раз меньше памяти, доступ чуть быстрее, но добавить новый атрибут нельзя. Выигрыш заметен на миллионах мелких объектов. Наследник без своих __slots__ снова получает __dict__; для слабых ссылок нужен слот __weakref__. С Python 3.10 — @dataclass(slots=True).
Что запомнить
__new__создаёт,__init__настраивает.- Равные объекты — равные хеши; переопределили
__eq__— определите__hash__. - Для прокси —
__getattr__, а не__getattribute__. - property и методы — это дескрипторы.