Dużo ważniejsze jest niepopełnianie krytycznych błędów niż dokonywanie genialnych decyzji.

Przemysław Gerschmann

Python 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.

Funkcje zagnieżdżone

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.

Instrukcja 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

Dzielenie zmiennych globalnych pomiędzy modułami

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.

Instrukcja 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.

Przykłady działania instrukcji 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.

Zmienne lokalne, oraz swobodne

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

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:

  • musi istnieć funkcja zagnieżdżona
  • funkcja zagnieżdżona musi odnosić się do zmiennych zdefiniowanych poza jej zakresem lokalnym - w funkcji zamykającej
  • funkcja zamykająca musi zwracać funkcję zagnieżdżoną

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.

Gdzie domknięcia znajdują zastosowanie?

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.

Podsumowanie

Liczę na to, że artykuł ten pomoże Ci w przyszłości świadomie operować zakresem zmiennych umieszczanych w kodzie.

Artykuł dodano 2020-08-28