Привет! Меня зовут Никита Соболев. Я занимаюсь опенсорс разработкой полный рабочий день. Тут я рассказываю про #python, #c, интересные проекты, коммиты, доклады, и тд. Поддержать: https://boosty.to/sobolevn Для связи: @sobolev_nikita
Привет! Стартуем новый проект для любителей опенсорса: помогаем меинтейнерам и контрибьюторам найти друг друга.
Как оно работает?
- В данном канале меинтейнеры разных Python проектов (от CPython, mypy, Litestar до taskiq) могут в любой момент выложить простые задачки, чтобы люди могли принять участие в разработке их проекта
- Если вы хотите поработать над задачкой – напишите в самой задаче на гитхабе: "Can I work on this?", получите подтверждение меинтейнера и приступайте
- Делитесь успехами / задавайте вопросы в нашем чате @opensource_findings_chat
Если вы меинтейнер какого-то крупного проекта (>= 100 ⭐), то пишите в чат – вас добавят как админа, чтобы вы смогли постить в канал свои задачи. Чем больше – тем лучше, не забывайте ставить тег своей технологии.
Всем хорошего опенсорса!
zen browser
После недавней оказии с FireFox, я понял, что нужно менять свой браузер.
Выбор пал на zen (почти arc, но для firefox), потому что я люблю минимализм.
Что мне нужно от браузера?
- Несколько вкладок, у меня их никогда не бывает сильно много, я все их закрываю примерно раз в день
- Панель для ввода адреса с минимумом функциональности (подсказки, история, поиск)
- Минималистичный интерфейс, без лишних кнопок
- Поддержка uBlock, нескольких других похожих плагинов
- Приватность по-умолчанию
Все. Остальные фичи мне скорее мешают. Я не пользуюсь закладками, workspacе'ами, профилями, синками и тд.
Что есть в zen?
Во-первых, браузер почти полностью позволяет убрать свой интерфейс, что приятно. Теперь по пунктам:
- Hidden Tabs: можно настроить "compact mode", чтобы вкладки исчезали, когда они не нужны, нажатие cmd+b
показывает вкладки, нажатие cmd+1
открывает первую вкладку и тд
- Floating Nav Bar: После настройки панель навигации сверху исчезает, когда ей не пользуешься (открывается на cmd+t
для открытия новой вкладки и cmd+L
фокуса в текущей)
- Busy Mode: при нажатие ctrl+b
включает интерфейс, если нужно что-то найти, если идет какой-то напряженный рабочий режим
- Tab Preview: отключаемая фича, которая позволяет сделать превью страницы и быстро ее закрыть, выглядит полезно для поиска
- Split View: отключаемая фича, которая позволяет открывать две вкладки слева и справа (у меня на `alt-v`) или сверху и снизу (`alt-h`), выглядит полезно для ревью PRов на гитхабе
Ну и конечно же работают все плагины для FireFox и даже есть свои уникальные.
Сверху я все шлифанул кастомным CSS для уничтожения некоторых объектов UI, которые меня отвлекали.
Пока пробую – и мне нравится.
Обсуждение: что сейчас еще есть интересного и удобного в мире браузеров?
| Поддержать | sobolevn">YouTube | GitHub | Чат |
Что такое GIL в Python? Вторая часть
Я не закончил! 🌚
Мы не поговорили про очень важную часть: переключение тредов интерпретатором. Треды могут быть очень долгими и не отпускать GIL, мы должны дать поработать каждому.
Простой пример, что так оно и работает:
import threading
def first():
while True:
print('first')
def two():
while True:
print('two')
a = threading.Thread(target=first).start()
b = threading.Thread(target=two).start()
first
two
first
first
two
two
two
first
first
_gil_runtime_state
:
unsigned long _PyEval_GetSwitchInterval(void)
{
PyInterpreterState *interp = _PyInterpreterState_GET();
struct _gil_runtime_state *gil = interp->ceval.gil;
assert(gil != NULL);
return gil->interval;
}
>>> import sys
>>> sys.getswitchinterval()
0.005
gil->interval
будет использовано для вызова pthread_cond_timedwait. Мы ждем interval
для передачи события (чтобы нам дали GIL) через gil->cond
. Если уходим в таймаут, значит пришло время забирать GIL на следующем выполнении байткода силой. Смотри про сигналы и PyCOND_T
тут.
MUTEX_LOCK(gil->mutex);
unsigned long interval = (gil->interval >= 1 ? gil->interval : 1);
int timed_out = 0;
COND_TIMED_WAIT(gil->cond, gil->mutex, interval, timed_out);
/* If we timed out and no switch occurred in the meantime, it is time
to ask the GIL-holding thread to drop it. */
if (timed_out && _Py_atomic_load_int_relaxed(&gil->locked)) {
PyThreadState *holder_tstate =
(PyThreadState*)_Py_atomic_load_ptr_relaxed(&gil->last_holder);
assert(_PyThreadState_CheckConsistency(tstate));
_Py_set_eval_breaker_bit(holder_tstate, _PY_GIL_DROP_REQUEST_BIT);
}
_PY_GIL_DROP_REQUEST_BIT
будет установлен, мы сможем в _Py_HandlePending передать GIL кому-то другому:
/* GIL drop request */
if ((breaker & _PY_GIL_DROP_REQUEST_BIT) != 0) {
/* Give another thread a chance */
_PyThreadState_Detach(tstate);
/* Other threads may run now */
_PyThreadState_Attach(tstate);
}
_PyThreadState_Detach(tstate)
текущий тред потеряет GIL. И снова будет ждать его при вызове _PyThreadState_Attach(tstate)
. Пока другой работает._Py_HandlePending
и когда?_CHECK_PERIODIC
. Раньше там был макрос CHECK_EVAL_BREAKER
, и его иногда забывали добавить в нужные места. Оттого события ОС не обрабатывались, GIL не переключался, было весело.
op(_CHECK_PERIODIC, (--)) {
_Py_CHECK_EMSCRIPTEN_SIGNALS_PERIODICALLY();
QSBR_QUIESCENT_STATE(tstate);
if (_Py_atomic_load_uintptr_relaxed(&tstate->eval_breaker) & _PY_EVAL_EVENTS_MASK) {
int err = _Py_HandlePending(tstate);
ERROR_IF(err != 0, error);
}
}
// Вызываем `_CHECK_PERIODIC` в конце каждого `CALL`.
macro(CALL) =
_SPECIALIZE_CALL
+ unused/2
+ _MAYBE_EXPAND_METHOD
+ _DO_CALL
+ _CHECK_PERIODIC;
PyGILState
АПИ, потому что оно не работает нормально с субинтерпретаторами Лучший курс по Python 13: print
https://www.youtube.com/watch?v=9aQ-GVlC0nY
В рамках данного видео я рассказываю про:
- Файловые дескрипторы
- Буферизацию вывода
- Устройство TextIOWrapper, BufferedWrite, FileIO
- Зачем нам _pyio?
- Что такое syscall write
- Что происходит после вызова syscall на запись
Для лучшего закрепления материала я предлагаю вам поучаствовать в переписывании print
на ASM. Внутри:
static long
sys_write_call(const char *msg, Py_ssize_t size)
{
// TODO: allow to pass `fd` as `print(file=...)` does.
long ret;
asm volatile (
// TODO: convert this ugly AT&T ASM into beautiful Intel one:
"mov $1, %%rax\n" // sys_write call number
"mov $1, %%rdi\n" // stdout=1 and stderr=2
"mov %1, %%rsi\n" // `msg` address
"mov %2, %%rdx\n" // `msg_len`
"syscall\n"
"mov %%rax, %0\n" // save the result
: "=r"(ret)
: "r"(msg), "r"(size) // inputs
: "rax", "rdi", "rsi", "rdx" // changed registers
);
// TODO: maybe handle special cases like `EINTR`
return ret;
}
print
в CPython на самом низком уровне. Мне было очень интересно! Надеюсь, и вам будет.print
в Python2 был ключевым словом, а не функцией. Нам приходилось писать так:
print 'Hello, world!'
print
- ключевое слово, а 'Hello, world!'
– объект класса bytes
.
print(1, 2)
print
- все еще ключевое слово, а (1, 2)
- tuple
.
from __future__ import print_function
print(1, 2)
print
как функцию. Ужас!Ковыряем внутрянку nogil
Некоторое время назад я прислал безобидный PR, который исправлял поведение list.insert
в nogil сборках CPython. Изменений на 3 строчки. И в ревью случилось два интересных момента.
Во-первых, я случайно удалил оптимизацию.
Было:
PyObject **items;
items = self->ob_item;
items[i+1] = items[i];
items[where] = Py_NewRef(v);
0x0000000000132860 <+96>: sub rdx,0x1
0x0000000000132864 <+100>: sub rax,0x8
0x0000000000132868 <+104>: mov rcx,QWORD PTR [rax]
0x000000000013286b <+107>: mov QWORD PTR [rax+0x8],rcx
0x000000000013286f <+111>: cmp rsi,rdx
0x0000000000132872 <+114>: jle 0x132860 <ins1+96>
self->ob_item[i+1] = self->ob_item[i];
self->ob_item[where] = Py_NewRef(v);
0x0000000000132858 <+88>: mov rdx,QWORD PTR [r12+0x28]
0x000000000013285d <+93>: lea rcx,[rax*8+0x0]
0x0000000000132865 <+101>: mov rdi,QWORD PTR [rdx+rax*8]
0x0000000000132869 <+105>: sub rax,0x1
0x000000000013286d <+109>: mov QWORD PTR [rdx+rcx*1+0x8],rdi
0x0000000000132872 <+114>: cmp rsi,rax
0x0000000000132875 <+117>: jle 0x132858 <ins1+88>
items = self.items
for _ in whatever:
some_func(items)
for _ in whatever:
some_func(self.items)
whatever
.
LOAD_NAME 3 (self)
LOAD_ATTR 8 (items) # <- won't happen when `items = self.items`
CALL 1
FT_ATOMIC_STORE_PTR_RELAXED
и FT_ATOMIC_STORE_PTR_RELEASE
.memory_order_relaxed
– Relaxed operation: there are no synchronization or ordering constraints imposed on other reads or writes, only this operation's atomicity is guaranteedmemory_order_release
– A store operation with this memory order performs the release operation: no reads or writes in the current thread can be reordered after this store. All writes in the current thread are visible in other threads that acquire the same atomic variable and writes that carry a dependency into the atomic variable become visible in other threads that consume the same atomic__atomic_store_n(&x->ob_item[0], &first, __ATOMIC_RELEASE)
будет скомпилировано в
lea rdx, [rsp+12]
mov QWORD PTR [rax], rdx
__atomic_store_n(&x->ob_item[0], &first, __ATOMIC_RELAXED)
скомпилируется в
mov rax, QWORD PTR [rbx]
mov QWORD PTR [rax], rdx
rsp+12
инструкцией lea
. А во втором случае – мы просто работаем со значением в регистре rbx
.В asyncio добавили возможность смотреть граф вызова корутин
Ждем в python3.14: https://github.com/python/cpython/commit/188598851d5cf475fa57b4ec21c0e88ce9316ff0
Пример:
import asyncio
async def test():
asyncio.print_call_graph()
async def main():
async with asyncio.TaskGroup() as g:
g.create_task(test(), name=test.__name__)
asyncio.run(main())
* Task(name='test', id=0x10304eee0)
+ Call stack:
| File '/Users/sobolev/Desktop/cpython2/Lib/asyncio/graph.py', line 278, in print_call_graph()
| File '/Users/sobolev/Desktop/cpython2/ex.py', line 4, in async test()
+ Awaited by:
* Task(name='Task-1', id=0x1034a1e60)
+ Call stack:
| File '/Users/sobolev/Desktop/cpython2/Lib/asyncio/taskgroups.py', line 121, in async TaskGroup._aexit()
| File '/Users/sobolev/Desktop/cpython2/Lib/asyncio/taskgroups.py', line 72, in async TaskGroup.__aexit__()
| File '/Users/sobolev/Desktop/cpython2/ex.py', line 7, in async main()
Frame.f_generator
– оно хранит генератор или корутину, которая владеет данным фреймом. Нужно чтобы отрисовывать + Call stack:
Future
@property
def _asyncio_awaited_by(self):
if self.__asyncio_awaited_by is None:
return None
return frozenset(self.__asyncio_awaited_by)
+ Awaited by:
.
/*[clinic input]
@critical_section
@getter
_asyncio.Future._asyncio_awaited_by
[clinic start generated code]*/
static PyObject *
_asyncio_Future__asyncio_awaited_by_get_impl(FutureObj *self)
/*[clinic end generated code: output=... input=...]*/
{
/* Implementation of a Python getter. */
if (self->fut_awaited_by == NULL) {
Py_RETURN_NONE;
}
if (self->fut_awaited_by_is_set) {
/* Already a set, just wrap it into a frozen set and return. */
assert(PySet_CheckExact(self->fut_awaited_by));
return PyFrozenSet_New(self->fut_awaited_by);
}
PyObject *set = PyFrozenSet_New(NULL);
if (set == NULL) {
return NULL;
}
if (PySet_Add(set, self->fut_awaited_by)) {
Py_DECREF(set);
return NULL;
}
return set;
}
LaranaJS – Рендерим фронтенд в картинку! 🌚
LaranaJS – это большой эксперимент по поиску альтернативных способов рисовать графические интерфейсы. Если большинство других фреймворков полагаются на такие устаревшие технологии как HTML и CSS и вендорлочат себя на браузеры, то Larana делает всё иначе.
Вот как устроены сетевые взаимодействия в LaranaJS.
Браузер запрашивает страницу
На этом этапе происходят создание сессии, резолв роута и инициализация страницы. В то же время разные подсистемы (рендерер, менеджер сессий, роутер и т. д.) генерируют клиентский код — он минимальный – просто canvas и немного работы с сетью по вебсокетам:
<html>
<!--Minimal head-->
<body>
<canvas id="canvas"></canvas>
<script>
// Network code
</script>
</body>
</html>
// event
{
"event": "mousemove",
"x": 0,
"y": 0,
}
// response
{
"image": "", // изображение в base64
"x": 0, // координаты для вставки изображения
"y": 0,
"w": 0,
"h": 0,
}
6KB
#js и запускаться в любом браузере c 2009 года. При этом есть возможность написать собственное клиентское приложение и запускать его хоть на esp32 с подключённым дисплеем.
class HomePage extends Page {
title() {
return 'Hello, World!'
}
init() {
const { initState } = this.useState()
initState({ counter: 0 })
}
root() {
return layout({
style: 'row',
children: [
button({ text: '+', onClick: () => this.increment() }),
text({ model: 'counter' }),
button({ text: '-', onClick: () => this.decrement() }),
],
})
}
}
figure
. Например, рисовать сложные фигуры вроде снежинок:
root() {
return figure({
template: (fig, queue) => {
line({
borderColor: '#aaaaff',
borderWidth: 2,
points: [
point({ x: x - halfRadius, y: y - halfRadius }),
point({ x: x + halfRadius, y: y + halfRadius }),
point({ x: x + halfRadius, y: y - halfRadius, moveTo: true }),
point({ x: x - halfRadius, y: y + halfRadius }),
point({ x: x + halfRadius, y, moveTo: true }),
point({ x: x - halfRadius, y }),
point({ x, y: y - halfRadius, moveTo: true }),
point({ x, y: y + halfRadius }),
],
}).to(queue)
},
})
}
const app = new LaranaApp({
config,
renderer: new ClientRenderer({}),
sessionManager: new MemorySessionManager({}),
router: new DefaultRouter({ routes }),
})
app.run()
wemake-python-styleguide@1.0.0 релизнут!
https://github.com/wemake-services/wemake-python-styleguide/releases/tag/1.0.0
Самый строгий линтер в мире стал еще строже и еще удобнее.
ruff
Некоторое время назад я понял, что если сейчас не поддержать ruff
, то проект умрет. Сказано – сделано.
Теперь wemake-python-styleguide
поддерживает работу вместе с ruff
. Что оно означает на практике?
- Теперь WPS
не выкидывает никаких ошибок, которые противоречили бы ruff
. Например, я убрал все стилистические правила, чтобы решать все простым ruff format
- Все дублирующие правила из WPS
были убраны в пользу ruff
. Ведь ruff
быстрее их находит и некоторые даже фиксит
- Теперь можно использовать ruff check && ruff format && flake8 --select=WPS .
, WPS
, конечно, может найти дополнительные ошибки, но не будет конфликтовать с ruff
как раньше
- Поддержка полная. От preview = true
до самых заковыристых правил PyLint
, да теперь WPS
совместим с PyLint
из ruff
Black, кстати, теперь тоже поддерживается.
Конфигурацию можно найти тут.
Что еще интересного в релизе?
- Множество новых правил сложности
- Крутая поддержка match
и case
. Находим дубликаты case
условий, проверяем сложность, находим много разных ошибок
- Много новой конфигурации, чтобы точечно настраивать отдельные правила линтера
- Куча багов поправлено!
Статистика релиза:
- WPS
стал минимум в 2.4 х быстрее, потому что я удалил много кода и много flake8
плагинов
- Количество коммитов с прошлого релиза: 294
- Количество задач, которые я закрыл в процессе работы (с 195 до 26) = ~170
- Изменений: 490 файлов, +15к, -26к
- Количество контрибьюторов в проект достигло двухсот!Страдайте Наслаждайтесь! Всех с наступающим 🎄
| Поддержать | sobolevn">YouTube | GitHub | Чат |
Финалим
- Метаграмматика создает метапарсер на питоне для парсинга грамматики
- Метапарсер парсит грамматику и создает парсер на Си
- Си парсер парсит наш код на питоне
Но где грамматика для метаграмматики? Ответ можно найти тут.
| Поддержать | sobolevn">YouTube | GitHub | Чат |
Аллокаторы в СPython: PyArena
Один из самых простых аллокаторов в питоне. Исходники.
По сути данный аллокатор является небольшой оберткой поверх PyMem_Malloc, но с интересной особенностью. Если PyMem_Malloc
имеет PyMem_Free для освобождения памяти каждого конкретного объекта, то PyArena
имеет только _PyArena_Free(PyArena *arena) для освобождения сразу всей арены со всеми объектами, которые являются ее частью.
Смотрим:
struct _arena {
/* Pointer to the first block allocated for the arena, never NULL.
It is used only to find the first block when the arena is
being freed. */
block *a_head;
/* Pointer to the block currently used for allocation. Its
ab_next field should be NULL. If it is not-null after a
call to block_alloc(), it means a new block has been allocated
and a_cur should be reset to point it. */
block *a_cur;
/* A Python list object containing references to all the PyObject
pointers associated with this arena. They will be DECREFed
when the arena is freed. */
PyObject *a_objects;
};
typedef struct _block {
/* Total number of bytes owned by this block available to pass out.
Read-only after initialization. The first such byte starts at
ab_mem */
size_t ab_size;
/* Total number of bytes already passed out. The next byte available
to pass out starts at ab_mem + ab_offset */
size_t ab_offset;
/* An arena maintains a singly-linked, NULL-terminated list of
all blocks owned by the arena. These are linked via the
ab_next member */
struct _block *ab_next;
/* Pointer to the first allocatable byte owned by this block. Read-
only after initialization */
void *ab_mem;
} block;
void _PyArena_Free(PyArena *arena)
{
assert(arena);
// ...
block_free(arena->a_head);
Py_DECREF(arena->a_objects);
PyMem_Free(arena);
}
PyArena
есть block'и и есть список обычных PyObject *
. Что достигается за счет следующих АПИ:block
нужного размера и сохраняет указатель на него в single-linked listPyObject *
в список отслеживаемых объектов и гарантирует, что он будет жить столько, сколько живет сама аренаКак работает диспатчеризация байткода внутри 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 уже после запуска приложения.Как работает диспатчеризация байткода внутри VM? Tail call dispatch
(перед прочтением – советую прочитать пост ^ про computed goto)
https://github.com/python/cpython/pull/128718
В CPython новая оптимизация, которая дает где-то 5% производительности. Я уже рассказывал, что такое computed goto, но теперь есть еще более прикольная и быстрая штука для диспатчеризации байткода.
То есть: вызов следующего опкода в Python коде будет быстрее, а значит – все программы просто бесплатно станут быстрее.
(не путать с tail call оптимизацией для рекурсии)
Как работает?
Сначала делаем два макроса, которые будут устанавливать нужные атрибуты для компилятора.
Пока только [[clang::musttail]], про поддержку компиляторов будет ниже. Зачем нужен preserve_none – можно прочитать тут.
#ifdef Py_TAIL_CALL_INTERP
// Note: [[clang::musttail]] works for GCC 15, but not __attribute__((musttail)) at the moment.
# define Py_MUSTTAIL [[clang::musttail]]
# define Py_PRESERVE_NONE_CC __attribute__((preserve_none))
// Для простоты еще два макроса, просто слишком часто повторяется код:
#define TAIL_CALL_PARAMS _PyInterpreterFrame *frame, _PyStackRef *stack_pointer, PyThreadState *tstate, _Py_CODEUNIT *next_instr, int oparg
#define TAIL_CALL_ARGS frame, stack_pointer, tstate, next_instr, oparg
Py_PRESERVE_NONE_CC typedef PyObject* (*py_tail_call_funcptr)(TAIL_CALL_PARAMS);
TARGET
и DISPATCH_GOTO
по аналогии с computed gotos.
# define TARGET(op) Py_PRESERVE_NONE_CC PyObject *_TAIL_CALL_##op(TAIL_CALL_PARAMS)
# define DISPATCH_GOTO() \
do { \
Py_MUSTTAIL return (INSTRUCTION_TABLE[opcode])(TAIL_CALL_ARGS); \
} while (0)
TARGET
макросы будут разворачиваться в отдельные функции:
Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_BINARY_OP(TAIL_CALL_PARAMS);
Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_BINARY_OP(TAIL_CALL_PARAMS);
Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_LIST_APPEND(TAIL_CALL_PARAMS);
// ...
INSTRUCTION_TABLE[opcode]
, но теперь мы вызываем функцию, которая там лежит в DISPATCH_GOTO
. То есть теперь – у нас теперь есть буквально:
callbacks = {
'BINARY_OP': lambda *args, **kwargs: ...
'LIST_APPEND': lambda *args, **kwargs: ...
}
callbacks[opcode](*args, **kwargs)
[[mustail]]
не создается дополнительный стекфрейм, asm получается более оптимальным. Я подготовил для вас пример: https://godbolt.org/z/T3Eqnd33e (для таких простых случаев -O2
более чем работает, но все равно)foo(int a)
было:
mov edi, dword ptr [rbp - 4]
call foo(int)@PLT
add rsp, 16
pop rbp
ret
mov edi, dword ptr [rbp - 4]
pop rbp
jmp foo(int)@PLT
call
-> jmp
!__attribute__((musttail))
--with-tail-call-interp
, по-умолчанию в 3.14 оно работать не будет. В следующих версиях – включат по-умолчанию для всех.__attribute__
компилятора поддерживает только clang в llvm>=19 на x86-64 и AArch64. В следующем релизе gcc, вроде бы, завезут поддержкуНаходки в опенсорсе: mypy@2.0
Так как чуваки на бусти собрали цель в 50 человек, я сделал видео, которое обещал.
https://www.youtube.com/watch?v=vrOwcOKIIf4
Теперь "Находки в опенсорсе" еще и в видео формате!
Рассказываю, что будет в новом релизе: что сломаем, что добавим.
Пока релиз планируется где-то на вторую половину года, а я уже про него рассказываю.
Если понравится формат – поддержи видео, покажи коллеге :)
| Поддержать | sobolevn">YouTube | GitHub | Чат |
Что такое GIL в Python?
Кажется, один из золотых вопросов для всех питонистов на собеседованиях.
Обычно, на встречный вопрос "а что конкретно в питоне является GIL?" не может ответить ни один спрашивающий.
Сегодня мы закроем данный пробел в знаниях питонистов.
Global Interpreter Lock не позволяет более чем одному треду работать с Python API за раз. Его можно отключить через --disable-gil
в 3.13+, но сегодня мы про такое не будем.
Обратите внимание на ключевую фразу "c Python API". С системными треды могут и должны работать в режиме настоящей параллельности, без GIL. Что и позволяет получить ускорение при использовании threading
, когда C код поддерживает такой способ.
Знакомьтесь – вот структура GIL _gil_runtime_state
и поведение в ceval_gil.c
.
Как можно отпустить GIL?
На уровне C есть макросы: Py_BEGIN_ALLOW_THREADS и Py_END_ALLOW_THREADS, которые отпускают GIL в нужных местах. Пример из модуля mmap:
Py_BEGIN_ALLOW_THREADS
m_obj->data = mmap(NULL, map_size, prot, flags, fd, offset);
Py_END_ALLOW_THREADS
{
PyThreadState *_save;
_save = PyEval_SaveThread();
// your code here
PyEval_RestoreThread(_save);
}
PyThreadState
является текущим состоянием треда в CPython. Внутри хранится много контекста. Нас особо сильно интересует часть с полями про GIL:
struct PyThreadState {
struct {
unsigned int initialized:1;
/* Has been bound to an OS thread. */
unsigned int bound:1;
/* Has been unbound from its OS thread. */
unsigned int unbound:1;
/* Has been bound aa current for the GILState API. */
unsigned int bound_gilstate:1;
/* Currently in use (maybe holds the GIL). */
unsigned int active:1;
/* Currently holds the GIL. */
unsigned int holds_gil:1;
} _status;
// Thread state (_Py_THREAD_ATTACHED, _Py_THREAD_DETACHED, _Py_THREAD_SUSPENDED).
int state;
// ...
}
tstate->_status.active = 0;
tstate->_status.unbound = 1;
tstate->_status.holds_gil = 0;
tstate->state = detached_state;
_gil_runtime_state
. Py_DECREF
, и в тредах есть свой refcount, который работает локально, чтобы можно было его вызывать без GIL._threadmodule.c
.
_PyThreadState_Bind(tstate);
PyEval_AcquireThread(tstate);
_Py_atomic_add_ssize(&tstate->interp->threads.count, 1);
--strict-bytes
в mypy@1.15
Вообще, внутри mypy есть много всякой дичи, которую нельзя выразить системой типов нормально. И потому разные хаки просто приколачивают гвоздями. Например, int
и float
связывают псевдо-"наследованием", чтобы штуки вроде 1 + 1.0 == 2.0
работали нормально.
Раньше так было и с bytes
/ bytearray
/ memoryview
. То есть буквально можно было писать:
def func(arg: bytes) -> None:
assert isinstance(arg, bytes)
func(b'') # ok
func(bytearray(b'123')) # type checks, fails in runtime
func(memoryview(b'abc')) # type checks, fails in runtime
--strict
флагами. Были спрятанные --disable-bytearray-promotion
и --disable-memoryview-promotion
, но кто же про них знал?Buffer
, который появился недавно. И его выражали сначала как просто bytes
(да, bytes
был синонимом readonly-buffer долгое время), а потом стали выражать как:
ReadOnlyBuffer: TypeAlias = bytes
WriteableBuffer: TypeAlias = bytearray | memoryview | array.array[Any] | mmap.mmap | ctypes._CData | pickle.PickleBuffer
ReadableBuffer: TypeAlias = ReadOnlyBuffer | WriteableBuffer
bytes
/ bytearray
/ memoryview
. И флаг --strict-bytes
убирает такое приведение из mypy:
# --strict-bytes
def func(arg: bytes) -> None:
assert isinstance(arg, bytes)
func(b'') # ok
func(bytearray(b'123')) # Argument 1 to "func" has incompatible type "bytearray"; expected "bytes"
func(memoryview(b'abc')) # Argument 1 to "func" has incompatible type "memoryview[int]"; expected "bytes"
--strict
с mypy@2.0
, так же как и --local-partial-types
. Лучше подготовиться заранее.Buffer
у себя в коде?Enum и сложность
Enum – один из самых сложных модулей в питоне, я не шучу. Количество нюансов – просто огромное. Так как я последние несколько дней занимаюсь улучшением поддержки Enum
в mypy, то я решил рассказать про интересные штуки из модуля enum
, которые вы скорее всего могли пропустить.
Доки: https://docs.python.org/3/library/enum.html Их все равно никто не читает.
global_enum
Чудовищная фича. Просто ужас. Засовывает все значения Enum
в sys.modules[module].__dict__
:
>>> from enum import Enum, global_enum
>>> @global_enum
... class Pets(Enum):
... CAT = 1
... DOG = 2
>>> print(CAT)
CAT
CAT
и DOG
как константы в модуле, а потом появляется Enum
. Но все равно не советую.
>>> from enum import IntEnum, _simple_enum
>>>
>>> @_simple_enum(IntEnum)
... class Pets:
... CAT = 1
... DOG = 2
...
>>> print(Pets.CAT)
1
Enum
классов. Используется в основном внутри CPython для ускорения импорта библиотек. Не поддерживается mypy.@unique
Enum
типах и вызвать ошибку. Обязателен для использования. Иначе, где-то можно сделать опечатку:
@unique # <- will find the problem
class Pets(Enum):
CAT = 1
DOG = 1 # should be 2
Flag
?
from enum import IntFlag, FlagBoundary
class Permission(IntFlag, boundary=FlagBoundary.STRICT):
READ = 0
WRITE = 1
Permission.WRITE | Permission.READ
. Есть 4 разных поведения для таких случаев: https://docs.python.org/3/library/enum.html#enum.FlagBoundaryFlagBoundary.STRICT
, если сомневаетесь.__dunder__
и имя не _sunder_
_ignore_
nonmember
from enum import Enum, member, nonmember
class Example(Enum):
_ignore_ = ['a'] # nonmember
a = 1 # nonmember
b = 2 # member
__c__ = 3 # nonmember
_d_ = 4 # nonmember
e = nonmember(5) # nonmember
__f = 6 # nonmember
def g(self): ... # nonmember
@member
def h(self): ... # member
member
и nonmember
..pyi
файлах мы аннотировали енамы так:
# mymodule.pyi
class Pets(Enum):
CAT: int
DOG: int
# mymodule.pyi
class Pets(Enum):
CAT = 1
DOG = 2
Enum
в Python? А сейчас?Лучший курс по Python 12: tuple
https://youtube.com/watch?v=P5OY3Y4Fc7k
Я решил окончательно упороться: сделал видео про tuple
на 1ч 30м. Зато я рассказал про tuple
вообще все, что знал сам. Для джунов:
- В чем разница между tuple и list?
- Аннотации tuple
- Тип произведение
- TypeVarTuple, PEP646, Unpack
Для мидлов:
- ast.Tuple
- tuple_iterator
- collections.abc
- collections.namedtuple
- typing.NamedTuple
Для сениоров:
- PyTupleObject
- PyVarObject
- tp_alloc, tp_dealloc, freelists
- __len__
- __hash__
- Мутабельность tuple
- PyTuple_Pack, Py_BuildValue
- Виртуальная машина и компилятор: BUILD_TUPLE
- INTRINSIC_LIST_TO_TUPLE
- Оптимизации компилятора
- PySequenceTuple
Обещанный бонус
В видео я обещал, что расскажу в тг, что такое Py_TRASHCAN_BEGIN
и Py_TRASHCAN_END
.
Документация и исходники: https://github.com/python/cpython/blob/d05140f9f77d7dfc753dd1e5ac3a5962aaa03eff/Include/cpython/object.h#L431-L507
По факту - данные два макроса представляют собой do/while
цикл, который позволяет более удобно управлять сборкой "контейнеров" (tuple, в нашем случае). Каждый объект внутри "контейнера" может тоже быть контейнером. Таким образом про Py_DECREF(op->ob_item[i])
можно начать каскадную деаллокацию объектов внутри. И мы можем столкнуться с переполнением стека вызовов.
#define Py_TRASHCAN_BEGIN(op, dealloc) \
do { \
PyThreadState *tstate = PyThreadState_Get(); \
if (tstate->c_recursion_remaining <= Py_TRASHCAN_HEADROOM && Py_TYPE(op)->tp_dealloc == (destructor)dealloc) { \
_PyTrash_thread_deposit_object(tstate, (PyObject *)op); \
break; \
} \
tstate->c_recursion_remaining--;
/* The body of the deallocator is here. */
#define Py_TRASHCAN_END \
tstate->c_recursion_remaining++; \
if (tstate->delete_later && tstate->c_recursion_remaining > (Py_TRASHCAN_HEADROOM*2)) { \
_PyTrash_thread_destroy_chain(tstate); \
} \
} while (0);
Статический анализ GitHub Actions
Сразу после релиза новой версии линтера, я задался вопросом обновления своего шаблона для создания новых питоновских библиотек: https://github.com/wemake-services/wemake-python-package
И я понял, что я несколько отстал в вопросе стат анализа GitHub Actions и прочей инфраструктуры.
Расскажу о своих находках.
pre-commit ci
Все знают про пакет pre-commit? Несколько лет назад он получил еще и свой собственный CI, который умеет запускаться без дополнительного конфига. И автоматически пушить вам в ветку любые изменения. Что супер удобно для всяких ruff
/ black
/ isort
и прочего. У нас такое стоит в большом количестве проектов. Вот пример из typeshed. Вот что поменялось автоматически.
Строить CI на базе pre-commit
очень удобно, потому что тебе просто нужно скопировать пару строк в конфиг. А плюсов много:
- Автоматически исправляются многие проблемы
- Автоматически запускается CI, 0 настроек
- Локально все тоже работает одной командой: pre-commit run TASK_ID -a
actionlint
Первый раз я увидел actionlint
внутри CPython и затащил его в mypy. Actionlint на #go, он предлагает набор проверок для ваших GitHub Actions от безопасности до валидации спеки вашего yml. Довольно полезно, позволяет найти много мест для улучшений.
test.yaml:3:5: unexpected key "branch" for "push" section. expected one of "branches", ..., "workflows" [syntax-check]
|
3 | branch: main
| ^~~~~~~
test.yaml:10:28: label "linux-latest" is unknown. available labels are "macos-latest", ..., "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file [runner-label]
|
10 | os: [macos-latest, linux-latest]
| ^~~~~~~~~~~~~
test.yaml:13:41: "github.event.head_commit.message" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions for more details [expression]
|
13 | - run: echo "Checking commit '${{ github.event.head_commit.message }}'"
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
run:
скрипты!
warning[artipacked]: credential persistence through GitHub Actions artifacts
--> mypy/.github/workflows/mypy_primer.yml:37:9
|
37 | - uses: actions/checkout@v4
| _________-
38 | | with:
39 | | path: mypy_to_test
40 | | fetch-depth: 0
| |________________________- does not set persist-credentials: false
|
= note: audit confidence → Low
error[dangerous-triggers]: use of fundamentally insecure workflow trigger
--> mypy/.github/workflows/mypy_primer_comment.yml:3:1
|
3 | / on:
4 | | workflow_run:
... |
7 | | types:
8 | | - completed
| |_________________^ workflow_run is almost always used insecurely
|
= note: audit confidence → Medium
dependabot.yml
, renovate.yml
, readthedocs.yml
и многое другое.
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.30.0
hooks:
- id: check-dependabot
- id: check-github-workflows
v2025
😎
Читать полностью…
Как работает CI для опенсорса?
Любой крупный опенсорс проект невозможен без обильного тестирования. CI-сервисы уже многие годы являются нашими обязательными спутниками. Но как они работают?
Давайте разбирать на примере GitVerse.
Важнейшие части:
- репозиторий – откуда мы берем задачи и код для запуска;
- DSL – описание того, как и что мы будем запускать. Обычно в yaml
;
- runner (self-hosted или shared) – где мы запускаем определенные нами задачи.
Поговорим про две последние части.
DSL
С DSL все очень интересно. В GitVerse синтаксис и рантайм совместимы с GitHub Actions — значит, можно переиспользовать почти все существующие actions из маркетплейса.
Пример переиспользования wemake-python-styleguide GHA можно найти тут:
name: wps
'on':
push:
branches:
- master
pull_request:
jobs:
build:
runs-on: ubuntu-cloud-runner # <- отличие от GHA
steps:
- uses: actions/checkout@v4
- uses: wemake-services/wemake-python-styleguide@master
» act -W .gitverse/workflows/wps.yaml --container-architecture linux/amd64 -P ubuntu-cloud-runner=node:16-buster-slim
[wps] ☁ git clone 'https://github.com/wemake-services/wemake-python-styleguide' # ref=master
[wps] ⭐ Run Main actions/checkout@v4
[wps] 🐳 docker cp src=/Users/sobolev/Desktop/wps-test/. dst=/Users/sobolev/Desktop/wps-test
[wps] ✅ Success - Main actions/checkout@v4
[wps] ⭐ Run Main wemake-services/wemake-python-styleguide@master
[wps] ❌ Failure - Main wemake-services/wemake-python-styleguide@master
[wps] ⚙ ::set-output:: output=./script.py
2:1 WPS421 Found wrong function call: print
print('hello world')
^
[wps] 🏁 Job failed
act
, чтобы было удобнее. Там гошный standalone бинарник, легко скачать и использовать. Кто парсит парсер? Метаграмматики
Звучит как название нового фильма Марвел, но на самом деле – перед нами достаточно интересная задача.
В CPython с недавних пор (вспоминаем gvanrossum_83706/peg-parsing-series-de5d41b2ed60">проект Guido van Rossum по внедрению PEG парсера) грамматика описана вот так (ссылка):
lambdef[expr_ty]:
| 'lambda' a=[lambda_params] ':' b=expression {
_PyAST_Lambda((a) ? a : CHECK(arguments_ty, _PyPegen_empty_arguments(p)), b, EXTRA) }
lambda_params[arguments_ty]:
| invalid_lambda_parameters
| lambda_parameters
lambda
функций.lambdef
и lambda_params
обозначают названия правил[expr_ty]
и [arguments_ty]
– метаинформация, которая будет использована парсером позже. Тут буквально куски C кода написаны|
, сначала пробуем первое, потом второе и тдlambdadef
содержит в себе: a=[lambda_params]
(что конечно же обозначает парсинг параметров lambda
функции)'lambda'
– обозначает ключевое слово lambda
, а ':'
- физический символ :
в коде{}
у нас снова идет C код: данная часть называется "действием", она буквально описывает, какой C код вызывать при успешном парсинге данного правила. _PyAST_Lambda((a) ? a : CHECK(arguments_ty, _PyPegen_empty_arguments(p)), b, EXTRA)
– в нашем случае вызывает функцию _PyAST_Lambda
if (
(_keyword = _PyPegen_expect_token(p, 609)) // token='lambda'
&& (a = lambda_params_rule(p), !p->error_indicator) // lambda_params?
&& (_literal = _PyPegen_expect_token(p, 11)) // token=':'
&& (b = expression_rule(p)) // expression
)
{
Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
_res = _PyAST_Lambda(
(a) ? a : CHECK(arguments_ty, _PyPegen_empty_arguments(p)),
b, EXTRA);
goto done;
}
lambda x, y: ...
в AST при работе питона. Подводка закончена.
rule[Rule]:
| rulename memoflag? ":" alts NEWLINE INDENT more_alts DEDENT {
Rule(rulename[0], rulename[1], Rhs(alts.alts + more_alts.alts), memo=opt) }
| rulename memoflag? ":" NEWLINE INDENT more_alts DEDENT {
Rule(rulename[0], rulename[1], more_alts, memo=opt) }
| rulename memoflag? ":" alts NEWLINE { Rule(rulename[0], rulename[1], alts, memo=opt) }
[Rule]
и Rule(rulename[0], rulename[1], Rhs(alts.alts + more_alts.alts), memo=opt)
, но они уже на питоне. Потому что метаграмматика генерирует парсер для грамматики на питоне:
@memoize
def rule(self) -> Optional[Rule]:
# rule: rulename memoflag? ":" alts NEWLINE INDENT more_alts DEDENT | rulename memoflag? ":" NEWLINE INDENT more_alts DEDENT | rulename memoflag? ":" alts NEWLINE
mark = self._mark()
if (
(rulename := self.rulename())
and (opt := self.memoflag(),)
and (literal := self.expect(":"))
and (alts := self.alts())
and (_newline := self.expect('NEWLINE'))
and (_indent := self.expect('INDENT'))
and (more_alts := self.more_alts())
and (_dedent := self.expect('DEDENT'))
):
return Rule(rulename[0], rulename[1],
Rhs(alts.alts + more_alts.alts), memo=opt)
Лучший курс по 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