¿Es posible modificar la variable en Python que está en el alcance externo, pero no global?

107

Dado el siguiente código:

def A() :
    b = 1

    def B() :
        # I can access 'b' from here.
        print( b )
        # But can i modify 'b' here? 'global' and assignment will not work.

    B()
A()

Porque el código en la B()función variable bestá en el alcance externo, pero no en el alcance global. ¿Es posible modificar la bvariable desde dentro de la B()función? Seguro que puedo leerlo desde aquí y print(), pero ¿cómo modificarlo?

grigoryvp
fuente
Lo siento, por supuesto 2.7 :). Para Python 3, las reglas de alcance han cambiado.
grigoryvp
Puede siempre que bsea ​​mutable. Una asignación a benmascarará el alcance externo.
JimB
4
Es una de las vergüenzas de Python que nonlocalno se ha actualizado a 2.x. Es una parte intrínseca del soporte de cierre.
Glenn Maynard

Respuestas:

96

Python 3.x tiene la nonlocalpalabra clave . Creo que esto hace lo que quieres, pero no estoy seguro de si estás ejecutando Python 2 o 3.

La declaración no local hace que los identificadores enumerados se refieran a variables vinculadas previamente en el ámbito adjunto más cercano. Esto es importante porque el comportamiento predeterminado para el enlace es buscar primero en el espacio de nombres local. La declaración permite que el código encapsulado vuelva a vincular variables fuera del ámbito local además del ámbito global (módulo).

Para python 2, generalmente solo uso un objeto mutable (como una lista o dict) y muto el valor en lugar de reasignarlo.

ejemplo:

def foo():
    a = []
    def bar():
        a.append(1)
    bar()
    bar()
    print a

foo()

Salidas:

[1, 1]
Adam Wagner
fuente
16
Una buena forma de hacer esto es class nonlocal: passen el ámbito externo. Luego nonlocal.xse puede asignar a en el ámbito interno.
poco todo
1
Hasta ahora, ya tengo dos consejos de Python que son simples, pero muy útiles: el tuyo es el segundo :) ¡Gracias @kindall!
swdev
@kindall eso es un gran truco: es mínimamente diferente de la sintaxis de Python 3 y mucho más legible que pasar un objeto mutable.
dimo414
2
@kindall muy ordenado, muchas gracias :) probablemente necesite un nombre diferente porque rompe la compatibilidad. En Python 3 es un conflicto de palabras clave y provocará un SyntaxError. NonLocal¿ Quizás ?
Adam Terrey
o, ya que técnicamente es una clase Nonlocal,? :-)
algo así
19

Puede usar una clase vacía para contener un ámbito temporal. Es como el mutable pero un poco más bonito.

def outer_fn():
   class FnScope:
     b = 5
     c = 6
   def inner_fn():
      FnScope.b += 1
      FnScope.c += FnScope.b
   inner_fn()
   inner_fn()
   inner_fn()

Esto produce el siguiente resultado interactivo:

>>> outer_fn()
8 27
>>> fs = FnScope()
NameError: name 'FnScope' is not defined
chrisk
fuente
Es extraño que la clase con sus campos sea "visible" en una función interna, pero las variables no, a menos que defina la variable externa con la palabra clave "no local".
Celdor
12

Soy un poco nuevo en Python, pero he leído un poco sobre esto. Creo que lo mejor que obtendrá es similar a la solución alternativa de Java, que consiste en envolver su variable externa en una lista.

def A():
   b = [1]
   def B():
      b[0] = 2
   B()
   print(b[0])

# The output is '2'

Editar: Supongo que esto probablemente era cierto antes de Python 3. Parece que nonlocales tu respuesta.

Mike Edwards
fuente
4

No, no puedes, al menos de esta manera.

Porque la "operación set" creará un nuevo nombre en el ámbito actual, que cubre el exterior.

Zchenah
fuente
"que cubre el exterior" ¿Qué quieres decir? Definir un objeto con el nombre b en una función anidada no tiene influencia en un objeto con el mismo nombre en el espacio exterior de esta función
eyquem
1
@eyquem es decir, donde sea que esté la declaración de asignación, introducirá el nombre en todo el ámbito actual. Como el código de muestra de la pregunta, si es: def C (): print (b) b = 2 el "b = 2" introducirá el nombre b en todo el alcance de la función C, así que cuando imprima (b), lo hará intente obtener b en el alcance de la función C local pero no en el externo, el b local aún no se ha inicializado, por lo que habrá un error.
zchenah
1

Para cualquiera que vea esto mucho más tarde, una solución alternativa más segura pero más pesada es. Sin necesidad de pasar variables como parámetros.

def outer():
    a = [1]
    def inner(a=a):
        a[0] += 1
    inner()
    return a[0]
Michael Giba
fuente
1

La respuesta corta que simplemente funcionará automágicamente

Creé una biblioteca de Python para resolver este problema específico. Se lanza bajo la falta de escucha, así que úselo como desee. Puede instalarlo pip install seapieo consultar la página de inicio aquí https://github.com/hirsimaki-markus/SEAPIE

user@pc:home$ pip install seapie

from seapie import Seapie as seapie
def A():
    b = 1

    def B():
        seapie(1, "b=2")
        print(b)

    B()
A()

salidas

2

los argumentos tienen el siguiente significado:

  • El primer argumento es el alcance de ejecución. 0 significaría local B(), 1 significa padre A()y 2 significa abuelo <module>también conocido como global
  • El segundo argumento es una cadena o un objeto de código que desea ejecutar en el ámbito dado
  • También puede llamarlo sin argumentos para shell interactivo dentro de su programa

La respuesta larga

Esto es más complicado. Seapie funciona editando los marcos en la pila de llamadas usando la api CPython. CPython es el estándar de facto, por lo que la mayoría de la gente no tiene que preocuparse por ello.

Las palabras mágicas que probablemente te interesen más si estás leyendo esto son las siguientes:

frame = sys._getframe(1)          # 1 stands for previous frame
parent_locals = frame.f_locals    # true dictionary of parent locals
parent_globals = frame.f_globals  # true dictionary of parent globals

exec(codeblock, parent_globals, parent_locals)

ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(frame),ctypes.c_int(1))
# the magic value 1 stands for ability to introduce new variables. 0 for update-only

Esto último obligará a las actualizaciones a pasar al ámbito local. Sin embargo, los ámbitos locales se optimizan de manera diferente al ámbito global, por lo que la introducción de nuevos objetos tiene algunos problemas cuando intenta llamarlos directamente si no se inicializan de ninguna manera. Copiaré algunas formas de eludir estos problemas de la página de github

  • Assingn, importe y defina sus objetos de antemano
  • Assingn marcador de posición a sus objetos de antemano
  • Reasignar el objeto a sí mismo en el programa principal para actualizar la tabla de símbolos: x = locals () ["x"]
  • Utilice exec () en el programa principal en lugar de llamar directamente para evitar la optimización. En lugar de llamar a x do: exec ("x")

Si siente que el uso exec()no es algo con lo que quiera ir, puede emular el comportamiento actualizando el diccionario local verdadero (no el devuelto por locals ()). Copiaré un ejemplo de https://faster-cpython.readthedocs.io/mutable.html

import sys
import ctypes

def hack():
    # Get the frame object of the caller
    frame = sys._getframe(1)
    frame.f_locals['x'] = "hack!"
    # Force an update of locals array from locals dict
    ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(frame),
                                          ctypes.c_int(0))

def func():
    x = 1
    hack()
    print(x)

func()

Salida:

hack!
Markus Hirsimäki
fuente
0

No creo que debas querer hacer esto. Las funciones que pueden alterar cosas en su contexto circundante son peligrosas, ya que ese contexto puede escribirse sin el conocimiento de la función.

Puede hacerlo explícito, ya sea haciendo de B un método público y C un método privado en una clase (probablemente la mejor manera); o usando un tipo mutable como una lista y pasándolo explícitamente a C:

def A():
    x = [0]
    def B(var): 
        var[0] = 1
    B(x)
    print x

A()
Bob acto secundario
fuente
2
¿Cómo se puede escribir una función sin conocer las funciones anidadas en su interior? Las funciones anidadas y los cierres son una parte intrínseca de la función en la que están incluidas.
Glenn Maynard
Necesita conocer la interfaz de las funciones incluidas en la suya, pero no debería tener que saber lo que sucede dentro de ellas. Además, no se puede esperar que sepa lo que sucede en las funciones que llaman, etc. Si una función modifica un miembro no global o de clase, normalmente debería hacerlo explícito a través de su interfaz, es decir, tomarlo como parámetro.
Sideshow Bob
Python no te obliga a ser tan bueno, por supuesto, de ahí la nonlocalpalabra clave, pero depende de ti usarla con mucha precaución.
Sideshow Bob
5
@Bob: Nunca he encontrado que el uso de cierres como este sea peligroso, salvo por peculiaridades del lenguaje. Piense en los locales como una clase temporal y las funciones locales como métodos en la clase, y no es más complicado que eso. YMMV, supongo.
Glenn Maynard
0

Puede, pero tendrá que usar la declaración global (no es una solución realmente buena como siempre cuando se usan variables globales, pero funciona):

def A():
    global b
    b = 1

    def B():
      global b
      print( b )
      b = 2

    B()
A()
Cédric Julien
fuente
Vea mi respuesta que explica el posible inconveniente de esta solución
eyquem
4
Usar una variable global es completamente diferente.
Glenn Maynard
0

No sé si hay un atributo de una función que da el __dict__del espacio exterior de la función cuando este espacio exterior no es el espacio global == el módulo, que es el caso cuando la función es una función anidada, en Python 3.

Pero en Python 2, hasta donde yo sé, no existe tal atributo.

Entonces, las únicas posibilidades para hacer lo que quieras son:

1) usar un objeto mutable, como dicen otros

2)

def A() :
    b = 1
    print 'b before B() ==', b

    def B() :
        b = 10
        print 'b ==', b
        return b

    b = B()
    print 'b after B() ==', b

A()

resultado

b before B() == 1
b == 10
b after B() == 10

.

No un

La solución de Cédric Julien tiene un inconveniente:

def A() :
    global b # N1
    b = 1
    print '   b in function B before executing C() :', b

    def B() :
        global b # N2
        print '     b in function B before assigning b = 2 :', b
        b = 2
        print '     b in function B after  assigning b = 2 :', b

    B()
    print '   b in function A , after execution of B()', b

b = 450
print 'global b , before execution of A() :', b
A()
print 'global b , after execution of A() :', b

resultado

global b , before execution of A() : 450
   b in function B before executing B() : 1
     b in function B before assigning b = 2 : 1
     b in function B after  assigning b = 2 : 2
   b in function A , after execution of B() 2
global b , after execution of A() : 2

La b global después de la ejecución deA() se ha modificado y puede que no se desee

Ese es el caso solo si hay un objeto con el identificador b en el espacio de nombres global

eyquem
fuente