Czy można modyfikować zmienną w Pythonie, która ma zasięg zewnętrzny, ale nie globalny?

109

Biorąc pod uwagę następujący kod:

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()

Ponieważ kod w B()zmiennej funkcji bznajduje się w zakresie zewnętrznym, ale nie w zasięgu globalnym. Czy można modyfikować bzmienną z poziomu B()funkcji? Z pewnością mogę to przeczytać stąd i print(), ale jak to zmodyfikować?

grigoryvp
źródło
Przepraszam, oczywiście 2.7 :). W przypadku Pythona 3 zasady określania zakresu uległy zmianie.
grigoryvp
Możesz, o ile bjest zmienna. Przypisanie do bspowoduje zamaskowanie zakresu zewnętrznego.
JimB,
4
To jedno z kłopotów Pythona, które nonlocalnie zostało przeniesione do wersji 2.x. To nieodłączna część wsparcia zamknięcia.
Glenn Maynard,

Odpowiedzi:

99

Python 3.x ma nonlocalsłowo kluczowe . Myślę, że robi to, co chcesz, ale nie jestem pewien, czy używasz Pythona 2 czy 3.

Instrukcja nielokalna powoduje, że wymienione identyfikatory odwołują się do poprzednio powiązanych zmiennych w najbliższym obejmującym zakresie. Jest to ważne, ponieważ domyślne zachowanie wiązania polega na przeszukaniu najpierw lokalnej przestrzeni nazw. Instrukcja umożliwia hermetyzowanemu kodowi ponowne wiązanie zmiennych poza zakresem lokalnym poza zakresem globalnym (modułu).

W przypadku Pythona 2 zwykle używam po prostu zmiennego obiektu (takiego jak lista lub dict) i mutuję wartość zamiast ponownie przypisywać.

przykład:

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

foo()

Wyjścia:

[1, 1]
Adam Wagner
źródło
16
Dobrym sposobem na zrobienie tego jest class nonlocal: passzewnętrzny zakres. Następnie nonlocal.xmożna przypisać do w zakresie wewnętrznym.
kindall
1
Do tej pory mam już dwie proste, ale bardzo pomocne wskazówki dotyczące Pythona: Twoja jest druga :) Dzięki @kindall!
swdev
@kindall to świetny hack - minimalnie różni się od składni Pythona 3 i znacznie bardziej czytelny niż przekazywanie zmiennego obiektu.
dimo414
2
@kindall bardzo schludne, dzięki sterty :) prawdopodobnie potrzebuję jednak innej nazwy, ponieważ łamie to kompatybilność z przodu. W Pythonie 3 jest to konflikt słów kluczowych i spowoduje SyntaxError. Może NonLocal?
Adam Terrey
czy, ponieważ technicznie rzecz biorąc jest to klasa Nonlocal,? :-)
kindall
19

Możesz użyć pustej klasy do przechowywania tymczasowego zakresu. To jest jak zmienne, ale trochę ładniejsze.

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()

Daje to następujące interaktywne wyjście:

>>> outer_fn()
8 27
>>> fs = FnScope()
NameError: name 'FnScope' is not defined
chrisk
źródło
To dziwne, że klasa ze swoimi polami jest „widoczna” w funkcji wewnętrznej, a zmienne nie, chyba że zdefiniujesz zmienną zewnętrzną za pomocą słowa kluczowego „nonlocal”.
Celdor
12

Jestem trochę nowy w Pythonie, ale trochę o tym czytałem. Uważam, że najlepsze, co uzyskasz, jest podobne do obejścia w Javie, które polega na zawinięciu zewnętrznej zmiennej w listę.

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

# The output is '2'

Edycja: Wydaje mi się, że to prawdopodobnie prawda przed Pythonem 3. Wygląda na nonlocalto, że to twoja odpowiedź.

Mike Edwards
źródło
4

Nie, nie możesz, przynajmniej w ten sposób.

Ponieważ „operacja set” utworzy nową nazwę w obecnym zakresie, który obejmuje zakres zewnętrzny.

zchenah
źródło
„która pokrywa zewnętrzną” Co masz na myśli? Zdefiniowanie obiektu o nazwie b w funkcji zagnieżdżonej nie ma wpływu na obiekt o tej samej nazwie w przestrzeni zewnętrznej tej funkcji
eyquem
1
@eyquem to znaczy, gdziekolwiek jest instrukcja przypisania, wprowadzi nazwę w całym bieżącym zakresie. Tak jak przykładowy kod pytania, jeśli jest to: def C (): print (b) b = 2, "b = 2" wprowadzi nazwę b do całego zakresu funkcji C, więc gdy print (b), będzie spróbuj uzyskać b w lokalnym zakresie funkcji C, ale nie zewnętrznym, lokalne b nie zostało jeszcze zainicjowane, więc wystąpi błąd.
zchenah
1

Dla każdego, kto patrzy na to znacznie później, jest bezpieczniejsze, ale cięższe obejście. Bez potrzeby przekazywania zmiennych jako parametrów.

def outer():
    a = [1]
    def inner(a=a):
        a[0] += 1
    inner()
    return a[0]
Michael Giba
źródło
1

Krótka odpowiedź, która po prostu zadziała automagicznie

Stworzyłem bibliotekę Pythona do rozwiązania tego konkretnego problemu. Jest uwalniany pod wpływem nielubienia, więc używaj go, jak chcesz. Możesz go zainstalować za pomocą pip install seapielub sprawdzić stronę główną tutaj 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()

wyjścia

2

argumenty mają następujące znaczenie:

  • Pierwszy argument to zakres wykonania. 0 oznaczałoby lokalny B(), 1 oznacza rodzica, A()a 2 oznaczałoby dziadka <module>znanego jako globalny
  • Drugi argument to łańcuch lub obiekt kodu, który chcesz wykonać w danym zakresie
  • Możesz również wywołać ją bez argumentów dla powłoki interaktywnej w swoim programie

Długa odpowiedź

To jest bardziej skomplikowane. Seapie działa poprzez edycję ramek w stosie wywołań przy użyciu interfejsu API CPython. CPython jest de facto standardem, więc większość ludzi nie musi się tym martwić.

Magiczne słowa, które prawdopodobnie najprawdopodobniej Cię interesują, jeśli to czytasz, to:

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

Ten ostatni wymusi przejście aktualizacji do zakresu lokalnego. Zasięg lokalny jest jednak optymalizowany inaczej niż zasięg globalny, więc wprowadzenie nowych obiektów powoduje pewne problemy przy próbie wywołania ich bezpośrednio, jeśli nie zostały one w żaden sposób zainicjowane. Skopiuję kilka sposobów obejścia tych problemów ze strony github

  • Wcześniej przypisz, zaimportuj i zdefiniuj swoje obiekty
  • Wcześniej przypisuj obiekt zastępczy do obiektów
  • Ponownie przypisz obiekt do siebie w programie głównym, aby zaktualizować tablicę symboli: x = locals () ["x"]
  • Użyj funkcji exec () w programie głównym zamiast bezpośredniego wywoływania, aby uniknąć optymalizacji. Zamiast wywoływać x wykonaj: exec ("x")

Jeśli czujesz, że używanie exec()nie jest czymś, z czym chciałbyś iść, możesz emulować zachowanie, aktualizując prawdziwy słownik lokalny (a nie ten zwracany przez locals ()). Skopiuję przykład z 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()

Wynik:

hack!
Markus Hirsimäki
źródło
0

Myślę, że nie powinieneś tego chcieć. Funkcje, które mogą zmieniać rzeczy w otaczającym ich kontekście, są niebezpieczne, ponieważ ten kontekst można zapisać bez znajomości funkcji.

Możesz to uczynić jawnym, albo przez uczynienie B metodą publiczną, a C metodą prywatną w klasie (prawdopodobnie najlepszy sposób); lub używając zmiennego typu, takiego jak lista, i przekazując go jawnie do C:

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

A()
Sideshow Bob
źródło
2
Jak napisać funkcję, nie wiedząc o zagnieżdżonych w niej funkcjach? Zagnieżdżone funkcje i domknięcia są nieodłączną częścią funkcji, w której są zawarte.
Glenn Maynard,
Musisz wiedzieć o interfejsie funkcji zawartych w twoim, ale nie powinieneś wiedzieć, co się w nich dzieje. Ponadto nie można oczekiwać, że będziesz wiedzieć, co dzieje się w wywoływanych przez nich funkcjach itp.! Jeśli funkcja modyfikuje element nieglobalny lub niebędący członkiem klasy, powinna to zwykle wyraźnie zaznaczyć za pośrednictwem swojego interfejsu, tj. Przyjąć jako parametr.
Sideshow Bob,
Python oczywiście nie zmusza cię do bycia tak dobrym, stąd nonlocalsłowo kluczowe - ale to od Ciebie zależy, czy będziesz go używać z wielką ostrożnością.
Sideshow Bob,
5
@Bob: Nigdy nie uważałem, że używanie takich zamknięć jest w ogóle niebezpieczne, poza dziwactwami językowymi. Pomyśl o lokalnych jako o tymczasowej klasie, a lokalne funkcje jako o metodach w klasie i nie jest to bardziej skomplikowane. Chyba YMMV.
Glenn Maynard,
0

Możesz, ale będziesz musiał użyć statystyki globalnej (niezbyt dobre rozwiązanie, jak zawsze, gdy używasz zmiennych globalnych, ale działa):

def A():
    global b
    b = 1

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

    B()
A()
Cédric Julien
źródło
Zobacz moją odpowiedź wyjaśniającą potencjalną wadę tego rozwiązania
eyquem
4
Używanie zmiennej globalnej jest zupełnie inne.
Glenn Maynard,
0

Nie wiem, czy istnieje atrybut funkcji, który daje __dict__przestrzeń zewnętrzną funkcji, gdy ta przestrzeń zewnętrzna nie jest przestrzenią globalną == moduł, co ma miejsce, gdy funkcja jest funkcją zagnieżdżoną, w Pythonie 3.

Ale w Pythonie 2, o ile wiem, nie ma takiego atrybutu.

Więc jedyne możliwości robienia tego, co chcesz, to:

1) używanie zmiennego obiektu, jak mówią inni

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()

wynik

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

.

Nota

Rozwiązanie Cédrica Juliena ma wadę:

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

wynik

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

Globalne b po wykonaniu A()zostało zmodyfikowane i może być inaczej

Dzieje się tak tylko wtedy, gdy istnieje obiekt o identyfikatorze b w globalnej przestrzeni nazw

eyquem
źródło