Chapter 18, Basic
Scope and function pitfalls
Where a variable exists, and the two traps that cause more people to waste time: poorly used local variable and mutable default value.
Code for this chapter: basico/cap18_escopo.py
Scope: where a name is valid
Python looks for a name in four places, in this order, which the community calls LEGB: Local (inside the function), Enclosing (outer function), Global (module) and Built-in (built-in names, like len).
mensagem = "global"
def externa():
mensagem = "local da externa"
def interna():
print(mensagem)
interna()
print(mensagem)
externa()
print(mensagem)
local da externa
local da externa
global
To modify a global variable inside a function, you need to declare global. I avoid it: global state makes the code hard to test and understand. Prefer to receive the value as a parameter and return the new one:
contador = 0
def incrementar():
global contador
contador += 1
incrementar()
incrementar()
print(contador)
2
UnboundLocalError
If you assign to a name inside the function, Python treats it as local throughout the entire function, including in the lines before the assignment. That's why the error below occurs even with a total global:
total = 10
def quebrado():
total += 1
try:
quebrado()
except UnboundLocalError as erro:
print(erro)
cannot access local variable 'total' where it is not associated with a value
The mutable default value
This is the most famous trap in the language. The default value of a parameter is created only once, when the function is defined. If it's a list, all calls share the same list:
def adicionar(item, lista=[]):
lista.append(item)
return lista
print(adicionar(1))
print(adicionar(2))
print(adicionar.__defaults__)
[1]
[1, 2]
([1, 2],)
The second call returns [1, 2], even though you didn't pass any list: the 1 was stored in its own default value. The solution is to use None as a sentinel and create the list inside the function:
def adicionar_correto(item, lista=None):
if lista is None:
lista = []
lista.append(item)
return lista
print(adicionar_correto(1))
print(adicionar_correto(2))
[1]
[2]
The rule I follow: is never use list, dictionary, or set as a default value. Linters like ruff automatically flag this error (rule B006).
Side effects
A function that alters the arguments it receives has a side effect. This is not prohibited, but it should be explicit in the name and documentation. Compare:
def ordenar_no_lugar(lista):
lista.sort()
def ordenada(lista):
return sorted(lista)
original = [3, 1, 2]
nova = ordenada(original)
print(original, nova)
ordenar_no_lugar(original)
print(original)
[3, 1, 2] [1, 2, 3]
[1, 2, 3]
Recursion
A function can call itself. Every recursive function needs a base case that stops the recursion. Python limits the depth (1000 calls by default) and does not optimizes tail recursion, so for deep problems prefer a loop:
import sys
def fatorial(n):
return 1 if n <= 1 else n * fatorial(n - 1)
print(fatorial(5))
print(sys.getrecursionlimit())
def sem_fim(n):
return sem_fim(n + 1)
try:
sem_fim(0)
except RecursionError:
print("limite de recursão atingido")
120
1000
limite de recursão atingido
Exercise 1
Fix the default value bug
The function below should return a new list on each call without an argument. Fix it.
def registrar(evento, historico=[]):
historico.append(evento)
return historico