web-dev-qa-db-fra.com

Fermeture Python: Écriture dans une variable de la portée parent

J'ai le code suivant dans une fonction:

stored_blocks = {}
def replace_blocks(m):
    block = m.group(0)
    block_hash = sha1(block)
    stored_blocks[block_hash] = block
    return '{{{%s}}}' % block_hash

num_converted = 0
def convert_variables(m):
    name = m.group(1)
    num_converted += 1
    return '<%%= %s %%>' % name

fixed = MATCH_DECLARE_NEW.sub('', template)
fixed = MATCH_PYTHON_BLOCK.sub(replace_blocks, fixed)
fixed = MATCH_FORMAT.sub(convert_variables, fixed)

Ajouter des éléments à stored_blocks fonctionne bien, mais je ne peux pas augmenter num_converted dans la deuxième sous-fonction:

UnboundLocalError: variable locale 'num_converted' référencée avant l'affectation

Je pourrais utiliser global mais les variables globales sont laides et je n’ai vraiment pas besoin que cette variable soit globale.

Je suis donc curieux de savoir comment écrire dans une variable de la portée de la fonction parent .nonlocal num_converted ferait probablement l'affaire, mais j'ai besoin d'une solution compatible avec Python 2.x.

73
ThiefMaster

Problème: C'est parce que les règles de portée de Python sont démentes. La présence de l'opérateur d'affectation += marque la cible, num_converted, comme étant locale à la portée de la fonction englobante, et Python 2.x ne permet pas d'accéder de manière rationnelle à un seul niveau de portée. Seul le mot clé global peut extraire les références de variable de la portée actuelle et vous emmène directement au sommet.

Correction: Transformez num_converted en un tableau à un seul élément.

num_converted = [0]
def convert_variables(m):
    name = m.group(1)
    num_converted[0] += 1
    return '<%%= %s %%>' % name
83
Marcelo Cantos

(voir ci-dessous pour la réponse modifiée)

Vous pouvez utiliser quelque chose comme:

def convert_variables(m):
    name = m.group(1)
    convert_variables.num_converted += 1
    return '<%%= %s %%>' % name

convert_variables.num_converted = 0

De cette façon, num_converted fonctionne comme une variable "statique" de type C de la méthode convert_variable 


(édité)

def convert_variables(m):
    name = m.group(1)
    convert_variables.num_converted = convert_variables.__dict__.get("num_converted", 0) + 1
    return '<%%= %s %%>' % name

De cette façon, vous n'avez pas besoin d'initialiser le compteur dans la procédure principale.

28
PabloG

L'utilisation du mot clé global convient. Si vous écrivez:

num_converted = 0
def convert_variables(m):
    global num_converted
    name = m.group(1)
    num_converted += 1
    return '<%%= %s %%>' % name

... num_converted ne devient pas une "variable globale" (c’est-à-dire qu’elle ne devient pas visible dans d’autres endroits inattendus), cela signifie simplement qu’elle peut être modifiée dans convert_variables. Cela semble être exactement ce que vous voulez.

En d'autres termes, num_converted est déjà une variable globale. Toute la syntaxe de global num_converted consiste à dire à Python "dans cette fonction, ne créez pas de variable num_converted locale, utilisez plutôt la variable globale existante.

9
Emile

Qu'en est-il de l'utilisation d'une instance de classe pour contenir l'état? Vous instanciez une classe et passez des méthodes d'instance à des sous-marins et ces fonctions auraient une référence à self.

7
Seb

J'ai quelques remarques.

Tout d'abord, une application pour de telles fonctions imbriquées apparaît lorsqu'il s'agit de rappels bruts, tels qu'ils sont utilisés dans des bibliothèques comme xml.parsers.expat. (Le fait que les auteurs de la bibliothèque aient choisi cette approche est peut-être choquant, mais ... il y a néanmoins des raisons de l'utiliser.)

Deuxièmement: au sein d'une classe, il existe des alternatives beaucoup plus agréables au tableau (num_converted [0]). Je suppose que c'est ce dont Sebastjan parlait.

class MainClass:
    _num_converted = 0
    def outer_method( self ):
        def convert_variables(m):
            name = m.group(1)
            self._num_converted += 1
            return '<%%= %s %%>' % name

C'est quand même assez étrange pour mériter un commentaire dans le code ... mais la variable est au moins locale à la classe.

6
Steve White

Modifié de: https://stackoverflow.com/a/40690954/819544

Vous pouvez utiliser le module inspect pour accéder au dict de règles globales de la portée de l'appel et y écrire. Cela signifie que cette astuce peut même être utilisée pour accéder à la portée de l'appel à partir d'une fonction imbriquée définie dans un sous-module importé.

import inspect 

def get_globals(scope_level=0):
    return dict(inspect.getmembers(inspect.stack()[scope_level][0]))["f_globals"]

num_converted = 0
def foobar():
    get_globals(0)['num_converted'] += 1

foobar()
print(num_converted) 
# 1

Jouez avec l'argument scope_level si nécessaire. Définir scope_level=1 fonctionne pour une fonction définie dans un sous-module, scope_level=2 pour la fonction interne définie dans un décorateur dans un sous-module, etc.

NB: Le fait que vous puissiez faire cela ne signifie pas que vous devriez le faire. 

0
David Marx