Jest to szczegółowy, długi wpis dotyczący zasięgów, nazw i zmiennych w Pythonie. Przedstawię w nim definicję nazwy, zmiennej oraz oczywiście wytłumaczę, jak działają zasięgi w Pythonie.
Czym jest nazwa, a czym zmienna?
Nazwa w Pythonie to nazwa zmiennej, czyli wartości. Przykładowo, w przypisaniu x = 10, x jest nazwą, z kolei 10 to wartość. Nazwa zmiennej musi zaczynać się od litery lub znaku podkreślenia, potem może zawierać również cyfry. Nazwa nie może zawierać spacji. Python odróżnia duże litery od małych, dlatego Variable i variable to nazwy dwóch różnych zmiennych.
Co się dzieje, gdy tworzymy zmienną w Pythonie?
Gdy używamy jakiejś nazwy, czyli zmiennej, Python tworzy, wyszukuje lub zmienia ją w tak zwanej przestrzeni nazw. Jest to miejsce, w którym znajdują się wszystkie nazwy przypisane w danej jednostce, czyli na przykład w module lub funkcji. Miejsce przypisania zmiennej do nazwy (przykład przypisania: x=10), jest zatem tym, co wiąże ją z daną przestrzenią nazwy. Przestrzeń nazwa jest zasięgiem jej widoczności – wszystkie nazwy przypisane na przykład wewnątrz funkcji są związane tylko z przestrzenią nazwy tej funkcji. Z tego względu do nazw przypisanych wewnątrz funkcji możemy się odnosić tylko w ciele funkcji – nie są one w żaden sposób widoczne dla kodu zewnętrznego. Zaletą tego rozwiązania jest to, że zmienne użyte w funkcji nie wchodzą w konflikt z innymi zmiennymi.
Zmienne można przypisać na 3 różnych poziomach, które odpowiadają 3 różnym zasięgom:
- wewnątrz instrukcji def (funkcji), czyli w zakresie lokalnym,
- wewnątrz funkcji def1 zawierającej inną funkcje def2 – wówczas zmienna staje się nielokalna dla funkcji def2,
- poza jakąkolwiek funkcją, wówczas zmienna znajduje się w zasięgu globalnym, czyli w zasięgu modułu.
W poniższym przykładzie przepisanie x tworzy zmienną globalną widoczną w obrębie całego pliku, ale już przypisanie tej samej nazwy w obrębie funkcji tworzy zmienną lokalną. Pomimo, że nazywają się one tak samo, to nie wchodzą ze sobą w konflikt właśnie ze względu na różne zasięgi.
>>> x = 99
>>> def print_x():
... x = 88
... print(x)
...
>>> x
99
>>> print_x()
88

Czym są zmienne globalne?
Tworząc zmienne globalne, tworzymy tak naprawdę zmienne przypisane do najwyższego poziomu, czyli poziomu modułu. Warto wiedzieć, że sesja interaktywna jest tak naprawdę modułem o nazwie __main__. Można zatem powiedzieć, że funkcje definiują zasięgi lokalne, a moduły definiują zasięg globalny. Każdy moduł jest zasięgiem globalnym, czyli przestrzenią nazw, w której zapisane są wszystkie zmienne przypisane na najwyższym poziomie pliku modułu. Te zmienne są również atrybutami obiektu modułu dla kodu z zewnątrz. Zasięg globalny obejmuje jeden plik.
Wszystkie zmienne zadeklarowane wewnątrz funkcji są domyślnie lokalne, choć można je zadeklarować jako globalne (niemal zawsze) lub nielokalne (jeśli istnieją już w funkcji zawierającej). Wywołanie funkcji to stworzenie nowego zasięgu lokalnego. Każde aktywne wywołanie otrzymuje własną kopię lokalnych zmiennych funkcji. Oznacza to, że przy każdym wywołaniu funkcji tworzony jest nowy zasięg lokalny.
Czym są zmienne lokalne?
Zmienna lokalna to nie tylko zmienna przypisana wewnątrz funkcji, ale również zmienna modułów instrukcji import oraz zmienna przekazana jako argument. Warto również zdawać sobie sprawę z tego, że modyfikacja obiektów miejscu nie zmienia zasięgu zmiennej, a zatem na przykład dodanie elementu do listy za pomocą instrukcji append nie wpłynie na jej zasięg.
Regula LEGB
W skrócie zasięgi oraz przypisywanie zmiennych sprowadza się do 3 głównych kwestii:
- zasięg zależy od miejsca przepisania nazwy, nie zależy od np. modyfikacji zmiennej przypisanej do nazwy,
- referencje do nazw przeszukują cztery zasięgi w następującej kolejności: lokalny, nielokalny – zasięg funkcji zawierających, globalny i na końcu wbudowany, czyli w skrócie LEGB – local, enclosing, global and built-in. Reguła ta nie ma zastosowania, jeżeli użyjemy kwalifikatora, czyli na przykład object.attribute – wówczas poszukiwany jest obiekt, a nie zasięg. Python zwróci wartość z pierwszego zasięgu, w którym znaleziona zostanie nazwa.
- Funkcje mogą wykorzystywać zmienne zapisane w funkcjach je zawierających, np. wykorzystywać je do obliczeń, a także nazwy z zasięgu globalnego, ale aby je zmodyfikować, muszą zadeklarować zmienne jako nielokalne oraz globalne.
Zmienne składane oraz zmienne wyjątków
- zmienne w wyrażeniach składanych, takich jak na przykład składana lista lub słownik, są lokalne samego wyrażenia (nie dotyczy to jednak pętli for),
- zmienne wyjątków, używany na przykład do przechwytywania zgłoszonego wyjątku, takie jak except E as X, są lokalne dla bloku except i usuwane po zakończeniu działania bloku.
Przykład działania zasiegów – globalny vs lokalny
>>> X = 99
>>> def func(Y):
... Z = X + Y
... return Z
>>> func(1)
100
Nazwy globalne: X i func
Moduł i funkcja wykorzystują kilka zmiennych. Globalnymi zmiennymi są tutaj X oraz funkcja func – obie przypisane są na najwyższym poziomie pliku modułu. Jeśli chcemy zmienić jej wartość na poziomie lokalnym za pomocą funkcji, wówczas musimy zadeklarować ją za pomocą wyrażenia global.
Nazwy lokalne: Y, Z
Zmienne Y oraz Z są lokalne, bo istnieją jedynie w funkcji func i do tego istnieją tylko w czasie jej działania. Do obu przypisaliśmy wartości, do Z za pomocą instrukcji =, a do Y poprzez przekazanie jej jako argumentu.
Można powiedzieć, że zmienne lokalne są tymczasowymi nazwami, istniejącymi jedynie w czasie działania funkcji, a w rzeczywistości po zakończeniu jej działania są one usuwane z pamięci. Dzięki podziałowi zmiennych na lokalnej globalne łatwiej jest je i dodać, przykładowo wiemy, że zmienna zadeklarowana w funkcji znajduje się gdzieś w jej wnętrzu.
Czym jest zasięg wbudowany?
Zasieg wbudowany to nic innego jak wbudowany moduł o nazwie builtins. Żeby z niego korzystać, należy go… zaimportować! Oznacza to, że zasięg wbudowany jest takim jedynie z nazwy. Do jego importu służy poniższa instrukcja:
import builtins
Żeby sprawdzić, jaka jest jego zawartość, należy skorzystać z instrukcji dir.
dir(builtins)
Po uruchomieniu powyższej komendy zobaczysz, że moduł ten zawiera wiele na co dzień wykorzystywanych funkcji, takich jak na przykład zip(), open() czy wbudowanych nazw, w tym True, False czy None.
Zasłanianie nazw – variables shadowing
W Pythonie możemy celowo (lub też nie) zasłaniać wbudowane nazwy. Python pobiera bowiem zawsze pierwszą nazwę znalezioną w zasięgu zgodnie z kolejnością LEGB. Możemy zatem nadpisać zmienne, które już istnieją w tym języku, takie jak na przykład wspomniane wcześniej open, zip czy nazwy zarezerwowane. W wielu przypadkach prowadzi to do niechcianych konsekwencji. W poniższym przykładzie zasłaniana jest wbudowana nazwa open, przez co program nie działa w sposób, jaki bymy oczekiwali.
>>> def hider():
... open = 'abc'
... open('data.txt')
Na tego typu błędy należy w szczególności uważać, jeśli definiujemy zmienną w zakresie globalnym. Lepszym sposobem na uniknięcie tego typu niespodzianek jest po prostu ni zmienianie definicji wbudowanych nazw tego języka. Jeśli to się zdarzy, skorzystaj z instrukcji del nazwa która pozwala usunąć re definicję nazwy z bieżącego zasięgu tym samym przywracając oryginał.
Instrukcja global
Instrukcja global oraz nonlocal to jedyne instrukcje, które przypominają instrukcje deklaracji. Pozwalają one deklarować przestrzeń nazw dla danej zmiennej. Instrukcja global użyta przed nazwą zmiennej pozwala modyfikować zmienną z poziomu na przykład funkcji. Instrukcji global powinniśmy używać tylko wtedy, gdy chcemy zmienić wartość zmiennej globalnej, a nie po prostu odwołać się do niej. Podsumowując:
- zmienne globalne są przypisywane na najwyższym poziomie pliku modułu,
- instrukcja global do zmiennych globalnych wykorzystuje się w funkcji tylko wtedy, gdy chce się zmienić ich wartość na poziomie globalnym,
- odwołanie do zmiennej globalnej wewnątrz funkcji nie wymaga użycia instrukcji global.
Instrukcja global pozwala modyfikować zmienne znajdujące się poza instrukcją def, na najwyższym poziomie, czyli jako część zasięgu modułu.
>>> X = 99
>>> def func():
... global X
... X = 0
...
>>> func()
>>> print(X)
0
Instrukcji global można użyć nie tylko do odniesienia i modyfikacji istniejącej zmiennej, ale również do jej utworzenia. Zmienna nie musi zatem istnieć wcześniej!
def func1():
global y
y = 1
func1()
print(y)
Zmienne globalne – zło konieczne?
Zgodnie z dobrymi praktykami funkcje powinny komunikować się, wykorzysując do tego argumenty. Zamiast modyfikować zmienne globalne, powinny zwracać własne wartości. W Pythonie celowo została wprowadzona instrukcja global – jeśli naprawdę chcesz zmienić zmienną globalną, musisz się postarać, eksplicytnie używając deklaracji global. Komunikacja między funkcjami i instrukcjami powinna opierać się o zmienne lokalne. Gdyby odbywała się poprzez zmienne globalne, trudno byłoby przewidzieć wartość danej zmiennej w momencie, gdy korzystałyby z niej np. 3 różne funkcje. Zmienne globalne są wykorzystywane głównie do wielowątkowości, czyli do równoległego przetwarzania danych.
Od początku przygody z Pythonem należy starać się pisać kod tak, aby komunikacja między częściami kodu odbywała się za pomocą argumentów i zwracanych wartości.
Kolejnym przyzwyczajeniem, którego powinniśmy unikać jest modyfikacja zmiennych z innych modołów. Plik importujący inny plik otrzymuje dostęp do jego zmiennych, czyli do zmiennych globalnych modułu (pliku). Zasięg globalny modułu, gdy zostaje on zaimportowany, staje się automatycznie przestrzenią nazw atrybutów importowanego modułu. Jeśli zmienimy zmienną z jednego modułu w innym module, wówczas zależność pomiędzy oboma modułami stanie się zbyt duża – nie wiemy wówczas nawet, czy możemy użyć jednego pliku, bez użycia drugiego. Do tego osoba korzystająca z jednego modułu może nie wiedzieć, że zmienna jest modyfikowana w innym module. Zmienne najlepiej przekazywać wówczas za pomocą wywoływania funkcji znanej pod nazwą akcesora. Wygląda ona mniej więcej tak:
plik first.py:
X = 99
def setX(new):
global X
X = new
plik second.py:
import first
first.setX(0)
Takie rozwiązanie eliminuje ryzyko wystąpienia niespodzianki, a funkcja akcesora setX jasno informuje użytkownika o tym, że zmienna może zostać gdzieś nadpisana.
Zasięgi a funkcje zagnieżdżone, czyli o zmiennych w funkcjach
Jeżeli wewnątrz funkcji umieścimy referencje do zmiennej, to najpierw jest ona wyszukiwana w zasięgu lokalnym funkcji, a dopiero potem w zasięgu lokalnym wszystkich funkcji zawierających tę funkcję, zaczynając od najbliższej funkcji, a kończąc na tej, która znajduje się na samej górze. Potem przeszukiwany jest zasięg globalny, a na końcu wbudowany – odbywa się to zgodnie z regułą LEGB. Jeżeli jednak zmienna jest zadeklarowana jako globalna przy użyciu deklaracji global, wówczas wyszukiwanie od razu zaczyna się w zasięgu globalnym, czyli zasięgu modułu.
Przypisanie wartości do zmiennej tworzy lub modyfikuje zmienną w bieżącym zasięgu lokalnym. Jeśli jest ona deklarowana jako globalna, wówczas przypisanie modyfikuje zmienną globalną o tej nazwie. Jeśli natomiast zmienna jest zadeklarowana jako nonlocal, to modyfikacji ulega zmienna w zasięgu lokalnym najbliższej funkcji zawierającej.
Jak wygląda zasięg zagnieżdżony?
X = 99
def f1():
X = 0
def f2():
print(X)
f2()
f1()
W tym kodzie zagnieżdżona funkcja f2 jest wykonywana w momencie wykonania funkcji f1. Szuka ona wówczas najbliższej zmiennej o nazwie X. Znajduje ją w funkcji zawierającej, czyli w f2 – wszystko działa zgodnie z zasadą LEGB.
Faktycznie funkcja zawierająca pamięta wartość zmiennych z otaczającej ją funkcji nawet wtedy, gdy otaczająca funkcja nie jest już aktywna. Przykład takiego zachowania znajduje się poniżej:
def f1():
X = 88
def f2():
print(X)
return f2
action = f1()
action()
Funkcje w Pythonie są obiektami takimi jak wszystkie inne, dzięki czemu mogą być zwracane również jako wartości. Tak właśnie dzieje się w powyższym przypadku.
Funkcje fabrykujące i domknięcia
Fakt, że obiekty funkcji pamiętają wartości w otaczających zasięgach, nawet wtedy, gdy te zasięgi już nie istnieją, nazywany jest domknięciem lub funkcją fabrykującą. W rezultacie bowiem do każdej kopii funkcji zagnieżdżonej dołączane są obszary pamięci, które pamiętają informacje o stanie. Są one lokalne dla każdej takiej kopii.
Kiedy przydają się funkcje fabrykujące? Gdy chcemy przechować jakieś informacje pozyskane za pomocą wywołania funkcji na dłużej. Przykładowo:
>>> def maker(N):
... def action(X):
... return X ** N
... return action
...
>>> f = maker(2)
>>> print(f(3))
9
>>> print(f(4))
16
>>> print(f(5))
25
Jak widzimy funkcja f ciągle pamięta wartość z funkcji maker, nawet jeśli ta skończyła swoje działanie. Dzieje się tak z powodu, o którym wspomnieliśmy wcześniej – funkcja zagnieżdżona pamięta zmienne z funkcji zawierającej.
Zasięgi funkcji zawierającej są szczególnie często wykorzystywane w wyrażeniach z funkcjami lambda:
>>> def lambdaMaker(N):
...
... return lambda X: X ** N
...
>>> l = lambdaMaker(2)
>>> print(l(2))
4
>>> print(l(3))
9
Zachowywanie stanu zawierającego za pomocą argumentów domyślnych
Kolejnym sposobem na przekazywanie stanu za pomocą funkcji jest użycie argumentów domyślnych. Jest to możliwe od wersji 3.x Pythona.
def f1():
x = 99
f2(x)
def f2(x):
print(x)
f1()
W tej funkcji odwołanie do f2 jest wykonywane z wyprzedzeniem. Jest to w Pythonie jak najbardziej możliwe. Jak widać funkcja f2 wywoływana z wewnątrz funkcji f1 pamięta zmienną nawet wtedy, gdy zasięg funkcji f1 tak naprawdę przestał istnieć. Zmienna x pełni tutaj funkcję argumentu domyślnego, który jest przekazywany do funkcji f2 jeszcze w definicji funkcji f1.
Zasięgi zagnieżdżone, wartości domyślne i wyrażenia lambda
Zasięgi funkcji zagnieżdżonych pojawiają się jeszcze częściej z wyrażeniami lambda. Są to wyrażenia generujące nową funkcję, która ma być wykorzystana później. Wyrażenia lambda wprowadzają nowy zasięg lokalny dla tworzonej przez siebie funkcji. Warstwa zasięgu zawierającego dane wyrażenie lambda sprawia, że widzi ono wszystkie zmienne w funkcji, w której zostało utworzone:
>>> def func():
... x = 4
... action = (lambda n: x **n)
... return action
...
>>> x = func()
>>> print(x(2))
16
Jak widać wyrażenie lambda pamięta wartość zmiennej istniejącej jedynie w funkcji func(). Jako, że wyrażenia lambda najczęściej zagnieżdża się wewnątrz instrukcji def, to właśnie ono najbardziej zyskało na zasięgach funkcji zawierającej.
Zmienne pętli wymagają wartości domyślnych, a nie zasięgów
Wyjątkiem od omawianych tutaj reguł jest przypadek, gdy funkcja def lub lambda, zdefiniowane wewnątrz funkcji zagnieżdżonej w pętli, zawierają zmienną odnoszącą się do zmiennej z zasięgu zawierającego. Wówczas wyniki jej każdej funkcji wygenerowanej w pętli będą miały tę samą wartość, czyli wartość ostatniej iteracji pętli.
def makeActions(): #funkcja def
acts = []
for i in range(5): #i to zmienna z zakresu zawierającego
acts.append(lambda x: i ** x) #funkcja zagnieżdżona w pętli
return acts
func = makeActions()
print(func[0](2)) #powinno być 0 ** 2
print(func[1](2)) #powinno być 1 ** 2
print(func[2](2)) #powinno być 2 ** 2
Tymczasem wynik każdego z tych wyrażeń to 16. Dzieje się tak dlatego, ponieważ do zmiennej i została przypisana ostatnia wartość zakresu zawierającego, czyli 4.
Żeby kod działał tak jak chcemy, musimy przypisać wartość zasięgu funkcji zawierającej w sposób jawny za pomocą argumentów domyślnych. Wartości domyślne są obliczane przy tworzeniu funkcji zagnieżdżonej, dzięki czemu każda pamięta swoją własną wartość zmiennej i.
>>> def makeActions():
... acts = []
... for i in range(5):
... acts.append(lambda x, i=i: i ** x)
... return acts
...
>>> func= makeActions()
>>> print(func[0](2))
0
>>> print(func[1](2))
1
>>> print(func[2](2))
4
>>> print(func[3](2))
9
Zasięgi mogą być również dowolnie zagnieżdżone – Python będzie wówczas po prostu szukał odpowiedniej referencji aż do znalezienia zmiennej.
>>> def scope():
... x = 99
... def scope2():
... def scope3():
... print(x)
... scope3()
... scope2()
... scope()
...
99
Instrukcja nonlocal
Instruckja nonlocal służy modyfikowaniu zmiennych z zasięgu funkcji zawierającej. Zapewnia ona dostęp do modyfikacji zmiennych w zasięgu zawierającym: funkcja2 zawarta w funkcji1 może zmieniać zmienne zawarte w funkcji1, jeśli tylko zostaną one zadeklarowane jako nonlocal w funkcji2.
W przypadku instrukcji nonlocal zmienna musi istnieć już w momencie deklaracji instrukcji w zasięgu funkcji zawierającej. Zmienne nonlocal muszą zatem istnieć w momencie tworzenia funkcji, a nie jej wywołania.
Przeszukiwanie zasięgów dla zmiennych typu nonlocal ogranicza się tylko do instrukcji def zawierających daną funkcję i całkowicie pomija zasięg lokalny danej funkcji, a także zasięg globalny.
Przeszukiwanie zasięgów ogranicza się zatem tylko do zasięgu funkcji zawierającej. Użycie instrukcji nonlocal poza funkcją nie ma sensu i takie zastosowanie zwraca również błąd. Warto dodać, że instrukcja nonlocal została dodana do Pythona dopiero od wersji 3, dlatego nie jest dostępna w żadnym Pythonie 2.x.
Jak korzystać z instrukcji nonlocal?
W tym przykładzie funkcja tester tworzyć zwraca funkcję nested, a zmienna state w funkcji nested zostaje odwzorowana na zasięg lokalny funkcji tester.
>>> def tester(start):
... state = start
... def nested(label):
... print(label, state)
... return nested
...
>>> F = tester(0)
>>> F('pierwszy')
pierwszy 0
>>> F('drugi')
drugi 0
Modyfikacja zmiennej state w zasięgu funkcji zagnieżdżonej nested nie jest jednak domyślnie dozwolona, lecz wymaga użycia instrukcji nonlocal.
def tester(start):
state = start
def nested(label):
state = state + 1
print(label, state)
return nested
F = tester(0)
F('pierwszy')
Wywołanie powyższego kodu zwraca błąd:
UnboundLocalError: cannot access local variable 'state' where it is not associated with a value
W takiej sytuacji należy użyć instrukcji nonlocal:
>>> def tester(start):
... state = start
... def nested(label):
... nonlocal state
... state = state + 1
... print(label, state)
... return nested
...
>>> F = tester(0)
>>> F('pierwszy')
pierwszy 1
>>> F('drugi')
drugi 2
Każde wywołanie funkcji tester generuje nowy obiekt state dołączany do obiektu funkcji nested. Każde wywołanie funkcj będzie pamiętało zatem własny stan, czyli własną wartość zmiennej.
Czemu służą zmienne nonlocal, czyli o zachowaniu stanu
Zmienne nonlocal są wykorzystywane do zapamiętywania informacji pomiędzy poszczególnymi wywołaniami danej funkcji lub metody. Instrukcji nonlocal należy używać tylko wtedy, gdy chce się je rzeczywiście zmienić, ponieważ odniesienia do zakresu zawierającego działają tak samo jak w innych przypadkach. Alternatywą dla instrukcji nonlocal jest korzystanie z jawnych atrybutów klas lub atrybutów funkcji.
Jak wykorzystać atrybuty funkcji do zachowywania informacji o stanie?
Alternatywą dla instrukcji nonlocal są atrybuty funkcji, czyli zmienne dołączane bezpośrednio do funkcji. Z tej opcji można skorzystać we wcześniejszej wersji Pythona, w której instrukcja nonlocal jest niedostępna. Do tego atrybuty funkcji są widoczne również poza funkcją zagnieżdżoną.
def tester_att(start):
state = start
def nested_att(label):
print(label, nested_att.state)
nested_att.state += 1
nested_att.state = start
return nested_att
W poniższym kodzie do zmiennej state dodawane jest 1 za każdym wywołaniem funkcji. Atrybuty funkcji jak widać są dostępne również spoza jej zasięgu:
F = tester_att(1)
F('pierwsza label')
print(F.state)
Wynik:
pierwsza label 1
2
Co z obiektami mutowalnymi?
Zmienne mutowalne zadeklarowane w funkcja zagnieżdżonych można zmieniać w miejscu nawet bez użycia instrukcji nonlocal.
>>> def tester_mut(start):
... state = [start]
... def nested(label):
... print(label, state[0])
... state[0] += 1
... return nested
...
>>> F = tester_mut(1)
>>> F('pierwszy')
pierwszy 1
>>> F('drugi')
drugi 2
W kolejnym wpisie skupimy się z kolei na argumentach 😉
Dodaj komentarz
Musisz się zalogować, aby móc dodać komentarz.