web-dev-qa-db-fra.com

Pourquoi Python ne peut-il pas incrémenter les variables en fermeture?

Dans cet extrait de code, je peux imprimer la valeur du compteur depuis l'intérieur de la fonction Bar

def foo():
    counter = 1
    def bar():
        print("bar", counter)
    return bar

bar = foo()

bar()

Mais lorsque j'essaie d'incrémenter un compteur depuis l'intérieur de la fonction de barre, une erreur UnboundLocalError est générée.

UnboundLocalError: local variable 'counter' referenced before assignment

Extrait de code avec instruction d’incrémentation en format.

def foo():
    counter = 1
    def bar():
        counter += 1
        print("bar", counter)
    return bar

bar = foo()

bar()

Avez-vous uniquement un accès en lecture aux variables de la fonction externe dans une fermeture Python?

28
Neil

Vous ne pouvez pas muter les variables de fermeture dans Python 2. En Python 3, que vous semblez utiliser en raison de votre print(), vous pouvez les déclarer nonlocal:

def foo():

  counter = 1

  def bar():
    nonlocal counter
    counter += 1
    print("bar", counter)

  return bar

Sinon, l'affectation dans bar() rend la variable locale et, comme vous n'avez pas affecté de valeur à la variable dans l'étendue locale, la tentative d'accès y est une erreur.

Dans Python 2, ma solution de contournement préférée ressemble à ceci:

def foo():

  class nonlocal:
    counter = 1

  def bar():
    nonlocal.counter += 1
    print("bar", nonlocal.counter)

  return bar

Cela fonctionne car la mutation d'un objet mutable ne nécessite pas de changer le nom de la variable. Dans ce cas, nonlocal est la variable de fermeture et elle n'est jamais réaffectée. seul son contenu est modifié. D'autres solutions utilisent des listes ou des dictionnaires.

Vous pouvez aussi utiliser un cours pour tout, comme @naomik le suggère dans un commentaire.

50
kindall

Pourquoi Python ne peut-il pas incrémenter les variables en fermeture?

Le message d'erreur est explicite. Je propose quelques solutions ici.

  • Utiliser un attribut de fonction (rare, mais fonctionne plutôt bien)
  • Utilisation d'une fermeture avec nonlocal (idéal, mais uniquement avec Python 3)
  • Utiliser une fermeture sur un objet mutable (idiomatique de Python 2)
  • Utiliser une méthode sur un objet personnalisé
  • Appelant directement l'instance de l'objet en implémentant __call__

Utilisez un attribut sur la fonction.

Définissez un attribut de compteur sur votre fonction manuellement après l'avoir créé:

def foo():
    foo.counter += 1
    return foo.counter

foo.counter = 0

Et maintenant:

>>> foo()
1
>>> foo()
2
>>> foo()
3

Ou vous pouvez configurer automatiquement la fonction:

def foo():
    if not hasattr(foo, 'counter'):
        foo.counter = 0
    foo.counter += 1
    return foo.counter

De même:

>>> foo()
1
>>> foo()
2
>>> foo()
3

Ces approches sont simples, mais rares, et il est peu probable que quelqu'un visualise votre code sans votre présence.

Les moyens les plus courants d’atteindre vos objectifs varient en fonction de votre version de Python.

Python 3, utilisant une fermeture avec nonlocal

En Python 3, vous pouvez déclarer nonlocal: 

def foo():
    counter = 0
    def bar():
        nonlocal counter
        counter += 1
        print("bar", counter)
    return bar

bar = foo()

Et cela augmenterait

>>> bar()
bar 1
>>> bar()
bar 2
>>> bar()
bar 3

C'est probablement la solution la plus idiomatique à ce problème. Dommage que ce soit limité à Python 3.

Contournement de Python 2 pour nonlocal:

Vous pouvez déclarer une variable globale, puis incrémenter dessus, mais cela encombre l'espace de noms du module. Ainsi, la solution de contournement idiomatique pour éviter de déclarer une variable globale consiste à pointer sur un objet mutable contenant le nombre entier sur lequel vous souhaitez incrémenter, de sorte que vous n'essayiez pas de réaffecter le nom de la variable:

def foo():
    counter = [0]
    def bar():
        counter[0] += 1
        print("bar", counter)
    return bar

bar = foo()

et maintenant:

>>> bar()
('bar', [1])
>>> bar()
('bar', [2])
>>> bar()
('bar', [3])

Je pense que cela est supérieur aux suggestions qui impliquent la création de classes simplement pour contenir votre variable incrémentante. Mais pour être complet, voyons ça.

Utiliser un objet personnalisé

class Foo(object):
    def __init__(self):
        self._foo_call_count = 0
    def foo(self):
        self._foo_call_count += 1
        print('Foo.foo', self._foo_call_count)

foo = Foo()

et maintenant:

>>> foo.foo()
Foo.foo 1
>>> foo.foo()
Foo.foo 2
>>> foo.foo()
Foo.foo 3

ou même implémenter __call__:

class Foo2(object):
    def __init__(self):
        self._foo_call_count = 0
    def __call__(self):
        self._foo_call_count += 1
        print('Foo', self._foo_call_count)

foo = Foo2()

et maintenant:

>>> foo()
Foo 1
>>> foo()
Foo 2
>>> foo()
Foo 3
8
Aaron Hall

Comme vous ne pouvez pas modifier les objets immuables en Python, il existe une solution de contournement dans Python 2.X:

Utiliser la liste

def counter(start):
    count = [start]
    def incr():
        count[0] += 1
        return count[0]
    return incr

a = counter(100)
print a()
b = counter(200)
print b()
0
Anurag Ratna