RRUNTIME LABruntime observatoryNNEON · Статьи на 90 языках
ГЛАВА 22
ПОДРОБНЫЙ РАЗБОР · ОТ БАЗЫ К КОДУ

Разбираем: Python-синтаксис для JavaScript-разработчика

Этот раздел можно читать до запуска опыта. После теории вернитесь к live trace и сопоставьте каждый шаг с реальным событием.

СНАЧАЛА ПРОСТЫМИ СЛОВАМИ

Python читается не как JavaScript без фигурных скобок. Отступы задают блоки, имена ссылаются на объекты, коллекции имеют разные свойства, а многие привычные циклы заменяются прямым обходом iterable. Цель главы — научиться видеть структуру чужого файла, даже если вы ещё не готовы писать библиотеку самостоятельно.

ТЕХНИЧЕСКАЯ ОСНОВА

Файл .py состоит из statements и expressions. Присваивание связывает имя с объектом; оно не объявляет ячейку фиксированного типа. Блок начинается после двоеточия и определяется одинаковым отступом. def создаёт функцию-объект, import выполняет и кэширует модуль, for запрашивает элементы у iterable, а with и try описывают управление ресурсами и ошибками.

Зачем это знатьJS-разработчику опаснее всего ложные аналогии: dict похож на object, но является mapping; list похож на Array, но slicing и comprehension меняют способ чтения; None не равен undefined; is проверяет идентичность, а не значение.
ГДЕ ВЫПОЛНЯЕТСЯ РАБОТА
01PYTHON SOURCE.py · statements · expressions
02PARSERtokens · AST · scopes
03CPYTHONcode objects · bytecode · frames
04OBJECTStypes · collections · stdlib
01 · СЛОВАРЬ

Термины этого эксперимента

Сначала поймите слова — затем порядок выполнения.

01

Имя

Идентификатор, связанный с объектом. Присваивание другого объекта меняет binding имени, а не тип отдельной переменной-ячейки.

02

Statement

Инструкция вроде if, for, import, return или assignment. Она управляет выполнением и не всегда производит значение.

03

Expression

Фрагмент, который вычисляется в объект-значение: вызов функции, арифметика, comprehension или доступ по индексу.

04

Iterable

Объект, элементы которого можно получать по одному в for, comprehension, sum, list и других потребителях.

05

list

Изменяемая упорядоченная последовательность. Ближайшая аналогия — JS Array, но методы и модель копирования отличаются.

06

tuple

Неизменяемая последовательность. Часто используется для фиксированной группы значений и распаковки.

07

dict

Mapping ключ → значение. Ключ должен быть hashable; доступ через [] выбрасывает KeyError, а get возвращает fallback.

08

set

Коллекция уникальных hashable-элементов с быстрыми membership-проверками и операциями множеств.

09

None

Единственный объект отсутствующего значения. Обычно сравнивается через is None, а не через ==.

10

Comprehension

Выражение, которое строит list, dict или set из iterable с преобразованием и необязательным фильтром.

02 · МЕХАНИКА

Что происходит по шагам

Каждый шаг соответствует наблюдаемому состоянию runtime.

  1. 01
    Интерпретатор загружает модуль

    Top-level statements файла выполняются по порядку. Поэтому import — не текстовая вставка и может иметь side effects.

  2. 02
    Отступ формирует блок

    После if, for, def, class, try, with или match двоеточие открывает suite; одинаковый indentation показывает принадлежность строк.

  3. 03
    Assignment связывает имя

    orders = [...] создаёт list и привязывает к нему имя orders. Другое имя может ссылаться на тот же list.

  4. 04
    for запрашивает элементы

    for order in orders получает объекты iterable. Индекс нужен только когда вы явно вызываете enumerate.

  5. 05
    Comprehension строит коллекцию

    Выражение слева преобразует элемент, for задаёт источник, if справа фильтрует.

  6. 06
    def создаёт функцию

    Тело не выполняется при объявлении. Параметры связываются с переданными объектами во время вызова.

  7. 07
    return завершает вызов

    Без явного return функция возвращает None. Несколько значений фактически упаковываются в tuple.

03 · КОНТЕКСТ

Где результат требует оговорки

Эти детали объясняют, почему похожий код иногда даёт другой trace.

01

and и or возвращают operand

Результатом может быть не bool. Поэтому value or fallback ошибочно заменит допустимые 0, пустую строку или пустой list.

02

== и is отвечают на разные вопросы

== проверяет равенство значений, is сравнивает идентичность объектов. is нужен для None и специальных sentinel-объектов.

03

Срез обычно создаёт shallow copy

items[:] возвращает новый list с теми же ссылками на вложенные элементы, а не глубокий клон.

04

dict — не JavaScript object

Ключами могут быть разные hashable-типы; отсутствующий [] даёт KeyError; attribute access и mapping access не одно и то же.

05

Аннотации не валидируют runtime

name: str помогает IDE и type checker, но обычный CPython не запрещает позднее связать name с объектом другого типа.

06

У for и while бывает else

else выполняется, если цикл завершился без break. Это не аналог else у ближайшего if.

07

Scope не блочный

if и for не создают отдельный local scope. Новый scope обычно дают module, function, class и comprehension.

01
Теория

Сначала разберитесь, какие части Node участвуют в выполнении.

02
Упрощённый код

Затем уберите служебные детали и рассмотрите только главную идею.

03
Runtime-код

После этого сопоставьте модель с кодом, который создаёт live trace.

04 · Упрощённый код

Минимальная модель без служебного кода

src/python-lab.py · учебный фрагментPython
orders = [
    {"id": "A-10", "status": "paid", "price": 120},
    {"id": "A-11", "status": "draft", "price": 80},
]

paid_ids = [
    order["id"]
    for order in orders
    if order["status"] == "paid"
]

def describe(order, *, currency="RUB"):
    return f"{order['id']} · {order['price']} {currency}"
05 · Runtime-код

Полный код, который выполняет сценарий

Это не альтернативный пример: ниже показаны функции и файлы, используемые кнопкой запуска.

ФАКТИЧЕСКИЙ SOURCE

Первый файл — настоящий CPython-сценарий; второй безопасно запускает child process и переводит его JSON Lines в live trace.

src/python-lab.py
CPython-сценарий294 строк
#!/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.

06 · РЕЦЕПТЫ

Практические шаблоны, которые можно подсмотреть

Сравнивайте цель, код и оговорки — не запоминайте синтаксис без модели.

01

Основные коллекции

Различать встроенные контейнеры.

numbers = [1, 2, 3]                 # list
point = (10, 20)                    # tuple
user = {"id": 7, "name": "Ada"}  # dict
roles = {"admin", "editor"}        # set
  • {} без элементов создаёт dict; пустой set записывается set().
  • Вложенные mutable-объекты остаются изменяемыми даже внутри tuple.
02

Безопасное чтение dict

Отличать отсутствие ключа от допустимого нуля.

discount = payload.get("discount")
if discount is None:
    discount = 10

quantity = payload["quantity"]
  • get удобен для необязательного ключа.
  • [] полезен, когда отсутствие ключа должно считаться ошибкой контракта.
03

Цикл без ручного индекса

Читать enumerate и распаковку пары.

for index, order in enumerate(orders, start=1):
    print(index, order["id"])
  • enumerate лениво выдаёт tuples (index, value).
  • index и order связываются распаковкой на каждой итерации.
04

Comprehension

Увидеть map и filter в Python-форме.

paid_ids = [
    order["id"]
    for order in orders
    if order["status"] == "paid"
]
  • Выражение результата записано перед for.
  • Для сложной логики обычный for часто читается лучше.
05

Функция и параметры

Понять positional, default и keyword-only arguments.

def connect(host: str, port: int = 5432, *, timeout: float = 2.0):
    return f"{host}:{port}; timeout={timeout}"

connect("db", timeout=1.5)
  • После * параметры передаются только по имени.
  • Аннотации не заставляют CPython проверять типы.
06

Entry point модуля

Не запускать приложение при import.

def main() -> None:
    print("start")

if __name__ == "__main__":
    main()
  • При прямом запуске __name__ равен __main__.
  • При import функции создаются, но main автоматически не вызывается.
07 · PRODUCTION-КЕЙСЫ

Как учебная ошибка превращается в инцидент

Реалистичный сервис: исходный код, наблюдаемая проблема, исправление и причина, по которой оно работает.

КЕЙС 01

Нулевой discount незаметно заменяется бизнес-default

Python checkout читает JSON payload от административной панели. Разработчик переносит привычный JS-паттерн с truthy fallback и считает, что or сработает только для отсутствующего значения.

КОНТЕКСТ ИНЦИДЕНТА

Законный discount=0 является falsy. Сервис подставляет 10%, сохраняет неверную цену и создаёт финансовое расхождение, хотя request прошёл без exception.

ДОПРОБЛЕМНАЯ РЕАЛИЗАЦИЯ
def build_order(payload: dict) -> dict:
    return {
        "quantity": payload.get("quantity") or 1,
        "discount": payload.get("discount") or 10,
        "note": payload.get("note") or "generated",
    }

or возвращает правый operand для любого falsy слева. Он не различает отсутствующий key, None, ноль и намеренно пустую строку.

ПОСЛЕИСПРАВЛЕННАЯ РЕАЛИЗАЦИЯ
from dataclasses import dataclass

@dataclass(frozen=True, slots=True)
class OrderInput:
    quantity: int
    discount: int
    note: str | None

def build_order(payload: dict) -> OrderInput:
    quantity = payload.get("quantity")
    discount = payload.get("discount")

    if quantity is None:
        quantity = 1
    if discount is None:
        discount = 10
    if quantity < 1 or not 0 <= discount <= 100:
        raise ValueError("invalid order values")

    return OrderInput(quantity, discount, payload.get("note"))

Проверка is None отделяет отсутствие от допустимого нуля. Явные constraints не позволяют тихо пропустить неверный диапазон, а dataclass делает форму результата видимой.

ФУНКЦИИ И КОНСТРУКЦИИ

Что делают непривычные вызовы из обоих фрагментов кода.

payload.get("discount")
Возвращает значение mapping key или None, если key отсутствует. В отличие от payload["discount"] не выбрасывает KeyError.
value or fallback
Возвращает value, если он truthy, иначе fallback. Ноль, пустая строка, None и пустая collection приводят к fallback.
value is None
Проверяет identity с singleton None и не смешивает отсутствие с другими falsy-значениями.
@dataclass(...)
Decorator генерирует типичные методы класса данных; frozen ограничивает assignment fields, slots меняет layout instances.
raise ValueError(...)
Создаёт и выбрасывает exception о некорректном значении, чтобы invalid contract не продолжил normal flow.
ПОЧЕМУ ИСПРАВЛЕНИЕ РАБОТАЕТ

При чтении Python всегда проверяйте, какую семантику автор вложил в truthiness. Fallback через or удобен только когда все falsy-значения действительно эквивалентны отсутствию.

ЧТО БЫЛО ВИДНО В PRODUCTION
  • Заказы с явным discount=0 неожиданно сохраняются с discount=10.
  • Нет exception или validation error, поэтому дефект виден только в бизнес-метриках.
  • Расхождение концентрируется на payload с нулевыми значениями.
08 · НЕ ПЕРЕПУТАЙТЕ

Популярные заблуждения

Миф слева, корректная модель справа.

МИФ

Отступы — только стиль форматирования.

НА САМОМ ДЕЛЕ

Отступы являются частью грамматики и определяют границы блока.

МИФ

range(5) создаёт list [0, 1, 2, 3, 4].

НА САМОМ ДЕЛЕ

range создаёт компактный iterable; list появится после явного list(range(5)).

МИФ

dict.key — аналог object.key.

НА САМОМ ДЕЛЕ

Обычный dict читается через data["key"] или data.get("key"). Атрибуты — другой протокол.

МИФ

if value проверяет только true/false.

НА САМОМ ДЕЛЕ

Python вызывает truth testing: ложными также считаются None, ноль и пустые коллекции.

МИФ

const из JavaScript заменяется tuple.

НА САМОМ ДЕЛЕ

const запрещает перепривязку binding, tuple запрещает изменение своей последовательности. Это разные гарантии.

МИФ

Type hints выполняют runtime-валидацию.

НА САМОМ ДЕЛЕ

Аннотации — metadata. Для проверки нужны type checker или отдельная runtime-библиотека.

09 · САМОПРОВЕРКА

Ответьте своими словами

Если ответ получается объяснить без терминов из документации, ментальная модель уже начала складываться.

  1. Почему orders_copy = orders не создаёт копию list?
  2. Чем data.get("count") or 10 опасен при count=0?
  3. Что создаёт [x * 2 for x in values if x > 0]?
  4. Почему is None корректнее == None?
  5. Что выполнится при import модуля впервые?