11222
Привет! Меня зовут Никита Соболев. Я занимаюсь опенсорс разработкой полный рабочий день. Тут я рассказываю про #python, #c, опенсорс и тд. Поддержать: https://boosty.to/sobolevn РКН: https://vk.cc/cOzn36 Связь: @sobolev_nikita
Нерегулярная рубрика "Посмотрите, что пишут!"
Кирилл - 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]:
...
История одного PR #prhistory
Некоторое время назад я добавил в mypy поддержку ReadOnly special form для TypedDict: https://github.com/python/mypy/pull/17644
PR большой, его будут смотреть еще некоторое время. Но, о самых важных принципах рассказать можно уже сейчас.
1. Что такое ReadOnly?
PEP: https://peps.python.org/pep-0705
По сути, он запрещает такой код:
from typing import ReadOnly, TypedDict
class User(TypedDict):
username: ReadOnly[str] # you cannot change the value of `user['username']`
user: User = {'username': 'sobolevn'}
user['username'] = 'changed' # type error! username is read-only
ReadOnly был добавлен в Python в версию 3.13 мной некоторое время назад: https://github.com/python/cpython/pull/116350typing_extensions еще раньше: https://github.com/python/typing_extensions/commit/0b0166d649cebcb48e7e208ae5da36cfab5965fetyping_extensions.ReadOnly можно будет как только выйдет новая версия mypy с поддержкой данной special form.ReadOnly?username: ReadOnly[Required[str]]age: NotRequired[Annotated[ReadOnly[int], Validate(min=18, max=120)]]TypedDict появились специальные атрибуты: __readonly_keys__ и __mutable_keys__:
>>> from typing import TypedDict, ReadOnly
>>> class User(TypedDict):
... username: ReadOnly[str]
... age: int
...
>>> User.__readonly_keys__
frozenset({'username'})
>>> User.__mutable_keys__
frozenset({'age'})
ReadOnly ключей, нужно помнить, про отношение подтипов.
User = TypedDict('User', {'username': ReadOnly[str]})
MutableUser = TypedDict('MutableUser', {'username': str})
def accepts_user(user: User): ...
def accepts_mutable_user(user: MutableUser): ...
ro_user: User
mut_user: MutableUser
# MutableUser является подвидом User, но User не является подвидом MutableUser
accepts_user(mut_user) # ok
accepts_mutable_user(ro_user) # error: expected: MutableUser, given: User
accepts_mutable_user может выглядеть как-то так:
def accepts_mutable_user(user: MutableUser):
user['username'] = ''
И сразу пример нового контента.
За последние два дня добавил поддержку двух новых типов в hypothesis:
- ReadOnly: https://github.com/HypothesisWorks/hypothesis/pull/4072
- LiteralString: https://github.com/HypothesisWorks/hypothesis/pull/4075
Что такое hypothesis? Такой фреймворк для тестирования на основе идеи Property-Based Testing: когда вы тестируете не конкретные значения, а целые законы или свойства вашей системы.
Например:
import datetime as dt
import attrs
@attrs.define
class User:
# ...
joined_at: dt.datetime
last_loggined_at: dt.datetime
def create_our_user() -> User:
... # your business logic
# Testing:
from hypothesis import given, strategies as st
@given(st.builds(create_our_user))
def test_user_login_cannot_be_after_created(user: User) -> None:
assert user.last_loggined_at >= user.joined_at
st.from_type(...) может просто по аннотациям генерировать вам объекты; потому поддержка новых типов - всегда полезна.
Привет!
Давайте знакомиться заново.
Меня зовут Никита, я опенсорс разработчик: https://github.com/sobolevn
Я люблю компиляторы, тайпчекеры, системы тестирования и статические анализаторы разных видов.
Я разрабатываю множество инструментов, которыми вы уже 100% пользуетесь.
Концепция канала поменялась. О чем тут теперь будет?
- Больше я не буду закидывать безличную информацию о других опенсорс проектах, но буду больше рассказывать про те, где я принимаю активное участие: CPython, mypy, typeshed, Django и тд
- Буду делиться видео своих и чужих интересных докладов
- Публиковать материалы для "Лучшего курса по Питону": sobolevn" rel="nofollow">https://www.youtube.com/@sobolevn
- Рассказывать про интересные проекты, которые мне встретились в опенсорсе, прикоснуться к которым действительно удалось
- Делиться знаниями про сложные и интересные штуки в моей работе :)
Поддержать мою работу можно тут: https://boosty.to/sobolevn
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 Скоро будет только версия из inspectannotationslib.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
Одна из самых проблемных частей CPython – вызов Python кода из С.
Делать такое нужно довольно регулярно. Примеры использований:
- Обращение к магическим методам объектов: PyObject_RichCompare, PyObject_GetIter, PyIter_Next, PyObject_GetItem, и тд
- Вызов переданных Python callback'ов: PyObject_Call*, PyObject_Vectorcall, и тд
- Создание новых объектов: PyObject_New
Но, такое всегда нужно делать осторожно. Буквально, почти весь стейт внутри C может измениться после вызова любого Python кода!
Например, такой простой код вызовет [1] 88503 segmentation fault python на версиях <3.12.5
class evil:
def __init__(self, lst):
self.lst = lst
def __iter__(self):
yield from self.lst
self.lst.clear()
lst = list(range(10))
lst[::-1] = evil(lst)
А еще я включил реакции и комментарии, наслаждайтесь! Правила: https://gist.github.com/sobolevn/d9a598a23e6bb89e51ada71033e9103f
Читать полностью…
привет!
в среду 10 июля играем в IT-шную опенсорсную настолку Ship IT в хорошей компании!
ссылка на игру: https://github.com/sobolevn/ship-it-boardgame
в программе:
- душные ITшные шутки
- специальный набор карт для взаимодействия со зрителями
- конкурс на самый смешную шутку в комментариях (приз: физическая настолка!)
участвуют:
- Аня Курносова Pytup: /channel/+Bz-uVcXh4Jk1YTNi
- Алексей Пирогов https://github.com/astynax
- Денис Пушкарев https://github.com/zloirock
- Паша Коршиков /channel/tech_meetup
- Олег Чирухин https://github.com/olegchir
ведущий: Никита Соболев https://github.com/sobolevn
начало: 19:30 по МСК
ссылка на трансляцию: https://www.youtube.com/watch?v=pR8tQaoOitc