2.16. Hatókör

Amikor a Python egy nevet keres egy függvényen belül, egy meghatározott hatókör-sorozatban kutat. Ennek a sorrendnek a megértése megmagyarázza, miért árnyékolnak egyes hozzárendelések külső neveket, mások miért módosítják azokat, és miért tudnak a beágyazott függvények emlékezni a definiálásuk helyéről származó értékekre.

Egymásba ágyazott dobozok, amelyek egy lokális hatókört mutatnak egy modulhatókörön belül egy beépített hatókörön belül, egy nyíllal jelezve, hogy a névkeresés a legbelső kerettől kifelé halad.

A névkeresés a lokális függvényhatókörben indul, és kifelé halad a modul- és beépített hatókörökhöz, amíg egyezést nem talál.

2.16.1. Lokális és modulhatókör

A függvényen belül definiált nevek lokálisak az adott függvényre nézve, és eltűnnek, amikor a hívás véget ér:

def f():
    x = 10
    print(x)

f()
print(x)              # NameError: x is not defined

Egy .py fájl legfelső szintjén definiált nevek modulszintűek (néha globálisnak nevezik), és láthatóak az adott fájl mindenhol, beleértve a függvényeken belül is:

CAMERA = "OpenMV"

def banner():
    print("running on", CAMERA)

Egy modulszintű név olvasása egy függvényen belülről automatikus. Az adott névhez való hozzárendelés egy függvényen belülről nem – a hozzárendelés egy új lokális változót hoz létre, amely a hívás hátralévő részében elárnyékolja a modulszintűt:

counter = 0

def bump():
    counter = counter + 1     # UnboundLocalError

A bal oldali counter a counter nevet lokális névvé teszi a bump függvényben, így a jobb oldali olvasásnak nincs megtalálható értéke.

2.16.1.1. A global kulcsszó

Ahhoz, hogy egy modulszintű nevet egy függvényen belülről ténylegesen újra hozzárendelj, először global kulcsszóval kell deklarálnod:

counter = 0

def bump():
    global counter
    counter += 1

A global kulcsszót takarékosan használd. A rejtett állapotot módosító függvényekről nehezebb gondolkodni, mint azokról, amelyek értékeket vesznek be argumentumként, és új értékeket adnak vissza. A „meg kell osztanom az állapotot” szokásos megoldása az, hogy egy objektumot (egy listát, egy dict-et, egy osztálypéldányt) adsz át argumentumként, és azt módosítod helyette.

2.16.2. Lambdák

Egy lambda egyetlen kifejezésben épít fel egy kis névtelen függvényt:

square = lambda x: x * x
square(7)             # 49

Pontosan egyenértékű ezzel:

def square(x):
    return x * x

Egy lambda törzsének egyetlen kifejezésnek kell lennie – nincsenek utasítások, nincsenek több sorok. A fő felhasználása egy apró függvény átadása argumentumként valaminek, ami egy függvényt vesz át:

pairs = [("b", 2), ("a", 3), ("c", 1)]
pairs.sort(key=lambda item: item[1])
# [('c', 1), ('b', 2), ('a', 3)]

Amikor a törzs egy kifejezésnél nagyobbra nő, válts át egy igazi def definícióra. Egy függvény def segítségével történő elnevezése a visszakövetésekben (traceback) is nevet ad neki, amivel egy lambda nem rendelkezik.

2.16.3. Lezárások (closure-ök)

Egy másik függvényen belül definiált függvény olvasni tudja a neveket a körülvevő függvény hatóköréből. A belső függvény megragadja ezeket a neveket, és tovább működik, még azután is, hogy a külső hívás visszatért:

def make_adder(n):
    def add(x):
        return x + n
    return add

add5 = make_adder(5)
add10 = make_adder(10)
print(add5(100), add10(100))

Kimenet:

105 110

Az add5 és az add10 két különálló függvény, mindegyik a saját n értékére emlékszik. Egy olyan függvényt, amely ilyen módon egy testreszabott belső függvényt épít fel és ad vissza, lezárásnak (closure) nevezünk. Ez a fő oka annak, hogy egy nyelvnek egyáltalán szüksége van beágyazott függvényekre – egy mód arra, hogy némi állapotot süssünk bele egy függvényértékbe, majd ezt az értéket egyetlen meghívható dologként adjuk tovább.

A megragadott nevek olvasása automatikusan történik. Egyikük újra hozzárendelése egy extra kulcsszót igényel. Az alábbi példa azt teszi, ami helyesnek tűnik, és elbukik:

def make_counter():
    count = 0
    def tick():
        count = count + 1     # UnboundLocalError
        return count
    return tick

A count névhez való hozzárendelés a tick függvényen belül a count nevet lokálissá teszi a tick függvényre nézve, ugyanúgy, ahogy egy legfelső szintű függvényben is lokálissá tette volna. A nonlocal kulcsszó azt mondja a Pythonnak: „ez a név a körülvevő függvényben él, ott rendeld újra hozzá”:

def make_counter():
    count = 0
    def tick():
        nonlocal count
        count += 1
        return count
    return tick

c = make_counter()
print(c(), c(), c())

Kimenet:

1 2 3

A nonlocal a körülvevő függvény hatókörének az, ami a global a modulhatókörnek. Vedd figyelembe, hogy egy megragadott objektum módosítása (a some_list.append(...), some_dict[k] = v meghívása) nem igényel nonlocal kulcsszót – a név nem kerül újra hozzárendelésre, csak az objektum változik, amelyre mutat.