Est-il possible de modifier une variable en python qui est dans une portée externe, mais pas globale?

109

Compte tenu du code suivant:

def A() :
    b = 1

    def B() :
        # I can access 'b' from here.
        print( b )
        # But can i modify 'b' here? 'global' and assignment will not work.

    B()
A()

Pour le code dans la B()variable de fonction best dans la portée externe, mais pas dans la portée globale. Est-il possible de modifier la bvariable à partir de la B()fonction? Je peux sûrement le lire d'ici et print(), mais comment le modifier?

grigoryvp
la source
Désolé, bien sûr 2.7 :). Pour python 3, les règles de portée ont changé.
grigoryvp
Vous pouvez aussi longtemps qu'il best mutable. Une affectation à bmasquera la portée extérieure.
JimB
4
C'est l'un des embarras de Python qui nonlocaln'a pas été rétroporté à 2.x. C'est une partie intrinsèque du support de fermeture.
Glenn Maynard

Réponses:

99

Python 3.x a le nonlocalmot - clé . Je pense que cela fait ce que vous voulez, mais je ne sais pas si vous utilisez python 2 ou 3.

L'instruction non locale fait que les identificateurs répertoriés font référence à des variables précédemment liées dans la portée englobante la plus proche. Ceci est important car le comportement par défaut pour la liaison consiste à rechercher d'abord l'espace de noms local. L'instruction permet au code encapsulé de relier des variables en dehors de la portée locale en plus de la portée globale (module).

Pour python 2, j'utilise généralement simplement un objet mutable (comme une liste ou un dict), et je mute la valeur au lieu de réaffecter.

exemple:

def foo():
    a = []
    def bar():
        a.append(1)
    bar()
    bar()
    print a

foo()

Les sorties:

[1, 1]
Adam Wagner
la source
16
Une bonne façon de faire est class nonlocal: passdans la portée externe. Ensuite, nonlocal.xpeut être affecté dans la portée interne.
kindall
1
Jusqu'à présent, j'ai déjà deux astuces python simples, mais très utiles: la vôtre est la seconde :) Merci @kindall!
swdev
@kindall c'est un excellent hack - il est légèrement différent de la syntaxe Python 3 et beaucoup plus lisible que de passer autour d'un objet mutable.
dimo414
2
@kindall très soigné grâce à des tas :) probablement besoin d'un nom différent car il rompt la compatibilité. En python 3, il s'agit d'un conflit de mots clés et provoquera un SyntaxError. Peut NonLocal- être ?
Adam Terrey
ou, puisque c'est techniquement une classe Nonlocal,? :-)
kindall
19

Vous pouvez utiliser une classe vide pour contenir une portée temporaire. C'est comme le mutable mais un peu plus joli.

def outer_fn():
   class FnScope:
     b = 5
     c = 6
   def inner_fn():
      FnScope.b += 1
      FnScope.c += FnScope.b
   inner_fn()
   inner_fn()
   inner_fn()

Cela donne la sortie interactive suivante:

>>> outer_fn()
8 27
>>> fs = FnScope()
NameError: name 'FnScope' is not defined
Chrisk
la source
Il est étrange que la classe avec ses champs soit "visible" dans une fonction interne mais les variables ne le sont pas, sauf si vous définissez une variable externe avec le mot clé "nonlocal".
Celdor
12

Je suis un peu nouveau dans Python, mais j'ai lu un peu à ce sujet. Je pense que le mieux que vous obtiendrez est similaire à la solution de contournement Java, qui consiste à envelopper votre variable externe dans une liste.

def A():
   b = [1]
   def B():
      b[0] = 2
   B()
   print(b[0])

# The output is '2'

Edit: Je suppose que c'était probablement vrai avant Python 3. On dirait que nonlocalc'est votre réponse.

Mike Edwards
la source
4

Non, vous ne pouvez pas, du moins de cette manière.

Parce que l '"opération set" créera un nouveau nom dans la portée actuelle, qui couvre le nom extérieur.

zchenah
la source
"qui couvre l'extérieur" Que voulez-vous dire? La définition d'un objet avec le nom b dans une fonction imbriquée n'a aucune influence sur un objet avec le même nom dans l'espace extérieur de cette fonction
eyquem
1
@eyquem c'est-à-dire, où que se trouve l'instruction d'affectation, il introduira le nom dans toute la portée actuelle. Comme l'exemple de code de la question, s'il est: def C (): print (b) b = 2 le "b = 2" introduira le nom b dans toute la portée de la fonction C, donc quand print (b), il sera essayez d'obtenir b dans la portée de la fonction C locale mais pas dans la portée externe, le b local n'a pas encore été initialisé, il y aura donc une erreur.
zchenah
1

Pour quiconque regarde cela beaucoup plus tard, une solution de contournement plus sûre mais plus lourde est. Sans avoir besoin de passer des variables comme paramètres.

def outer():
    a = [1]
    def inner(a=a):
        a[0] += 1
    inner()
    return a[0]
Michael Giba
la source
1

La réponse courte qui fonctionnera automatiquement

J'ai créé une bibliothèque python pour résoudre ce problème spécifique. Il est publié sous la dissidence, alors utilisez-le comme vous le souhaitez. Vous pouvez l'installer avec pip install seapieou consulter la page d'accueil ici https://github.com/hirsimaki-markus/SEAPIE

user@pc:home$ pip install seapie

from seapie import Seapie as seapie
def A():
    b = 1

    def B():
        seapie(1, "b=2")
        print(b)

    B()
A()

les sorties

2

les arguments ont la signification suivante:

  • Le premier argument est la portée de l'exécution. 0 signifierait local B(), 1 signifie parent A()et 2 signifierait grand-parent <module>aka global
  • Le deuxième argument est une chaîne ou un objet de code que vous souhaitez exécuter dans la portée donnée
  • Vous pouvez également l'appeler sans arguments pour le shell interactif dans votre programme

La longue réponse

C'est plus compliqué. Seapie fonctionne en éditant les cadres dans la pile d'appels à l'aide de l'API CPython. CPython est la norme de facto, donc la plupart des gens n'ont pas à s'en soucier.

Les mots magiques qui vous intéressent probablement le plus si vous lisez ceci sont les suivants:

frame = sys._getframe(1)          # 1 stands for previous frame
parent_locals = frame.f_locals    # true dictionary of parent locals
parent_globals = frame.f_globals  # true dictionary of parent globals

exec(codeblock, parent_globals, parent_locals)

ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(frame),ctypes.c_int(1))
# the magic value 1 stands for ability to introduce new variables. 0 for update-only

Ce dernier forcera les mises à jour à passer dans la portée locale. Les portées locales sont cependant optimisées différemment de la portée globale, de sorte que l'introduction de nouveaux objets pose quelques problèmes lorsque vous essayez de les appeler directement s'ils ne sont initialisés d'aucune façon. Je vais copier quelques moyens de contourner ces problèmes à partir de la page github

  • Assingn, importer et définir au préalable vos objets
  • Assigner au préalable un espace réservé à vos objets
  • Réaffecter l'objet à lui-même dans le programme principal pour mettre à jour la table des symboles: x = locals () ["x"]
  • Utilisez exec () dans le programme principal au lieu d'appeler directement pour éviter l'optimisation. Au lieu d'appeler x do: exec ("x")

Si vous pensez que l'utilisation exec()n'est pas quelque chose que vous voulez utiliser, vous pouvez émuler le comportement en mettant à jour le vrai dictionnaire local (et non celui renvoyé par les locaux ()). Je vais copier un exemple de https://faster-cpython.readthedocs.io/mutable.html

import sys
import ctypes

def hack():
    # Get the frame object of the caller
    frame = sys._getframe(1)
    frame.f_locals['x'] = "hack!"
    # Force an update of locals array from locals dict
    ctypes.pythonapi.PyFrame_LocalsToFast(ctypes.py_object(frame),
                                          ctypes.c_int(0))

def func():
    x = 1
    hack()
    print(x)

func()

Production:

hack!
Markus Hirsimäki
la source
0

Je ne pense pas que tu devrais vouloir faire ça. Les fonctions qui peuvent modifier les choses dans leur contexte englobant sont dangereuses, car ce contexte peut être écrit à l'insu de la fonction.

Vous pouvez le rendre explicite, soit en faisant de B une méthode publique et C une méthode privée dans une classe (le meilleur moyen probablement); ou en utilisant un type mutable tel qu'une liste et en le passant explicitement à C:

def A():
    x = [0]
    def B(var): 
        var[0] = 1
    B(x)
    print x

A()
Sideshow Bob
la source
2
Comment pouvez-vous écrire une fonction sans connaître les fonctions imbriquées à l'intérieur? Les fonctions imbriquées et les fermetures font partie intégrante de la fonction dans laquelle elles sont enfermées.
Glenn Maynard
Vous devez connaître l'interface des fonctions incluses dans la vôtre, mais vous ne devriez pas avoir à savoir ce qui se passe à l'intérieur. De plus, on ne peut pas s'attendre à ce que vous sachiez ce qui se passe dans les fonctions qu'ils appellent, etc. Si une fonction modifie un membre non global ou non de classe, elle devrait généralement le rendre explicite via son interface, c'est-à-dire le prendre comme paramètre.
Sideshow Bob
Python ne vous oblige pas à être aussi bon bien sûr, d'où le nonlocalmot-clé - mais c'est à vous de l'utiliser avec beaucoup de prudence.
Sideshow Bob
5
@Bob: Je n'ai jamais trouvé que l'utilisation de fermetures comme celle-ci était dangereuse du tout, sauf à cause de bizarreries linguistiques. Considérez les locaux comme une classe temporaire et les fonctions locales comme des méthodes sur la classe, et ce n'est pas plus compliqué que cela. YMMV, je suppose.
Glenn Maynard
0

Vous pouvez, mais vous devrez utiliser la déclaration globale (ce n'est pas une très bonne solution comme toujours lorsque vous utilisez des variables globales, mais cela fonctionne):

def A():
    global b
    b = 1

    def B():
      global b
      print( b )
      b = 2

    B()
A()
Cédric Julien
la source
Voir ma réponse expliquant l'inconvénient potentiel de cette solution
eyquem
4
L'utilisation d'une variable globale est complètement différente.
Glenn Maynard
0

Je ne sais pas s'il y a un attribut d'une fonction qui donne le __dict__de l'espace extra-atmosphérique de la fonction quand cet espace extérieur n'est pas l'espace global == le module, ce qui est le cas lorsque la fonction est une fonction imbriquée, en Python 3.

Mais dans Python 2, pour autant que je sache, il n'y a pas un tel attribut.

Donc, les seules possibilités pour faire ce que vous voulez sont:

1) utiliser un objet mutable, comme l'ont dit d'autres

2)

def A() :
    b = 1
    print 'b before B() ==', b

    def B() :
        b = 10
        print 'b ==', b
        return b

    b = B()
    print 'b after B() ==', b

A()

résultat

b before B() == 1
b == 10
b after B() == 10

.

Nota

La solution de Cédric Julien présente un inconvénient:

def A() :
    global b # N1
    b = 1
    print '   b in function B before executing C() :', b

    def B() :
        global b # N2
        print '     b in function B before assigning b = 2 :', b
        b = 2
        print '     b in function B after  assigning b = 2 :', b

    B()
    print '   b in function A , after execution of B()', b

b = 450
print 'global b , before execution of A() :', b
A()
print 'global b , after execution of A() :', b

résultat

global b , before execution of A() : 450
   b in function B before executing B() : 1
     b in function B before assigning b = 2 : 1
     b in function B after  assigning b = 2 : 2
   b in function A , after execution of B() 2
global b , after execution of A() : 2

Le b global après exécution deA() a été modifié et il peut ne pas être souhaité ainsi

C'est le cas uniquement s'il y a un objet avec l'identifiant b dans l'espace de noms global

eyquem
la source