Некоторое время назад прилетел баг: https://github.com/python/cpython/issues/118033
from dataclasses import dataclass
@dataclass(slots=True, weakref_slot=True)
class Token[T]:
ctx: T
print(hasattr(Token, '__weakref__'))
# 3.12.2: True
# 3.12.3: False
Причина? Причина нам пока не очень понятна. Давайте разбираться. Баг состоит из нескольких частей.
Weakref
Что вообще такое
__weakref__? Конечно же, оно связано с модулем weakref для создания слабых ссылок, которые не увеличивают ob_refcnt объекта. Внутри __weakref__ будет хранится объект ссылки. Смотрим:
>>> class My: ...
...
>>> import weakref
>>> m = My()
>>> w = weakref.ref(m)
>>> m.__weakref__
<weakref at 0x103c77d20; to 'My' at 0x103be9920>
>>> m.__weakref__ is w
True
Следовательно: когда мы создаем объект со слотами, нам необходимо, чтобы слот
__weakref__ существовал. Иначе – будет ошибка:
>>> class WithSlots:
... __slots__ = () # no '__weakref__'
...
>>> weakref.ref(WithSlots())
TypeError: cannot create weak reference to 'WithSlots' object
PEP 695
В Python3.12, как мы все знаем, добавили новый синтаксис для
TypeVar, ParamSpec, TypeVarTuple и Generic классов, функций и алиасов.Чтобы реализовать данную возможность, часть кода была переписана с Python на C. Например, появились такие файлы как:
- https://github.com/python/cpython/blob/main/Objects/typevarobject.c
- https://github.com/python/cpython/blob/main/Include/internal/pycore_typevarobject.h
- https://github.com/python/cpython/blob/main/Modules/_typingmodule.c
Значит, что в Python3.12
Generic стал С типом.А значит, что у них поменялись внутренности устройства.
И теперь классам с
[] автоматически назначается родитель: _typing.Generic
>>> class Example[T]: ...
...
>>> Example.__bases__
(<class 'typing.Generic'>,)
Следовательно, чтобы правильно работали слоты – нужно проверить родителя.
Offsets
В сишных типах обычно не объявляют
__slots__, потому что используют другие - сишные - слоты.Что за слоты такие?
Если вы смотрите "Лучший курс по питону", то вы их уже много раз видели (а если не смотрите, то почему?!). Слоты =
tp_* места для вставки разных обработчиков под разные случаи жизни. Например:-
tp_new для __new__-
tp_richcompare для сравнений >, <, тд- Полный список: https://docs.python.org/3/c-api/typeobj.html#tp-slots
Есть несколько специальных слотов, которые нас сегодня интересуют:
- Слот
tp_dictoffset или макро Py_TPFLAGS_MANAGED_DICT, который указывает, что у объекта есть __dict__- Слот
tp_weakrefoffset или макро Py_TPFLAGS_MANAGED_WEAKREF, который указывает, что у объекта есть __weakref__Посмотрим:
PyType_Spec typevar_spec = {
.name = "typing.TypeVar",
.flags = ... | Py_TPFLAGS_MANAGED_DICT | Py_TPFLAGS_MANAGED_WEAKREF,
};
// vs
PyType_Spec generic_spec = {
.name = "typing.Generic",
// No `__dictoffset__` and no `__weakrefoffset__`:
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
};
Смотрим на разницу:
>>> from _typing import TypeVar, Generic
>>> TypeVar.__dictoffset__, TypeVar.__weakrefoffset__
(-1, -32)
>>> Generic.__dictoffset__, Generic.__weakrefoffset__
(0, 0)
>>> weakref.ref(TypeVar('A'))
<weakref at 0x103c77c40; dead>
>>> weakref.ref(Generic())
TypeError: cannot create weak reference to 'typing.Generic' object
Фатальное изменение
И последняя часть. К нам пришел вот такой PR: https://github.com/python/cpython/commit/a22d05f04c074dbb4f71e7837f54c0bb693db75d
def _get_slots(cls):
match cls.__dict__.get('__slots__'):
# A class which does not define __slots__ at all is equivalent
# to a class defining __slots__ = ('__dict__', '__weakref__')
case None:
yield from ('__dict__', '__weakref__')
# ...