Python читается не как JavaScript без фигурных скобок. Отступы задают блоки, имена ссылаются на объекты, коллекции имеют разные свойства, а многие привычные циклы заменяются прямым обходом iterable. Цель главы — научиться видеть структуру чужого файла, даже если вы ещё не готовы писать библиотеку самостоятельно.
Разбираем: Python-синтаксис для JavaScript-разработчика
Этот раздел можно читать до запуска опыта. После теории вернитесь к live trace и сопоставьте каждый шаг с реальным событием.
Файл .py состоит из statements и expressions. Присваивание связывает имя с объектом; оно не объявляет ячейку фиксированного типа. Блок начинается после двоеточия и определяется одинаковым отступом. def создаёт функцию-объект, import выполняет и кэширует модуль, for запрашивает элементы у iterable, а with и try описывают управление ресурсами и ошибками.
Термины этого эксперимента
Сначала поймите слова — затем порядок выполнения.
Имя
Идентификатор, связанный с объектом. Присваивание другого объекта меняет binding имени, а не тип отдельной переменной-ячейки.
Statement
Инструкция вроде if, for, import, return или assignment. Она управляет выполнением и не всегда производит значение.
Expression
Фрагмент, который вычисляется в объект-значение: вызов функции, арифметика, comprehension или доступ по индексу.
Iterable
Объект, элементы которого можно получать по одному в for, comprehension, sum, list и других потребителях.
list
Изменяемая упорядоченная последовательность. Ближайшая аналогия — JS Array, но методы и модель копирования отличаются.
tuple
Неизменяемая последовательность. Часто используется для фиксированной группы значений и распаковки.
dict
Mapping ключ → значение. Ключ должен быть hashable; доступ через [] выбрасывает KeyError, а get возвращает fallback.
set
Коллекция уникальных hashable-элементов с быстрыми membership-проверками и операциями множеств.
None
Единственный объект отсутствующего значения. Обычно сравнивается через is None, а не через ==.
Comprehension
Выражение, которое строит list, dict или set из iterable с преобразованием и необязательным фильтром.
Что происходит по шагам
Каждый шаг соответствует наблюдаемому состоянию runtime.
- 01Интерпретатор загружает модуль
Top-level statements файла выполняются по порядку. Поэтому import — не текстовая вставка и может иметь side effects.
- 02Отступ формирует блок
После if, for, def, class, try, with или match двоеточие открывает suite; одинаковый indentation показывает принадлежность строк.
- 03Assignment связывает имя
orders = [...] создаёт list и привязывает к нему имя orders. Другое имя может ссылаться на тот же list.
- 04for запрашивает элементы
for order in orders получает объекты iterable. Индекс нужен только когда вы явно вызываете enumerate.
- 05Comprehension строит коллекцию
Выражение слева преобразует элемент, for задаёт источник, if справа фильтрует.
- 06def создаёт функцию
Тело не выполняется при объявлении. Параметры связываются с переданными объектами во время вызова.
- 07return завершает вызов
Без явного return функция возвращает None. Несколько значений фактически упаковываются в tuple.
Где результат требует оговорки
Эти детали объясняют, почему похожий код иногда даёт другой trace.
and и or возвращают operand
Результатом может быть не bool. Поэтому value or fallback ошибочно заменит допустимые 0, пустую строку или пустой list.
== и is отвечают на разные вопросы
== проверяет равенство значений, is сравнивает идентичность объектов. is нужен для None и специальных sentinel-объектов.
Срез обычно создаёт shallow copy
items[:] возвращает новый list с теми же ссылками на вложенные элементы, а не глубокий клон.
dict — не JavaScript object
Ключами могут быть разные hashable-типы; отсутствующий [] даёт KeyError; attribute access и mapping access не одно и то же.
Аннотации не валидируют runtime
name: str помогает IDE и type checker, но обычный CPython не запрещает позднее связать name с объектом другого типа.
У for и while бывает else
else выполняется, если цикл завершился без break. Это не аналог else у ближайшего if.
Scope не блочный
if и for не создают отдельный local scope. Новый scope обычно дают module, function, class и comprehension.
Сначала разберитесь, какие части Node участвуют в выполнении.
Затем уберите служебные детали и рассмотрите только главную идею.
После этого сопоставьте модель с кодом, который создаёт live trace.
Минимальная модель без служебного кода
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}"Полный код, который выполняет сценарий
Это не альтернативный пример: ниже показаны функции и файлы, используемые кнопкой запуска.
Первый файл — настоящий 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.
Практические шаблоны, которые можно подсмотреть
Сравнивайте цель, код и оговорки — не запоминайте синтаксис без модели.
Основные коллекции
Различать встроенные контейнеры.
numbers = [1, 2, 3] # list
point = (10, 20) # tuple
user = {"id": 7, "name": "Ada"} # dict
roles = {"admin", "editor"} # set- {} без элементов создаёт dict; пустой set записывается set().
- Вложенные mutable-объекты остаются изменяемыми даже внутри tuple.
Безопасное чтение dict
Отличать отсутствие ключа от допустимого нуля.
discount = payload.get("discount")
if discount is None:
discount = 10
quantity = payload["quantity"]- get удобен для необязательного ключа.
- [] полезен, когда отсутствие ключа должно считаться ошибкой контракта.
Цикл без ручного индекса
Читать enumerate и распаковку пары.
for index, order in enumerate(orders, start=1):
print(index, order["id"])- enumerate лениво выдаёт tuples (index, value).
- index и order связываются распаковкой на каждой итерации.
Comprehension
Увидеть map и filter в Python-форме.
paid_ids = [
order["id"]
for order in orders
if order["status"] == "paid"
]- Выражение результата записано перед for.
- Для сложной логики обычный for часто читается лучше.
Функция и параметры
Понять 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 проверять типы.
Entry point модуля
Не запускать приложение при import.
def main() -> None:
print("start")
if __name__ == "__main__":
main()- При прямом запуске __name__ равен __main__.
- При import функции создаются, но main автоматически не вызывается.
Как учебная ошибка превращается в инцидент
Реалистичный сервис: исходный код, наблюдаемая проблема, исправление и причина, по которой оно работает.
Нулевой 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.
Популярные заблуждения
Миф слева, корректная модель справа.
Отступы — только стиль форматирования.
Отступы являются частью грамматики и определяют границы блока.
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-библиотека.
Ответьте своими словами
Если ответ получается объяснить без терминов из документации, ментальная модель уже начала складываться.
- Почему orders_copy = orders не создаёт копию list?
- Чем data.get("count") or 10 опасен при count=0?
- Что создаёт [x * 2 for x in values if x > 0]?
- Почему is None корректнее == None?
- Что выполнится при import модуля впервые?