Привет! Меня зовут Никита Соболев. Я занимаюсь опенсорс разработкой полный рабочий день. Тут я рассказываю про #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
values
будет NULL
, потому как удаление (сишный аналог __delitem__
из питона работает так)slice
в переменные тутКстати, у нас в Нижнем Новгороде будет митап по питону 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'}
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
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;
};
co_consts
:
op(_LOAD_SMALL_INT, (-- value)) {
PyObject *val = PyLong_FromLong(this_instr->oparg);
value = sym_new_const(ctx, val);
}
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.htmlinclude_trailing_comma
добавляет финальные запятые для уменьшения diff при добавлении новых имен в импортuse_parentheses
для использования ()
вместо \
- опять же для уменьшения diffline_length
- максимальный размер строки, кстати он ничего не имеет общего с размером ваших мониторов. потому что длина строки - метрика сложности кода. код на 160 символов - в два раза сложнее. подробности тут: https://sobolevn.me/2019/10/complexity-waterfallline_length
и поправил значение в --profile=wemake
с 80 на 79 https://github.com/PyCQA/isort/pull/2183isort
. line_length = 80
в дополнение к --profile=wemake
, что все еще ломает опыт всем пользователям https://github.com/wemake-services/wemake-python-styleguideisort
, что тоже стремный хак--profile
https://docs.astral.sh/ruff/settings/#lintisortБольшая сходка любителей настолок, питона и пива в Москве!
Где? Ресторан 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й версии
- Разговоры про программирование
Ждем всех :)
Кажется, что я случайно создал самый смешной багрепорт месяца :)
(ответ релиз-менеджера 3.13)
Кстати, в питон3.14 хотят добавить другой прикол: https://github.com/python/cpython/issues/119535
И еще один прикол про сравнения забыл!
>>> from __future__ import barry_as_FLUFL
>>> 1 <> 2
True
>>> from __future__ import barry_as_FLUFL
>>> 1 <> 2
File "<python-input-1>", line 1
1 <> 2
^^
SyntaxError: invalid syntax
Вышел 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
}
ob_type
, где хранится его тип. А значит, тип можно менять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
для создания слабых ссылок, которые не увеличивают 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
TypeVar
, ParamSpec
, TypeVarTuple
и Generic
классов, функций и алиасов.Generic
стал С типом.[]
автоматически назначается родитель: _typing.Generic
>>> class Example[T]: ...
...
>>> Example.__bases__
(<class 'typing.Generic'>,)
__slots__
, потому что используют другие - сишные - слоты.tp_*
места для вставки разных обработчиков под разные случаи жизни. Например:tp_new
для __new__
tp_richcompare
для сравнений >
, <
, тд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
Читать полностью…
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',)
__slots__
корректны__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'>)
__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
__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
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р
одителях класса со 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
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
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-L223Never
в первом @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]]: ...
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]:
...
Как работает диспатчеризация байткода внутри 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
по сути является самой горячей частью кода во всем интерпретаторе, он выполняется буквально на любое действие. Любое ускорение данного места дает ускорение всему коду на питоне. А значит – такие ускорения были придуманы.goto
. Назовем ее opcode_targetsswitch
просто используем goto *opcode_targets[opcode]
--with-computed-gotos
по-умолчанию включено)
#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
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
}
switch
. Но для простоты все продолжают говорить, что внутри VM switch+case 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
.Был смержен тред-локал байткод - 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]*/
{
// ...
}
METH_O
и тд), то все становится не так уж и просто.[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
// ...
};
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://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"
__init__
и должны быть доставлены позже.
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:
...
connection
). Соответственно, нам нобходимо доставить этот объект до subscriber'ов FastStream уже после запуска приложения.Техническое объявление
Я научился делать открытые чаты для канала 😅
Теперь можно вступать в @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
>>> 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]
>>> 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 и ниже.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]))
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
позволяет сохранить само значение объекта в стеке, для дальнейшей работы с ним.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;
}
a == b == c
или a == b and b == c
? Какой стиль вы выбираете у себя?Практическое применение?
Все мои посты всегда объединяет одно – понятная практическая ценность! 🌚️️️️️️
Не будем же отступать от традиции и здесь.
Зачем такое может понадобиться в реальном проекте? Я вижу две основные задачи:
- Написание кастомных моков / стабов в тестах. Замена .__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
, а не сам прокси.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.
Который предполагал, что если __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
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.signature
только для создания рантайм имплементациия каррирования для dry-python/returns
: https://github.com/dry-python/returns/blob/master/returns/curry.pyinspect
для интроспекции в самых неожиданных местах: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,
};
disable_bytearray_promotion
и disable_memoryview_promotion
https://github.com/python/mypy/commit/2d70ac0b33b448d5ef51c0856571068dd0754af6bytes
https://docs.python.org/3.13/c-api/bytes.html#c._PyBytes_ResizePyBytes_Writer
API: https://github.com/capi-workgroup/decisions/issues/39ob_shash
deprecation: https://github.com/python/cpython/issues/91020