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

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

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

Связь: @sobolev_nikita
Download Telegram
`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

Так что - будьте осторожны!
🔥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
Лучший курс по 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
3🔥5719👍10🤔1🤯1
Атрибут `__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)
1🤯29🔥9👍3🤔3
Практическое применение?

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

Зачем такое может понадобиться в реальном проекте? Я вижу две основные задачи:
- Написание кастомных моков / стабов в тестах. Замена .__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__ на уровне модуля

Давайте обсудим: разрешили бы вы такое в своей кодовой базе? И почему нет?
😱23👍11🤔3🤯21😁1
Вышел 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
🔥31👍18😱5
Лучший курс по 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
1🔥3817👍7😱5
Находки в опенсорсе
Лучший курс по Python 10: == 44 минуты про сравнения, что может быть лучше? https://www.youtube.com/watch?v=o-Ng_73kdik В видео будет про: - Сравнения в Python2 и усиление типизации в Python3 - Оптимизация байткода в Tier1: COMPARE_OP превращается в COMPARE_OP_{INT…
И еще один прикол про сравнения забыл!


>>> 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
😁38🤔73👍1🤬1💩1
Находки в опенсорсе
Все смешнее и смешнее 😂️️️️️️
Кажется, что я случайно создал самый смешной багрепорт месяца :)

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

Кстати, в питон3.14 хотят добавить другой прикол: https://github.com/python/cpython/issues/119535
😁43🔥82👍2
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&python=3.12&enable-incomplete-feature=NewGenericSyntax&flags=strict&gist=7b33597fb3f0cb723ac816efc2e2caef

Как вы используете type alias'ы в своем коде?
Какой вид предпочитаете: TypeAlias, TypeAliasType, ключевое слово type?
🤯28👍9😱62
Forwarded from Никита Соболев
Большая сходка любителей настолок, питона и пива в Москве!

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

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

Ждем всех :)
24🔥10👍3💩1
Техническое объявление

Я научился делать открытые чаты для канала 😅
Теперь можно вступать в @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
🔥21👍52👎1
Как 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

Статический анализ – ад!
😢4627🤯14🤬5😁4👍3👎1🔥1🤔1
Нерегулярная воскресная рубрика про интересный опенсорс

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

А сегодняшний пост будет про очень прикольную библиотеку 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
1🔥73👍2111🕊2🤡2
`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 тип.

Обсуждение

Задумываетесь ли вы про подобные микро-оптимизации, когда пишите код?
35👍9👌2
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

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