← Назад к вопросам

Как реализован Python внутри?

3.0 Senior🔥 121 комментариев
#Python Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как реализован Python внутри

Python — это интерпретируемый язык, но его внутренняя реализация значительно сложнее, чем просто прямое выполнение исходного кода. Понимание внутреннего устройства критично для оптимизации производительности и отладки.

Общий процесс выполнения кода

Когда вы запускаете Python скрипт:

Исходный код (.py)
        ↓
    Лексер (Tokenizer) — разбивает на токены
        ↓
    Парсер (Parser) — создаёт AST (Abstract Syntax Tree)
        ↓
    Компилятор — преобразует AST в байт-код
        ↓
    Байт-код (.pyc в __pycache__)
        ↓
    Виртуальная машина Python (PVM) — выполняет байт-код
        ↓
    Результат

1. Лексический анализ (Tokenization)

import tokenize
import io

code = "x = 5 + 3"
tokens = tokenize.generate_tokens(io.StringIO(code).readline)

for token in tokens:
    print(token)

# Вывод:
# TokenInfo(type=NAME, string="x", ...)
# TokenInfo(type=OP, string="=", ...)
# TokenInfo(type=NUMBER, string="5", ...)
# TokenInfo(type=OP, string="+", ...)
# TokenInfo(type=NUMBER, string="3", ...)

2. Синтаксический анализ (Parser)

Парсер создаёт Abstract Syntax Tree (AST) — древовидную структуру, представляющую программу.

import ast

code = "x = 5 + 3"
tree = ast.parse(code)
print(ast.dump(tree, indent=2))

# Вывод показывает структуру дерева:
# Module(
#   body=[
#     Assign(
#       targets=[Name(id="x")],
#       value=BinOp(
#         left=Constant(value=5),
#         op=Add(),
#         right=Constant(value=3)
#       )
#     )
#   ]
# )

Вы можете даже вывести диаграмму AST:

import ast
from graphviz import Digraph

def visualize_ast(code):
    tree = ast.parse(code)
    # Рекурсивно визуализируем дерево
    # ...

3. Компиляция в байт-код

AST компилируется в байт-код — промежуточное представление, которое быстрее выполнять, чем парсить исходный код каждый раз.

import dis

def add(a, b):
    return a + b

# Disassemble показывает байт-код
dis.dis(add)

# Вывод:
#   2           0 LOAD_FAST                0 (a)
#               2 LOAD_FAST                1 (b)
#               4 BINARY_ADD
#               6 RETURN_VALUE

Каждая инструкция имеет опкод (например, LOAD_FAST, BINARY_ADD, RETURN_VALUE).

4. Кэширование байт-кода

Скомпилированный байт-код сохраняется в __pycache__ для быстрой загрузки:

# После первого запуска:
python my_script.py

# Создаётся:
__pycache__/my_script.cpython-310.pyc

# При следующем запуске Python загружает .pyc напрямую

5. Виртуальная машина Python (PVM)

PVM — это стек-машина, которая выполняет байт-код пошагово.

Стек-машина означает, что операции работают со стеком значений:

# Для выражения: x = 5 + 3

# PVM выполняет:
# 1. LOAD_CONST 5       # Положи 5 на стек: [5]
# 2. LOAD_CONST 3       # Положи 3 на стек: [5, 3]
# 3. BINARY_ADD         # Поп 3 и 5, сложи их, положи результат: [8]
# 4. STORE_NAME "x"     # Поп 8, присвой переменной x

Структура объектов в памяти

Всё в Python — это объект. Каждый объект имеет:

import sys

obj = 42

# PyObject в C имеет структуру:
class PyObject:
    ob_refcnt: int      # Количество ссылок (для сборки мусора)
    ob_type: type       # Указатель на тип объекта

# Проверяем информацию об объекте
print(type(obj))        # <class "int">
print(id(obj))          # Адрес объекта в памяти
print(sys.getrefcount(obj))  # Количество ссылок

# Пример с пользовательским классом
class Person:
    def __init__(self, name):
        self.name = name

person = Person("Alice")
print(object.__sizeof__(person))  # Размер объекта в байтах

Управление памятью и сборка мусора

Python использует reference counting (подсчёт ссылок) как основной механизм:

import sys

# Создаём объект
list_obj = [1, 2, 3]
print(sys.getrefcount(list_obj))  # 2 (сама переменная + аргумент getrefcount)

# Ещё одна ссылка
list_obj2 = list_obj
print(sys.getrefcount(list_obj))  # 3

# Удаляем ссылку
del list_obj2
print(sys.getrefcount(list_obj))  # 2 (снова)

# Когда счётчик ссылок = 0, объект удаляется из памяти
del list_obj  # Объект удалён

Проблема циклических ссылок:

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

# Циклическая ссылка
node1 = Node(1)
node2 = Node(2)
node1.next = node2
node2.next = node1  # Циклические ссылки!

# Reference counting не сможет удалить эти объекты
# Помогает garbage collector

import gc
gc.collect()  # Ищет и удаляет циклы

GIL (Global Interpreter Lock)

GIL — это мьютекс, который позволяет только одному потоку выполнять байт-код Python одновременно:

import threading
import time

def cpu_bound():
    # Эта работа НЕ будет распараллелена из-за GIL
    total = 0
    for i in range(100_000_000):
        total += i
    return total

# Однопоточный вариант
start = time.time()
cpu_bound()
print(f"Однопоточно: {time.time() - start:.2f}s")

# Многопоточный вариант (медленнее из-за GIL!)
start = time.time()
threads = [
    threading.Thread(target=cpu_bound),
    threading.Thread(target=cpu_bound)
]
for t in threads:
    t.start()
for t in threads:
    t.join()
print(f"Двухпоточно: {time.time() - start:.2f}s")  # Медленнее!

Почему GIL существует:

  • Упрощает реализацию CPython
  • Избегает проблем с многопоточностью в расширениях C
  • Управление памятью (reference counting) не потокобезопасно

Решения для распараллеливания:

import multiprocessing
import time

def cpu_bound():
    total = 0
    for i in range(100_000_000):
        total += i
    return total

# Многопроцессный вариант (работает параллельно!)
start = time.time()
with multiprocessing.Pool(2) as pool:
    results = pool.map(cpu_bound, [None, None])
print(f"Двухпроцессно: {time.time() - start:.2f}s")  # Быстрее

# Для I/O-bound работы async/await не требует процессов
import asyncio

async def fetch_data():
    # I/O операция
    pass

async def main():
    await asyncio.gather(
        fetch_data(),
        fetch_data()  # Параллельно!
    )

Оптимизации в CPython

1. Object interning

# Маленькие целые числа кэшируются
a = 5
b = 5
print(a is b)  # True (один и тот же объект)

# Большие целые числа — разные объекты
c = 257
d = 257
print(c is d)  # False (разные объекты)

2. String interning

# Строковые литералы интернируются
s1 = "hello"
s2 = "hello"
print(s1 is s2)  # True

# Но не динамически созданные строки
s3 = "".join(["hel", "lo"])
print(s1 is s3)  # False

Альтернативные реализации Python

  • CPython — стандартная реализация на C
  • PyPy — JIT-компилятор, часто быстрее CPython
  • Jython — Python для JVM (Java Virtual Machine)
  • IronPython — Python для .NET

Практические выводы

  • Байт-код кэшируется — первый запуск медленнее, но потом быстрее
  • GIL ограничивает многопоточность для CPU-bound задач; используй multiprocessing
  • Reference counting быстрое но может быть фрагментировано; регулярно вызывай gc.collect()
  • Объекты как структуры в памяти — поэтому is работает на адреса, а == на значения
  • Байт-код оптимизируется компилятором — даже если исходный код выглядит неоптимально

Понимание этих механизмов помогает писать быстрый и эффективный Python код, особенно при работе с производительностью и отладкой странных багов.

Как реализован Python внутри? | PrepBro