web-dev-qa-db-fra.com

Python fonctions imbriquées portée des variables

J'ai lu presque toutes les autres questions sur le sujet, mais mon code ne fonctionne toujours pas.

Je pense que je manque quelque chose à propos de python portée variable.

Voici mon code:

PRICE_RANGES = {
                64:(25, 0.35),
                32:(13, 0.40),
                16:(7, 0.45),
                8:(4, 0.5)
                }

def get_order_total(quantity):
    global PRICE_RANGES
    _total = 0
    _i = PRICE_RANGES.iterkeys()
    def recurse(_i):
        try:
            key = _i.next()
            if quantity % key != quantity:
                _total += PRICE_RANGES[key][0]
            return recurse(_i) 
        except StopIteration:
            return (key, quantity % key)

    res = recurse(_i)

Et je reçois

"le nom global '_total' n'est pas défini"

Je sais que le problème vient de l'affectation _total, Mais je ne comprends pas pourquoi. recurse() ne devrait-il pas avoir accès aux variables de la fonction parent?

Quelqu'un peut-il m'expliquer ce qui me manque à propos de python portée variable?

77

Lorsque j'exécute votre code, j'obtiens cette erreur:

UnboundLocalError: local variable '_total' referenced before assignment

Ce problème est dû à cette ligne:

_total += PRICE_RANGES[key][0]

La documentation sur les étendues et les espaces de noms dit ceci:

Une particularité particulière de Python est que - si aucune instruction global n'est en vigueur - les affectations aux noms vont toujours dans la portée la plus intérieure . Les affectations ne copient pas les données - elles lient simplement les noms aux objets.

Donc, puisque la ligne dit effectivement:

_total = _total + PRICE_RANGES[key][0]

il crée _total dans l'espace de noms de recurse(). Puisque _total Est alors nouveau et non attribué, vous ne pouvez pas l'utiliser dans l'ajout.

55
Dave Webb

Voici une illustration qui va à l'essentiel de la réponse de David.

def outer():
    a = 0
    b = 1

    def inner():
        print a
        print b
        #b = 4

    inner()

outer()

Avec la déclaration b = 4 commenté, ce code génère 0 1, exactement ce que vous attendez.

Mais si vous décommentez cette ligne, sur la ligne print b, vous obtenez l'erreur

UnboundLocalError: local variable 'b' referenced before assignment

Il semble mystérieux que la présence de b = 4 pourrait en quelque sorte faire disparaître b sur les lignes qui le précèdent. Mais le texte que David cite explique pourquoi: lors d'une analyse statique, l'interpréteur détermine que b est affecté à dans inner, et qu'il s'agit donc d'une variable locale de inner. La ligne d'impression tente d'imprimer le b dans cette portée interne avant qu'il ne soit affecté.

132
moos

Dans Python 3, vous pouvez utiliser l'instruction nonlocal pour accéder aux étendues non locales et non globales.

101
Michael Hoffman

Plutôt que de déclarer un objet spécial, une carte ou un tableau, on peut également utiliser un attribut de fonction. Cela rend la portée de la variable très claire.

def sumsquares(x,y):
  def addsquare(n):
    sumsquares.total += n*n

  sumsquares.total = 0
  addsquare(x)
  addsquare(y)
  return sumsquares.total

Bien sûr, cet attribut appartient à la fonction (définition) et non à l'appel de fonction. Il faut donc être attentif au filetage et à la récursivité.

29
Hans

Il s'agit d'une variante de la solution de redman, mais en utilisant un espace de noms approprié au lieu d'un tableau pour encapsuler la variable:

def foo():
    class local:
        counter = 0
    def bar():
        print(local.counter)
        local.counter += 1
    bar()
    bar()
    bar()

foo()
foo()

Je ne sais pas si l'utilisation d'un objet de classe de cette façon est considérée comme un hack laid ou une technique de codage appropriée dans la communauté python, mais cela fonctionne très bien dans python = 2.x et 3.x (testé avec 2.7.3 et 3.2.3). Je ne suis pas sûr non plus de l'efficacité d'exécution de cette solution.

16
CliffordVienna

Vous avez probablement obtenu la réponse à votre question. Mais je voulais indiquer une façon dont je contourne généralement cela et c'est en utilisant des listes. Par exemple, si je veux faire ceci:

X=0
While X<20:
    Do something. ..
    X+=1

Je préfère faire ceci:

X=[0]
While X<20:
   Do something....
   X[0]+=1

De cette façon, X n'est jamais une variable locale

7
redman

Alors que j'avais l'habitude d'utiliser l'approche basée sur une liste de @ redman, elle n'est pas optimale en termes de lisibilité.

Voici une approche modifiée de @Hans, sauf que j'utilise un attribut de la fonction interne, plutôt que externe. Cela devrait être plus compatible avec la récursivité, et peut-être même le multithreading:

def outer(recurse=2):
    if 0 == recurse:
        return

    def inner():
        inner.attribute += 1

    inner.attribute = 0
    inner()
    inner()
    outer(recurse-1)
    inner()
    print "inner.attribute =", inner.attribute

outer()
outer()

Cela imprime:

inner.attribute = 3
inner.attribute = 3
inner.attribute = 3
inner.attribute = 3

Si je s/inner.attribute/outer.attribute/g, Nous obtenons:

outer.attribute = 3
outer.attribute = 4
outer.attribute = 3
outer.attribute = 4

Il semble donc préférable d'en faire les attributs de la fonction intérieure.

En outre, cela semble raisonnable en termes de lisibilité: car alors la variable se rapporte conceptuellement à la fonction interne, et cette notation rappelle au lecteur que la variable est partagée entre les portées des fonctions internes et externes. Un léger inconvénient pour la lisibilité est que le inner.attribute Ne peut être défini que syntaxiquement après la def inner(): ....

3
Evgeni Sergeev

Plus d'un point de vue philosophique, une réponse pourrait être "si vous rencontrez des problèmes d'espace de noms, donnez-lui un espace de noms qui lui est propre!"

Le fournir dans sa propre classe vous permet non seulement d'encapsuler le problème, mais facilite également les tests, élimine ces globaux embêtants et réduit le besoin de pelleter les variables entre les différentes fonctions de haut niveau (sans doute, il y aura plus que simplement get_order_total).

Préserver le code du PO pour se concentrer sur le changement essentiel,

class Order(object):
  PRICE_RANGES = {
                  64:(25, 0.35),
                  32:(13, 0.40),
                  16:(7, 0.45),
                  8:(4, 0.5)
                  }


  def __init__(self):
    self._total = None

  def get_order_total(self, quantity):
      self._total = 0
      _i = self.PRICE_RANGES.iterkeys()
      def recurse(_i):
          try:
              key = _i.next()
              if quantity % key != quantity:
                  self._total += self.PRICE_RANGES[key][0]
              return recurse(_i) 
          except StopIteration:
              return (key, quantity % key)

      res = recurse(_i)

#order = Order()
#order.get_order_total(100)

En tant que PS, un hack qui est une variante de l'idée de liste dans une autre réponse, mais peut-être plus clair,

def outer():
  order = {'total': 0}

  def inner():
    order['total'] += 42

  inner()

  return order['total']

print outer()
2
tantrix
>>> def get_order_total(quantity):
    global PRICE_RANGES

    total = 0
    _i = PRICE_RANGES.iterkeys()
    def recurse(_i):
    print locals()
    print globals()
        try:
            key = _i.next()
            if quantity % key != quantity:
                total += PRICE_RANGES[key][0]
            return recurse(_i)
        except StopIteration:
            return (key, quantity % key)
    print 'main function', locals(), globals()

    res = recurse(_i)


>>> get_order_total(20)
main function {'total': 0, 'recurse': <function recurse at 0xb76baed4>, '_i': <dictionary-keyiterator object at 0xb6473e64>, 'quantity': 20} {'__builtins__': <module '__builtin__' (built-in)>, 'PRICE_RANGES': {64: (25, 0.34999999999999998), 32: (13, 0.40000000000000002), 16: (7, 0.45000000000000001), 8: (4, 0.5)}, '__package__': None, 's': <function s at 0xb646adf4>, 'get_order_total': <function get_order_total at 0xb646ae64>, '__name__': '__main__', '__doc__': None}
{'recurse': <function recurse at 0xb76baed4>, '_i': <dictionary-keyiterator object at 0xb6473e64>, 'quantity': 20}
{'__builtins__': <module '__builtin__' (built-in)>, 'PRICE_RANGES': {64: (25, 0.34999999999999998), 32: (13, 0.40000000000000002), 16: (7, 0.45000000000000001), 8: (4, 0.5)}, '__package__': None, 's': <function s at 0xb646adf4>, 'get_order_total': <function get_order_total at 0xb646ae64>, '__name__': '__main__', '__doc__': None}
{'recurse': <function recurse at 0xb76baed4>, '_i': <dictionary-keyiterator object at 0xb6473e64>, 'quantity': 20}
{'__builtins__': <module '__builtin__' (built-in)>, 'PRICE_RANGES': {64: (25, 0.34999999999999998), 32: (13, 0.40000000000000002), 16: (7, 0.45000000000000001), 8: (4, 0.5)}, '__package__': None, 's': <function s at 0xb646adf4>, 'get_order_total': <function get_order_total at 0xb646ae64>, '__name__': '__main__', '__doc__': None}
{'recurse': <function recurse at 0xb76baed4>, '_i': <dictionary-keyiterator object at 0xb6473e64>, 'quantity': 20}
{'__builtins__': <module '__builtin__' (built-in)>, 'PRICE_RANGES': {64: (25, 0.34999999999999998), 32: (13, 0.40000000000000002), 16: (7, 0.45000000000000001), 8: (4, 0.5)}, '__package__': None, 's': <function s at 0xb646adf4>, 'get_order_total': <function get_order_total at 0xb646ae64>, '__name__': '__main__', '__doc__': None}

Traceback (most recent call last):
  File "<pyshell#32>", line 1, in <module>
    get_order_total(20)
  File "<pyshell#31>", line 18, in get_order_total
    res = recurse(_i)
  File "<pyshell#31>", line 13, in recurse
    return recurse(_i)
  File "<pyshell#31>", line 13, in recurse
    return recurse(_i)
  File "<pyshell#31>", line 12, in recurse
    total += PRICE_RANGES[key][0]
UnboundLocalError: local variable 'total' referenced before assignment
>>> 

comme vous le voyez, total est dans la portée locale de la fonction principale, mais ce n'est pas dans la portée locale de recurse (évidemment) mais ni dans la portée globale, car il est défini uniquement dans la portée locale de get_order_total

0
Ant