opensource_findings | Неотсортированное

Telegram-канал opensource_findings - Находки в опенсорсе

8500

Привет! Меня зовут Никита Соболев. Я занимаюсь опенсорс разработкой полный рабочий день. Тут я рассказываю про #python, #c, интересные проекты, коммиты, доклады, и тд. Поддержать: https://boosty.to/sobolevn Для связи: @sobolev_nikita

Подписаться на канал

Находки в опенсорсе

Лучший курс по Python 11: bytearray

42 минуты С-шного хардкора про bytearray, что может быть лучше?

https://www.youtube.com/watch?v=5UFx29EVlkU

В видео будет про:
- Разные аллокаторы в CPython: PyMem_Malloc и PyMem_Realloc
- C-pointer math (для самых маленьких)
- Разные хитрые оптимизации для работы с bytearray

Бонус 1

Я обещал поделиться логикой стратегии изменения размера bytearray.

Бонус 2

Интересный вопрос, который я не осветил в видео. Почему код работает так?


>>> b = bytearray(b'1234')
>>> del b[:4:2]
>>> b.__alloc__()
5


Почему так?

1. Вызывается функция: bytearray_ass_subscript
2. values будет NULL, потому как удаление (сишный аналог __delitem__ из питона работает так)
3. Дальше распаковываем slice в переменные тут
4. Удаляем тут хитрым и достаточно быстрым способом

Ответы

Ответ на вопрос из видео с поиском бага на слайде в Cшном коде: https://github.com/python/cpython/pull/126981

| Поддержать | sobolevn">YouTube | GitHub |

Читать полностью…

Находки в опенсорсе

Кстати, у нас в Нижнем Новгороде будет митап по питону 22 ноября: /channel/pytho_nn/15099

4 крутейших спикера:
- "Уязвимый" Python – Юлия Волкова (CodeScoring, Санкт-Петербург)
- "Английский для разработчика" – James Stuart Black (JB Teach, Нижний Новгород)
- "Программирование и искусство" – Дмитрий Сошников (НИУ ВШЭ/МАИ/Yandex Cloud, Москва)
- "Квантовое программирование на Python: Погружение в квантовые вычисления для разработчиков" – Бейлак Алиев (Райффайзен банк, Москва)

И еще:
- Общение в баре после митапа
- Игра в мою настолку: https://github.com/sobolevn/ship-it-boardgame

Регистрация: https://pytho-nn.timepad.ru/event/3089004/
Чат местного сообщества: @pytho_nn

Если будете в Нижнем - заходите! Ждем всех любителей питонов :)

Читать полностью…

Находки в опенсорсе

Чем корутина реально отличается от генератора?

Достаточно часто можно услышать, что корутины произошли от генераторов. Живы еще те динозавры, которые помнят @asyncio.coroutine в py3.4 и корутины на yield

Определения

На самом деле, определить, что такое генератор и корутина - не так просто, как может показаться. Я пользуюсь следующими определениям:
- генератор = что-то, что соответствует интерфейсу _collections_abc.Generator
- корутина = что-то, что соответствует интерфейсу _collections_abc.Coroutine

(да, вы можете создавать свои нестандартные генераторы и корутины на Python и C)

Уже в самих интерфейсах, вы можете увидеть, что часть методов идентична, часть отличается:


>>> set(dir(gen)) - set(dir(coro))
{'gi_running', '__next__', 'gi_frame', 'gi_yieldfrom', 'gi_suspended', '__iter__', 'gi_code'}

>>> set(dir(coro)) - set(dir(gen))
{'cr_running', '__await__', 'cr_frame', 'cr_suspended', 'cr_origin', 'cr_code', 'cr_await'}


Копнем чуть глубже, создадим "generator function" и "coroutine function" (функции, которые вернут генератор / корутину):


import asyncio

async def coro_func():
await asyncio.sleep(1)

def gen_func():
yield 1


Байткод их создания будет абсотютно идентичным (разница только в именах):


2 LOAD_CONST 0 (<code object ...>)
MAKE_FUNCTION
STORE_NAME 0 (<NAME>)


Потому что сами объекты функций будут очень похожими, отличаться будут объекты CodeObject, которые создаются компилятором в assemble.c. Проходимся по всем AST нодам в функции и вычисляем, есть ли там yield или yield from:


static int
symtable_visit_expr(struct symtable *st, expr_ty e)
{
switch (e->kind) {
// ...
case Yield_kind:
if (e->v.Yield.value)
VISIT(st, expr, e->v.Yield.value);
st->st_cur->ste_generator = 1; // теперь функция будет "generator function"
if (st->st_cur->ste_comprehension) {
return symtable_raise_if_comprehension_block(st, e);
}
break;
case YieldFrom_kind:
VISIT(st, expr, e->v.YieldFrom.value);
st->st_cur->ste_generator = 1;
if (st->st_cur->ste_comprehension) {
return symtable_raise_if_comprehension_block(st, e);
}
break;
}
}


И для корутины:


// symtable_visit_stmt(struct symtable *st, stmt_ty s)
case AsyncFunctionDef_kind: {
if (!symtable_add_def(st, s->v.AsyncFunctionDef.name, DEF_LOCAL, LOCATION(s)))
return 0;
// ...
st->st_cur->ste_coroutine = 1;

// symtable_visit_expr(struct symtable *st, expr_ty e)
case Await_kind:
if (!allows_top_level_await(st)) {
if (!_PyST_IsFunctionLike(st->st_cur)) {
return PyErr_SetString(PyExc_SyntaxError, "'await' outside function");
}
if (!IS_ASYNC_DEF(st) && st->st_cur->ste_comprehension == NoComprehension) {
return PyErr_SetString(PyExc_SyntaxError, "'await' outside async function");
}
}
VISIT(st, expr, e->v.Await.value);
st->st_cur->ste_coroutine = 1;


И потом уже просто вычисляем флаги для CodeObject:


static int compute_code_flags(compiler *c)
{
if (_PyST_IsFunctionLike(ste)) {
flags |= ...;
if (ste->ste_generator && !ste->ste_coroutine)
flags |= CO_GENERATOR;
if (ste->ste_generator && ste->ste_coroutine)
flags |= CO_ASYNC_GENERATOR;
// ...
}
if (ste->ste_coroutine && !ste->ste_generator) {
flags |= CO_COROUTINE;
}
return flags;
}


Результат:


>>> from inspect import CO_COROUTINE, CO_GENERATOR
>>> gen_func.__code__.co_flags & CO_GENERATOR
32
>>> coro_func.__code__.co_flags & CO_GENERATOR
0
>>> coro_func.__code__.co_flags & CO_COROUTINE
128


Что происходит при вызове?

Если сделать dis функций gen_func и coro_func, то у них первым байткодом будет RETURN_GENERATOR (см _Py_MakeCoro):

Читать полностью…

Находки в опенсорсе

Нерегулярная рубрика "Посмотрите, что пишут!"

Кирилл - core разработчик CPython, пристально следит за обсуждениями, новыми фичами, интересными багами в питоне.

Если вам нравится мой контент - его канал вам тоже понравится. Один из немногих, на кого я сам подписан.
Узнаю оттуда много интересного.

Подписывайтесь! @cpython_notes

Читать полностью…

Находки в опенсорсе

`LOAD_CONST` разделили на три опкода в 3.14

https://github.com/python/cpython/pull/125972

В Python 3.14 распилили один из самых популярных опкодов: LOAD_CONST. Он, как можно понять из названия, он загружал константы из frame->co_consts:


// 3.13:
pure inst(LOAD_CONST, (-- value)) {
value = GETITEM(FRAME_CO_CONSTS, oparg);
Py_INCREF(value);
}



>>> def func():
... return 1

>>> func.__code__.co_consts
(None, 1)


Теперь LOAD_CONST разделен на:
- LOAD_SMALL_INT для интов в range(256)
- LOAD_CONST_IMMORTAL для загрузки бесмертных объектов (на 1 Py_INCREF меньше, см PyStackRef_FromPyObjectNew vs `PyStackRef_FromPyObjectImmortal`)
- LOAD_CONST для оставшихся

А еще и RETURN_CONST удалили под шумок.

И вот демо байткода:


>>> import dis
>>> def func():
... x = 1
... y = ...
... z = 'привет, мир'

>>> dis.dis(func, adaptive=True)
2 LOAD_SMALL_INT 1
STORE_FAST 0 (x)

3 LOAD_CONST 1 (Ellipsis)
STORE_FAST 1 (y)

4 LOAD_CONST 2 ('привет, мир')
STORE_FAST 2 (z)
LOAD_CONST 0 (None)
RETURN_VALUE

>>> # Create caches for tier1 adaptive interpreter to work:
>>> for _ in range(100):
... func()
>>> dis.dis(func, adaptive=True)
2 LOAD_SMALL_INT 1
STORE_FAST 0 (x)

3 LOAD_CONST_IMMORTAL 1 (Ellipsis)
STORE_FAST 1 (y)

4 LOAD_CONST 2 ('привет, мир')
STORE_FAST 2 (z)
LOAD_CONST_IMMORTAL 0 (None)
RETURN_VALUE


Зачем нужен LOAD_SMALL_INT?

https://github.com/python/cpython/issues/101291

Если вы внимательно смотрели мой видос про 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;
};


Большие и сложные объекты. Но, для очень частых маленьких чисел, такое переусложнение замедляет работу. Мы можем просто представлять числа в рамках одного машинного слова и складывать их сразу в oparg, без необходимости заргужать их из co_consts:


op(_LOAD_SMALL_INT, (-- value)) {
PyObject *val = PyLong_FromLong(this_instr->oparg);
value = sym_new_const(ctx, val);
}


В Python2, кстати, работало быстрее, потому что там был честный int тип.

Обсуждение

Задумываетесь ли вы про подобные микро-оптимизации, когда пишите код?

Читать полностью…

Находки в опенсорсе

Как ruff убил isort и поломал все мои проекты

Я пользовался isort сколько себя помню. Буквально с первых релизов, когда весь isort еще был написан в одном файле на много тысяч строк. Пользовался настолько активно, что у меня даже был свой --profile=wemake https://pycqa.github.io/isort/docs/configuration/profiles.html#wemake

Который включал:


[isort]
# profile = wemake
# =
multi_line_output = 3
include_trailing_comma = true
use_parentheses = true
line_length = 80


Где:
- multi_line_output указывает, как разбивать на новые строки длинные импорты. Демо тут: https://pycqa.github.io/isort/docs/configuration/multi_line_output_modes.html
- include_trailing_comma добавляет финальные запятые для уменьшения diff при добавлении новых имен в импорт
- use_parentheses для использования () вместо \ - опять же для уменьшения diff
- line_length - максимальный размер строки, кстати он ничего не имеет общего с размером ваших мониторов. потому что длина строки - метрика сложности кода. код на 160 символов - в два раза сложнее. подробности тут: https://sobolevn.me/2019/10/complexity-waterfall

Все было хорошо, все работало годами. И тут появляется ruff. Большинство меинтейнеров проектов, которые ruff "заменил" – выгорают от таких поворотов. И перестают заниматься своими проектами.

В целом - и норм, потому что оно уже работало. До одного очень странного случая.
Буквально один из последних коммитов в isort - сломал мой профиль. Пришел человек, кто неправильно понял суть line_length и поправил значение в --profile=wemake с 80 на 79 https://github.com/PyCQA/isort/pull/2183

Его PR без уточнений с моей стороны приняли. Релизнули новую версию isort.
И у меня на всех проектах начал отваливаться линтер. Говорит: неправильно ты импорты оформляешь. Я очень удивился.

Какое-то время у меня ушло на дебаг, потому что случай странный. В итоге я нашел баг, сделал свой PR: https://github.com/PyCQA/isort/pull/2241
И тут авторы окончательно выгорели. Больше уже никто ничего не мерджил.
Я писал письма им в личку, пинговал коллег по PyCQA, заходил к ним в дискорд. Тишина.

Какие у меня есть варианты?
- Везде явно ставить line_length = 80 в дополнение к --profile=wemake, что все еще ломает опыт всем пользователям https://github.com/wemake-services/wemake-python-styleguide
- Ставить прошлую версию isort, что тоже стремный хак

Ну и в ruff нет --profile https://docs.astral.sh/ruff/settings/#lintisort

Статический анализ – ад!

Читать полностью…

Находки в опенсорсе

Большая сходка любителей настолок, питона и пива в Москве!

Где? Ресторан Paulaner на Полянке: https://yandex.ru/maps/org/paulaner/44880575916/?ll=37.620383%2C55.734745&amp;z=17.97
Когда? Четверг 24 октября с 18:30 и до закрытия

Что в планах?
- Игра в https://github.com/sobolevn/ship-it-boardgame 0.0.19й версии
- Разговоры про программирование

Ждем всех :)

Читать полностью…

Находки в опенсорсе

Кажется, что я случайно создал самый смешной багрепорт месяца :)

(ответ релиз-менеджера 3.13)

Кстати, в питон3.14 хотят добавить другой прикол: https://github.com/python/cpython/issues/119535

Читать полностью…

Находки в опенсорсе

И еще один прикол про сравнения забыл!


>>> from __future__ import barry_as_FLUFL
>>> 1 <> 2
True


Речь, конечно же, идет про многолетнего кор-разработчика и релиз-менеджера: https://github.com/warsaw

Кстати, в новом REPL в Python3.13 данную фичу сломали:


>>> from __future__ import barry_as_FLUFL
>>> 1 <> 2
File "<python-input-1>", line 1
1 <> 2
^^
SyntaxError: invalid syntax


Я открыл багрепорт: https://github.com/python/cpython/issues/124960

Читать полностью…

Находки в опенсорсе

Вышел 3.13-rc3

Новости одной строкой:
- Последний релиз перед 3.13.0
- Официальная дата релиза 3.13 перенесена на 7 октября
- В релизе был ревертнут новый инкрементальный GC (https://github.com/python/cpython/pull/124770), потому что он вызывал регрессии по перформансу. Например: sphinx-build стал на 48% медленней (https://github.com/python/cpython/issues/124567)
- Такое уже случалось, первая версия инкрементального GC сделала CPython в примерно 20 раз медленнее (https://github.com/python/cpython/issues/117108)
- Как теперь будет работать nogil со старым сборщиком – я пока не понимаю 🤔️️️️️️
- Ждем новый сборщик мусора в 3.14

Читать полностью…

Находки в опенсорсе

Атрибут `__class__` в Python можно переписывать! 😱

Пример:


>>> class Cat:
... def meow(self):
... print('meow')

>>> class Dog:
... def bark(self):
... print('woof!')

>>> c = Cat()
>>> c.__class__ = Dog # превращаем котика в собачку!
>>> isinstance(c, Dog)
True
>>> c.bark()
woof!


Но как?

Обратимся к исходникам в typeobject.c:


static PyGetSetDef object_getsets[] = {
{"__class__", object_get_class, object_set_class,
PyDoc_STR("the object's class")},
{0}
};

static PyObject *
object_get_class(PyObject *self, void *closure)
{
return Py_NewRef(Py_TYPE(self));
}

static int
object_set_class(PyObject *self, PyObject *value, void *closure)
{
// ...
PyTypeObject *newto = (PyTypeObject *)value;

#ifdef Py_GIL_DISABLED
PyInterpreterState *interp = _PyInterpreterState_GET();
_PyEval_StopTheWorld(interp);
#endif
PyTypeObject *oldto = Py_TYPE(self);
// Calls:
// ob->ob_type = newto;
int res = object_set_class_world_stopped(self, newto);
#ifdef Py_GIL_DISABLED
_PyEval_StartTheWorld(interp);
#endif
}


Тут можно увидеть сразу несколько важных моментов:
1. Данное действие совсем-совсем не thread-safe, чтобы его совершить приходится останавливать все остальные треды в режиме noGIL
2. У каждого объекта в Python есть поле ob_type, где хранится его тип. А значит, тип можно менять

Важно, оба класса, должны быть совместимы, иначе будут ошибки:
- Оба класса должны быть мутабельными
- Оба класса должны иметь совместимый binary layout (см функцию compatible_for_assignment в `typeobject.c`)
- __slots__ должны быть одинаковыми

Но зачем?

Данный хак используется в большом количестве мест в исходниках.

- Например, такое есть прямо в доках Mock: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.__class__


>>> mock = Mock()
>>> mock.__class__ = dict
>>> isinstance(mock, dict)
True


И его можно менять:


@property
def __class__(self):
if self._spec_class is None:
return type(self)
return self._spec_class


Внутри есть __setattr__, который вместо .__class__ = X будет менять ._spec_class = X. А свойство будет отражать изменение.

- LazyLoader в importlib делает такое: https://github.com/python/cpython/blob/46f5cbca4c37c57f718d3de0d7f7ddfc44298535/Lib/importlib/util.py#L273


def exec_module(self, module):
"""Make the module load lazily."""
# Threading is only needed for lazy loading, and importlib.util can
# be pulled in at interpreter startup, so defer until needed.
import threading
module.__spec__.loader = self.loader
module.__loader__ = self.loader
loader_state = {}
loader_state['__dict__'] = module.__dict__.copy()
loader_state['__class__'] = module.__class__
loader_state['lock'] = threading.RLock()
loader_state['is_loading'] = False
module.__spec__.loader_state = loader_state
module.__class__ = _LazyModule # <---


- В annotationlib / typing такое используется, что превращать строковое представление аннотаций в ForwardRef: https://github.com/python/cpython/blob/46f5cbca4c37c57f718d3de0d7f7ddfc44298535/Lib/annotationlib.py#L580-L581


class _Stringifier:
# Must match the slots on ForwardRef, so we can turn an instance of one into an
# instance of the other in place.
__slots__ = _SLOTS

# ...
for obj in globals.stringifiers:
assert isinstance(obj, _Stringifier)
obj.__class__ = ForwardRef


- В threading используется, чтоб _DummyThread мог притворяться MainThread: https://github.com/python/cpython/blob/46f5cbca4c37c57f718d3de0d7f7ddfc44298535/Lib/threading.py#L1419


def _after_fork(self, new_ident=None):
if new_ident is not None:
self.__class__ = _MainThread
self._name = 'MainThread'
self._daemonic = False
Thread._after_fork(self, new_ident=new_ident)

Читать полностью…

Находки в опенсорсе

Внимательный читатель (спасибо, Вася) заметил, что у меня ОПЕЧАТКА В ФИКСЕ. там написано __dictrefoffset__, а не __dictoffset__, как должно быть.

https://github.com/python/cpython/issues/123935

Данный кейс не был протестирован. И сейчас я уже отправляют ЕЩЕ ОДИН PR, теперь уже точно финальный.

Вот и польза от поста 🌚️️

Читать полностью…

Находки в опенсорсе

Продолжаем ломать 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__')
# ...

Читать полностью…

Находки в опенсорсе

`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_descriptor'>)


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

Так что - будьте осторожны!

Читать полностью…

Находки в опенсорсе

Одна из самых сложных частей в устройстве 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 Subscription

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

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


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

#prhistory

Читать полностью…

Находки в опенсорсе

Как работает диспатчеризация байткода внутри VM? Computed GOTOs

Многие из вас знают, что внутри питона есть большой switch-case, который выполняется в цикле, он находит нужный байткод и выполняет его. Выглядит оно примерно как-то так:


#define LOAD_CONST 79

PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyThreadState *tstate,
_PyInterpreterFrame *frame,
int throwflag)
{
DISPATCH_GOTO(); // разворачивается в `goto dispatch_opcode`
dispatch_opcode:
switch (opcode) {
TARGET(LOAD_CONST): // разворачивается в `case 79:`
{
frame->instr_ptr = next_instr;
next_instr += 1;
_PyStackRef value = PyStackRef_FromPyObjectNew(
GETITEM(FRAME_CO_CONSTS, oparg));
// ...
}
// ...
}

opcode = next_instr->op.code;
DISPATCH_GOTO(); // разворачивается в `goto dispatch_opcode;`

exit:
// end of cycle: success or error
}


Но, на самом деле – все не совсем так просто! Данный switch по сути является самой горячей частью кода во всем интерпретаторе, он выполняется буквально на любое действие. Любое ускорение данного места дает ускорение всему коду на питоне. А значит – такие ускорения были придуманы.

Концепт Computed GOTOs

Вводная статья на тему, кто вообще никогда о таком не слышал. Если очень кратко:
- Создаем известную в compile-time таблицу переходов, которая использует лейблы для goto. Назовем ее opcode_targets
- Вместо switch просто используем goto *opcode_targets[opcode]
- Проверяем в configure, что компилятор поддерживает такую фичу (`gcc` поддерживает, --with-computed-gotos по-умолчанию включено)
- Накручиваем DSL для виртуальной машины:


#if USE_COMPUTED_GOTOS
# define TARGET(op) TARGET_##op:
# define DISPATCH_GOTO() goto *opcode_targets[opcode]
#else
# define TARGET(op) case op: TARGET_##op:
# define DISPATCH_GOTO() goto dispatch_opcode
#endif


Итого, используя тот же DSL на макросах, благодаря флагу USE_COMPUTED_GOTOS (который выставляется в configure) – получаем совсем другой код в _PyEval_EvalFrameDefault:


#define LOAD_CONST 79

PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyThreadState *tstate,
_PyInterpreterFrame *frame,
int throwflag)
{
DISPATCH_GOTO(); // // goto *opcode_targets[opcode]
TARGET(LOAD_CONST): // TARGET_79:
{
frame->instr_ptr = next_instr;
next_instr += 1;
_PyStackRef value = PyStackRef_FromPyObjectNew(
GETITEM(FRAME_CO_CONSTS, oparg));
// ...
}
// ...

opcode = next_instr->op.code;
DISPATCH_GOTO(); // goto *opcode_targets[opcode]

exit:
// end of cycle: success or error
}


Данная реализация где-то на 15% быстрее реализации на switch. Но для простоты все продолжают говорить, что внутри VM switch+case

Узнали сегодня что-то новое? :)

| Поддержать | sobolevn">YouTube | GitHub |

Читать полностью…

Находки в опенсорсе

PyObject *
_Py_MakeCoro(PyFunctionObject *func)
{
int coro_flags = ((PyCodeObject *)func->func_code)->co_flags &
(CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR);
assert(coro_flags);
if (coro_flags == CO_GENERATOR) {
return make_gen(&PyGen_Type, func);
}
if (coro_flags == CO_ASYNC_GENERATOR) {
return make_gen(&PyAsyncGen_Type, func);
}

assert (coro_flags == CO_COROUTINE);
PyObject *coro = make_gen(&PyCoro_Type, func);
return coro;
}


Вот тут как раз при выполнении функции и будут создаваться инстансы types.GeneratorType и types.CoroutineType.

Вот и вся разница :)

Дальше уже во многих местах на основе флагов / методов - объекты начинают вести себя по-разному. Однако, все еще генератор можно превратить в корутину при помощи types.coroutine

Узнали сегодня что-то новое?

Читать полностью…

Находки в опенсорсе

Был смержен тред-локал байткод - https://github.com/python/cpython/pull/123926

Зачем это нужно и как это связано с nogil aka free-threaded?

Напомню, что в версии 3.11 был добавлен так называемый <адаптивный> интерпретатор, который умеет в специализацию.
Типичная операция x + y в обычном случае превращается в BINARY_OP опкод, который делает x.__add__(y), что несомненно не очень-то и дешево, так как надо производить лукап __add__ и прочие связанные с этим операции.
Адаптивный интерпретатор "подстраивается" под ситуации когда у вас достаточно много случаев в коде когда в операции x + y оба операнда являются интами, и тогда можно сэкономить на лукапе и дергать специализированную сишную функцию для сложения интов, которая не производит никакого лукапа атрибутов, и именно на этом мы выигрываем по производительности.

Так вот, в случае с nogil сборкой адаптивный интерпретатор не является потокобезопасным, что делало nogil сборку действительно медленнее(потому что специализация была выключена в nogil сборке). Pull request отмеченный вышел делает адаптивный интерпретатор потокобезопасным и вместе с этим у codeobject появляется атрибут co_tlbc :)

Читать полностью…

Находки в опенсорсе

Argument Clinic

https://devguide.python.org/development-tools/clinic/

Если вы когда-нибудь смотрели исходники питона, то вы замечали внутри вот такие комментарии (взял за пример `sum()`):


/*[clinic input]
sum as builtin_sum

iterable: object
/
start: object(c_default="NULL") = 0

Return the sum of a 'start' value (default: 0) plus an iterable of numbers.
[clinic start generated code]*/

static PyObject *
builtin_sum_impl(PyObject *module, PyObject *iterable, PyObject *start)
/*[clinic end generated code: output=df758cec7d1d302f input=162b50765250d222]*/
{
// ...
}


Есть достаточно понятная проблема: нужно как-то иметь возможность передавать аргументы из Python кода в C код. Учитывая, что бывает много всяких видов Python и C функций (`METH_FASTCALL`, METH_O и тд), то все становится не так уж и просто.

AC позволяет делать достаточно просто описание сигнатуры функции при помощи специального DSL в комментариях.
И даже больше:
- Он генерирует сигнатуру сишной функции со всеми параметрами сразу после тега [clinic start generated code]
- Он хранит последнее состояние в /*[clinic end generated code: output=df758cec7d1d302f input=162b50765250d222]*/
- А еще он создает макросы вида:


PyDoc_STRVAR(builtin_sum__doc__,
"sum($module, iterable, /, start=0)\n"
"--\n"
"\n"
"Return the sum of a \'start\' value (default: 0) plus an iterable of numbers");

#define BUILTIN_SUM_METHODDEF \
{"sum", _PyCFunction_CAST(builtin_sum), METH_FASTCALL|METH_KEYWORDS, builtin_sum__doc__},


Чтобы потом использовать их для добавления методов в модули / классы:


static PyMethodDef builtin_methods[] = {
BUILTIN_SUM_METHODDEF
// ...
};


Как оно внутри?

- Есть большая либа внутри питона для работы с AC (с тестами и mypy)
- Есть make clinic для вызова данной либы на код, который вы меняете
- Можно кастомизировать выполнение либы на питоне, создавая питон код внутри C комментариев
- Мы используем AC даже для C-API тестов
- Сам генератор использует публичный C-API для выдергивания агрументов из переданных объектов. Код генерируется страшный, но читаемый, для примера кусок из файла Python/clinic/bltinmodule.c.h:


static PyObject *
builtin_sum(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
{
PyObject *return_value = NULL;
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)

// ...

static const char * const _keywords[] = {"", "start", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "sum",
.kwtuple = KWTUPLE,
};
PyObject *argsbuf[2];
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1;
PyObject *iterable;
PyObject *start = NULL;

args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 2, 0, argsbuf);
if (!args) {
goto exit;
}
iterable = args[0];
if (!noptargs) {
goto skip_optional_pos;
}
start = args[1];
skip_optional_pos:
return_value = builtin_sum_impl(module, iterable, start);

exit:
return return_value;
}


С ним значительно удобнее, чем писать такое руками!

---

Кстати, скоро мы с моими друзьями с Хабра делаем совместную движуху: https://vibe.habr.com/?utm_source=opensource_findings

В программе:
- Общение с разными ребятами, кто занимается карьерой
- Игра в карьерную настолку
- Специальные активности, чтобы понять, какие вайбы в работе подходят именно вам

Читать полностью…

Находки в опенсорсе

Нерегулярная воскресная рубрика про интересный опенсорс

Если у вас есть интересные опенсорсные проекты, про которые вы хотите рассказать, то пишите в чат.
С вас пост. С меня редактура и размещение. Давайте помогать друг другу!

А сегодняшний пост будет про очень прикольную библиотеку https://github.com/airtai/faststream от ее автора.

FastStream

Это современный фреймворк для разработки асинхронных сервисов поверх брокеров сообщений. Он взлетел за счет очень простого, интуитивного API:


from faststream import FastStream
from faststream.rabbit import RabbitBroker

broker = RabbitBroker()
app = FastStream(broker)

@broker.subscriber("in-queue")
@broker.publisher("out-queue")
async def handle_msg(user: str) -> str:
return f"User: {user} registered"


Но за простотой кроется достаточно интересное внутреннее устройство. Основная трудность, с которой борется FastStream (и почему у инструмента нет аналогов) - двухэтапная инициализация объектов. Это значит, что все вложенные объекты и данные, необходимые для функционирования "запчастей" не известны на момент их создания через __init__ и должны быть доставлены позже.

Как, например, в случае с мидлварями и декомпозицией приложения на отдельные router'ы


from faststream.nats import NatsBroker, NatsRouter
from faststream.nats.prometheus import NatsPrometheusMiddleware

router = NatsRouter()
publisher = router.publisher("out")

@router.subscriber("in")
async def handler(msg):
await publisher.publish("in")

broker = NatsBroker(middlewares=[NatsPrometheusMiddleware()])
broker.include_router(router)


На момент регистрации ни publisher, ни subscriber ничего не знают о своих будущих мидлварях. Они создаются позже, в брокере. Что значит: на момент включения router'а в брокер мы должны передать подобные зависимости в роутер. FastAPI, например, решает эту проблему путем создания новых эндпоинтов как копий из экземпляра router'a. Однако, тут данный подход не сработает - тот же publisher используется внутри кода обработчика. Пересоздавать объекты мы не можем - старая ссылка должна быть валидна.

Все объекты должны создаваться готовыми к использования настолько, насколько возможно, но также поддерживать операции переноса между различными контейнерами (роутеры / брокеры).

Более того, мидлвари и другие объекты, передаваемые в Broker.__init__ - самая безобидная часть айсберга. Большая часть объектов требует наличия реального объекта connection к брокеру (который появляется только после асинхронного await broker.start() ). Взглянем на небольшой кусочек кода aiokafka (используется внутри FastStream):


consumer = AIOKafkaConsumer(...)
await consumer.start()
async for msg in consumer:
...


Как мы видим, для чтения сообщений необходим объект Consumer'а (который и держит connection ). Соответственно, нам нобходимо доставить этот объект до subscriber'ов FastStream уже после запуска приложения.

При запуске брокера, мы должны рекурсивно пройтись по всему дереву вложенных объектов и проинициализаровать его повторно реальными объектами, необходимыми для функционирования компонентов. Все это развестистое дерево двухэтапной инициализации с необходимостью сохранения валидности всех ссылок на уже созданные объекты приводит к довольно сложной, но интересной внутренней структуре проекта.

И это только небольшая часть сложностей, с которой вынужден бороться фреймворк! А там еще есть:
- in-memory тестирование
- собственный DI вдохновленный FastAPI
- сериализация на интроспекции типов
- поддержка разных бекендов: Kafka, RabbitMQ, Redis, NATS
- свой CLI
- много всякого-разного!

Если вы ищете интересный проект для участия в Open Source - FastStream сейчас нуждается в контрибуторах: ревью PR'ов, участие в обсуждениях, большие и маленькие фичи, правки в документацию - мы будем рады любому участию!

* Telegram группа проекта
* Доклад от создателя фреймворка с PiterPy

Читать полностью…

Находки в опенсорсе

Техническое объявление

Я научился делать открытые чаты для канала 😅
Теперь можно вступать в @opensource_findings_chat и общаться на темы: Python, опенсорса, программирования и всего такого :)
Не забывайте о правилах: https://gist.github.com/sobolevn/d9a598a23e6bb89e51ada71033e9103f

В связи с последними событиями, я продублировал и закрытый чат из Дискорда в ТГ. Всех, кто подписан на https://boosty.to/sobolevn должно было пригласить автоматически. Если будут проблемы - пишите в чате, решим.

А в рамках ЛКПП 11 уже началось голосование за новую тему выпуска для желающих: https://boosty.to/sobolevn/posts/127ef142-1864-48e1-b410-fe49409c3192

Читать полностью…

Находки в опенсорсе

type alias'ы в пятницу вечером

История одного PR: https://github.com/python/cpython/pull/124795

Как вы знаете, в PEP695 (https://peps.python.org/pep-0695/) были добавлены новые тайпалиасы, которые работают поверх нового синтаксиса: type ResultE[T] = Result[T, Exception]

У него довольно много граничений. Например:
- Нельзя использовать дубликаты в именах параметров:


>>> type A[T, T] = ...
SyntaxError: duplicate type parameter 'T'


- Нельзя использовать литералы вместо имен:


>>> type A[1] = ...
SyntaxError: invalid syntax


- С Python3.13 нельзя использовать типовые параметры без дефолта после параметра с дефолтами


>>> type A[T=int, S] = ...
SyntaxError: non-default type parameter 'S' follows default type parameter


Сколько на самом деле тайпалиасов?

Однако, все не так просто. Ведь на самом деле синтаксис type просто создает обычный рантайм объект типа typing.TypeAliasType. И его можно создать руками.


>>> type A[T] = list[T]

>>> type(A)
<class 'typing.TypeAliasType'>
>>> A.__type_params__
(T,)
>>> A.__value__
list[T]


Сравните с "ручным" способом (они почти идентичны, кроме ленивости вычисления `list[T]`):


>>> from typing import TypeAliasType, TypeVar
>>> T = TypeVar('T', infer_variance=True)
>>> A = TypeAliasType('A', list[T], type_params=(T,))

>>> type(A)
<class 'typing.TypeAliasType'>
>>> A.__type_params__
(T,)
>>> A.__value__
list[T]


Так вот 🌚️️️️
У меня еще не все. Есть еще один TypeAliasType 😱
В typing_extensions для портирования поддержки библиотеками, кто инспектирует аннотации на 3.11 и ниже.
Вот он: https://github.com/python/typing_extensions/blob/17d3a37635bad3902c4e913a48d969cbebfb08c3/src/typing_extensions.py#L3503

Проблемы в реализации

Пользователь нашел проблему, что при ручном создании TypeAliasType не было никаких проверок. Почему? потому что все проверки были не в самом коде объекта, а на уровне парсера / компилятора. Руками туда можно было отправиль что угодно! И в C версию, и в Python версию.

Теперь проверки есть, например вот так мы проверяем, что типовые параметры без дефолта не идут после параметров с дефолтами:


for (Py_ssize_t index = 0; index < length; index++) {
PyObject *type_param = PyTuple_GET_ITEM(type_params, index);
PyObject *dflt = get_type_param_default(ts, type_param);
if (dflt == NULL) {
*err = 1;
return NULL;
}
if (dflt == &_Py_NoDefaultStruct) {
if (default_seen) {
*err = 1;
PyErr_Format(PyExc_TypeError,
"non-default type parameter '%R' "
"follows default type parameter",
type_param);
return NULL;
}
} else {
default_seen = 1;
Py_DECREF(dflt);
}
}


Практическая ценность

Кстати, такие тайпалиасы уже работают! Смотрите на --enable-incomplete-feature=NewGenericSyntax в mypy:


type A[T] = list[T]

def get_first[T](arg: A[T]) -> T:
return arg[0]

reveal_type(get_first([1, 2, 3]))


https://mypy-play.net/?mypy=latest&amp;python=3.12&amp;enable-incomplete-feature=NewGenericSyntax&amp;flags=strict&amp;gist=7b33597fb3f0cb723ac816efc2e2caef

Как вы используете type alias'ы в своем коде?
Какой вид предпочитаете: TypeAlias, TypeAliasType, ключевое слово type?

Читать полностью…

Находки в опенсорсе

Все смешнее и смешнее 😂️️️️️️

Читать полностью…

Находки в опенсорсе

Лучший курс по Python 10: ==

44 минуты про сравнения, что может быть лучше?

https://www.youtube.com/watch?v=o-Ng_73kdik

В видео будет про:
- Сравнения в Python2 и усиление типизации в Python3
- Оптимизация байткода в Tier1: COMPARE_OP превращается в COMPARE_OP_{INT,STR,FLOAT}
- Разницу байткода и перформанса между a == b == c и a == b and b == c
- PyObject_RichCompare C-API
- Работу с NotImplemented
- Дефолтную реализацию object.__eq__, object.__lt__ и других

И даже за 44 минуты я не успел рассказать все! Делюсь дополнительными материалами здесь.

1. В ролике был вопрос: почему перед опкодом TO_BOOL идет дополнительный COPY? Ответ будет такой. Как выглядит опредление опкода TO_BOOL? op(_TO_BOOL, (value -- res)). Теперь, давайте разбираться, что такое (value -- res). bytecodes.c написан на специальном DSL, который упрощает определение работы с байткодом для разных уровней оптимизаторов, а так же делает работу со стеком виртуальной машины похожей на "вызов функций". (value -- res) значит: возьми со стека value, положи на стек res


op(_TO_BOOL, (value -- res)) {
int err = PyObject_IsTrue(PyStackRef_AsPyObjectBorrow(value));
DECREF_INPUTS();
ERROR_IF(err < 0, error);
res = err ? PyStackRef_True : PyStackRef_False;
}


Следовательно, если у нас не будет COPY:


pure inst(COPY, (bottom, unused[oparg-1] -- bottom, unused[oparg-1], top)) {
assert(oparg > 0);
top = PyStackRef_DUP(bottom);
}


То, мы для превращения объекта в bool в TO_BOOL возьмем со стека value, превратим его в bool, сложим на стек результат, его проверит POP_JUMP_IF_FALSE. А самого значения уже не останется. COPY позволяет сохранить само значение объекта в стеке, для дальнейшей работы с ним.

Документация: https://github.com/python/cpython/blob/main/Tools/cases_generator/interpreter_definition.md

2. Я не показал long_compare. Исправляюсь:


static Py_ssize_t
long_compare(PyLongObject *a, PyLongObject *b)
{
if (_PyLong_BothAreCompact(a, b)) {
return _PyLong_CompactValue(a) - _PyLong_CompactValue(b);
}
Py_ssize_t sign = _PyLong_SignedDigitCount(a) - _PyLong_SignedDigitCount(b);
if (sign == 0) {
Py_ssize_t i = _PyLong_DigitCount(a);
sdigit diff = 0;
while (--i >= 0) {
diff = (sdigit) a->long_value.ob_digit[i] - (sdigit) b->long_value.ob_digit[i];
if (diff) {
break;
}
}
sign = _PyLong_IsNegative(a) ? -diff : diff;
}
return sign;
}


Исходник: https://github.com/python/cpython/blob/656b7a3c83c79f99beac950b59c47575562ea729/Objects/longobject.c#L3548-L3567

Код кажется очевидным. Теперь полный цикл сравнений пройден :)

Вопрос для обсуждения: a == b == c или a == b and b == c? Какой стиль вы выбираете у себя?

Поддержать такой контент можно тут:
- https://boosty.to/sobolevn
- https://github.com/sponsors/wemake-services

Читать полностью…

Находки в опенсорсе

Практическое применение?

Все мои посты всегда объединяет одно – понятная практическая ценность! 🌚️️️️️️
Не будем же отступать от традиции и здесь.

Зачем такое может понадобиться в реальном проекте? Я вижу две основные задачи:
- Написание кастомных моков / стабов в тестах. Замена .__class__ в таком случае имеет понятную ценность, что объект делает вид, что он совсем другой объект. Ну и понимание, как работает стандартный Mock() и Mock(spec=X)
- Можно хачить модули!


import sys
import types

class VerboseModule(types.ModuleType):
def __setattr__(self, attr, value):
print(f'setting {attr} to {value}')
super().__setattr__(attr, value)

sys.modules[__name__].__class__ = VerboseModule


Таким образом вы можете менять поведение модулей для каких-то супер специфичных штук помимо __dir__ и __getattr__ на уровне модуля

Давайте обсудим: разрешили бы вы такое в своей кодовой базе? И почему нет?

Читать полностью…

Находки в опенсорсе

Лучший курс по Python 9: Переменные

https://www.youtube.com/watch?v=crSzQKfevZU

Я хотел сделать видео про переменные, которое бы рассказывало: а как на самом деле происходит создание и поиск имени? Все рассказывают про переменные, как про какие "коробки" для значений. А не они не коробки! Потому, в видео про переменные я рассказываю:
- Что никаких переменных в Python – нет 🌚
- Про frame.f_locals и frame.f_globals
- Про генерацию байткода: покрываем все стадии через symtable.c / compile.c / codegen.c
- Про замыкания с .__closure__ и MAKE_CELL
- Ну и про рантайм конечно же! Как работает, например globals() и locals() на самом деле


/*[clinic input]
globals as builtin_globals

Return the dictionary containing the current scope's global variables.

NOTE: Updates to this dictionary *will* affect name lookups in the current
global scope and vice-versa.
[clinic start generated code]*/

static PyObject *
builtin_globals_impl(PyObject *module)
/*[clinic end generated code: output=e5dd1527067b94d2 input=9327576f92bb48ba]*/
{
PyObject *d;

d = PyEval_GetGlobals();
return Py_XNewRef(d);
}


Бонус! Я показывал видео Грише Петрову до публикации. Он дал ценную обратную связь: я не упомянул, почему иногда модификация locals().update() работает, а иногда нет. Исправляюсь!

- locals(), как показано в видео, обычно возвращает новый dict, потому что использует прокси внутри C. Внутри функции модификация locals() работать не будет. И вот почему, код:


// PyObject * _PyEval_GetFrameLocals(void)
if (PyFrameLocalsProxy_Check(locals)) {
PyObject* ret = PyDict_New();
if (ret == NULL) {
Py_DECREF(locals);
return NULL;
}
if (PyDict_Update(ret, locals) < 0) {
Py_DECREF(ret);
Py_DECREF(locals);
return NULL;
}
Py_DECREF(locals);
return ret;
}

assert(PyMapping_Check(locals));
return locals;


На самом деле


def some():
locals().update({'a': 1})
print(a)


будет делать .update на *другом* dict объекте, который мы создали из PyFrameLocalsProxy, а не сам прокси.
И изменения видны не будут.

- А вот тут я пропустил шаг: так а почему на уровне модуля / REPL оно работает?

Потому что в REPL / модуле frame.f_locals is frame.f_globals. Вот код:


static int
PyRun_InteractiveOneObjectEx(FILE *fp, PyObject *filename,
PyCompilerFlags *flags)
{
PyArena *arena = _PyArena_New();
if (arena == NULL) {
return -1;
}

mod_ty mod;
PyObject *interactive_src;
int parse_res = pyrun_one_parse_ast(fp, filename, flags, arena, &mod, &interactive_src);

PyObject *main_module = PyImport_AddModuleRef("__main__");
PyObject *main_dict = PyModule_GetDict(main_module); // borrowed ref

PyObject *res = run_mod(mod, filename, main_dict, main_dict, flags, arena, interactive_src, 1);
// ...


Где


static PyObject *
run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals,
PyCompilerFlags *flags, PyArena *arena, PyObject* interactive_src,
int generate_new_source)
{


- Далее, на слайде я показываю, но повторю данную мысль еще раз:


NOTE: Whether or not updates to this dictionary will affect name lookups in
the local scope and vice-versa is *implementation dependent* and not
covered by any backwards compatibility guarantees.


Не надо так! Не модифицируйте скоупы, просто потому что можно!

Надеюсь, что было интересно.
Поддержать такой контент можно тут:
- https://boosty.to/sobolevn
- https://github.com/sponsors/wemake-services

#lkpp

Читать полностью…

Находки в опенсорсе

Который предполагал, что если __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__. И нам нужно его добавить в наш новый датакласс.

Дело закрыто! Ну как вам погружение? :)

Читать полностью…

Находки в опенсорсе

Материалы с PythoNN 30 августа 2024

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

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

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

Друзья, спасибо большое за интересные доклады. Спасибо гостям за отличную атмосферу и интересные вопросы.
Если хотите побывать в НН и заодно сделать доклад – пишите, буду рад помочь с подготовкой!

Читать полностью…

Находки в опенсорсе

Проблемы модуля `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&amp;q=inspect.iscoroutinefunction
- https://github.com/search?type=code&amp;q=inspect.getfullargspec
- https://github.com/search?type=code&amp;q=inspect.getargvalues

Расскажите: а у вас были проблемы с inspect? Если да, то какие?

Читать полностью…

Находки в опенсорсе

Сегодня говорим про 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

Читать полностью…
Подписаться на канал