PEP-800: typing.disjoint_base
ПЕП: https://peps.python.org/pep-0800
Обсуждение: https://discuss.python.org/t/99910
Реализация в typing_extensions: https://github.com/python/typing_extensions/blob/a7610ef567132cac2b5319fa193c830b655364c6/src/typing_extensions.py#L358
Когда я критикую систему типов в питоне, говоря, что её не продумывали заранее, то disjoint_base - отличный пример моих слов.
В питоне можно наследоваться от нескольких классов (и вообще всего с методом __mro_entries__, да). Что иногда довольно удобно, если использовать без фанатизма. Но проблема в том, что не все наборы классов подходят для множественного наследования.
Например:
>>> class What(str, int): ...
Traceback (most recent call last):
TypeError: multiple bases have instance lay-out conflict
Почему такое происходит почитать можно тут. Но, нам важно узнать, что в CPython есть концепция "solid base", которая считается для всех созданных классов вот тут. Если очень кратко, то CPython должен уметь построить правильный memory-layout для всех классов потомков. Например, все потомки int должны быть вот так уложены в памяти:
typedef struct _PyLongValue {
uintptr_t lv_tag; / Number of digits, sign and flags /
digit ob_digit[1];
} _PyLongValue;
struct _longobject {
PyObject_HEAD
_PyLongValue long_value;
};
Иначе - перестанет работать базовая логика int. А str устроены по-другому.
/ Object format for Unicode subclasses. /
typedef struct {
PyCompactUnicodeObject _base;
union {
void *any;
Py_UCS1 *latin1;
Py_UCS2 *ucs2;
Py_UCS4 *ucs4;
} data; / Canonical, smallest-form Unicode buffer /
} PyUnicodeObject;
Сделать область памяти для потомка int и str сразу - невозможно. Потому и выкидывается ошибка.
Но, сделать так можно и со своими классами, не обязательно использовать C, достаточно конфликта в __slots__:
>>> class A:
... __slots__ = ('a',)
>>> class B:
... __slots__ = ('b',)
>>> class C(A, B): ...
Traceback (most recent call last):
TypeError: multiple bases have instance lay-out conflict
Подробности из ПЕПа про "solid bases".
Типизация
Теперь int, str и многие другие классы в typeshed помечены как @disjoint_base типы. Значит, что только один такой класс может быть в mro. Раньше такое костылили все тайпчекеры по-своему.
Как следствие, тайпчекеры теперь более четко смогут находить и другие проблемы. Например, в местах где мы создаем "временный" тип:
def g(x: int):
match x:
case str(): # unreachable
print("It's both!")
Раньше такой код проходил в некоторых тайпчекерах. Ведь они думали, что тип подкласс int и str может существовать. А теперь - будут знать, что такое невозможно на уровне определения и выкидывать правильную ошибку.
Отличный ПЕП, система типов стала чуть лучше.
Обсуждение: знали ли вы про solid и disjoint bases в питоне? Стреляли ли себе в ногу таким?
| Поддержать | YouTube | GitHub | Чат |
ПЕП: https://peps.python.org/pep-0800
Обсуждение: https://discuss.python.org/t/99910
Реализация в typing_extensions: https://github.com/python/typing_extensions/blob/a7610ef567132cac2b5319fa193c830b655364c6/src/typing_extensions.py#L358
Когда я критикую систему типов в питоне, говоря, что её не продумывали заранее, то disjoint_base - отличный пример моих слов.
В питоне можно наследоваться от нескольких классов (и вообще всего с методом __mro_entries__, да). Что иногда довольно удобно, если использовать без фанатизма. Но проблема в том, что не все наборы классов подходят для множественного наследования.
Например:
>>> class What(str, int): ...
Traceback (most recent call last):
TypeError: multiple bases have instance lay-out conflict
Почему такое происходит почитать можно тут. Но, нам важно узнать, что в CPython есть концепция "solid base", которая считается для всех созданных классов вот тут. Если очень кратко, то CPython должен уметь построить правильный memory-layout для всех классов потомков. Например, все потомки int должны быть вот так уложены в памяти:
typedef struct _PyLongValue {
uintptr_t lv_tag; / Number of digits, sign and flags /
digit ob_digit[1];
} _PyLongValue;
struct _longobject {
PyObject_HEAD
_PyLongValue long_value;
};
Иначе - перестанет работать базовая логика int. А str устроены по-другому.
/ Object format for Unicode subclasses. /
typedef struct {
PyCompactUnicodeObject _base;
union {
void *any;
Py_UCS1 *latin1;
Py_UCS2 *ucs2;
Py_UCS4 *ucs4;
} data; / Canonical, smallest-form Unicode buffer /
} PyUnicodeObject;
Сделать область памяти для потомка int и str сразу - невозможно. Потому и выкидывается ошибка.
Но, сделать так можно и со своими классами, не обязательно использовать C, достаточно конфликта в __slots__:
>>> class A:
... __slots__ = ('a',)
>>> class B:
... __slots__ = ('b',)
>>> class C(A, B): ...
Traceback (most recent call last):
TypeError: multiple bases have instance lay-out conflict
Подробности из ПЕПа про "solid bases".
Типизация
Теперь int, str и многие другие классы в typeshed помечены как @disjoint_base типы. Значит, что только один такой класс может быть в mro. Раньше такое костылили все тайпчекеры по-своему.
Как следствие, тайпчекеры теперь более четко смогут находить и другие проблемы. Например, в местах где мы создаем "временный" тип:
def g(x: int):
match x:
case str(): # unreachable
print("It's both!")
Раньше такой код проходил в некоторых тайпчекерах. Ведь они думали, что тип подкласс int и str может существовать. А теперь - будут знать, что такое невозможно на уровне определения и выкидывать правильную ошибку.
Отличный ПЕП, система типов стала чуть лучше.
Обсуждение: знали ли вы про solid и disjoint bases в питоне? Стреляли ли себе в ногу таким?
| Поддержать | YouTube | GitHub | Чат |