Большая часть смысла Python спрятана в протоколах объектов: функция может быть значением, class создаёт новый тип, generator приостанавливается на yield, with гарантирует выход из контекста, а decorator заменяет объект результатом другого вызова.
Разбираем: Объекты, функции и протоколы Python
Этот раздел можно читать до запуска опыта. После теории вернитесь к live trace и сопоставьте каждый шаг с реальным событием.
В Python всё, включая функции, классы и модули, представлено объектами. Атрибуты разрешаются через data model, методы получают instance как первый параметр self, исключения раскручивают стек до подходящего except, iter/next образуют протокол итерации, а context manager реализует __enter__/__exit__.
Термины этого эксперимента
Сначала поймите слова — затем порядок выполнения.
Identity
Идентичность конкретного объекта. Оператор is отвечает, являются ли две ссылки одним объектом.
Mutable
Объект, значение которого можно изменить без создания нового объекта: например list, dict и set.
Callable
Объект, который можно вызвать через (). Функции и классы callable; объект может реализовать __call__.
Decorator
Callable, получающий функцию или class и связывающий исходное имя с возвращённым результатом.
Generator
Iterator, который сохраняет frame и продолжает выполнение между yield.
Context manager
Объект протокола входа/выхода, который with использует для детерминированного cleanup.
Exception
Объект ошибки, передаваемый вверх по call stack, пока подходящий except не перехватит его.
dataclass
Decorator стандартной библиотеки, генерирующий __init__, __repr__ и другие типичные методы класса данных.
Closure
Функция вместе со ссылками на free variables из enclosing function scope.
Module cache
sys.modules хранит загруженные модули; повторный import обычно возвращает тот же object.
Что происходит по шагам
Каждый шаг соответствует наблюдаемому состоянию runtime.
- 01class выполняет body
Python выполняет class body в namespace и затем создаёт class object. Это executable statement.
- 02Вызов class создаёт instance
__new__ создаёт объект, __init__ инициализирует уже созданный instance.
- 03Метод связывается с self
При instance.method(...) Python создаёт bound method и передаёт instance первым аргументом.
- 04Decorator применяется при определении
@decorator над def эквивалентен name = decorator(name).
- 05Generator стартует по требованию
Вызов generator function создаёт object. Тело начинает выполняться только при next или обходе.
- 06with гарантирует выход
__exit__ вызывается и при normal flow, и при exception, что делает cleanup предсказуемым.
- 07Exception ищет handler
Стек раскручивается до совместимого except; finally выполняется при любом способе выхода.
Где результат требует оговорки
Эти детали объясняют, почему похожий код иногда даёт другой trace.
Defaults вычисляются один раз
Значения defaults создаются во время выполнения def. Mutable default поэтому накапливает состояние.
Class variable разделяется
Mutable-объект из class body общий для instances, пока attribute не переопределён на instance.
property может выполнять дорогой код
Доступ order.total вызывает descriptor/property и может даже выбросить exception.
finally может подменить результат
return внутри finally подавляет ранее возвращаемое значение или exception. Обычно это ошибка.
Generator одноразовый
После StopIteration тот же generator не начнётся заново; нужно снова вызвать generator function.
Closure удерживает объект
Пока живёт внутренняя функция, captured object остаётся достижимым — как замыкание в JavaScript.
Private — в основном соглашение
_name обозначает internal API; __name применяет name mangling, но не строгую приватность.
Сначала разберитесь, какие части Node участвуют в выполнении.
Затем уберите служебные детали и рассмотрите только главную идею.
После этого сопоставьте модель с кодом, который создаёт live trace.
Минимальная модель без служебного кода
from dataclasses import dataclass
@dataclass(slots=True)
class CartLine:
sku: str
price: int
quantity: int = 1
@property
def subtotal(self) -> int:
return self.price * self.quantity
def even_squares(limit):
for value in range(limit):
if value % 2 == 0:
yield value * valueПолный код, который выполняет сценарий
Это не альтернативный пример: ниже показаны функции и файлы, используемые кнопкой запуска.
Первый файл — настоящий CPython-сценарий; второй безопасно запускает child process и переводит его JSON Lines в live trace.
#!/usr/bin/env python3
"""Fixed, input-free CPython scenarios used by Runtime Lab."""
from __future__ import annotations
import asyncio
import dis
import gc
import io
import json
import platform
import sys
import time
import weakref
from dataclasses import dataclass
from typing import Any, Iterator
def emit(lane: str, event_type: str, key: str, **data: Any) -> None:
print(
json.dumps(
{"lane": lane, "type": event_type, "key": key, "data": data},
ensure_ascii=False,
),
flush=True,
)
def version_event() -> None:
emit(
"python",
"runtime",
"python.version",
implementation=platform.python_implementation(),
version=platform.python_version(),
)
def run_syntax() -> None:
version_event()
orders = [
{"id": "A-10", "status": "paid", "price": 120, "qty": 2},
{"id": "A-11", "status": "draft", "price": 80, "qty": 1},
{"id": "A-12", "status": "paid", "price": 50, "qty": 3},
]
emit("objects", "state", "syntax.objects", count=len(orders))
paid = [order for order in orders if order["status"] == "paid"]
total = sum(order["price"] * order["qty"] for order in paid)
emit(
"comprehension",
"result",
"syntax.comprehension",
ids=", ".join(order["id"] for order in paid),
total=total,
)
first, *middle, last = [order["id"] for order in orders]
emit(
"sequence",
"result",
"syntax.unpack",
first=first,
middle=middle,
last=last,
)
labels = [f"{index}:{order['id']}" for index, order in enumerate(orders, start=1)]
emit("loop", "result", "syntax.enumerate", labels=", ".join(labels))
def format_order(order: dict[str, Any], *, currency: str = "RUB") -> str:
return f"{order['id']} · {order['price'] * order['qty']} {currency}"
emit(
"function",
"result",
"syntax.function",
rendered=format_order(orders[0], currency="₽"),
)
emit("result", "result", "syntax.result")
@dataclass(slots=True)
class CartLine:
sku: str
price: int
quantity: int = 1
@property
def subtotal(self) -> int:
return self.price * self.quantity
def append_bad(item: str, bucket: list[str] = []) -> list[str]:
bucket.append(item)
return list(bucket)
def append_safe(item: str, bucket: list[str] | None = None) -> list[str]:
target = [] if bucket is None else bucket
target.append(item)
return target
def even_squares(limit: int) -> Iterator[int]:
for value in range(limit):
if value % 2 == 0:
yield value * value
def classify_event(event: dict[str, Any]) -> str:
match event:
case {"type": "order.paid", "payload": {"id": order_id}}:
return f"paid order {order_id}"
case {"type": event_type}:
return f"other event {event_type}"
case _:
return "invalid event"
def run_semantics() -> None:
version_event()
line = CartLine("book", 450, quantity=2)
emit(
"class",
"result",
"semantics.dataclass",
rendered=repr(line),
subtotal=line.subtotal,
)
original = ["node"]
alias = original
alias.append("python")
emit("objects", "mutation", "semantics.alias", shared=original == alias == ["node", "python"])
bad_first = append_bad("api")
bad_second = append_bad("worker")
emit(
"function",
"warning",
"semantics.mutable-default",
first=bad_first,
second=bad_second,
)
safe_first = append_safe("api")
safe_second = append_safe("worker")
emit(
"function",
"result",
"semantics.safe-default",
first=safe_first,
second=safe_second,
)
emit("generator", "result", "semantics.generator", values=list(even_squares(7)))
emit(
"pattern",
"result",
"semantics.match",
label=classify_event({"type": "order.paid", "payload": {"id": "A-42"}}),
)
try:
int("not-a-number")
except ValueError as error:
emit("exception", "caught", "semantics.exception", message=str(error))
stream = io.StringIO()
with stream:
stream.write("cleanup is deterministic")
text = stream.getvalue()
emit("context", "cleanup", "semantics.context", closed=stream.closed, text=text)
emit("result", "result", "semantics.result")
def doubled_total(values: list[int]) -> int:
return sum(value * 2 for value in values)
class CycleNode:
def __init__(self) -> None:
self.peer: CycleNode | None = None
async def traced_task(name: str, delay: float, completion: list[str]) -> str:
emit("asyncio", "start", "asyncio.started", name=name)
await asyncio.sleep(delay)
completion.append(name)
emit("asyncio", "resume", "asyncio.resumed", name=name)
return name
async def measure_timer_while(blocking_call: Any) -> float:
started = time.perf_counter()
timer = asyncio.create_task(asyncio.sleep(0.01))
await blocking_call()
await timer
return max(0.0, (time.perf_counter() - started - 0.01) * 1_000)
async def run_asyncio_round() -> None:
completion: list[str] = []
first = asyncio.create_task(traced_task("A", 0.025, completion))
second = asyncio.create_task(traced_task("B", 0.005, completion))
emit("asyncio", "schedule", "asyncio.created")
await asyncio.gather(first, second)
emit("asyncio", "result", "asyncio.result", order=" → ".join(completion))
async def block_loop() -> None:
time.sleep(0.055)
async def offload_sleep() -> None:
await asyncio.to_thread(time.sleep, 0.055)
blocked_delay = await measure_timer_while(block_loop)
emit("asyncio", "blocking", "asyncio.blocking", delay=round(blocked_delay, 1))
offloaded_delay = await measure_timer_while(offload_sleep)
emit("asyncio", "offload", "asyncio.offload", delay=round(offloaded_delay, 1))
def run_runtime() -> None:
implementation = platform.python_implementation()
gil_probe = getattr(sys, "_is_gil_enabled", None)
gil_enabled = gil_probe() if gil_probe else implementation == "CPython"
emit(
"runtime",
"config",
"runtime.config",
implementation=implementation,
version=platform.python_version(),
gil=gil_enabled,
)
operations = [instruction.opname for instruction in dis.get_instructions(doubled_total)]
emit(
"bytecode",
"result",
"runtime.bytecode",
operations=" → ".join(operations[:12]),
)
frame = sys._getframe()
visible_locals = ", ".join(sorted(name for name in frame.f_locals if not name.startswith("_")))
emit(
"frame",
"state",
"runtime.frame",
functionName=frame.f_code.co_name,
locals=visible_locals,
)
left = CycleNode()
right = CycleNode()
left.peer = right
right.peer = left
left_ref = weakref.ref(left)
right_ref = weakref.ref(right)
alive_before = left_ref() is not None and right_ref() is not None
del left, right
collected = gc.collect()
alive_after = left_ref() is not None or right_ref() is not None
emit(
"gc",
"result",
"runtime.gc",
collected=collected,
aliveBefore=alive_before,
aliveAfter=alive_after,
)
asyncio.run(run_asyncio_round())
emit("result", "result", "runtime.result")
SCENARIOS = {
"syntax": run_syntax,
"semantics": run_semantics,
"runtime": run_runtime,
}
def main() -> None:
scenario = sys.argv[1] if len(sys.argv) > 1 else ""
runner = SCENARIOS.get(scenario)
if runner is None:
raise SystemExit(f"unknown scenario: {scenario}")
runner()
if __name__ == "__main__":
main()
Trace инструментирован сценарием: Python вызывает emit(...) и печатает события как упорядоченные JSON Lines, а Node фиксирует timestamp при получении каждой строки. Названия source/lane задаёт код, это не profiler CPython/asyncio. Node bridge читает вывод без shell, ограничивает время и объём и передаёт события HTTP stream.
Практические шаблоны, которые можно подсмотреть
Сравнивайте цель, код и оговорки — не запоминайте синтаксис без модели.
Функция как значение
Увидеть callable без вызова.
def normalize(value: str) -> str:
return value.strip().lower()
pipeline = [normalize, str.upper]
result = " Ada "
for transform in pipeline:
result = transform(result)- Имя функции без () ссылается на function object.
- Каждый callable получает результат предыдущего.
Безопасный default
Не делить mutable state.
def add_tag(tag: str, tags: list[str] | None = None):
result = [] if tags is None else list(tags)
result.append(tag)
return result- None — immutable sentinel.
- list(tags) не изменяет list вызывающей стороны.
Класс данных
Читать dataclass и property.
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Money:
amount: int
currency: str = "RUB"
@property
def label(self):
return f"{self.amount} {self.currency}"- frozen не даёт глубокой immutable-гарантии.
- slots меняет layout instance.
Generator
Обрабатывать поток лениво.
def read_valid(lines):
for line in lines:
text = line.strip()
if text:
yield text
for item in read_valid(file):
consume(item)- Результат не хранится целиком.
- Внешний ресурс всё равно нужно закрыть.
Exception chaining
Добавить domain-смысл без потери причины.
try:
user = repository.get(user_id)
except DatabaseError as error:
raise UserLoadError(user_id) from error
else:
return user
finally:
metrics.increment("load.attempt")- raise ... from сохраняет causal chain.
- else выполняется только без exception в try.
Context manager
Гарантировать transaction cleanup.
with database.transaction() as transaction:
order = transaction.insert_order(payload)
transaction.insert_outbox(order)- Commit/rollback определяет конкретный manager.
- Exception передаётся в __exit__.
Как учебная ошибка превращается в инцидент
Реалистичный сервис: исходный код, наблюдаемая проблема, исправление и причина, по которой оно работает.
Tags одного HTTP request появляются в другом
Helper добавляет audit tags и вызывается для каждого запроса. Автор ожидает, что default list создаётся заново при каждом вызове функции.
Default вычисляется один раз при выполнении def. Долгоживущий process переиспользует list, поэтому metadata пользователей смешивается и память постепенно растёт.
def attach_tag(tag: str, tags: list[str] = []) -> list[str]:
tags.append(tag)
return tags
def audit_request(request):
return attach_tag(f"user:{request.user_id}")Один function object хранит одну ссылку на default list в __defaults__. Каждый вызов без tags мутирует тот же объект.
def attach_tag(
tag: str,
tags: list[str] | None = None,
) -> list[str]:
result = [] if tags is None else list(tags)
result.append(tag)
return result
def audit_request(request):
return attach_tag(f"user:{request.user_id}")Immutable sentinel None безопасно хранится как default. Новый list создаётся внутри каждого вызова, а переданный list копируется, поэтому helper не мутирует владельца.
Что делают непривычные вызовы из обоих фрагментов кода.
tags=[]- Default expression выполняется один раз при создании function object, поэтому один mutable list разделяется вызовами.
function.__defaults__- Tuple с positional default objects функции; через него можно буквально увидеть сохранённую ссылку на общий list.
tags is None- Использует immutable singleton как сигнал, что вызывающая сторона не передала собственную collection.
list(tags)- Создаёт shallow copy iterable, чтобы append helper-а не изменял исходный list вызывающей стороны.
result.append(tag)- Мутирует конкретный result list на месте и возвращает None; поэтому append не присваивают обратно переменной.
Популярные заблуждения
Миф слева, корректная модель справа.
items=[] создаёт новый list на каждый вызов.
Один default list создаётся при выполнении def и переиспользуется.
self — специальное ключевое слово.
Это обычное имя первого параметра instance method и важное соглашение.
yield навсегда завершает функцию.
yield приостанавливает frame; следующий next продолжает код после остановки.
except Exception ловит вообще всё.
Системные сигналы вроде KeyboardInterrupt наследуются от BaseException.
with работает только с файлами.
Протокол подходит для locks, transactions, tracing и временного состояния.
Import только объявляет зависимость.
Первый import выполняет top-level code и может совершить I/O или изменить state.
Ответьте своими словами
Если ответ получается объяснить без терминов из документации, ментальная модель уже начала складываться.
- Когда вычисляется default argument?
- Почему instances могут разделить class-level list?
- Когда generator body начинает выполняться?
- Какие методы вызывает with?
- Почему property нельзя автоматически считать дешёвым полем?