Находки в опенсорсе – Telegram
Находки в опенсорсе
10.8K subscribers
11 photos
1 video
3 files
820 links
Привет!

Меня зовут Никита Соболев. Я занимаюсь опенсорс разработкой полный рабочий день.

Тут я рассказываю про #python, #c, опенсорс и тд.
Поддержать: https://boosty.to/sobolevn
РКН: https://vk.cc/cOzn36

Связь: @sobolev_nikita
Download Telegram
Hi 👋

We are happy to announce the second edition of our long-awaited testing course!
The second edition will be even more advanced and feature-full 🙂

We all know that sometimes people struggle with tests, because there are so many things to get right:
- Frameworks
- Mocking
- Data generation
- Flakyness
- Speed
- Different levels and kinds of tests

Sounds hard? We are here to help!

We will start with just one free webinar, where we will cover the most essential part, it is a foundation fore every developer who writes tests.

We even created a rather big project to be tested as a part of the homework for the later parts of this course. Check it out: https://github.com/tough-dev-school/python-testing-homework

Date: 06.09.2023
Time: 19:00 GMT+3
Language: ru

Register via our telegram bot to get a translation link: @tough_dev_bot
And prepare your questions 🙂

The course itself will start on 11.09
https://education.borshev.com/python-testing

See you there! 👍️
Forwarded from Никита Соболев
всем привет! я очень долго обещал сделать бесплатный курс на ютюбе для всех желающих. и вот я, наконец, начал его делать! 🎉

встречайте: https://www.youtube.com/@sobolevn

уникальность формата в том, что рассматриваю одну узкую тему с трех уровней сложности: junior, middle, senior. так что, контент должен быть интересным для всех уровней Python разработчиков!

обратите внимание, что курс не для тех, кто идет учить питон с нуля. он для тех, кто уже хоть немного знает, как погромировать на питоне.

важные ссылки:
- все материалы курса: https://github.com/sobolevn/the-best-...
- мой гитхаб: https://github.com/sobolevn
- поддержать мою работу: https://boosty.to/sobolevn
- вступить в наше новое глобальное сообщество: https://discord.python.ru

буду рад обратной связи!
в ближайших планах:
- починить звук и свет
- избавиться от слова "интерсный" в описании примерно всего 😄
- сделать много новых видео по разным темам
👍102
Находки в опенсорсе pinned «всем привет! я очень долго обещал сделать бесплатный курс на ютюбе для всех желающих. и вот я, наконец, начал его делать! 🎉 встречайте: https://www.youtube.com/@sobolevn уникальность формата в том, что рассматриваю одну узкую тему с трех уровней сложности:…»
привет!

в среду 10 июля играем в IT-шную опенсорсную настолку Ship IT в хорошей компании!
ссылка на игру: https://github.com/sobolevn/ship-it-boardgame

в программе:
- душные ITшные шутки
- специальный набор карт для взаимодействия со зрителями
- конкурс на самый смешную шутку в комментариях (приз: физическая настолка!)

участвуют:
- Аня Курносова Pytup: https://news.1rj.ru/str/+Bz-uVcXh4Jk1YTNi
- Алексей Пирогов https://github.com/astynax
- Денис Пушкарев https://github.com/zloirock
- Паша Коршиков https://news.1rj.ru/str/tech_meetup
- Олег Чирухин https://github.com/olegchir

ведущий: Никита Соболев https://github.com/sobolevn

начало: 19:30 по МСК
ссылка на трансляцию: https://www.youtube.com/watch?v=pR8tQaoOitc
👍3
Находки в опенсорсе pinned «привет! в среду 10 июля играем в IT-шную опенсорсную настолку Ship IT в хорошей компании! ссылка на игру: https://github.com/sobolevn/ship-it-boardgame в программе: - душные ITшные шутки - специальный набор карт для взаимодействия со зрителями - конкурс…»
Channel name was changed to «Находки в опенсорсе»
Привет!

Давайте знакомиться заново.
Меня зовут Никита, я опенсорс разработчик: https://github.com/sobolevn
Я люблю компиляторы, тайпчекеры, системы тестирования и статические анализаторы разных видов.
Я разрабатываю множество инструментов, которыми вы уже 100% пользуетесь.

Концепция канала поменялась. О чем тут теперь будет?
- Больше я не буду закидывать безличную информацию о других опенсорс проектах, но буду больше рассказывать про те, где я принимаю активное участие: CPython, mypy, typeshed, Django и тд
- Буду делиться видео своих и чужих интересных докладов
- Публиковать материалы для "Лучшего курса по Питону": https://www.youtube.com/@sobolevn
- Рассказывать про интересные проекты, которые мне встретились в опенсорсе, прикоснуться к которым действительно удалось
- Делиться знаниями про сложные и интересные штуки в моей работе :)

Поддержать мою работу можно тут: https://boosty.to/sobolevn
🔥94👍2813👏6😱1🙏1
Находки в опенсорсе pinned «Привет! Давайте знакомиться заново. Меня зовут Никита, я опенсорс разработчик: https://github.com/sobolevn Я люблю компиляторы, тайпчекеры, системы тестирования и статические анализаторы разных видов. Я разрабатываю множество инструментов, которыми вы уже…»
А еще я включил реакции и комментарии, наслаждайтесь! Правила: https://gist.github.com/sobolevn/d9a598a23e6bb89e51ada71033e9103f
62🔥34🥰10💩5😱4🎉4👏3🤩3🕊2🤯1😢1
И сразу пример нового контента.

За последние два дня добавил поддержку двух новых типов в hypothesis:
- ReadOnly: https://github.com/HypothesisWorks/hypothesis/pull/4072
- LiteralString: https://github.com/HypothesisWorks/hypothesis/pull/4075

Что такое hypothesis? Такой фреймворк для тестирования на основе идеи Property-Based Testing: когда вы тестируете не конкретные значения, а целые законы или свойства вашей системы.

Например:


import datetime as dt

import attrs

@attrs.define
class User:
# ...
joined_at: dt.datetime
last_loggined_at: dt.datetime

def create_our_user() -> User:
... # your business logic

# Testing:
from hypothesis import given, strategies as st

@given(st.builds(create_our_user))
def test_user_login_cannot_be_after_created(user: User) -> None:
assert user.last_loggined_at >= user.joined_at


Что очень удобно, st.from_type(...) может просто по аннотациям генерировать вам объекты; потому поддержка новых типов - всегда полезна.

Hypothesis является частью CI самого CPython: https://github.com/python/cpython/blob/main/Lib/test/support/hypothesis_helper.py и является очень крутой технологией.

На ее основе можно делать безумные вещи: https://sobolevn.me/2021/02/make-tests-a-part-of-your-app
👍36😱8🔥3
Одна из самых проблемных частей CPython – вызов Python кода из С.

Делать такое нужно довольно регулярно. Примеры использований:
- Обращение к магическим методам объектов: PyObject_RichCompare, PyObject_GetIter, PyIter_Next, PyObject_GetItem, и тд
- Вызов переданных Python callback'ов: PyObject_Call*, PyObject_Vectorcall, и тд
- Создание новых объектов: PyObject_New

Но, такое всегда нужно делать осторожно. Буквально, почти весь стейт внутри C может измениться после вызова любого Python кода!

Например, такой простой код вызовет [1] 88503 segmentation fault python на версиях <3.12.5


class evil:
def __init__(self, lst):
self.lst = lst
def __iter__(self):
yield from self.lst
self.lst.clear()

lst = list(range(10))
lst[::-1] = evil(lst)


Мне нравится править такое, одно из самых интересных направлений:
- https://github.com/python/cpython/pull/120442
- https://github.com/python/cpython/pull/120303

А вот как такое находить?
1. Внутри CPython есть свой фаззер: https://github.com/python/cpython/tree/main/Modules/_xxtestfuzz Иногда он находит код, который крашит какой-то кусок. Было довольно много полезных срабатываний
2. Есть отдельные инструменты и команды по всему миру, кто заинтересован в разметке исходников CPython и выявлении таких проблем статически
3. Собирать баги от пользователей :(

Если видите crash – бегом репортить багу!
🔥35👍5😱3👏2
История одного PR #prhistory

Некоторое время назад я добавил в mypy поддержку ReadOnly special form для TypedDict: https://github.com/python/mypy/pull/17644
PR большой, его будут смотреть еще некоторое время. Но, о самых важных принципах рассказать можно уже сейчас.

1. Что такое ReadOnly?

PEP: https://peps.python.org/pep-0705

По сути, он запрещает такой код:


from typing import ReadOnly, TypedDict

class User(TypedDict):
username: ReadOnly[str] # you cannot change the value of `user['username']`

user: User = {'username': 'sobolevn'}
user['username'] = 'changed' # type error! username is read-only


Крайне полезная вещь для бизнес логики.

2. ReadOnly был добавлен в Python в версию 3.13 мной некоторое время назад: https://github.com/python/cpython/pull/116350

Однако, он был добавлен в typing_extensions еще раньше: https://github.com/python/typing_extensions/commit/0b0166d649cebcb48e7e208ae5da36cfab5965fe
Так что пользоваться typing_extensions.ReadOnly можно будет как только выйдет новая версия mypy с поддержкой данной special form.

3. Как устроен ReadOnly?

Основная сложность, что разные special form'ы могут идти вместе:
- username: ReadOnly[Required[str]]
- age: NotRequired[Annotated[ReadOnly[int], Validate(min=18, max=120)]]
- и тд в любых комбинациях

Внутри TypedDict появились специальные атрибуты: __readonly_keys__ и __mutable_keys__:


>>> from typing import TypedDict, ReadOnly
>>> class User(TypedDict):
... username: ReadOnly[str]
... age: int
...
>>> User.__readonly_keys__
frozenset({'username'})
>>> User.__mutable_keys__
frozenset({'age'})


4. Какие делатали типизации важны?

Помимо очевидного запрета на изменение ReadOnly ключей, нужно помнить, про отношение подтипов.

Пример:


User = TypedDict('User', {'username': ReadOnly[str]})
MutableUser = TypedDict('MutableUser', {'username': str})

def accepts_user(user: User): ...
def accepts_mutable_user(user: MutableUser): ...

ro_user: User
mut_user: MutableUser

# MutableUser является подвидом User, но User не является подвидом MutableUser
accepts_user(mut_user) # ok
accepts_mutable_user(ro_user) # error: expected: MutableUser, given: User


Но почему?
Потому что тело функции accepts_mutable_user может выглядеть как-то так:


def accepts_mutable_user(user: MutableUser):
user['username'] = ''


Таким образом – мы могли бы допустить ошибку и изменить "неизменяемый" ключ.

Ждём? 🤔
🔥49👍14🤔3👎1😱1
Сегодня говорим про bytes!

Вышел восьмой урок "Лучшего курса по Питону": https://www.youtube.com/watch?v=RbznhbK3vC0

Что вообще такое "Лучший курс по Питону"?
- Я решил разобрать все исходники CPython и показать, как на самом деле работают все его части
- В каждом видео я рассказываю про одну узкую тему
- Каждое видео я делю на три уровня сложности: для джунов, мидлов и сениоров
- Переодически беру интервью у других core-разработчиков CPython про разные части интерпретатора в их зоне интересов
- Получается очень хардкорно!

Например, в bytes я показываю, как устроен PyBytesObject (он простой):


typedef struct {
PyObject_VAR_HEAD
Py_DEPRECATED(3.11) Py_hash_t ob_shash;
char ob_sval[1];

/* Invariants:
* ob_sval contains space for 'ob_size+1' elements.
* ob_sval[ob_size] == 0.
* ob_shash is the hash of the byte string or -1 if not computed yet.
*/
} PyBytesObject;


Как устроен Buffer протокол для bytes с его __buffer__ и __release_buffer__:


static int
bytes_buffer_getbuffer(PyBytesObject *self, Py_buffer *view, int flags)
{
return PyBuffer_FillInfo(view, (PyObject*)self, (void *)self->ob_sval, Py_SIZE(self), 1, flags);
}

static PyBufferProcs bytes_as_buffer = {
(getbufferproc)bytes_buffer_getbuffer,
NULL,
};


Дополнительные материалы (не вошли в выпуск):
- mypy: bytes и bytearray c disable_bytearray_promotion и disable_memoryview_promotion https://github.com/python/mypy/commit/2d70ac0b33b448d5ef51c0856571068dd0754af6
- Мутабельность bytes https://docs.python.org/3.13/c-api/bytes.html#c._PyBytes_Resize
- PyBytes_Writer API: https://github.com/capi-workgroup/decisions/issues/39
- ob_shash deprecation: https://github.com/python/cpython/issues/91020

Поддержать проект можно тут: https://boosty.to/sobolevn

#лкпп #python #c
🔥5115👍1👏1
Одна из самых сложных частей в устройстве mypy – type narrowing.

Что такое type narrowing? По-русски оно называется "сужение типа". Например:


def accepts_both(arg: int | str):
reveal_type(arg) # int | str
if isinstance(arg, int):
reveal_type(arg) # int
else:
reveal_type(arg) # str


Тут все достаточно очевидно. Следующие важные части для сужения типов:
- TypeGuard – определяет, каким будет тип в if при вызове функции-проверки. Почти не имеет ограничений.
- TypeIs – определяет, каким будет тип в if при вызове функции-проверки и что будет в else. Имеет множество ограничений.

Пример:


from typing import TypeIs

from my_project import Schema

def is_schema(obj: object) -> TypeIs[Schema]:
return hasattr(obj, "_schema") # actually returns `bool`

def accepts_both(obj: str | Schema):
reveal_type(arg) # str | Schema
if is_schema(arg):
reveal_type(arg) # Schema
else:
reveal_type(arg) # str


Есть еще много разных механизмов для сужения, подробно можно посмотреть тут: https://github.com/python/mypy/blob/fe15ee69b9225f808f8ed735671b73c31ae1bed8/mypy/checker.py#L5805

Но сейчас поговорим про новую фичу, которая скоро появится в mypy. TypeIs / TypeGuard + @overload 😱

Допустим у вас есть такой код:


from typing import Any, TypeIs, overload

@overload
def func1(x: str) -> TypeIs[str]:
...

@overload
def func1(x: int) -> TypeIs[int]:
...

def func1(x: Any) -> Any:
return True # does not matter


После моего PR (https://github.com/python/mypy/pull/17678) вызов данной функции и сужение типа будут работать корректно:


def accepts_both(val: Any):
if func1(val):
reveal_type(val) # N: Revealed type is "Union[builtins.int, builtins.str]"
else:
reveal_type(val) # N: Revealed type is "Any"


Данная функция, например, используется для типизации встроенной функции dataclasses.is_dataclass: https://github.com/python/typeshed/blob/53be87bbb45d0b294a4f5b12683e7684e20032d9/stdlib/dataclasses.pyi#L217-L223

Сейчас ей требуется грязный хак с Never в первом @overload:



# HACK: `obj: Never` typing matches if object argument is using `Any` type.
@overload
def is_dataclass(obj: Never) -> TypeIs[DataclassInstance | type[DataclassInstance]]: ... # type: ignore[narrowed-type-not-subtype] # pyright: ignore[reportGeneralTypeIssues]
@overload
def is_dataclass(obj: type) -> TypeIs[type[DataclassInstance]]: ...
@overload
def is_dataclass(obj: object) -> TypeIs[DataclassInstance | type[DataclassInstance]]: ...


Дальше – я его уберу.

В практических задачах данный подход может использоваться для создания typesafe бизнес логики, которая сужает типы ваших бизнес объектов по условию:


from typing import TypeIs, overload

from my_app.models import User, PaidUser, FreeUser # User and its subclasses
from my_app.models import Subnoscription

@overload
def is_paid_user(user: User, subnoscription: None) -> TypeIs[FreeUser]:
...

@overload
def is_paid_user(user: User, subnoscription: Subnoscription) -> TypeIs[PaidUser]:
...


В комментах можно обсудить: приходилось ли вам использовать type narrowing для решения ваших бизнес задач? Помогает ли такой подход в написании более надежного кода?

#prhistory
👍35🔥83
Проблемы модуля `inspect`.

Модуль inspect в питоне – сборник костылей и легаси.

Если вы не любите людей, то можете спрашивать их:
1. Чем отличается typing.get_type_hints от inspect.get_annotations? А от annotationslib.get_annotations?
2. Какие проблемы есть у getargvalues?
3. Чем отличаются getargs, getfullargspec и singature?
4. В чем разница между inspect.iscoroutinefunction и asyncio.iscoroutinefunction? А между inspect.iscoroutine и asyncio.iscoroutine?
5. Чем будет отличаться inspect.getmembers от inspect.getmembers_static?
6. Как конкретно работает получение сигнатуры у разных объектов? 😱

Некоторое время назад я взялся исправить несколько самых сломанных частей: https://github.com/python/cpython/issues/108901

И даже сделал пакет с бекпортами для <=3.13: https://github.com/wemake-services/inspect313
Но все опять оказалось совсем не просто. Я не успел до фича фриза в 3.13, так что надеюсь, что успею в 3.14

Что сломано?

Например: inspect.getargvalues. Оно не работает с pos-only параметрами:


>>> import inspect

>>> def func(a: int = 0, /, b: int = 1, *, c: int = 2):
... return inspect.currentframe()

>>> frame = func()
>>> # notice that pos-only and kw-only args are not supported properly:
>>> inspect.formatargvalues(*inspect.getargvalues(frame))
'(a=0, b=1, c=2)'


Должно быть так:


>>> from inspect import Signature

>>> str(Signature.from_frame(frame)) # this API does not exist yet
'(a=0, /, b=1, *, c=2)'


Но, возникает вопрос: а нужно ли вообще добавлять такой метод? Насколько полезено получать сигнатуры из фреймов и код-обжектов?

Далее: getfullargspec. Он не поддерживает pos-only параметры и не совсем корректно работает с параметрами self, cls, тд.


>>> import inspect

>>> class A:
... def method(self, arg, /): ...

>>> inspect.getfullargspec(A.method)
FullArgSpec(args=['self', 'arg'], varargs=None, varkw=None, defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={})
>>> inspect.getfullargspec(A().method).args # must not report `self`! :(
['self', 'arg']

>>> inspect.signature(A.method)
<Signature (self, arg, /)>
>>> inspect.signature(A().method)
<Signature (arg, /)>


Но, все-таки работа ведется довольно активно:
- asyncio.iscoroutinefunction уже задепрекейчена: https://github.com/python/cpython/pull/122875 Скоро будет только версия из inspect
- Добавили annotationslib.get_annotations (которая переехала из inspect и теперь будет самым-правильным-способом): https://github.com/python/cpython/blob/9e108b8719752a0a2e390eeeaa8f52391f75120d/Lib/annotationlib.py#L582
- Пофиксили кучу багов

Для чего `inspect` можно использовать на практике?

Я пользовался inspect.signature только для создания рантайм имплементациия каррирования для dry-python/returns: https://github.com/dry-python/returns/blob/master/returns/curry.py

Довольно много библиотечного кода используют inspect для интроспекции в самых неожиданных местах:
- https://github.com/search?type=code&q=inspect.iscoroutinefunction
- https://github.com/search?type=code&q=inspect.getfullargspec
- https://github.com/search?type=code&q=inspect.getargvalues

Расскажите: а у вас были проблемы с inspect? Если да, то какие?
🔥26😁3👍21
`slots=True` ломает ваши датаклассы!

Когда прям с заголовка набросил, то дальше уже всегда проще.

Давайте посмотрим, какую пользу и какой вред приносит использование @dataclass(slots=True) или @attr.define(slots=True). В целом - различий не так много.

Во-первых, что делает __slots__ = ('a',) внутри класса?


class My:
__slots__ = ('a',)


1. Валидирует, что __slots__ корректны
2. Генерирует дескрипторы для всех имен в __slots__, см https://github.com/python/cpython/blob/91ff700de28f3415cbe44f58ce84a2670b8c9f15/Objects/descrobject.c#L793-L796


>>> class My:
... __slots__ = ('a',)
...
>>> My.a, type(My.a)
(<member 'a' of 'My' objects>, <class 'member_denoscriptor'>)


3. Если слот __dict__ не проставлен, то меняет базовую функцию доступа к и установки аттрибутов


/* Special case some slots */
if (type->tp_dictoffset != 0 || ctx->nslot > 0) {
PyTypeObject *base = ctx->base;
if (base->tp_getattr == NULL && base->tp_getattro == NULL) {
type->tp_getattro = PyObject_GenericGetAttr;
}
if (base->tp_setattr == NULL && base->tp_setattro == NULL) {
type->tp_setattro = PyObject_GenericSetAttr;
}
}


Из-за чего больше нельзя будет назначать произвольные атрибуты:


>>> class My:
... __slots__ = ('a',)
...
>>> m = My()
>>> m.custom = 0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'My' object has no attribute 'custom'


Но, достаточно добавить '__dict__' внутрь __slots__, чтобы вернуть данное поведение: __slots__ = ('a', '__dict__'):


>>> class My:
... __slots__ = ('a', '__dict__')
...
>>> m = My()
>>> m.custom = 0


4. __slots__ ускоряет доступ к атрибутам и уменьшает размер объектов.


>>> import sys
>>> class A:
... __slots__ = ('a',)
... def __init__(self, a):
... self.a = a

>>> class B:
... def __init__(self, a):
... self.a = a

>>> sys.getsizeof(A(1))
40
>>> sys.getsizeof(B(1))
56


Разница в скорости доступа не очень большая, но есть:


» pyperf timeit -s '
class A:

def __init__(self, a):
self.a = a

a = A(1)' 'a.a'
.....................
Mean +- std dev: 13.9 ns +- 0.1 ns


Против доступа со __slots__:


» pyperf timeit -s '
. class A:
. __slots__ = ("a",)
. def __init__(self, a):
. self.a = a
.
. a = A(1)' 'a.a'
.....................
Mean +- std dev: 13.3 ns +- 0.1 ns


Так что там с датаклассами?

Штука в том, что создать слоты в существующем классе – нельзя физически. Слишком много всего написано на C.
Можно только пересоздать класс еще раз 😱
https://github.com/python/cpython/blob/91ff700de28f3415cbe44f58ce84a2670b8c9f15/Lib/dataclasses.py#L1224-L1276


cls = type(cls)(cls.__name__, cls.__bases__, cls_dict)


Что порождает много проблем. Например:
- Нельзя использовать super() без параметров в методах внутри тела класса:


>>> @dataclass(slots=True)
... class My:
... def __str__(self) -> str:
... return super().__str__()
...
>>> str(My())
Traceback (most recent call last):
File "<python-input-2>", line 1, in <module>
str(My())
~~~^^^^^^
File "<python-input-1>", line 4, in __str__
return super().__str__()
^^^^^^^^^^^^^^^
TypeError: super(type, obj): obj (instance of My) is not an instance or subtype of type (My).


Потому что __str__.closure не обновляет cell объекты на другой класс при пересоздании. Есть PR, но все сложно: https://github.com/python/cpython/pull/111538

- Нельзя использовать `__init_subclass__` в родителях класса со slots, где ожидаются параметры. Тут только документацией можно помочь: https://github.com/python/cpython/pull/123342

Так что - будьте осторожны!
🔥35👍13🤔75🤯3🤡3
Материалы с PythoNN 30 августа 2024

Мы проводим питон митапы в Нижнем Новгороде раз в квартал уже несколько лет.
30 августа к нам приезжало два замечательных гостя:

- Александр Гончаров – "Это вообще не просто!" https://www.youtube.com/watch?v=0EFHpmEgXak
- Андрей Пронин – "Как увеличить зарплату в два раза за год?" https://www.youtube.com/watch?v=IfLT_ssxOhU

Внутри есть ссылки и на презентации к докладам, и на личные страницы гостей.

Друзья, спасибо большое за интересные доклады. Спасибо гостям за отличную атмосферу и интересные вопросы.
Если хотите побывать в НН и заодно сделать доклад – пишите, буду рад помочь с подготовкой!
🔥34👏5😱1
Находки в опенсорсе
`slots=True` ломает ваши датаклассы! Когда прям с заголовка набросил, то дальше уже всегда проще. Давайте посмотрим, какую пользу и какой вред приносит использование @dataclass(slots=True) или @attr.define(slots=True). В целом - различий не так много. Во…
Продолжаем ломать dataclass'ы со `__slots__`!

Некоторое время назад прилетел баг: 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__')
# ...
👍133🔥3
Находки в опенсорсе
`slots=True` ломает ваши датаклассы! Когда прям с заголовка набросил, то дальше уже всегда проще. Давайте посмотрим, какую пользу и какой вред приносит использование @dataclass(slots=True) или @attr.define(slots=True). В целом - различий не так много. Во…
Который предполагал, что если __slots__ явно не объявлены у типа, то по-умолчанию стоят __dict__ и __weakref__. Что правда для Python типов, но нельзя забывать про C типы, как я показывал выше.

Я пофиксил вот так: https://github.com/python/cpython/commit/fa9b9cb11379806843ae03b1e4ad4ccd95a63c02


def _get_slots(cls):
match cls.__dict__.get('__slots__'):
# `__dictoffset__` and `__weakrefoffset__` can tell us whether
# the base type has dict/weakref slots, in a way that works correctly
# for both Python classes and C extension types. Extension types
# don't use `__slots__` for slot creation
case None:
slots = []
if getattr(cls, '__weakrefoffset__', -1) != 0:
slots.append('__weakref__')
if getattr(cls, '__dictrefoffset__', -1) != 0:
slots.append('__dict__')
yield from slots


Теперь мы правильно учитываем наличие C слотов в том числе. И правильно определяем, что у базового класса Generic нет слота __weakref__. И нам нужно его добавить в наш новый датакласс.

Дело закрыто! Ну как вам погружение? :)
1🤯28🔥11👍6