Dużo ważniejsze jest niepopełnianie krytycznych błędów niż dokonywanie genialnych decyzji.
Przemysław Gerschmann
global
and nonlocal
Historycznie od początku istnienia języka Python
możliwe było powiązanie nazw
zmiennych jedynie w zakresie lokalnym - poprzez ich definicję oraz globalnie w
obrębie modułu z użyciem instrukcji global
. Istniał jedynie zakres globalny i
lokalny. W wielu językach programowania jest to naturalny podział z uwagi na
brak możliwości definiowania zagnieżdżonych funkcji. Za przykład takiego języka
może posłużyć C
. W Python
natomiast problem ograniczenia do zakresu
lokalnego i globalnego dość mocno dawał się programistom we znaki. Ostatecznie
po wielu dyskusjach i propozycjach rozwiązań w składni języka pojawiła się
instrukcja nonlocal
. Deklaracja zmiennej z jej użyciem zmieni zakres jej
widoczności do okalającym zakresie. Z rozwojem języka Python
zmianom ulegał
zakres zmiennych. W tym artykule skupiam się jedynie na Python 3.X
.
Wcześniejsze wersje zakończyły już oficjalne wsparcie i nie widze potrzeby
zajmowania się ich niuansami.
Wykonanie instrukcji x = 1
tworzy lub modyfikuje nazwę x
jako zmienną
lokalną, po czym przypisuje do niej wartość 1. Zmienne lokalne są widoczne
tylko dla kodu z tego samego zasięgu. By zmienić zakres widoczności używamy
instrukcji global
i nonlocal
.
By w móc w pełni rozmawiać o zakresie zmiennych konieczne jest operowanie się funkcjami zagnieżdżonymi. Funkcja zdefiniowana wewnątrz innej funkcji jest funkcją zagnieżdżoną. Przykład:
def outer(value):
# Zewnętrzna fukcja zamykająca
def inner():
# Funkcja zagnieżdżona
print(value)
return inner
Na tym etapie musisz jedynie rozumieć czym jest funkcja zagnieżdżona. Więcej światła na niuanse funkcji zagnieżdżonych pojawi się w dalszej części artykułu.
global
Zmienne zdefiniowane poza ciałem funkcji to zmienne globalne. Zmienne
globalne mogą być używane zarówno wewnątrz jak i na zewnątrz funkcji. Domyślnie
dostępne są w trybie tylko do odczytu. By umożliwić modyfikację zmiennej musi
zadeklarować ją z użyciem instrukcji global
. Jeśli zmienna zostanie
zdefiniowana wewnątrz funkcji - bez użycia instrukcji global
- uznawana jest
domyślnie za zmienną lokalną. Jest to ogólna zasada.
W języku Python
wszystkie zmienne spoza lokalnego zakresu dostępne są w trybie
read-only
. Dokonanie na nich modyfikacji wymaga wyraźnego zdeklarowania ich
z użyciem nonlocal
, bądź global
.
var = 0 # Global variable
def func_ro():
print(var) # var access is read-only
def func_rw()
global var
print(var) # var access is read/write
Standardowym sposobem dzielenia informacji między modułami - w ramach jednego programu - jest utworzenie specjalnego modułu przechowującego współdzielone informacje. Moduł taki jest następnie importowany do wszystkich modułów w których informacje są wymagane. Istnieje tylko jedna instancja każdego modułu dlatego też wszelkie wprowadzane w nim zmiany będą dostępne wszędzie, gdzie został zaimportowany. Poniżej przykład modułu konfiguracyjnego.
# File: config.py
# Configuration module example
MODE = "TEST" # Default value of the MODE configuration setting
Gdziekolwiek zostanie od zaimportowany umożliwi dostęp do zmiennej MODE
, a jej
modyfikacja będzie widoczna globalnie.
nonlocal
Instrukcji tej należy się kilka słów komentarza. nonlocal
umożliwia ponowne
wiązanie zmiennych poza zakresem lokalnym lecz z wyłączeniem zakresu globalnego
(modułu).
Użycie nonlocal
powoduje, że oznaczone modyfikatorem identyfikatory
odwołują się do poprzednio zdefiniowanych zmiennych w najbliższym dostępnym
zasięgu. Przykładem zastosowania jest nadanie funkcji praw do modyfikacji
zmiennych okalającej funkcji zewnętrznej.
def sum():
x = 0
def add(y):
nonlocal x
x += y
print(x)
return add
WAŻNE Nazwy zmiennych wymienionych w instrukcji nonlocal
nie mogą
kolidować z nazwami zmiennych zdefiniowanych a obecnym zakresie.
s = "something"
def func()
s = 'value'
nonlocal s
^
SyntaxError: name 's' is assigned to before nonlocal declaration
W powyższym przykładzie w ciele funkcji została zadeklarowana i zdefiniowana
zmienna s
po czym próbowaliśmy ją przesłonić zmienną swobodną. Tego typu
zabieg jest niedozwolony i wywołuje błąd składni.
global
oraz nonlocal
Działanie instrukcji global
, oraz nonlocal
najlepiej ilustrują poniższe
przykłady.
# Przykład bez zastosowania modyfikatorów zakresu -----------------------------
s = 'global' # Zmienna globalna
def outer():
s = 'outer' # Zmienna lokalna funkcji outer
def inner():
s = 'inner' # Zmienna lokalna funkcji inner
print(f'Zakres inner, s={s}')
inner()
print(f'Zakres outer, s={s}')
outer()
print(f'Zakres global, s={s}')
""" Wynik działania:
Zakres inner, s=inner
Zakres outer, s=outer
Zakres global, s=global
"""
# Przykład z zastosowaniem modyfikatora 'global' ------------------------------
s = 'global' # Zmienna globalna
def outer():
s = 'outer' # Zmienna lokalna funkcji outer
def inner():
global s
s = 'inner' # Zmienna lokalna funkcji inner
print(f'Zakres inner, s={s}')
inner()
print(f'Zakres outer, s={s}')
outer()
print(f'Zakres global, s={s}')
""" Wynik działania:
Zakres inner, s=inner
Zakres outer, s=outer
Zakres global, s=inner
"""
# Przykład z zastosowaniem modyfikatora 'nonlocal' ----------------------------
s = 'global' # Zmienna globalna
def outer():
s = 'outer' # Zmienna lokalna funkcji outer
def inner():
nonlocal s
s = 'inner' # Zmienna lokalna funkcji inner
print(f'Zakres inner, s={s}')
inner()
print(f'Zakres outer, s={s}')
outer()
print(f'Zakres global, s={s}')
""" Wynik działania:
Zakres inner, s=inner
Zakres outer, s=inner
Zakres global, s=global
"""
W tym przypadku przykład wart jest więcej niż 1000 słów.
Zmienna lokalna to zmienna związana z lokalnym zakresem, natomiast zmienna
swobodna to zmienna nie związana z lokalnym zakresem. Nazwy zmiennych
lokalnych, oraz swobodnych przechowywane są w atrybucie __code__
obiektu
funkcji. __code__
jest obiektem reprezentującym `skompilowany kod maszynowy
funkcji.
Dowiedzmy się jak przechowywane są zmienne lokalne i swobodne w __code__
. Za
przykład posłuży nam poniższy kod zawierający definicję jednej funkcji
zagnieżdżonej:
def sum():
x = 0
def add(y):
nonlocal x
x += y
print(x)
return add
func = sum()
del sum # Not needed anymore
func(3) # Variable x = 3
Prowadząc inspekcję skompilowanego kodu zauważymy, że zmienne lokalne, oraz
swobodne przechowywane są w różnych atrybutach __code__
:
func.__code__.co_freevars # Outputs ('x',)
func.__code__.co_varnames # Outputs ('y',)
Wiązanie zmiennej x
jest utrzymywane w atrybucie __closure__
obiektu
func
. Każdemu elementowi tupli func.__code__.co_freevars
odpowiada element
func.__closure__
przechowujący aktualną wartość zmiennej.
for idx, _ in enumerate(func.__closure__):
print(func.__closure__[idx].cell_contents) # prints only digit '3'
W powyższym przykładzie mamy jedynie jeden element w func.__closure__
ponieważ func
ma tylko jedną zmienną swobodną.
Domknięcia
są silnie związane z funkcjami zagnieżdżonymi. Domknięcie
to
sytuacja w której dane spoza zakresu lokalnego funkcji są trwale dołączane do
maszynowego kodu funkcji. Zachowanie wiązań do zmiennych swobodnych jest
wymagane by zmienne te mogły być używane w czasie wywołania funkcji, gdy zasięg
definiujący te zmienne nie jest już dostępny.
Możliwe to jest przy spełnieniu następujących warunków:
Innymi słowy domknięcie
to funkcja ze stanem, funkcja przechowująca zmienne
wartości znajdujące się poza jej zasięgiem.
def nth_power(exp):
def power_of(base):
return pow(base, exp)
return power_of
W przykładzie występuje taka sytuacja. Funkcja power_of
odnosi się do
zmiennej exp
, która to nie należy do jej lokalnego zakresu.
Funkcja może mieć do czynienia ze zmiennymi swobodnymi zewnętrznymi nie będącymi zmiennymi globalnymi jedynie, gdy funkcja ta jest zagnieżdżona w innej funkcji.
Najczęściej domknięcia
wykorzystywane są przez dekoratory, lecz nie jest to
ich jedyne użycie.
Mając do zaimplementowania klasę z jedną metodą, domknięcia zapewniają alternatywne rozwiązanie problemu. Natomiast przy dużej ilości metod i atrybutów lepiej użyć klasy.
Pozwalają również na uniknięcie definiowania zmiennych globalnych, zdefiniowanie stałych, oraz zapewniają pewien rodzaj ukrywania danych.
def increment_by(v1):
def increment(v2):
return v1 + v2
return increment
increment_by_2 = increment_by(2)
del increment_by
print(increment_by_2(5)) # returns 7
W powyższym przykładzie zmienna v1
została na stałe dodana do kodu maszynowego
funcji increment_by_2
. Nie mamy do niej bezpośredniego dostępu, lecz jest ona
dostępna pomimo iż zakres fukcji w której została zdefiniowana increment_by
nie istnieje.
Liczę na to, że artykuł ten pomoże Ci w przyszłości świadomie operować zakresem zmiennych umieszczanych w kodzie.