Pular para o conteúdo

    Capítulo 18, Básico

    Escopo e armadilhas das funções

    Onde uma variável existe, e as duas armadilhas que fazem mais gente perder tempo: variável local mal usada e valor padrão mutável.

    Código deste capítulo: basico/cap18_escopo.py

    Escopo: onde um nome vale

    O Python procura um nome em quatro lugares, nesta ordem, que a comunidade chama de LEGB: Local (dentro da função), Enclosing (função externa), Global (módulo) e Built-in (nomes embutidos, como len).

    basico/cap18_escopo.pylinhas 10 a 24
    mensagem = "global"
    
    
    def externa():
        mensagem = "local da externa"
    
        def interna():
            print(mensagem)
    
        interna()
        print(mensagem)
    
    
    externa()
    print(mensagem)
    
    Saída
    local da externa
    local da externa
    global
    

    Para alterar uma variável global dentro de uma função, é preciso declarar global. Eu evito: estado global torna o código difícil de testar e de entender. Prefira receber o valor como parâmetro e devolver o novo:

    basico/cap18_escopo.pylinhas 26 a 36
    contador = 0
    
    
    def incrementar():
        global contador
        contador += 1
    
    
    incrementar()
    incrementar()
    print(contador)
    
    Saída
    2
    

    UnboundLocalError

    Se você atribui a um nome dentro da função, o Python o trata como local na função inteira, inclusive nas linhas anteriores à atribuição. Por isso o erro abaixo acontece mesmo existindo um total global:

    basico/cap18_escopo.pylinhas 41 a 51
    total = 10
    
    
    def quebrado():
        total += 1
    
    
    try:
        quebrado()
    except UnboundLocalError as erro:
        print(erro)
    
    Saída
    cannot access local variable 'total' where it is not associated with a value
    

    O valor padrão mutável

    Esta é a armadilha mais famosa da linguagem. O valor padrão de um parâmetro é criado uma única vez, quando a função é definida. Se for uma lista, todas as chamadas compartilham a mesma lista:

    basico/cap18_escopo.pylinhas 56 a 63
    def adicionar(item, lista=[]):
        lista.append(item)
        return lista
    
    
    print(adicionar(1))
    print(adicionar(2))
    print(adicionar.__defaults__)
    
    Saída
    [1]
    [1, 2]
    ([1, 2],)
    

    A segunda chamada devolve [1, 2], embora você não tenha passado lista nenhuma: o 1 ficou guardado no próprio valor padrão. A solução é usar None como sentinela e criar a lista dentro da função:

    basico/cap18_escopo.pylinhas 65 a 73
    def adicionar_correto(item, lista=None):
        if lista is None:
            lista = []
        lista.append(item)
        return lista
    
    
    print(adicionar_correto(1))
    print(adicionar_correto(2))
    
    Saída
    [1]
    [2]
    

    A regra que eu sigo: nunca use lista, dicionário ou conjunto como valor padrão. Linters como o ruff acusam esse erro automaticamente (regra B006).

    Efeitos colaterais

    Uma função que altera os argumentos que recebe tem um efeito colateral. Isso não é proibido, mas deve ser explícito no nome e na documentação. Compare:

    basico/cap18_escopo.pylinhas 78 a 90
    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)
    
    Saída
    [3, 1, 2] [1, 2, 3]
    [1, 2, 3]
    

    Recursão

    Uma função pode chamar a si mesma. Toda função recursiva precisa de um caso base que pare a recursão. O Python limita a profundidade (1000 chamadas por padrão) e não otimiza recursão de cauda, então para problemas profundos prefira um laço:

    basico/cap18_escopo.pylinhas 95 a 113
    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")
    
    Saída
    120
    1000
    limite de recursão atingido
    

    Exercício 1

    Corrija o bug do valor padrão

    A função abaixo deveria devolver uma lista nova a cada chamada sem argumento. Corrija-a.

    Texto
    def registrar(evento, historico=[]):
        historico.append(evento)
        return historico