mot clé non local dans Python 2.x

117

J'essaie d'implémenter une fermeture en Python 2.6 et j'ai besoin d'accéder à une variable non locale mais il semble que ce mot clé ne soit pas disponible dans python 2.x. Comment accéder aux variables non locales dans les fermetures dans ces versions de python?

Adinsa
la source

Réponses:

125

Les fonctions internes peuvent lire des variables non locales dans 2.x, mais pas les relier . C'est ennuyeux, mais vous pouvez le contourner. Créez simplement un dictionnaire et stockez vos données sous forme d'éléments. Il n'est pas interdit aux fonctions internes de muter les objets auxquels les variables non locales font référence.

Pour utiliser l'exemple 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.
la source
4
pourquoi est-il possible de modifier la valeur du dictionnaire?
coelhudo
9
@coelhudo Parce que vous pouvez modifier des variables non locales. Mais vous ne pouvez pas affecter des variables non locales. Par exemple, cela soulèvera UnboundLocalError: def inner(): print d; d = {'y': 1}. Ici, print dlit externe dcréant ainsi une variable non locale ddans la portée interne.
suzanshakya
21
Merci pour cette réponse. Je pense que vous pourriez améliorer la terminologie cependant: au lieu de "peut lire, ne peut pas changer", peut-être "peut faire référence, ne peut pas attribuer à". Vous pouvez modifier le contenu d'un objet dans une portée non locale, mais vous ne pouvez pas modifier l'objet auquel il est fait référence.
metamatt
3
Vous pouvez également utiliser une classe arbitraire et instancier un objet au lieu du dictionnaire. Je le trouve beaucoup plus élégant et lisible car il n'inonde pas le code avec des littéraux (clés de dictionnaire), en plus vous pouvez utiliser des méthodes.
Alois Mahdal
6
Pour Python, je pense qu'il est préférable d'utiliser le verbe "bind" ou "rebind" et d'utiliser le nom "name" plutôt que "variable". Dans Python 2.x, vous pouvez fermer un objet mais vous ne pouvez pas relier le nom dans la portée d'origine. Dans un langage comme C, déclarer une variable réserve un peu de stockage (soit statique soit temporaire sur la pile). En Python, l'expression X = 1lie simplement le nom Xà un objet particulier (et intà la valeur 1). X = 1; Y = Xlie deux noms au même objet exact. Quoi qu'il en soit, certains objets sont modifiables et vous pouvez modifier leurs valeurs.
steveha
37

La solution suivante est inspirée de la réponse d'Elias Zamaria , mais contrairement à cette réponse gère correctement plusieurs appels de la fonction externe. La "variable" inner.yest locale à l'appel courant de outer. Seulement ce n'est pas une variable, puisque cela est interdit, mais un attribut d'objet (l'objet étant la fonction innerelle-même). C'est très moche (notez que l'attribut ne peut être créé qu'une fois la innerfonction définie) mais semble efficace.

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
la source
Cela ne doit pas être laid. Au lieu d'utiliser inner.y, utilisez external.y. Vous pouvez définir external.y = 0 avant le def inner.
jgomo3
1
Ok, je vois que mon commentaire est faux. Elias Zamaria a également implémenté la solution external.y. Mais comme le commente Nathaniel [1], vous devez vous méfier. Je pense que cette réponse devrait être présentée comme la solution, mais notez la solution external.y et les cavets. [1] stackoverflow.com/questions/3190706/…
jgomo3
Cela devient moche si vous voulez que plus d'une fonction interne partage un état mutable non local. Dites a inc()et a dec()renvoyés de l'extérieur qui incrémentent et décrémentent un compteur partagé. Ensuite, vous devez décider à quelle fonction attacher la valeur de compteur actuelle et référencer cette fonction à partir des autres. Ce qui semble un peu étrange et asymétrique. Par exemple, dans dec()une ligne comme inc.value -= 1.
BlackJack le
33

Plutôt qu'un dictionnaire, il y a moins d'encombrement dans une classe non locale . Modification de l' exemple de @ ChrisB :

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

ensuite

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

Chaque appel external () crée une nouvelle classe distincte appelée context (pas simplement une nouvelle instance). Cela évite donc à @ Nathaniel de se méfier du contexte partagé.

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

assert f() == 5
Bob Stein
la source
Agréable! C'est en effet plus élégant et lisible que le dictionnaire.
Vincenzo
Je recommanderais d'utiliser les machines à sous en général ici. Il vous protège des fautes de frappe.
DerWeh
@DerWeh intéressant, je n'en ai jamais entendu parler . Pourriez-vous dire la même chose de n'importe quelle classe? Je déteste être décontenancé par des fautes de frappe.
Bob Stein
@BobStein Désolé, je ne comprends pas vraiment ce que tu veux dire. Mais en ajoutant __slots__ = ()et en créant un objet au lieu d'utiliser la classe, par exemple context.z = 3, lèverait un AttributeError. C'est possible pour toutes les classes, sauf si elles héritent d'une classe ne définissant pas de slots.
DerWeh
@DerWeh Je n'ai jamais entendu parler de l'utilisation de machines à sous pour détecter les fautes de frappe. Vous avez raison, cela n'aiderait que dans les instances de classe, pas dans les variables de classe. Vous souhaitez peut-être créer un exemple de travail sur pyfiddle. Il faudrait au moins deux lignes supplémentaires. Je ne peux pas imaginer comment vous le feriez sans plus de désordre.
Bob Stein
14

Je pense que la clé ici est ce que vous entendez par «accès». Il ne devrait y avoir aucun problème avec la lecture d'une variable en dehors de la portée de fermeture, par exemple,

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

devrait fonctionner comme prévu (impression 3). Cependant, remplacer la valeur de x ne fonctionne pas, par exemple,

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

imprimeront toujours 3. D'après ma compréhension de PEP-3104, c'est ce que le mot-clé non local est censé couvrir. Comme mentionné dans le PEP, vous pouvez utiliser une classe pour accomplir la même chose (un peu désordonné):

class Namespace(object): pass
ns = Namespace()
ns.x = 3
def outer():
    def inner():
        ns.x = 5
    inner()
outer()
print ns.x
ig0774
la source
Au lieu de créer une classe et de l'instancier, on peut simplement créer une fonction: def ns(): passsuivie de ns.x = 3. Ce n'est pas joli, mais c'est un peu moins moche à mes yeux.
davidchambers
2
Une raison particulière pour le vote défavorable? J'avoue que la mienne n'est pas la solution la plus élégante, mais ça marche ...
ig0774
2
Et quoi class Namespace: x = 3?
Feuermurmel
3
Je ne pense pas que cette façon simule non locale, car elle crée une référence globale au lieu d'une référence dans une fermeture.
CarmeloS
1
Je suis d'accord avec @CarmeloS, votre dernier bloc de code à ne pas faire ce que PEP-3104 suggère comme moyen d'accomplir quelque chose de similaire en Python 2. nsest un objet global, c'est pourquoi vous pouvez faire référence ns.xau niveau du module dans l' printinstruction à la toute fin .
martineau
13

Il existe une autre façon d'implémenter des variables non locales dans Python 2, au cas où l'une des réponses ici ne serait pas souhaitable pour une raison quelconque:

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

Il est redondant d'utiliser le nom de la fonction dans l'instruction d'affectation de la variable, mais cela me semble plus simple et plus propre que de mettre la variable dans un dictionnaire. La valeur est mémorisée d'un appel à l'autre, tout comme dans la réponse de Chris B.

Elias Zamaria
la source
19
Attention: une fois implémenté de cette manière, si vous le faites f = outer(), puis plus tard g = outer(), fle compteur de sera réinitialisé. C'est parce qu'ils partagent tous deux une seule outer.y variable, plutôt que chacun ayant sa propre variable indépendante. Bien que ce code semble plus esthétique que la réponse de Chris B, sa manière semble être le seul moyen d'émuler la portée lexicale si vous souhaitez appeler outerplus d'une fois.
Nathaniel
@Nathaniel: Voyons si je comprends bien. L'affectation à outer.yn'implique rien de local à l'appel de fonction (instance) outer(), mais affecte à un attribut de l'objet fonction qui est lié au nom outerdans sa portée englobante . Et donc on aurait tout aussi bien pu utiliser, par écrit outer.y, n'importe quel autre nom à la place de outer, à condition qu'il soit connu pour être lié dans cette portée. Est-ce correct?
Marc van Leeuwen
1
Correction, j'aurais dû dire, après "lié dans cette portée": à un objet dont le type permet de définir des attributs (comme une fonction, ou toute instance de classe). De plus, puisque cette portée est en fait plus éloignée que celle que nous voulons, cela ne suggérerait-il pas la solution extrêmement laide suivante: au lieu d' outer.yutiliser le nom inner.y(puisque innerest lié à l'intérieur de l'appel outer(), qui est exactement la portée que nous voulons), mais en mettant l'initialisation inner.y = 0 après la définition de inner (comme l'objet doit exister lorsque son attribut est créé), mais bien sûr avant return inner?
Marc van Leeuwen
@MarcvanLeeuwen On dirait que votre commentaire a inspiré la solution avec inner.y par Elias. Bonne pensée de votre part.
Ankur Agarwal
L'accès et la mise à jour des variables globales n'est probablement pas ce que la plupart des gens essaient de faire lors de la mutation des captures d'une fermeture.
binki
12

Voici quelque chose inspiré d'une suggestion faite par Alois Mahdal dans un commentaire concernant une autre réponse :

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)

Mettre à jour

Après y avoir repensé récemment, j'ai été frappé de voir à quel point il ressemblait à un décorateur - quand il m'est apparu que son implémentation en tant que tel le rendrait plus générique et utile (bien que cela dégrade sans doute sa lisibilité dans une certaine mesure).

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

Notez que les deux versions fonctionnent à la fois en Python 2 et 3.

Martineau
la source
Celui-ci semble être le meilleur en termes de lisibilité et de préservation de la portée lexicale.
Aaron S.Kurland
3

Il y a une verrue dans les règles de portée de python - l'affectation rend une variable locale à sa portée de fonction immédiatement englobante. Pour une variable globale, vous résoudriez cela avec leglobal mot clé.

La solution est d'introduire un objet partagé entre les deux portées, qui contient des variables mutables, mais qui est lui-même référencé via une variable non affectée.

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

Une alternative est le piratage de certaines portées:

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

Vous pourrez peut-être trouver une astuce pour obtenir le nom du paramètre outer, puis le passer en tant que varname, mais sans vous fier au nom, outervous voudriez utiliser un combinateur Y.

Marcin
la source
Les deux versions fonctionnent dans ce cas particulier mais ne remplacent pas nonlocal. locals()crée un dictionnaire des outer()sections locales au moment où inner()est défini, mais changer ce dictionnaire ne change pas le vin outer(). Cela ne fonctionnera plus lorsque vous aurez plus de fonctions internes qui souhaitent partager une variable fermée. Dites a inc()et dec()que incrémentez et décrémentez un compteur partagé.
BlackJack le
@BlackJack nonlocalest une fonctionnalité python 3.
Marcin le
Oui je sais. La question était de savoir comment obtenir l'effet de Python 3 nonlocaldans Python 2 en général . Vos idées ne couvrent pas le cas général mais juste celui avec une fonction interne. Jetez un œil à cet essentiel pour un exemple. Les deux fonctions internes ont leur propre conteneur. Vous avez besoin d'un objet mutable dans la portée de la fonction externe, comme d'autres réponses l'ont déjà suggéré.
BlackJack le
@BlackJack Ce n'est pas la question, mais merci pour votre contribution.
Marcin le
Oui , c'est la question. Jetez un œil en haut de la page. adinsa a Python 2.6 et a besoin d'accéder à des variables non locales dans une fermeture sans le nonlocalmot clé introduit dans Python 3.
BlackJack
3

Une autre façon de le faire (même si c'est trop verbeux):

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
la source
1
en fait, je préfère la solution utilisant un dictionnaire, mais celui-ci est cool :)
Ezer Fernandes
0

Étendre la solution élégante Martineau ci-dessus à un cas d'utilisation pratique et un peu moins élégant que j'obtiens:

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)
Amnon Harel
la source
-3

Utiliser une variable globale

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

Personnellement, je n'aime pas les variables globales. Mais ma proposition est basée sur la réponse https://stackoverflow.com/a/19877437/1083704

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

où l'utilisateur doit déclarer une variable globale ranks, chaque fois que vous devez appeler le report. Mon amélioration élimine le besoin d'initialiser les variables de fonction de l'utilisateur.

Val
la source
Il est préférable d'utiliser un dictionnaire. Vous pouvez référencer l'instance dans inner, mais vous ne pouvez pas l'attribuer, mais vous pouvez modifier ses clés et ses valeurs. Cela évite l'utilisation de variables globales.
johannestaas