python scope

Alcance variable y resolución de nombre en Python



scope (6)

Creo que fundamentalmente no entiendo cómo Python hace cosas como el alcance variable y la resolución de nombres. En particular, el hecho de que la función broken() continuación no funciona realmente me sorprende. Y, aunque he buscado en la web durante un tiempo en busca de una explicación útil, todavía no la entiendo. ¿Alguien puede explicar o vincular a una buena descripción de cómo funciona esto en Python, con suficientes detalles que parecerán obvios por qué broken() no funciona después de leer los materiales relevantes?

# 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")


El concepto más importante que desea conocer es environment evaluation model , que es simple pero potente.

Déjame ofrecerte un buen material .

Si desea leer el documento de Python, puede leer 4. Modelo de ejecución: documentación de Python 3.7.4 , es muy breve.

Cuando se usa un nombre en un bloque de código, se resuelve usando el ámbito de cierre más cercano. El conjunto de todos estos ámbitos visibles para un bloque de código se denomina entorno del bloque.


Es más simple de lo que parece.

El primer caso es probablemente el más obvio:

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

Aquí todo lo que tienes son las reglas de alcance regular. L y bar pertenecen al mismo ámbito, y L se declara primero. Entonces bar() puede acceder a L

La segunda muestra también es similar:

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

Aquí tanto L como bar() pertenecen al mismo ámbito. Son locales para foo() . Puede parecer diferente porque Python usa enlace dinámico. Es decir, la resolución del nombre L en foo() solo se resuelve cuando se llama a la función. En ese momento, Python ya sabe que L es una variable local para la misma función que contiene foo() , por lo que el acceso es válido.

Sin embargo, aunque Python tiene enlace dinámico, no tiene alcance dinámico, por lo que esto fallará:

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

Aquí, hay dos variables llamadas L Uno es local para foo() y otro es local para bar() . Dado que estas funciones no están anidadas y Python no tiene un alcance dinámico, son dos variables diferentes. Debido a que bar() no usa L en una asignación, obtienes una excepción.


Honestamente, creo que las respuestas existentes complican demasiado las cosas. Claro, la información está ahí, pero es difícil de entender para el no experto.

El punto clave es este: bajo un sistema llamado alcance estático que utiliza Python (y la mayoría de los lenguajes de programación modernos), la relación entre los nombres de las variables y las ubicaciones de memoria está determinada por el lugar donde se define una función, no el lugar donde se llama . Esto está en contraste con el alcance dinámico, en el que la relación entre los nombres de las variables y las ubicaciones de memoria está determinada por el lugar donde se llama una función, no donde se define. Por lo tanto, como explica caxcaxcoatl, la función broken() no funciona porque, en ese contexto, bar() y foo() son hermanos y, por lo tanto, no saben nada del alcance de los demás. Pero la razón subyacente de esto no es que broken() no funcione en todos los lenguajes de programación concebibles, sino que Python (y la mayoría de los otros lenguajes de programación modernos) usa una convención de alcance en lugar de otra.


La función broken () arroja el siguiente error:

NameError: name ''L'' is not defined

Es porque L se define dentro de foo () y es local para esa función. Cuando intenta hacer referencia a él en alguna otra función, como bar (), no se definirá.

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

Básicamente, si declaras una variable dentro de una función, será local para esa función ...


La línea con L = ... en fixed declara L en el alcance de fixed . (El return antes de asegurarse de que la asignación no se ejecuta realmente, solo se usa para la determinación del alcance). La línea con nonlocal L declara que L dentro de foo refiere a un L de alcance externo, en este caso, fixed . De lo contrario, dado que existe una asignación a L dentro de foo , se referiría a una variable L dentro de foo .

Básicamente:

  • Una asignación a una variable hace que se limite a la función de cierre.
  • Una declaración nonlocal o global anula el alcance, en lugar de usar el alcance (¿el más interno? ¿El más externo?) Con la variable declarada o el alcance global, respectivamente.

def fixed(): def foo(): nonlocal L # Added L = [] bar() return L def bar(): L.append(5) foo() return # Added L = ... # Added


Una función definida dentro de otra función puede acceder al alcance de su padre.

En su caso específico, L siempre se define dentro de foo() . En los primeros dos ejemplos, bar() se define dentro de foo() , por lo que puede acceder a L por la regla anterior (es decir, foo() es el padre de bar() ).

Sin embargo, en broken() , bar() y foo() son hermanos. No saben nada del alcance de los demás, por lo que bar() no puede ver L

De la documentation :

Aunque los ámbitos se determinan estáticamente, se usan dinámicamente. En cualquier momento durante la ejecución, hay al menos tres ámbitos anidados cuyos espacios de nombres son directamente accesibles:

  • el ámbito más interno, que se busca primero, contiene los nombres locales
  • Los ámbitos de las funciones de cierre, que se buscan comenzando por el ámbito de cierre más cercano, contienen nombres no locales, pero también no globales.
  • el penúltimo alcance contiene los nombres globales del módulo actual
  • el alcance más externo (último buscado) es el espacio de nombres que contiene los nombres integrados

Ahora, ¿por qué funciona okay1 , si L se define textualmente después de la bar() ?

Python no intenta resolver los identificadores hasta que realmente tenga que ejecutar el código ( enlace dinámico , como se explica en la respuesta de @ Giusti).

Cuando Python ejecuta la función, ve un identificador L y lo busca en el espacio de nombres local. En la implementación de cpython, es un diccionario real, por lo que busca en un diccionario una clave llamada L

Si no lo encuentra, verifica los alcances de cualquier función de cierre , es decir, los otros diccionarios que representan los espacios de nombres locales de las funciones de cierre.

Tenga en cuenta que, incluso si L se define después de bar() , cuando se llama a bar() , L ya se ha definido. Entonces, cuando se ejecuta bar() , L ya existe en el espacio de nombres local de foo() , que se busca cuando Python no ve L dentro de bar() .

Pieza de apoyo de la documentación:

Un espacio de nombres es una asignación de nombres a objetos. La mayoría de los espacios de nombres se implementan actualmente como diccionarios de Python, pero eso normalmente no se nota de ninguna manera (excepto por el rendimiento), y puede cambiar en el futuro.

(...)

El espacio de nombres local para una función se crea cuando se llama a la función y se elimina cuando la función devuelve o genera una excepción que no se maneja dentro de la función. (En realidad, olvidar sería una mejor manera de describir lo que realmente sucede). Por supuesto, las invocaciones recursivas tienen su propio espacio de nombres local.

Un ámbito es una región textual de un programa Python donde se puede acceder directamente a un espacio de nombres. "Accesible directamente" aquí significa que una referencia no calificada a un nombre intenta encontrar el nombre en el espacio de nombres.