palabra clave no local en Python 2.x

117

Estoy tratando de implementar un cierre en Python 2.6 y necesito acceder a una variable no local, pero parece que esta palabra clave no está disponible en Python 2.x. ¿Cómo se debe acceder a las variables no locales en los cierres en estas versiones de Python?

adinsa
fuente

Respuestas:

125

Las funciones internas pueden leer variables no locales en 2.x, pero no volver a enlazarlas . Esto es molesto, pero puede solucionarlo. Simplemente cree un diccionario y almacene sus datos como elementos en él. Las funciones internas no tienen prohibido mutar los objetos a los que se refieren las variables no locales.

Para usar el ejemplo de Wikipedia:

def outer():
    d = {'y' : 0}
    def inner():
        d['y'] += 1
        return d['y']
    return inner

f = outer()
print(f(), f(), f()) #prints 1 2 3
Chris B.
fuente
4
¿Por qué es posible modificar el valor del diccionario?
coelhudo
9
@coelhudo Debido a que usted puede modificar las variables no locales. Pero no puede realizar asignaciones a variables no locales. Por ejemplo, esto elevará UnboundLocalError: def inner(): print d; d = {'y': 1}. Aquí, print dlee el exterior dcreando así una variable no local den el ámbito interior.
suzanshakya
21
Gracias por esta respuesta. Sin embargo, creo que podría mejorar la terminología: en lugar de "puede leer, no puede cambiar", tal vez "puede hacer referencia, no puede asignar a". Puede cambiar el contenido de un objeto en un ámbito no local, pero no puede cambiar a qué objeto se hace referencia.
metamatt
3
Alternativamente, puede usar una clase arbitraria y crear una instancia del objeto en lugar del diccionario. Lo encuentro mucho más elegante y legible ya que no inunda el código con literales (claves de diccionario), además puedes hacer uso de métodos.
Alois Mahdal
6
Para Python, creo que es mejor usar el verbo "vincular" o "volver a vincular" y usar el sustantivo "nombre" en lugar de "variable". En Python 2.x, puede cerrar un objeto, pero no puede volver a enlazar el nombre en el alcance original. En un lenguaje como C, declarar una variable reserva algo de almacenamiento (ya sea almacenamiento estático o temporal en la pila). En Python, la expresión X = 1simplemente vincula el nombre Xcon un objeto en particular (y intcon el valor 1). X = 1; Y = Xune dos nombres al mismo objeto exacto. De todos modos, algunos objetos son mutables y puedes cambiar sus valores.
steveha
37

La siguiente solución está inspirada en la respuesta de Elias Zamaria , pero contrariamente a esa respuesta, maneja múltiples llamadas de la función externa correctamente. La "variable" inner.yes local a la llamada actual de outer. Solo que no es una variable, ya que está prohibido, sino un atributo de objeto (el objeto es la función en innersí). Esto es muy feo (tenga en cuenta que el atributo solo se puede crear después de innerque se define la función) pero parece efectivo.

def outer():
    def inner():
        inner.y += 1
        return inner.y
    inner.y = 0
    return inner

f = outer()
g = outer()
print(f(), f(), g(), f(), g()) #prints (1, 2, 1, 3, 2)
Marc van Leeuwen
fuente
No tiene por qué ser feo. En lugar de utilizar inner.y, use outer.y. Puede definir external.y = 0 antes de def inner.
jgomo3
1
Ok, veo que mi comentario es incorrecto. Elias Zamaria implementó la solución external.y también. Pero como comenta Nathaniel [1], debes tener cuidado. Creo que esta respuesta debe promoverse como la solución, pero tenga en cuenta la solución y las advertencias de external.y. [1] stackoverflow.com/questions/3190706/…
jgomo3
Esto se pone feo si desea que más de una función interna comparta un estado mutable no local. Dicen una inc()y dec()regresaron de exterior que incrementar y disminuir un contador compartido. Luego, debe decidir a qué función adjuntar el valor actual del contador y hacer referencia a esa función desde las otras. Lo que parece algo extraño y asimétrico. Por ejemplo, en dec()una línea como inc.value -= 1.
BlackJack
33

En lugar de un diccionario, hay menos desorden en una clase no local . Modificando el ejemplo de @ ChrisB :

def outer():
    class context:
        y = 0
    def inner():
        context.y += 1
        return context.y
    return inner

Luego

f = outer()
assert f() == 1
assert f() == 2
assert f() == 3
assert f() == 4

Cada llamada externa () crea una clase nueva y distinta llamada contexto (no simplemente una nueva instancia). Por lo tanto, evita que @ Nathaniel tenga cuidado con el contexto compartido.

g = outer()
assert g() == 1
assert g() == 2

assert f() == 5
Bob Stein
fuente
¡Agradable! De hecho, es más elegante y legible que el diccionario.
Vincenzo
Recomendaría usar tragamonedas en general aquí. Te protege de errores tipográficos.
DerWeh
@DerWeh interesante, nunca había oído hablar de eso. ¿Podrías decir lo mismo de cualquier clase? Odio quedar desconcertado por errores tipográficos.
Bob Stein
@BobStein Lo siento, realmente no entiendo a qué te refieres. Pero al agregar __slots__ = ()y crear un objeto en lugar de usar la clase, por ejemplo, context.z = 3generaría un AttributeError. Es posible para todas las clases, a menos que hereden de una clase que no define espacios.
DerWeh
@DerWeh Nunca escuché hablar de usar tragamonedas para detectar errores tipográficos. Tienes razón, solo ayudaría en instancias de clase, no en variables de clase. Tal vez le gustaría crear un ejemplo funcional en pyfiddle. Requeriría al menos dos líneas adicionales. No puedo imaginar cómo lo haría sin más desorden.
Bob Stein
14

Creo que la clave aquí es lo que quieres decir con "acceso". No debería haber ningún problema con la lectura de una variable fuera del alcance del cierre, por ejemplo,

x = 3
def outer():
    def inner():
        print x
    inner()
outer()

debería funcionar como se esperaba (impresión 3). Sin embargo, anular el valor de x no funciona, por ejemplo,

x = 3
def outer():
    def inner():
        x = 5
    inner()
outer()
print x

todavía imprimirá 3. Según mi entendimiento de PEP-3104, esto es lo que la palabra clave nonlocal debe cubrir. Como se menciona en el PEP, puede usar una clase para lograr lo mismo (un poco complicado):

class Namespace(object): pass
ns = Namespace()
ns.x = 3
def outer():
    def inner():
        ns.x = 5
    inner()
outer()
print ns.x
ig0774
fuente
En lugar de crear una clase y instanciarla, uno puede simplemente crear una función: def ns(): passseguida de ns.x = 3. No es bonito, pero es un poco menos feo para mí.
davidchambers
2
¿Alguna razón en particular para el voto negativo? Admito que la mía no es la solución más elegante, pero funciona ...
ig0774
2
¿Qué hay de class Namespace: x = 3?
Feuermurmel
3
No creo que esta forma simule lo no local, ya que crea una referencia global en lugar de una referencia en un cierre.
CarmeloS
1
Estoy de acuerdo con @CarmeloS, su último bloque de código al no hacer lo que sugiere PEP-3104 como una forma de lograr algo similar en Python 2. nses un objeto global, por lo que puede hacer referencia ns.xa nivel de módulo en la printdeclaración al final .
martineau
13

Hay otra forma de implementar variables no locales en Python 2, en caso de que alguna de las respuestas aquí no sea deseable por cualquier motivo:

def outer():
    outer.y = 0
    def inner():
        outer.y += 1
        return outer.y
    return inner

f = outer()
print(f(), f(), f()) #prints 1 2 3

Es redundante usar el nombre de la función en la declaración de asignación de la variable, pero me parece más simple y limpio que poner la variable en un diccionario. El valor se recuerda de una llamada a otra, al igual que en la respuesta de Chris B.

Elias Zamaria
fuente
19
Tenga cuidado: cuando se implemente de esta manera, si lo hace f = outer()y luego lo hace g = outer(), fel contador de 'se restablecerá. Esto se debe a que ambos comparten una sola outer.y variable, en lugar de que cada uno tenga su propia variable independiente. Aunque este código parece más agradable desde el punto de vista estético que la respuesta de Chris B, su forma parece ser la única forma de emular el alcance léxico si desea llamar outermás de una vez.
Nathaniel
@Nathaniel: Déjame ver si entiendo esto correctamente. La asignación a outer.yno implica nada local a la llamada de función (instancia) outer(), pero asigna a un atributo del objeto de función que está vinculado al nombre outeren su ámbito adjunto . Y, por tanto, se podría haber utilizado igualmente, por escrito outer.y, cualquier otro nombre en lugar de outer, siempre que se sepa que está vinculado a ese ámbito. ¿Es esto correcto?
Marc van Leeuwen
1
Corrección, debería haber dicho, después de "enlazado en ese ámbito": a un objeto cuyo tipo permite establecer atributos (como una función o cualquier instancia de clase). Además, dado que este alcance está en realidad más lejos del que queremos, ¿no sugeriría esto la siguiente solución extremadamente fea: en lugar de outer.yusar el nombre inner.y(ya que innerestá vinculado dentro de la llamada outer(), que es exactamente el alcance que queremos), pero poniendo la inicialización inner.y = 0 después de la definición de interno (ya que el objeto debe existir cuando se crea su atributo), pero, por supuesto, antes return inner?
Marc van Leeuwen
@MarcvanLeeuwen Parece que tu comentario inspiró la solución con inner.y de Elias. Buen pensamiento tuyo.
Ankur Agarwal
Es probable que acceder a las variables globales y actualizarlas no sea lo que la mayoría de la gente intenta hacer al modificar las capturas de un cierre.
binki
12

Aquí hay algo inspirado en una sugerencia que Alois Mahdal hizo en un comentario con respecto a otra respuesta :

class Nonlocal(object):
    """ Helper to implement nonlocal names in Python 2.x """
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)


def outer():
    nl = Nonlocal(y=0)
    def inner():
        nl.y += 1
        return nl.y
    return inner

f = outer()
print(f(), f(), f()) # -> (1 2 3)

Actualizar

Después de mirar hacia atrás en esto recientemente, me sorprendió lo parecido a un decorador que era, cuando me di cuenta de que implementarlo como uno lo haría más genérico y útil (aunque podría decirse que hacerlo degrada su legibilidad hasta cierto punto).

# Implemented as a decorator.

class Nonlocal(object):
    """ Decorator class to help implement nonlocal names in Python 2.x """
    def __init__(self, **kwargs):
        self._vars = kwargs

    def __call__(self, func):
        for k, v in self._vars.items():
            setattr(func, k, v)
        return func


@Nonlocal(y=0)
def outer():
    def inner():
        outer.y += 1
        return outer.y
    return inner


f = outer()
print(f(), f(), f()) # -> (1 2 3)

Tenga en cuenta que ambas versiones funcionan tanto en Python 2 como en 3.

martineau
fuente
Este parece ser el mejor en términos de legibilidad y preservación del alcance léxico.
Aaron S. Kurland
3

Hay una verruga en las reglas de alcance de Python: la asignación hace que una variable sea local al alcance de la función que la encierra inmediatamente. Para una variable global, resolvería esto con la globalpalabra clave.

La solución es introducir un objeto que se comparte entre los dos ámbitos, que contiene variables mutables, pero se hace referencia a sí mismo a través de una variable que no está asignada.

def outer(v):
    def inner(container = [v]):
        container[0] += 1
        return container[0]
    return inner

Una alternativa es la piratería de algunos ámbitos:

def outer(v):
    def inner(varname = 'v', scope = locals()):
        scope[varname] += 1
        return scope[varname]
    return inner

Es posible que pueda descubrir algunos trucos para obtener el nombre del parámetro outery luego pasarlo como varname, pero sin depender del nombre outerque le gustaría, debe usar un combinador Y.

Marcin
fuente
Ambas versiones funcionan en este caso particular, pero no reemplazan nonlocal. locals()crea un diccionario de outer()s de los locales en el momento inner()se define pero cambiando ese diccionario qué no cambiar el ven outer(). Esto ya no funcionará cuando tenga más funciones internas que quieran compartir una variable cerrada. Decir una inc()y dec()ese incremento y disminuir un contador compartido.
BlackJack
@BlackJack nonlocales una característica de Python 3.
Marcin
Sí, lo sé. La pregunta era cómo lograr el efecto de Python 3 nonlocalen Python 2 en general . Tus ideas no cubren el caso general, sino solo el que tiene una función interna. Eche un vistazo a esta esencia para ver un ejemplo. Ambas funciones internas tienen su propio contenedor. Necesita un objeto mutable en el alcance de la función externa, como ya sugirieron otras respuestas.
BlackJack
@BlackJack Esa no es la pregunta, pero gracias por tu aporte.
Marcin
Sí, esa es la pregunta. Eche un vistazo a la parte superior de la página. adinsa tiene Python 2.6 y necesita acceso a variables no locales en un cierre sin la nonlocalpalabra clave introducida en Python 3.
BlackJack
3

Otra forma de hacerlo (aunque es demasiado detallado):

import ctypes

def outer():
    y = 0
    def inner():
        ctypes.pythonapi.PyCell_Set(id(inner.func_closure[0]), id(y + 1))
        return y
    return inner

x = outer()
x()
>> 1
x()
>> 2
y = outer()
y()
>> 1
x()
>> 3
Ezer Fernandes
fuente
1
en realidad prefiero la solución usando un diccionario, pero este es genial :)
Ezer Fernandes
0

Al extender la elegante solución de Martineau anterior a un caso de uso práctico y algo menos elegante, obtengo:

class nonlocals(object):
""" Helper to implement nonlocal names in Python 2.x.
Usage example:
def outer():
     nl = nonlocals( n=0, m=1 )
     def inner():
         nl.n += 1
     inner() # will increment nl.n

or...
    sums = nonlocals( { k:v for k,v in locals().iteritems() if k.startswith('tot_') } )
"""
def __init__(self, **kwargs):
    self.__dict__.update(kwargs)

def __init__(self, a_dict):
    self.__dict__.update(a_dict)
Amnón Harel
fuente
-3

Usa una variable global

def outer():
    global y # import1
    y = 0
    def inner():
        global y # import2 - requires import1
        y += 1
        return y
    return inner

f = outer()
print(f(), f(), f()) #prints 1 2 3

Personalmente, no me gustan las variables globales. Pero, mi propuesta se basa en la respuesta https://stackoverflow.com/a/19877437/1083704

def report():
        class Rank: 
            def __init__(self):
                report.ranks += 1
        rank = Rank()
report.ranks = 0
report()

donde el usuario necesita declarar una variable global ranks, cada vez que necesite llamar a report. Mi mejora elimina la necesidad de inicializar las variables de función del usuario.

Val
fuente
Es mucho mejor usar un diccionario. Puede hacer referencia a la instancia en inner, pero no puede asignarla, pero puede modificar sus claves y valores. Esto evita el uso de variables globales.
johannestaas