Przestrzeń nazw

Posted on 2019-02-17 Updated on 2019-02-24

W poście poświęconym zmiennym napisałem, że zmienne przechowywane są w przestrzeniach nazw. Co to jest i dlaczego jedna przestrzeń na nazwy nie wystarczy?

Zrozumienie tego tematu pojawia się na rozmowach o pracę dla programistów. Przynajmniej ja się o to pytam. Pytanie czy to jest dobry temat dla kogoś, kto zaczyna? Odpowiedź jest dość prosta - nie wiem. Wiem, że ciężko mi poruszać inne zagadnienia, z zupełnym pominięciem tego zagadnienia.

Co to jest przestrzeń nazw?

To takie specjalne miejsce, gdzie program trzyma nazwy, żeby wiedzieć, gdzie one są jak trzeba się do nich odwołać. Kolejna banalna sprawa, z której składa się program. W praktyce banalne nie jest, ale o tym za chwilę.

Innymi słowy, jak trzeba stworzyć nową zmienną, albo funkcję program ma przygotowaną listę, w której przechowuje takie nazwy, każda z nich wskazuje na to, co programista określił.

Dlaczego jedna przestrzeń nie wystarczy?

Skoro tworzymy zmienne, to może wystarczy jedno miejsce? W zasadzie w żadnym języku programowania nie wystarcza. Jak wywołujemy kilka funkcji, każde takie wywołanie może potrzebować osobnego zestawu zmiennych. Dokładniej te zmienne mogą się tak samo nazywać, ale wskazywać na inne wartości. Przykład:

a = 1
def fun1():
    a = 2
    b = 3
    print(f'fun1: a = {a}')
    print(f'fun1: b = {b}')
print(f'a = {a}')
fun1()
print(f'a = {a}')

Litera f powoduje, że trzeba mieć Pythona 3.6, z drugiej strony to co jest w "wąsatych nawiasach" zostanie wykonane i wstawine do naszego ciągu znaków.

Wracając do przykładu. Widać, że zmienna globalna a pozostaje bez zmian. <imo, że funkcja też ma zmienną a. Wywołanie funkcji stworzyło nową przestrzeń nazw, a operacja przypisania stworzyła w niej nowy wpis, z naszą zmienną. Tak jak opisuje post zmienne.

Funkcja w funkcji

Pobawmy się w ulubioną zabawę osób prowadzących rekrutację - "Co się pojawi na ekranie?". Przykład:

a = 1
def fun1():
    a = 2
    b = 3
    print(f'fun1: a = {a}')
    print(f'fun1: b = {b}')
    def fun2():
        c = 5
        print(f'fun2: a = {a}')
        print(f'fun2: b = {b}')
        print(f'fun2: c = {c}')
    b = 6
    c = 7
    fun2()
fun1()
print(f'a = {a}')

Wykonujemy program, trzeba powiedzieć co zostanie wypisane przez program. Oczywiście to jest malo ciekawe zadanie. Istotna jest odpowiedź na pytanie dlaczego akurat takie rzeczy zostały wypisane?

Każda funkcja ma swoją własną przestrzeń nazw, program ma swoją. Dziwnie wygląda fun2 wrzucone do środka fun1 - można, da się, więc jest przykład. Problemów jest parę - pierwsza operacja print pokaże 2, a może 1? Przy rozwiązywaniu nazw najpierw sprawdzana jest najbardziej specyficzna przestrzeń nazw, czyli ta, z fun1, czyli wypisze 2. Z b jest prosto, bo deklaracja jest tylko w fun1 - czyli wypisze 3. Dalej jest deklaracja funkcji, zmiana wartości zmiennej b i deklaracja zmiennej c. Na końcu fun1 jest wywołanie funkcji wewnętrznej fun2. Tu pojawia się pytanie, czy zmiana b na 6 zostanie uwzględniona w wykonaniu fun2? A co będzie ze zmienną c, pojawiła się przed wywołaniem fun2, to program wypisze 7, a może 5? Na początek b - zajrzy do przestrzeni nadrzędnej, ale to jest jedna przestrzeń z jedną zmienną b - skoro tak to ważna jest ostatnia wartość, czyli 6. Druga sprawa c - tu jest podobnie jak z a wcześniej - obowiązje najbliższa przestrzeń, czyli c jest 5. Na końcu wypisze 1, bo przecież pozostałe zmiany odbyły się w innych przestrzeniach nazw.

Pytanie - po co to wszystko? Chodzi o sytuację, w której mam wiele funkcji, skomplikowany program i muszę wiedzieć co się dzieje, które dane skąd są brane i gdzie są zmieniane. W szczególności sytuacja, że do zmiennej a przypisaliśmy dane, ale tylko w fun1 jest dość istotna.

Komplikujemy

Zacznę od przykładu:

a = 1
def fun1():
    a = 2
    b = 3
    print(f'fun1: a = {a}')
    print(f'fun1: b = {b}')
    def fun2():
        c = 5
        print(f'fun2: a = {a}')
        print(f'fun2: b = {b}')
        print(f'fun2: c = {c}')
    b = 6
    c = 7
    return fun2
a = 8
f2 = fun1()
print(f'a = {a}')
a = 9
f2()
print('callback')
def fun3(param):
    a = 10
    b = 11
    param()
fun3(f2)

A teraz omówienie. Tym razem fun1 nie wykonuje fun2 tylko zwraca funkcję. Taką funkcję można przypisać do zmiennej - tu jest to f2. Funkcję zapisaną w zmiennej można wykonać, można przekazać komuś innemu do wykonania. Bajeczna sprawa. Pytanie do czego nam to? No i co się wypisze... bo dalej gramy w grę...

Zacznę od powodu. W skrócie chodzi o coś, co nazywa się callback. Nasza funkcja robi robotę, ale na jamiś etapie, być może do odebrania wyniku przebna jest inna funkcja, a właściwie jej wywołanie. Takie rozwiązanie było dość popularne w JavaScript'cie - wywoływaliśmy metodę sciągającą dane, ale że to mogło trwać, to przekazywana była też funkja do wywołania jak już się dane uda pobrać. Dla wszystkiego też przekazywana była druga funkcja - jak się nie udało.

Co się pokaże na ekranie... Można wykonać i będzie jasność, ale tak jak wcześniej napisałem - znaczenie ma odpowiedź na pytanie - dlaczego?

Kluczowe jest to, że fun2 jest zdefiniowane w fun1 i to jest jego przestrzeń nadrzędna. Czyli przekazanie danych do wywołania param pozwala na aktualizację danych w środku w fun1. Jak masz chwilę prześledź i zastanów się dlaczego tak zadziałało.