178 votes

Fonctions imbriquées en Python : délimitation des variables

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

Je pense qu'il me manque quelque chose à propos de la portée des variables python.

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 se situe au niveau de la _total mais je ne comprends pas pourquoi. Ne devrait-on pas recurse() ont accès aux variables de la fonction mère ?

Quelqu'un peut-il m'expliquer ce que j'ai manqué à propos de la portée des variables en python ?

358voto

Michael Hoffman Points 8557

Dans Python 3, vous pouvez utiliser l'option nonlocal déclaration pour accéder à des champs d'application non locaux et non globaux.

Les nonlocal permet de lier une définition de variable à une variable précédemment créée dans la portée la plus proche. Voici quelques exemples pour illustrer ce phénomène :

def sum_list_items(_list):
    total = 0

    def do_the_sum(_list):
        for i in _list:
            total += i

    do_the_sum(_list)

    return total

sum_list_items([1, 2, 3])

L'exemple ci-dessus échouera avec l'erreur : UnboundLocalError: local variable 'total' referenced before assignment

Utilisation nonlocal nous pouvons faire fonctionner le code :

def sum_list_items(_list):
    total = 0

    def do_the_sum(_list):

        # Define the total variable as non-local, causing it to bind
        # to the nearest non-global variable also called total.
        nonlocal total

        for i in _list:
            total += i

    do_the_sum(_list)

    return total

sum_list_items([1, 2, 3])

Mais que signifie "le plus proche" ? Voici un autre exemple :

def sum_list_items(_list):

    total = 0

    def do_the_sum(_list):

        # The nonlocal total binds to this variable.
        total = 0

        def do_core_computations(_list):

            # Define the total variable as non-local, causing it to bind
            # to the nearest non-global variable also called total.
            nonlocal total

            for i in _list:
                total += i

        do_core_computations(_list)

    do_the_sum(_list)

    return total

sum_list_items([1, 2, 3])

Dans l'exemple ci-dessus, total se liera à la variable définie à l'intérieur de l'élément do_the_sum et non la variable externe définie dans la fonction sum_list_items le code renverra donc 0 . Notez qu'il est toujours possible d'effectuer une double imbrication comme suit : if total est déclarée nonlocal en do_the_sum l'exemple ci-dessus fonctionnerait comme prévu.

def sum_list_items(_list):

    # The nonlocal total binds to this variable.
    total = 0

    def do_the_sum(_list):

        def do_core_computations(_list):

            # Define the total variable as non-local, causing it to bind
            # to the nearest non-global variable also called total.
            nonlocal total

            for i in _list:
                total += i

        do_core_computations(_list)

    do_the_sum(_list)

    return total

sum_list_items([1, 2, 3])

Dans l'exemple ci-dessus, l'assignation non locale remonte deux niveaux avant de localiser l'élément total qui est locale à sum_list_items .

193voto

moos Points 578

Voici une illustration qui illustre l'essence 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 produit 0 1 Le résultat est à la hauteur des espérances.

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

UnboundLocalError: local variable 'b' referenced before assignment

Il semble mystérieux que la présence de b = 4 pourrait, d'une manière ou d'une autre, rendre b disparaît sur les lignes qui la précèdent. Mais le texte cité par David explique pourquoi : lors de l'analyse statique, l'interprète détermine que b est assigné à 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 intérieure avant qu'elle n'ait été attribuée.

73voto

Dave Webb Points 90034

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 champs d'application et les espaces de noms dit ceci :

Une particularité de Python est que - si aucun global est en vigueur - les affectations aux noms vont toujours dans le champ d'application le plus proche . Les affectations ne copient pas les données, elles lient simplement des noms à des objets.

Ainsi, puisque la ligne dit en fait :

_total = _total + PRICE_RANGES[key][0]

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

34voto

Hans Points 11

Plutôt que de déclarer un objet spécial, une carte ou un tableau, on peut aussi utiliser un attribut de fonction. Le champ d'application de la variable est alors très clair.

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

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

Bien entendu, cet attribut appartient à la fonction (définition), et non à l'appel de fonction. Il faut donc faire attention aux fils d'exécution et à la récursivité.

17voto

CliffordVienna Points 1187

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 affreux hack ou une technique de codage correcte dans la communauté python, mais cela fonctionne 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é de cette solution au niveau de l'exécution.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X