Scope en veranderlijke standaardwaarden
Waar een naam leeft en wie hem kan zien, het verschil tussen lezen en schrijven van een buitenste naam, UnboundLocalError, en Python's bekendste valkuil — de lege lijst als standaard.
Hoe wil je leren?
Het probleem dat we oplossen
De "wanneer het breekt"-lijst van hoofdstuk negentien liet één regel onopgelost: "Een variabele binnen de functie is buiten niet zichtbaar — dat is verwacht, en dat is het onderwerp van hoofdstuk eenentwintig."
Dit is dat hoofdstuk.
def add_tax(price):
tax = price * 0.15
return price + tax
print(add_tax(100))
print(tax)115.0
NameError: name 'tax' is not defined. Did you mean: 'max'?De eerste regel werkte, dus tax moet hebben bestaan. En buiten bestaat het niet. (Vóór de foutregel print Python ook enkele traceback-regels; in dit hoofdstuk wordt alleen de laatste regel getoond.)
Dit is bescherming, geen beperking. In een programma met vijftig functies zouden, als elke interne naam naar buiten lekte, twee functies met een variabele total elkaars waarde overschrijven — en niemand zou het merken. Namen binnen een functie blijven binnen, dus het schrijven van één functie vereist geen kennis van de namen in de andere negenenveertig.
Dit hoofdstuk behandelt die regel, en één gevolg ervan dat Python's bekendste valkuil is.
Aan het einde van dit hoofdstuk kun je
- Zeggen waar een naam "leeft" en wie hem kan zien
- Een buitenste naam van binnen een functie lezen, en uitleggen waarom je er geen kunt schrijven
- Een
UnboundLocalErrorlezen en vertellen wat er gebeurde - Uitleggen waarom een lijst die in een functie wordt doorgegeven, veranderd terug kan komen
- Python's bekendste valkuil herkennen en vermijden: de veranderlijke standaardwaarde
Voorkennis: Functie-argumenten.
Binnen blijft binnen
Elke naam die binnen een functie wordt aangemaakt — parameters inbegrepen — leeft alleen voor die aanroep. Wanneer de functie eindigt, zijn de namen verdwenen.
Dit heet scope (bereik): het gebied waarin een naam bekend is.
Een buitenste naam lezen werkt
RATE = 0.15
def add_tax(price):
return price + price * RATE
print(add_tax(100))115.0Er is geen RATE binnen de functie, dus Python kijkt er buiten en vindt er een. Lezen van een buitenste naam van binnenuit is prima.
De volledig in hoofdletters geschreven naam is een conventie, geen Python-regel — het vertelt een lezer "dit is een constante, het verandert niet". Op die manier gebruikt is een buitenste naam het veiligst.
Maar een buitenste naam schrijven lukt niet
count = 10
def bump():
count = 99
return count
print(bump())
print(count)99
10De binnenste count = 99 raakte de buitenste count niet aan. Het maakte een nieuwe, lokale naam die alleen binnen de functie leeft en de buitenste verstopte voor de duur van de aanroep.
Dat is de regel, en het is bewust: een functie kan per ongeluk niets buiten veranderen.
En daar komt een vreemd uitziende fout uit:
count = 10
def bump():
count = count + 1
return count
print(bump())UnboundLocalError: cannot access local variable 'count' where it is not associated with a valueDe regel leest alsof het de buitenste count pakt en er één bij optelt. Maar Python inspecteert de hele functie voordat het hem uitvoert, ziet dat er aan count wordt toegewezen, en besluit dat count vanaf het begin tot het einde lokaal is voor deze functie.
Dan, bij het evalueren van count + 1 rechts, zoekt het de lokale count, die nog geen waarde heeft gekregen — en stopt.
De melding is dus ongebruikelijk nauwkeurig: de naam is lokaal, en er is nog niets aan geassocieerd.
De oplossing is bijna altijd dezelfde, en het is niet global: neem de waarde als argument en return het resultaat.
def bump(count):
return count + 1
count = bump(count)Python heeft wél een global-sleutelwoord, en deze cursus leert het niet, omdat het probleem waarvoor mensen ernaar grijpen een betere oplossing heeft. Een functie die de buitenstaat verandert, kan niet begrepen worden vanaf de plek waar hij wordt aangeroepen — en dat is de definitie van code die moeilijk te testen en moeilijk te vertrouwen is.
Maar het ding dat je doorgaf kan veranderen
Scope beschermt namen, geen objecten. Het verschil is subtiel en belangrijk.
def add_item(items):
items.append("pen")
return items
basket = []
add_item(basket)
add_item(basket)
print(basket)['pen', 'pen']De buitenste basket veranderde, zelfs zonder dat een return werd bewaard.
De reden is weer die van hoofdstuk twaalf: de naam items is nieuw, maar wijst naar dezelfde lijst. append verandert die lijst, en beide namen tonen de verandering.
Vergelijk:
def replace(items):
items = ["new"]
return items
basket = ["old"]
result = replace(basket)
print(basket)
print(result)['old']
['new']Hier items = [...] wierp een nieuwe waarde toe, de lokale naam wijzend naar een andere lijst. De buitenste basket is waar hij was.
De regel in één regel: het object wijzigen (append, sort, [i] =) is zichtbaar buiten; de naam naar een ander object wijzen (=) is niet.
Python's bekendste valkuil
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item("pen"))
print(add_item("bag"))
print(add_item("ink"))['pen']
['pen', 'bag']
['pen', 'bag', 'ink']Drie losse aanroepen, en elk houdt wat de vorigen achterlaten.
Begrijp de reden en je vergeet het nooit: de standaardwaarde wordt één keer geëvalueerd, wanneer de def-regel draait — niet wanneer de functie wordt aangeroepen. Die lege lijst is één enkele lijst die aan de functie hangt, en elke aanroep deelt hem.
De oplossing is conventioneel en simpel:
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item("pen"))
print(add_item("bag"))['pen']
['bag']None is onveranderlijk, dus het delen kost niets, en de nieuwe lijst wordt vers aangemaakt bij elke aanroep.
Als regel: schrijf nooit een lijst, dictionary of set als standaardwaarde. Getallen, tekst, True/False en None zijn veilig, precies omdat ze niet kunnen veranderen.
Waaromis Noneen niet== None?isvraagt "is dit het exacte zelfde object?", en er is maar éénNonein een heel programma.==test gelijkheid, wat sommige typen op verrassende manieren definiëren. VoorNoneisiszowel conventioneel als betrouwbaar.
Een compleet voorbeeld
basket.py:
# Where a name lives decides who can see it and who can change it
TAX_RATE = 0.15
def line_total(price, quantity):
"""Pure: takes values, returns a value, touches nothing outside."""
return round(price * quantity * (1 + TAX_RATE), 2)
def add_line(basket, name, price, quantity):
"""Impure on purpose: it changes the basket it was handed."""
basket.append({"name": name, "total": line_total(price, quantity)})
return basket
def summarise(basket, note=None):
"""`None` as the default, so no list or dict is shared between calls."""
notes = [] if note is None else [note]
total = sum(line["total"] for line in basket)
notes.append(f"{len(basket)} lines, {total:.2f} in total")
return notes
shopping = []
add_line(shopping, "pen", 15.0, 3)
add_line(shopping, "bag", 850.0, 1)
for line in shopping:
print(f"{line['name']:<6} {line['total']:>8.2f}")
print()
for note in summarise(shopping, "morning order"):
print(note)
print()
print("basket outside the function:", len(shopping), "lines")pen 51.75
bag 977.50
morning order
2 lines, 1029.25 in total
basket outside the function: 2 linesDrie functies met drie verschillende verhoudingen tot de wereld erbuiten, en dat is waar je naar moet kijken.
line_total raakt niets aan. Het neemt waarden en geeft een waarde. Het leest TAX_RATE en verandert hem nooit. Een functie als deze heet zuiver — dezelfde input geeft altijd dezelfde output, zonder bijwerkingen waar ook. Het is de makkelijkste om te testen en de moeilijkste om fout te maken.
add_line verandert bewust iets buiten. Het krijgt een lijst mee en voegt eraan toe — en de laatste regel bewijst dat de buitenste shopping echt twee regels vasthoudt. Er is niets mis mee, zolang de naam het zegt. add_line lezen vertelt je dat er iets wordt toegevoegd.
summarise gebruikt None als standaard. Geschreven als note=[] zou elke aanroep één lijst delen, en het tweede rapport de notitie van de eerste meedragen.
Wanneer het breekt
NameError: name 'tax' is not defined Een naam uit binnen een functie werd buiten gebruikt. return wat er buiten nodig is.
UnboundLocalError: cannot access local variable ... where it is not associated with a value Een naam wordt binnen de functie en gelezen vóór die toewijzing. Python besloot dat het lokaal was. Neem de waarde als argument en return het resultaat.
Mijn lijst veranderde nadat ik hem in een functie doorgaf Dat is het verwachte gedrag — de naam is nieuw, de lijst is dezelfde. Om het te vermijden, schrijf items = items.copy() bovenaan de functie, of bouw en return een nieuwe lijst.
De functie onthoudt zijn eerdere resultaten Een veranderlijke standaardwaarde. Niet def f(x, acc=[]) maar def f(x, acc=None), met if acc is None: acc = [] erbinnen.
Ik gebruikte dezelfde naam in twee functies — is dat een probleem? Het is geen probleem, en dat is precies het doel van scope. Twee functies hun total variabelen zijn volledig apart.
Stap 4 van 6 — Voorspellen
Test je begrip
Er wordt een waarde aan count binnen de functie toegekend. Wat printen de twee regels?
count = 10
def bump():
count = 99
return count
print(bump())
print(count)- A10 10
- B99 10
- CEen `UnboundLocalError`
- D99 99
Er is geen return, toch wordt de buitenste lijst geprint. Wat verschijnt er?
def add_item(items):
items.append("pen")
basket = []
add_item(basket)
add_item(basket)
print(basket)- A[]
- B['pen']
- CNone
- D['pen', 'pen']
Twee losse aanroepen, met een lege lijst als standaard. Wat wordt geprint?
def collect(item, box=[]):
box.append(item)
return box
print(collect("pen"))
print(collect("bag"))- A['pen', 'bag'] ['pen', 'bag']
- B['pen'] ['bag']
- C['pen'] ['pen', 'bag']
- D['pen'] ['pen']
Voor antwoorden heb je een account nodig
Log in om je antwoorden te controleren
De vragen staan hierboven, en ze in je hoofd uitwerken is waar het om draait. Log in om de antwoorden, de uitleg en de hints in drie niveaus te zien.
Jouw beurt
Schrijf een bestand genaamd cart.py met drie functies:
line_total(price, quantity, tax_rate=0.15)— zuiver, raakt niets aanadd_to(cart, name, price, quantity)— voegt toe aan de cart die hem wordt meegegevenreceipt(cart, header=None)— geeft een lijst regels terug, bedenk een header als er geen wordt gegeven
Bouw daarna een lege cart, voeg een paar dingen toe, en print de receipt.
Ten slotte, drie experimenten, allemaal de moeite waard om met eigen ogen te zien:
- Verander
receipt's standaard naarheader=[], roep de functie tweemaal aan en print beide resultaten. Wat zie je de tweede keer? - Voeg
cart = cart.copy()toe als eerste regel vanadd_to, print dan de buitenste cart. Hoeveel dingen zitten erin, en waarom? - Wijs
TAX_RATE = 0.2toe binnen een functie, print danTAX_RATEerbuiten. Welke waarde zie je?
De drie antwoorden samen vormen het hele hoofdstuk: welke namen waar leven, en welke veranderingen de buitenkant bereiken.
Stap 6 van 6
Uitdaging — de hoofdstukquiz
Tien vragen van makkelijk naar moeilijk. De laatste zijn met opzet lastig.
Log in om de quiz te maken