2.16. Scope¶
Wanneer Python een naam binnen een functie opzoekt, doorzoekt het een specifieke reeks scopes. Het begrijpen van die reeks verklaart waarom sommige toewijzingen buitenste namen overschaduwen, waarom andere ze wijzigen, en waarom geneste functies waarden kunnen onthouden van waar ze gedefinieerd werden.
Het opzoeken van namen begint in de lokale functie-scope en loopt naar buiten naar de module- en ingebouwde scopes totdat er een overeenkomst wordt gevonden.¶
2.16.1. Lokale en module-scope¶
Namen die binnen een functie zijn gedefinieerd, zijn lokaal voor die functie en verdwijnen wanneer de aanroep eindigt:
def f():
x = 10
print(x)
f()
print(x) # NameError: x is not defined
Namen die op het hoogste niveau van een .py-bestand zijn gedefinieerd, zijn op moduleniveau (soms globaal genoemd) en zijn overal in dat bestand zichtbaar, ook binnen functies:
CAMERA = "OpenMV"
def banner():
print("running on", CAMERA)
Het lezen van een naam op moduleniveau vanuit een functie gaat automatisch. Het toewijzen aan die naam vanuit een functie gaat dat niet – de toewijzing creëert een nieuwe lokale variabele die de naam op moduleniveau overschaduwt voor de rest van de aanroep:
counter = 0
def bump():
counter = counter + 1 # UnboundLocalError
De counter aan de linkerkant maakt counter een lokale naam in bump, dus de lezing aan de rechterkant heeft geen waarde om te vinden.
2.16.1.1. Het global-sleutelwoord¶
Om een naam op moduleniveau daadwerkelijk opnieuw toe te wijzen vanuit een functie, declareer je deze eerst als global:
counter = 0
def bump():
global counter
counter += 1
Gebruik global spaarzaam. Functies die verborgen toestand muteren zijn moeilijker te doorgronden dan functies die waarden als argumenten binnenkrijgen en nieuwe waarden teruggeven. De gebruikelijke oplossing voor “ik moet toestand delen” is een object (een lijst, een dict, een class-instantie) als argument door te geven en dat te muteren.
2.16.2. Lambdas¶
Een lambda bouwt een kleine anonieme functie in één enkele expressie:
square = lambda x: x * x
square(7) # 49
Het is precies gelijk aan
def square(x):
return x * x
De body van een lambda moet één enkele expressie zijn – geen statements, geen meerdere regels. Het belangrijkste gebruik is het doorgeven van een piepkleine functie als argument aan iets dat een functie aanneemt:
pairs = [("b", 2), ("a", 3), ("c", 1)]
pairs.sort(key=lambda item: item[1])
# [('c', 1), ('b', 2), ('a', 3)]
Wanneer de body langer wordt dan één expressie, schakel dan over naar een echte def. Een functie benoemen met def geeft deze ook een naam in tracebacks, wat een lambda niet heeft.
2.16.3. Closures¶
Een functie die binnen een andere functie is gedefinieerd, kan namen lezen uit de scope van de omsluitende functie. De binnenste functie vangt die namen op en blijft werken, zelfs nadat de buitenste aanroep is teruggekeerd:
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))
Uitvoer:
105 110
add5 en add10 zijn twee afzonderlijke functies, die elk hun eigen n onthouden. Een functie die op deze manier een aangepaste binnenste functie bouwt en retourneert, wordt een closure genoemd. Het is de belangrijkste reden waarom een taal überhaupt geneste functies nodig heeft – een manier om wat toestand in een functiewaarde te bakken en die waarde vervolgens als één enkele callable door te geven.
Het lezen van opgevangen namen gebeurt automatisch. Er opnieuw aan toewijzen vereist een extra sleutelwoord. Het onderstaande voorbeeld doet wat juist lijkt en faalt:
def make_counter():
count = 0
def tick():
count = count + 1 # UnboundLocalError
return count
return tick
De toewijzing aan count binnen tick maakt count lokaal voor tick, op dezelfde manier als het de variabele lokaal zou hebben gemaakt in een top-level functie. Het sleutelwoord nonlocal vertelt Python “deze naam leeft in de omsluitende functie, wijs hem daar opnieuw toe”:
def make_counter():
count = 0
def tick():
nonlocal count
count += 1
return count
return tick
c = make_counter()
print(c(), c(), c())
Uitvoer:
1 2 3
nonlocal is voor de scope van de omsluitende functie wat global is voor de module-scope. Merk op dat het muteren van een opgevangen object (het aanroepen van some_list.append(...), some_dict[k] = v) geen nonlocal nodig heeft – de naam wordt niet opnieuw toegewezen, alleen het object waar deze naar verwijst wordt gewijzigd.