4. Модель выполнения

4.1. Структура программы

Программа на Python состоит из блоков кода. Блок — это фрагмент текста программы на Python, который выполняется как единое целое. Блоками являются модуль, тело функции и определение класса. Каждая команда, введённая в интерактивном режиме, — это блок. Файл скрипта (файл, переданный интерпретатору на стандартный ввод или указанный в аргументе командной строки) — это блок кода. Команда скрипта (команда, указанная в командной строке интерпретатора с помощью параметра -c) — тоже блок кода. Модуль, запущенный из командной строки как скрипт верхнего уровня (как модуль __main__) с аргументом -m, также является блоком кода. Строковый аргумент, передаваемый встроенным функциям eval() и exec(), является блоком кода.

Блок кода выполняется в кадре выполнения. Кадр содержит некоторую служебную информацию (используемую при отладке) и определяет, где и как продолжится выполнение после того, как блок кода завершится.

4.2. Именование и связывание

4.2.1. Связывание имён

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

Следующие конструкции связывают имена:

  • формальные параметры функций,

  • определения классов,

  • определения функций,

  • выражения присваивания,

  • цели, являющиеся идентификаторами, если они встречаются в присваивании:

    • заголовок цикла for,

    • после as в инструкции with, в ветви except, в ветви except* или в as-шаблоне при структурном сопоставлении с шаблоном,

    • в шаблоне захвата при структурном сопоставлении с шаблоном

  • инструкции import.

  • инструкции type.

  • списки параметров типа.

Инструкция import вида from ... import * связывает все имена, определённые в импортируемом модуле, кроме начинающихся с подчёркивания. Эту форму можно использовать только на уровне модуля.

Цель, встречающаяся в инструкции del, в этом смысле тоже считается связанной (хотя фактически семантика этого состоит в отвязывании имени).

Каждая инструкция присваивания или импорта находится либо в блоке, заданном определением класса или функции, либо на уровне модуля (в блоке кода верхнего уровня).

Если имя связано в блоке, оно является локальной переменной этого блока, если только не объявлено как nonlocal или global. Если имя связано на уровне модуля, оно является глобальной переменной. (Переменные блока кода модуля являются и локальными, и глобальными.) Если переменная используется в блоке кода, но не определена в нём, это свободная переменная.

Каждое вхождение имени в тексте программы ссылается на связывание этого имени, определяемое приведёнными ниже правилами разрешения имён.

4.2.2. Разрешение имён

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

Когда имя используется в блоке кода, оно разрешается в ближайшей охватывающей области видимости. Набор всех таких областей видимости, доступных блоку кода, называется окружением блока.

Если имя не найдено вообще, возбуждается исключение NameError. Если текущая область видимости — это область функции, и имя ссылается на локальную переменную, которая к моменту использования имени ещё не связана со значением, возбуждается UnboundLocalError. Исключение UnboundLocalError является подклассом NameError.

Если операция связывания имени встречается где угодно в блоке кода, все использования этого имени в блоке трактуются как ссылки на имя в текущем блоке. Это может приводить к ошибкам, когда имя используется в блоке до того, как оно связано. Это правило неочевидно. В Python нет объявлений, а операции связывания имён могут находиться в любом месте блока кода. Локальные переменные блока кода можно определить, просмотрев весь текст блока в поисках операций связывания имён. Примеры см. в статье ЧаВо об ошибке UnboundLocalError.

Если в блоке встречается инструкция global, все использования указанных в ней имён ссылаются на связывания этих имён в пространстве имён верхнего уровня. Имена разрешаются в пространстве имён верхнего уровня путём поиска в глобальном пространстве имён, то есть в пространстве имён модуля, содержащего блок кода, и во встроенном пространстве имён — пространстве имён модуля builtins. Сначала поиск идёт в глобальном пространстве имён. Если имена там не найдены, поиск продолжается во встроенном пространстве имён. Если имена не найдены и во встроенном пространстве имён, новые переменные создаются в глобальном пространстве имён. Инструкция global должна предшествовать всем использованиям перечисленных имён.

Инструкция global имеет ту же область действия, что и операция связывания имени в том же блоке. Если ближайшая охватывающая область видимости свободной переменной содержит инструкцию global, свободная переменная рассматривается как глобальная.

Инструкция nonlocal заставляет соответствующие имена ссылаться на ранее связанные переменные в ближайшей охватывающей области видимости функции. Если указанное имя не существует ни в одной из охватывающих областей видимости функции, во время компиляции возбуждается исключение SyntaxError. Параметры типа нельзя связать заново с помощью инструкции nonlocal.

Пространство имён модуля автоматически создаётся при первом импорте модуля. Главный модуль скрипта всегда называется __main__.

Блоки определений классов и аргументы exec() и eval() — особый случай в контексте разрешения имён. Определение класса — это исполняемая инструкция, которая может использовать и определять имена. Эти ссылки подчиняются обычным правилам разрешения имён, с той особенностью, что несвязанные локальные переменные ищутся в глобальном пространстве имён. Пространство имён определения класса становится словарём атрибутов класса. Область видимости имён, определённых в блоке класса, ограничена этим блоком и не распространяется на блоки кода методов. Это относится и к включениям и генераторным выражениям, но не относится к областям видимости аннотаций, которые имеют доступ к охватывающим их областям видимости класса. Это означает, что следующий код завершится ошибкой:

class A:
    a = 42
    b = list(a + i for i in range(10))

Однако следующий код выполнится успешно:

class A:
    type Alias = Nested
    class Nested: pass

print(A.Alias.__value__)  # <type 'A.Nested'>

4.2.3. Области видимости аннотаций

Аннотации, списки параметров типа и инструкции type создают области видимости аннотаций. Они в основном ведут себя как области видимости функций, но с некоторыми исключениями, о которых речь пойдёт ниже.

Области видимости аннотаций используются в следующих контекстах:

Области видимости аннотаций отличаются от областей видимости функций следующим образом:

  • Области видимости аннотаций имеют доступ к пространству имён охватывающего класса. Если область видимости аннотаций находится непосредственно внутри области видимости класса или внутри другой области видимости аннотаций, которая сама находится непосредственно внутри области видимости класса, код в ней может использовать имена, определённые в области видимости класса, как если бы он выполнялся прямо в теле класса. В отличие от них, обычные функции, определённые внутри классов, не могут обращаться к именам, определённым в области видимости класса.

  • Выражения в областях видимости аннотаций не могут содержать выражения yield, yield from, await или :=. (В других областях видимости, вложенных в область видимости аннотаций, эти выражения разрешены.)

  • Имена, определённые в областях видимости аннотаций, нельзя связать заново с помощью инструкций nonlocal во внутренних областях видимости. Это относится только к параметрам типа, поскольку никакие другие синтаксические элементы, которые могут встречаться внутри областей видимости аннотаций, не могут создавать новые имена.

  • Хотя у областей видимости аннотаций есть внутреннее имя, оно не отражается в квалифицированном имени объектов, определённых внутри такой области. Вместо этого __qualname__ таких объектов получается таким, как если бы объект был определён в охватывающей области видимости.

Добавлено в версии 3.12: Области видимости аннотаций появились в Python 3.12 как часть PEP 695.

Изменено в версии 3.13: Области видимости аннотаций также используются для значений по умолчанию параметров типа, введённых в PEP 696.

Изменено в версии 3.14: Теперь области видимости аннотаций используются и для аннотаций, как определено в PEP 649 и PEP 749.

4.2.4. Ленивые вычисления

Большинство областей видимости аннотаций вычисляются лениво. Это относится к аннотациям, значениям псевдонимов типов, созданных с помощью инструкции type, а также к границам, ограничениям и значениям по умолчанию переменных типа, созданных с помощью синтаксиса параметров типа. Это означает, что они не вычисляются при создании псевдонима типа или переменной типа и при создании объекта с аннотациями. Вместо этого они вычисляются только тогда, когда это необходимо, например при обращении к атрибуту __value__ псевдонима типа.

Пример:

>>> type Alias = 1/0
>>> Alias.__value__
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero
>>> def func[T: 1/0](): pass
>>> T = func.__type_params__[0]
>>> T.__bound__
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero

Здесь исключение возбуждается только при обращении к атрибуту __value__ псевдонима типа или к атрибуту __bound__ переменной типа.

Такое поведение в первую очередь полезно для ссылок на типы, которые ещё не определены на момент создания псевдонима типа или переменной типа. Например, ленивые вычисления позволяют создавать взаимно рекурсивные псевдонимы типов:

from typing import Literal

type SimpleExpr = int | Parenthesized
type Parenthesized = tuple[Literal["("], Expr, Literal[")"]]
type Expr = SimpleExpr | tuple[SimpleExpr, Literal["+", "-"], Expr]

Лениво вычисляемые значения вычисляются в области видимости аннотаций. Это означает, что имена, встречающиеся внутри такого значения, ищутся так, как если бы они использовались в непосредственно охватывающей области видимости.

Добавлено в версии 3.12.

4.2.5. Встроенные имена и ограниченное выполнение

Пользователям не следует трогать __builtins__: это исключительно деталь реализации. Тем, кто хочет переопределить значения во встроенном пространстве имён, следует выполнить инструкцию import для модуля builtins и соответствующим образом изменить его атрибуты.

Встроенное пространство имён, связанное с выполнением блока кода, на самом деле находится поиском имени __builtins__ в глобальном пространстве имён этого блока. Это должен быть словарь или модуль (во втором случае используется словарь модуля). По умолчанию в модуле __main__ __builtins__ — это встроенный модуль builtins, а в любом другом модуле __builtins__ — псевдоним словаря самого модуля builtins.

4.2.6. Взаимодействие с динамическими возможностями

Разрешение имён для свободных переменных происходит во время выполнения, а не во время компиляции. Это означает, что следующий код выведет 42:

i = 10
def f():
    print(i)
i = 42
f()

Функции eval() и exec() не имеют доступа к полному окружению для разрешения имён. Имена могут разрешаться в локальном и глобальном пространствах имён вызывающего кода. Свободные переменные разрешаются не в ближайшем охватывающем пространстве имён, а в глобальном. [1] У функций exec() и eval() есть необязательные аргументы, позволяющие переопределить глобальное и локальное пространство имён. Если указано только одно пространство имён, оно используется для обоих.

4.3. Исключения

Исключения являются средством прерывания обычного потока управления блока кода с целью обработки ошибок или других исключительных ситуаций. Исключение возбуждается в точке, где обнаружена ошибка. Оно может быть обработано окружающим блоком кода или любым блоком кода, который прямо или косвенно вызвал блок кода, где произошла ошибка.

Интерпретатор Python возбуждает исключение, когда обнаруживает ошибку времени выполнения (например, деление на ноль). Программа на Python также может явно возбудить исключение с помощью инструкции raise. Обработчики исключений задаются инструкцией try … except. Ветвь finally такой инструкции можно использовать для указания кода очистки, который не обрабатывает исключение, но выполняется независимо от того, возникло ли оно в предшествующем коде.

Python использует «модель завершения» при обработке ошибок: обработчик исключения может выяснить, что произошло, и продолжить выполнение на внешнем уровне, но не может устранить причину ошибки и повторить неудавшуюся операцию (разве что повторным входом в проблемный фрагмент кода с самого начала).

Если исключение не обработано вообще, интерпретатор завершает выполнение программы или возвращается в свой основной интерактивный цикл. В обоих случаях он выводит трассировку стека, кроме случая, когда исключение — SystemExit.

Исключения идентифицируются экземплярами классов. Ветвь except выбирается в зависимости от класса экземпляра: она должна ссылаться на класс этого экземпляра или на его невиртуальный базовый класс. Экземпляр может быть получен обработчиком и может нести дополнительную информацию об исключительной ситуации.

Примечание

Сообщения об исключениях не являются частью API Python. Их содержимое может измениться без предупреждения от одной версии Python к другой, и на него не следует полагаться в коде, который будет работать в нескольких версиях интерпретатора.

См. также описание инструкции try в разделе The try statement и инструкции raise в разделе Инструкция raise.

4.4. Компоненты среды выполнения

4.4.1. Общая модель вычислений

Модель выполнения Python существует не в вакууме. Она работает на хост-машине и через среду выполнения этого хоста, включая его операционную систему (ОС), если она есть. Когда программа запущена, концептуальные уровни того, как она работает на хосте, выглядят примерно так:

хост-машина
процесс (глобальные ресурсы)
поток (выполняет машинный код)

Каждый процесс представляет программу, запущенную на хосте. Сам процесс можно считать частью данных своей программы, а потоки процесса — её исполняющей частью. Это различие важно для понимания концептуальной среды выполнения Python.

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

Процессы изолированы и независимы друг от друга. (То же верно и для хостов.) Хост управляет доступом процесса к выделенным ему ресурсам, а также координирует работу процессов между собой.

Каждый поток представляет собой фактическое выполнение машинного кода программы в рамках ресурсов, выделенных процессу этой программы. Как и когда это выполнение происходит, целиком определяет хост.

С точки зрения Python программа всегда начинается ровно с одного потока. Однако затем она может расшириться до нескольких одновременно работающих потоков. Не все хосты поддерживают несколько потоков на процесс, но большинство поддерживает. В отличие от процессов, потоки внутри одного процесса не изолированы и не независимы друг от друга. Точнее, все потоки процесса совместно используют все ресурсы этого процесса.

Главное в потоках то, что каждый из них действительно выполняется независимо, одновременно с остальными. Это «одновременно» может быть лишь концептуальным («конкурентно») или физическим («параллельно»). В любом случае потоки фактически работают с несинхронизированной скоростью.

Примечание

Несинхронизированная скорость работы означает, что ни для какой части памяти процесса нет гарантии, что она останется согласованной для кода, выполняющегося в любом заданном потоке. Поэтому многопоточные программы должны следить за координацией доступа к намеренно общим ресурсам. Точно так же они должны проявлять предельную аккуратность и не обращаться из нескольких потоков ни к каким другим ресурсам. Иначе два потока, работающие одновременно, могут случайно помешать друг другу в использовании каких-то общих данных. Всё это верно как для программ на Python, так и для среды выполнения Python.

Эта широкая и неструктурированная обязанность — плата за ту сырую конкурентность, которую обеспечивают потоки. Альтернатива необходимой дисциплине, как правило, — борьба с недетерминированными ошибками и порчей данных.

4.4.2. Модель среды выполнения Python

Те же концептуальные уровни применимы к каждой программе на Python, с некоторыми дополнительными уровнями данных, специфичными для Python:

хост-машина
процесс (глобальные ресурсы)
глобальная среда выполнения Python (состояние)
интерпретатор Python (состояние)
поток (выполняет байт-код Python и «C-API»)
состояние потока Python

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

Примечание

Реализация Python не обязана реализовывать уровни среды выполнения отдельно или даже конкретно. Исключение составляют лишь места, где отдельные уровни прямо определены или доступны пользователям, например через модуль threading.

Примечание

Начальный интерпретатор обычно называют «главным» интерпретатором. Некоторые реализации Python, например CPython, наделяют главный интерпретатор особыми ролями.

Аналогично, поток хоста, в котором была инициализирована среда выполнения, называют «главным» потоком. Он может отличаться от начального потока процесса, хотя часто они совпадают. В некоторых случаях «главный поток» может означать ещё более конкретное понятие и обозначать начальное состояние потока. Среда выполнения Python может возлагать на главный поток особые обязанности, например обработку сигналов.

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

Глобальная среда выполнения на концептуальном уровне — это просто набор интерпретаторов. Хотя в остальном эти интерпретаторы изолированы и независимы друг от друга, они могут совместно использовать некоторые данные или другие ресурсы. Среда выполнения отвечает за безопасное управление этими глобальными ресурсами. Фактическая природа этих ресурсов и управление ими зависят от реализации. В конечном счёте для внешнего мира назначение глобальной среды выполнения сводится к управлению интерпретаторами.

Напротив, «интерпретатор» концептуально — это то, что мы обычно понимаем под (полнофункциональной) «средой выполнения Python». Когда машинный код, выполняющийся в потоке хоста, взаимодействует со средой выполнения Python, он обращается к Python в контексте конкретного интерпретатора.

Примечание

Термин «интерпретатор» здесь не то же самое, что «интерпретатор байт-кода», который регулярно работает в потоках, выполняя скомпилированный код Python.

В идеальном мире «средой выполнения Python» называлось бы то, что мы сейчас называем «интерпретатором». Однако его называют «интерпретатором» как минимум с момента появления в 1997 году (CPython:a027efa5b).

Каждый интерпретатор полностью инкапсулирует то состояние, необходимое для работы среды выполнения Python, которое не является глобальным для процесса и не привязано к конкретному потоку. Важно, что состояние интерпретатора сохраняется между обращениями к нему. Оно включает фундаментальные данные, такие как sys.modules. Среда выполнения обеспечивает безопасное совместное использование интерпретатора несколькими потоками.

Реализация Python может поддерживать одновременное использование нескольких интерпретаторов в одном процессе. Они независимы и изолированы друг от друга. Например, у каждого интерпретатора есть свой sys.modules.

Для состояния среды выполнения, специфичного для потока, у каждого интерпретатора есть набор состояний потоков, которыми он управляет, так же как глобальная среда выполнения содержит набор интерпретаторов. У него может быть состояние потока для любого нужного ему числа потоков хоста. Может быть даже несколько состояний потока для одного потока хоста, хотя это встречается реже.

Каждое состояние потока концептуально содержит все данные среды выполнения, специфичные для потока, которые нужны интерпретатору для работы в одном потоке хоста. Состояние потока включает текущее возбуждённое исключение и стек вызовов Python этого потока. Оно может включать и другие ресурсы, специфичные для потока.

Примечание

Термин «поток Python» иногда может обозначать состояние потока, но обычно он означает поток, созданный с помощью модуля threading.

Каждое состояние потока на протяжении своей жизни всегда привязано ровно к одному интерпретатору и ровно к одному потоку хоста. Оно будет использоваться только в этом потоке и только с этим интерпретатором.

К одному потоку хоста может быть привязано несколько состояний потока: как для разных интерпретаторов, так и для одного и того же. Однако для любого заданного потока хоста в каждый момент времени он может использовать только одно из привязанных к нему состояний.

Состояния потоков изолированы и независимы друг от друга и не разделяют никаких данных, за исключением того, что могут совместно использовать интерпретатор, а также объекты и другие ресурсы, принадлежащие этому интерпретатору.

Когда программа уже запущена, новые потоки Python можно создавать с помощью модуля threading (на платформах и в реализациях Python, поддерживающих потоки). Дополнительные процессы можно создавать с помощью модулей os, subprocess и multiprocessing. Интерпретаторы можно создавать и использовать с помощью модуля interpreters. Сопрограммы (async) можно запускать с помощью asyncio в каждом интерпретаторе, обычно только в одном потоке (часто в главном).

Примечания