Переменная область и разрешение имен в Python

Я думаю, что я принципиально не понимаю, как Python делает такие вещи, как переменная область видимости и разрешение имен. В частности, тот факт, что функция broken() ниже не работает, действительно удивляет меня. И хотя я какое-то время ловил рыбу в Интернете в поисках полезного объяснения, но я до сих пор не понимаю. Может кто-нибудь объяснить или дать ссылку на хорошее описание того, как этот материал работает в Python, с достаточным количеством подробностей, чтобы было очевидно, почему broken() не работает после прочтения соответствующих материалов?

# Why does this code work fine
def okay0():
    def foo():
        L = []
        def bar():
            L.append(5)
        bar()
        return L
    foo()

# and so does this
def okay1():
    def foo():
        def bar():
            L.append(5)
        L = []
        bar()
        return L
    foo()

# but the following code raises an exception?
def broken():
    def foo():
        L = []
        bar()
        return L
    def bar():
        L.append(5)
    foo()

# Example
test_list = [okay0, okay1, broken]
for test_function in test_list:
    try:
        test_function()
    except:
        print("broken")
    else:
        print("okay")

Ответы

Ответ 1

Функция, определенная в другой функции, может получить доступ к своей родительской области.

В вашем конкретном случае L всегда определяется в foo(). В первых двух примерах bar() также определен в foo(), поэтому он может обращаться к L по приведенному выше правилу (т.е. foo() является родителем bar()).

Тем не менее, на broken(), bar() и foo() являются братьями и сестрами. Они ничего не знают о сферах действия друг друга, поэтому bar() не может видеть L.

Из документации:

Хотя области действия определены статически, они используются динамически. В любой момент во время выполнения существует как минимум три вложенные области, пространства имен которых доступны напрямую:

  • самая внутренняя область, которая ищется первой, содержит локальные имена
  • области действия любых включающих функций, поиск которых начинается с ближайшей охватывающей области, содержит нелокальные, но и неглобальные имена
  • следующая за последней область содержит текущие глобальные имена модулей
  • самая внешняя область (последний поиск) - это пространство имен, содержащее встроенные имена

Теперь, почему okay1 работает, если L определяется текстовым образом после bar()?

Python не пытается разрешить идентификаторы до тех пор, пока он фактически не выполнит код (динамическое связывание, как объяснено в ответе @Giusti).

Когда Python выполняет функцию, он видит идентификатор L и ищет его в локальном пространстве имен. В реализации cpython это фактический словарь, поэтому он ищет в словаре ключ с именем L.

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

Обратите внимание, что даже если L определен после bar(), когда вызывается bar(), L уже определен. Таким образом, когда выполняется bar(), L уже существует в локальном пространстве имен foo(), которое ищется, когда Python не видит L в bar().

Вспомогательная часть документации:

Пространство имен - это отображение имен в объекты. Большинство пространств имен в настоящее время реализованы в виде словарей Python, но это обычно не заметно (кроме производительности) и может измениться в будущем.

(...)

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

Область действия - это текстовая область программы на Python, где пространство имен доступно напрямую. "Непосредственно доступный" здесь означает, что безусловная ссылка на имя пытается найти имя в пространстве имен.

Ответ 2

Это проще, чем кажется.

Первый случай, вероятно, самый очевидный:

 def okay0():
    def foo():
        L = []
        def bar():
            L.append(5)
        bar()
        return L
    foo()

Здесь все, что у вас есть, это обычные правила. L и bar принадлежат одной и той же области видимости, и L объявляется первым. Таким образом, bar() может получить доступ к L.

Второй пример также похож:

def okay1():
    def foo():
        def bar():
            L.append(5)
        L = []
        bar()
        return L
    foo()

Здесь и L, и bar() принадлежат к одной и той же области видимости. Они являются локальными для foo(). Это может выглядеть иначе, потому что Python использует динамическое связывание. То есть разрешение имени L в foo() разрешается только при вызове функции. К тому времени Python уже знает, что L является локальной переменной для той же функции, которая содержит foo(), поэтому доступ действителен.

Однако, хотя Python имеет динамическое связывание, он не имеет динамического контекста, поэтому произойдет сбой:

def broken():
    def foo():
        L = []
        bar()
        return L
    def bar():
        L.append(5)
    foo()

Здесь есть две переменные с именем L. Один локальный для foo(), а другой локальный для bar(). Поскольку эти функции не являются вложенными и Python не имеет динамической области видимости, они представляют собой две разные переменные. Поскольку bar() не использует L в назначении, вы получаете исключение.

Ответ 3

Функция broken() выдает следующую ошибку:

NameError: name 'L' is not defined

Это потому, что L определено внутри foo() и является локальным для этой функции. Когда вы пытаетесь сослаться на него в какой-либо другой функции, такой как bar(), она не будет определена.

def broken():
    def foo():
        L = []
        bar()
        return L
    def bar():
        L.append(5)
    foo()

По сути, если вы объявите переменную внутри функции, она будет локальной для этой функции....

Ответ 4

Строка с L = ... в fixed объявляет L в области видимости fixed. (return, прежде чем он удостоверится, что присвоение фактически не выполняется, просто используется для определения области.) Строка с nonlocal L объявляет, что L внутри foo ссылается на внешнюю область L, в этом дело, fixed. В противном случае, поскольку назначение в L существует внутри foo, оно будет ссылаться на переменную L внутри foo.

В основном:

  • Присвоение переменной делает ее доступной для включающей функции.
  • Объявление nonlocal или global переопределяет область, вместо этого используется область (самый внутренний? Внешний?) С объявленной переменной или глобальной областью действия соответственно.
def fixed():
    def foo():
        nonlocal L  # Added
        L = []
        bar()
        return L
    def bar():
        L.append(5)
    foo()
    return  # Added
    L = ...  # Added

Ответ 5

Самая важная концепция, которую вы хотите знать, - это environment evaluation model, которая проста, но мощна.

Позвольте мне предложить вам хороший материал.

Если вы хотите прочитать документ Python, вы можете прочитать 4. Модель исполнения - документация по Python 3.7.4, она очень краткая.

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

Ответ 6

Честно говоря, я думаю, что существующие ответы усложняют вещи. Конечно, информация есть, но ее трудно понять неспециалисту.

Ключевой момент заключается в следующем: в системе под названием static scoping, которую использует Python (и большинство других современных языков программирования), связь между именами переменных и местами в памяти определяется местом, в котором определяется функция, а не место, где это называется. Это в отличие от динамической области видимости, в которой взаимосвязь между именами переменных и ячейками памяти определяется местом, в котором вызывается функция, а не тем, где она определена. Таким образом, как объясняет caxcaxcoatl, функция broken() не работает, потому что в этом контексте bar() и foo() являются братьями и сестрами и, таким образом, ничего не знают о сферах деятельности друг друга. Но основная причина этого не в том, что broken() не работает на всех мыслимых языках программирования, а в том, что Python (и большинство других современных языков программирования) использует одно соглашение об ограничениях вместо другого.