Skip to content
This page has been auto-translated and may contain errors.View in English

Funções

docs.scrimba.com

Conforme seus programas crescem, você escreverá a mesma lógica em mais de um lugar. Funções permitem escrever a lógica uma vez, dar um nome a ela e usá-la em qualquer lugar. Corrija-a em um lugar e toda chamada recebe a correção automaticamente.

Funções são a unidade principal de reutilização de código e abstração. Elas envolvem um comportamento, dão um nome a ele, definem uma interface clara (seus parâmetros e valor de retorno) e o tornam chamável de qualquer lugar. Funções bem nomeadas também funcionam como documentação: validate_email() diz o que um bloco de código faz sem ler o corpo.

Em Python uma função é um objeto de primeira classe: um valor que você pode passar por aí como qualquer outro, então ele pode ser atribuído a uma variável, armazenado em uma lista ou dict, passado a outra função como um argumento e retornado de uma. def cria esse objeto e o vincula a um nome no escopo atual (a região do código onde esse nome é visível). Tratar funções como valores é o que torna padrões como sorted(key=...) e pequenas tabelas de plugins possíveis, e vale a pena se acostumar cedo.

python
def greet(name):
    return f"Olá, {name}!"

print(greet("Maria"))   # "Olá, Maria!"
print(greet("João"))     # "Olá, João!"

Escreva uma vez, use em qualquer lugar, corrija em um só lugar.

Definindo uma função

A palavra-chave def inicia uma definição de função, seguida pelo nome, parênteses, dois-pontos e um corpo indentado. Uma função não faz nada até você chamá-la. Defina-a com def, depois chame-a pelo nome com ().

def é uma declaração que cria um objeto de função e o vincula ao nome dado no escopo atual. O corpo não é executado no momento da definição; ele apenas é executado quando a função é chamada. Funções sem uma instrução return retornam implicitamente None.

def constrói um objeto de função e o vincula ao nome no escopo atual. O corpo não é executado quando Python lê o def, ele é executado apenas quando você chama a função. Essa divisão importa na prática: um erro de digitação dentro de um corpo de função (um nome que não existe, digamos) permanece silencioso até que a função seja realmente chamada, então uma função que você nunca chama pode esconder um NameError que só aparece na produção no dia em que alguém a chamar.

python
def say_hello():
    print("Olá!")

say_hello()   # chame a função
JunoDefinindo uma função Escreva def nome(): e recue o corpo abaixo. Nada acontece até você chamá-la com nome(), o def apenas a configura. E se você esquecer um return, a função silenciosamente devolve None, o que me pegou mais de uma vez no começo.
JunoDefinindo uma funçãodef cria a função e a vincula a um nome, mas o corpo só é executado quando você a chama. Sem return significa que a função retorna None. Lembre-se disso quando um resultado voltar vazio sem motivo aparente.
JunoDefinindo uma função O corpo não é executado no momento do def, apenas quando você chama a função, então um nome quebrado dentro pode passar despercebido até que algo realmente o chame. Uma função sem return retorna None, que é seu próprio bug silencioso quando quem a chamou espera um valor.

Parâmetros e argumentos

Parâmetros são as entradas que sua função espera. Liste-os dentro dos parênteses. Quando você chama a função, os valores que você passa são correspondidos aos parâmetros em ordem.

Parâmetros definem a interface de uma função. Argumentos são os valores concretos passados no momento da chamada. Argumentos posicionais são correspondidos pela posição; argumentos nomeados são correspondidos pelo nome. Valores padrão tornam os parâmetros opcionais.

No momento da chamada, argumentos posicionais são vinculados da esquerda para direita e argumentos nomeados se vinculam pelo nome. Passar o mesmo parâmetro dos dois jeitos, ou um que Python não consiga colocar, lança TypeError na chamada, não dentro do corpo. Python também permite que você restrinja uma assinatura: um * vazio torna tudo depois dele apenas nomeado (chamadores devem nomeá-lo) e um / torna tudo antes dele apenas posicional. Opte por apenas nomeado em flags booleanas e assinaturas amplas, onde um True posicional no lugar errado é o tipo de erro que compila bem e envia um bug.

python
def greet(name, greeting):
    print(f"{greeting}, {name}!")

greet("Maria", "Olá")    # "Olá, Maria!"
greet("João", "Oi")         # "Oi, João!"

Parâmetro é o nome na definição da função. Argumento é o valor real que você passa quando a chama. Na prática as pessoas usam as palavras de forma intercambiável; a distinção importa principalmente ao ler documentação.

JunoParâmetros e argumentos Parâmetros são os nomes que você lista nos parênteses, argumentos são os valores reais que você entrega quando chama. Python os alinha da esquerda para a direita, então a ordem em que você os passa é a ordem em que eles chegam.
JunoParâmetros e argumentos Parâmetros são os nomes na definição, argumentos são os valores na chamada. Posicionais se vinculam da esquerda para a direita, então a posição é o contrato. As duas palavras são usadas de forma intercambiável, a diferença importa principalmente ao ler documentação.
JunoParâmetros e argumentos Argumentos posicionais se vinculam da esquerda para a direita, nomeados pelo nome, e um conflito lança TypeError bem na chamada. Um `*` vazio força chamadores a nomearem o que vem depois, que é a correção rápida para uma função cujos flags `True`/`False` seriam um jogo de adivinhação no local da chamada.

Valores padrão

Você pode dar aos parâmetros um valor padrão. Se quem chama não fornecer esse argumento, o padrão é usado. Parâmetros com padrões devem vir após parâmetros sem padrões.

Valores padrão tornam os parâmetros opcionais. Eles são avaliados uma vez no momento da definição, não em cada chamada. Isso importa para padrões mutáveis: def f(items=[]) compartilha a mesma lista em todas as chamadas. A correção é usar None como padrão e criar a lista dentro do corpo da função.

O detalhe que morde: um valor padrão é construído uma vez, quando def é executado, e compartilhado por cada chamada que o usa. Para um padrão imutável como 0 ou "Olá" (um que você não pode mudar no lugar) tudo bem. Para um padrão mutável como uma lista ou dict (um que você pode mudar no lugar) é uma armadilha, porque toda chamada que volta para o padrão reutiliza o mesmo objeto:

python
def add_item(item, items=[]):   # a lista é criada uma vez, compartilhada para sempre
    items.append(item)
    return items

add_item("a")   # ['a']
add_item("b")   # ['a', 'b']  <- não é uma lista nova

A correção é a padrão: padrão para None, depois constrói um recipiente novo dentro do corpo.

python
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items
python
def greet(name, greeting="Olá"):
    print(f"{greeting}, {name}!")

greet("Maria")           # "Olá, Maria!"
greet("Maria", "Oi")     # "Oi, Maria!"

Parâmetros com padrões devem vir após parâmetros sem padrões.

JunoValores padrão Dê a um parâmetro um padrão e ele se torna opcional, quem chama pode pular. Os padrões têm que vir após os obrigatórios, essa ordem é uma regra que Python aplica. Economiza escrever o mesmo valor em cada chamada.
JunoValores padrão Padrões tornam um parâmetro opcional e devem vir após os obrigatórios. A armadilha: um padrão é avaliado uma vez na definição, então nunca use um mutável como []. Padrão para None e construa a lista dentro do corpo em vez disso.
JunoValores padrão Um padrão é construído uma vez no momento do def e compartilhado em chamadas, então items=[] silenciosamente se acumula entre chamadas. Padrão para None e faça uma nova no corpo. Essa é uma das que aparece em revisão de código real mais do que quase qualquer outro escorregão Python.

Argumentos nomeados

Ao chamar uma função, você pode nomear os argumentos. Isso torna as chamadas legíveis, especialmente para funções com muitos parâmetros, e permite que você os passe em qualquer ordem.

Argumentos nomeados tornam chamadas de função autodocumentadas. Você pode misturar posicionais e nomeados: argumentos posicionais devem vir primeiro. Para funções com flags booleanas ou muitos parâmetros de tipos semelhantes, argumentos nomeados previnem erros silenciosos de passar argumentos na ordem errada.

Argumentos nomeados se vinculam pelo nome, então leem bem e o liberam de lembrar posições. A regra no local da chamada: argumentos posicionais vêm primeiro, nomeados depois, e nomear o mesmo parâmetro duas vezes lança TypeError. O movimento de design que vale a pena fazer é o contrário: coloque um * vazio em sua própria assinatura para que chamadores devam nomear um parâmetro (def connect(host, *, timeout=30)). Não custa nada e transforma um opaco connect("db", 30) em um connect("db", timeout=30) que sobrevive a alguém adicionando um parâmetro no meio depois.

python
def describe_player(name, score, level):
    print(f"{name} | Score: {score} | Level: {level}")

describe_player("Maria", 87, 5)                        # posicional
describe_player(name="Maria", level=5, score=87)       # nomeado, qualquer ordem
describe_player("Maria", level=5, score=87)            # misto: posicional primeiro
JunoArgumentos nomeados Nomeie os argumentos na chamada (score=87) e a chamada se explica, além disso você pode passá-los em qualquer ordem. A única regra: argumentos posicionais vêm antes dos nomeados. Ótimo para funções com um monte de parâmetros.
JunoArgumentos nomeados Nomear argumentos torna uma chamada autodocumentada e independente de ordem, com posicionais obrigatórios primeiro. Para assinaturas amplas ou flags booleanas, prefira argumentos nomeados para ninguém ter que contar posições para ler a chamada.
JunoArgumentos nomeados Argumentos nomeados se vinculam pelo nome e devem seguir posicionais. A verdadeira alavanca é sua própria assinatura: um `*` vazio força chamadores a nomear o que vem depois, para que a chamada continue lendo bem depois que alguém insere um novo parâmetro no meio.

Valores de retorno

return envia um valor de volta para quem chamou. Sem return, uma função devolve None. Uma vez que return é executado, a função sai imediatamente. Qualquer código após ele naquele bloco é pulado.

return sai da função e passa um valor para quem chamou. Uma função sem um return explícito retorna implicitamente None. return pode aparecer em qualquer lugar do corpo da função e pode ser usado várias vezes; o primeiro alcançado termina a função. Isso torna retornos antecipados úteis para cláusulas de guarda.

return entrega um valor para quem chamou e encerra a função no mesmo instante, e uma função que termina sem return retorna None. Múltiplos retornos são bom estilo, não um cheiro: um return antecipado para a falha ou caso extremo no topo (o padrão "cláusula de guarda") lê muito melhor do que aninhar todo o corpo dentro de um grande if. Uma sutileza a saber antes que a surpreenda: um return dentro de um bloco try ainda executa o bloco finally correspondente primeiro, então limpeza em finally acontece mesmo quando você retorna antecipadamente.

python
def add(a, b):
    return a + b

result = add(3, 4)   # result = 7
print(result)

return também sai da função imediatamente. Qualquer código após ele naquele bloco não é executado.

JunoValores de retornoreturn devolve um valor para quem chamou a função, e a função para bem ali, qualquer coisa depois dela é pulada. Deixe de lado return e você recebe None de volta. print mostra um valor, return o devolve para usar, levou um tempo para eu sentir essa diferença.
JunoValores de retornoreturn passa um valor de volta e sai imediatamente, sem return significa None. Você pode ter vários, e um `return` antecipado para o caso extremo no topo geralmente lê mais limpo do que envolver tudo em um grande `if`.
JunoValores de retornoreturn sai no mesmo instante e cair do final dá None. Apoie-se em retornos antecipados como cláusulas de guarda em vez de aninhamento profundo. E lembre-se que um `return` dentro de `try` ainda executa o bloco `finally`, então limpeza ali dispara mesmo em uma saída antecipada.

Retornando múltiplos valores

Python permite que você retorne múltiplos valores separando-os com vírgulas. Quem chama os recebe como uma tupla e pode desempacotá-los em nomes separados em uma linha.

Retornar múltiplos valores com uma vírgula os empacota em uma tupla. Quem chama desempacota com nomes correspondentes. Isso é Python idiomático para funções que naturalmente produzem mais de um resultado. Não é uma funcionalidade especial; é empacotamento e desempacotamento de tupla.

return a, b empacota os valores em uma tupla, e quem chama os puxa para fora com low, high = f(). Lê limpamente para dois ou três resultados. Além disso, posição se torna uma responsabilidade: ninguém chamando a função lembra se o terceiro item era a contagem ou a média, e um par trocado é um bug silencioso. Nesse ponto, retorne algo nomeado em vez disso, uma NamedTuple ou um @dataclass, para que chamadores alcancem result.average em vez de result[2]. Anote o retorno como tuple[int, str] de qualquer jeito para que um verificador de tipo possa pegar um chamador desempacotando o número errado de valores.

python
def min_max(numbers):
    return min(numbers), max(numbers)

low, high = min_max([3, 7, 1, 9, 4])
print(low, high)   # 1 9

A sintaxe low, high = ... é desempacotamento: Python atribui cada valor retornado ao nome correspondente.

JunoRetornando múltiplos valores Separe valores com uma vírgula e a função os retorna juntos como uma tupla. Quem chama os pega em uma linha, low, high = min_max(...), um nome por valor. Útil sempre que uma função naturalmente produz mais de uma resposta.
JunoRetornando múltiplos valores Uma vírgula empacota os valores em uma tupla, quem chama desempacota com nomes correspondentes. É empacotamento e desempacotamento de tupla comum, não uma funcionalidade especial. Limpo para dois ou três resultados, menos para além disso.
JunoRetornando múltiplos valores Retorno com vírgula empacota uma tupla, bem para dois ou três valores. Além disso, posição vira uma armadilha que ninguém pode ler, então retorne uma `NamedTuple` ou `@dataclass` e deixe chamadores usarem `result.average` em vez de contar índices.

Escopo

Variáveis criadas dentro de uma função existem apenas dentro dessa função. Você não pode vê-las de fora. Variáveis definidas fora de todas as funções são visíveis em qualquer lugar, mas você não pode mudá-las de dentro de uma função sem uma declaração explícita.

Um nome criado dentro de uma função é local: ele vive apenas lá e desaparece quando a função retorna. Um nome definido no nível superior do arquivo é global. Ler um global de dentro de uma função funciona sem cerimônia, mas atribuir a um precisa de global name, caso contrário Python trata a atribuição como criando um novo local que encobre o global. Na prática você raramente quer global em tudo: passar valores como argumentos e devolver resultados com return mantém os efeitos de uma função visíveis no local da chamada.

Python resolve um nome com a regra LEGB, verificando quatro lugares em ordem: Local (esta função), Enclosing (uma função externa envolvendo esta), Global (o módulo), depois Built-in (nomes como len). Leitura percorre essa cadeia; atribuição sempre cria um local a menos que você diga o contrário. Essa assimetria é a fonte do bug: count += 1 contra um global sem uma linha global count lança UnboundLocalError, porque a atribuição marca count como local para toda a função e a leitura acontece antes de qualquer valor local existir. global name opta uma atribuição de volta para o nível do módulo; nonlocal name visa a função envolvente mais próxima em vez disso, que é como um closure atualiza uma variável da função que a criou. Trate ambos como um cheiro: uma função que alcança para mutar estado externo é mais difícil de testar do que uma que toma entradas e retorna saídas.

JunoEscopo Uma variável feita dentro de uma função vive apenas lá, o exterior não pode vê-la. Você pode ler uma variável externa de dentro, mas mudar uma precisa de uma linha global primeiro. Alcance isso quase nunca: passe valores, retorne resultados, e a função permanece simples de seguir.
JunoEscopo Nomes feitos dentro de uma função são locais e idos quando ela retorna. Ler um global é livre, escrever um precisa de `global name` ou Python faz um local que o encobre. Prefira argumentos dentro e `return` fora para que os efeitos de uma função permaneçam visíveis na chamada.
JunoEscopo LEGB para buscas, mas atribuição sempre faz um local a menos que você declare `global` ou `nonlocal`, que é por que `count += 1` em um global lança `UnboundLocalError`. Ambas as palavras-chave são um cheiro: uma função que muta estado externo é mais difícil de testar do que uma que toma entradas e retorna saídas.
python
def calculate():
    result = 42   # local para essa função
    return result

calculate()
print(result)   # NameError, result não existe aqui
python
count = 0

def increment():
    global count    # declare que quer modificar o global
    count += 1

increment()
print(count)   # 1

Usar global deve ser um último recurso. Torna o código mais difícil de raciocinar. Prefira passar valores dentro e retorná-los. Escopo é construído diretamente sobre como atribuição vincula um nome a um valor, coberto no capítulo Variáveis e tipos.

*args e **kwargs

Às vezes você não sabe quantos argumentos uma função receberá. *args coleta qualquer número de argumentos posicionais em uma tupla. **kwargs coleta qualquer número de argumentos nomeados em um dicionário. Os nomes args e kwargs são convenções; os asteriscos é que importam.

*args coleta argumentos posicionais em excesso em uma tupla. **kwargs coleta argumentos nomeados em excesso em um dict. Ambos podem ser combinados com parâmetros regulares. Parâmetros regulares vêm primeiro, então *args, depois parâmetros apenas nomeados, depois **kwargs. São úteis para funções envoltório que passam argumentos para outra função.

*args coleta os argumentos posicionais restantes em uma tuple, **kwargs coleta os nomeados restantes em um dict. A imagem de espelho acontece no local da chamada: func(*some_list) espalha uma lista em argumentos posicionais e func(**some_dict) espalha um dict em nomeados. Essa simetria é o que torna um envoltório pass-through funcionar, def timed(*args, **kwargs): return inner(*args, **kwargs) encaminha o que quer que tenha recebido sem nomear um único parâmetro. Útil, mas custa a assinatura legível: um envoltório que engole tudo em *args, **kwargs mostra chamadores e verificadores de tipo nada sobre o que aceita, então alcance-o quando você realmente precisa encaminhar argumentos arbitrários, não como a forma padrão para uma função comum.

python
def total(*args):
    return sum(args)

total(1, 2, 3)          # 6
total(1, 2, 3, 4, 5)   # 15
python
def display(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

display(name="Maria", score=87, level=5)

Você pode misturá-los com parâmetros regulares. Parâmetros regulares vêm primeiro:

python
def describe(title, *tags, **metadata):
    print(f"{title} | tags: {tags} | meta: {metadata}")

describe("Introdução a Python", "iniciante", "python", author="Maria", year=2024)
Juno*args e **kwargs*args recolhe qualquer número de argumentos posicionais extras como uma tupla, **kwargs recolhe nomeados extras como um dict. As palavras args e kwargs são apenas convenção, o * e ** fazem o trabalho real. Útil quando você não consegue dizer antecipadamente quantas coisas entram.
Juno*args e **kwargs*args coleta argumentos posicionais extras em uma tupla, **kwargs argumentos nomeados extras em um dict. Ordem na assinatura: params regulares, depois `*args`, depois apenas nomeados, depois `**kwargs`. Mais útil para envoltórios que encaminham argumentos direto para outra função.
Juno*args e **kwargs* e ** coletam em uma assinatura e se espalham em um local de chamada, que é o que permite um envoltório encaminhar tudo com inner(*args, **kwargs). O custo é uma assinatura que diz nada aos chamadores, então mantenha para pass-throughs reais, não funções comuns.

Docstrings

Uma docstring é uma string no topo de uma função que descreve o que ela faz. Editores Python e ferramentas a usam para mostrar ajuda quando você passa o mouse sobre uma chamada de função. Use aspas triplas e escreva uma linha para funções simples.

Uma docstring é a primeira coisa no corpo da função, uma string em aspas triplas. Ferramentas a leem: help(func) a imprime e seu editor a mostra quando você passa o mouse sobre uma chamada, então compensa exatamente quando você esqueceu o que a função faz. A convenção é uma linha de resumo, depois uma linha em branco e mais detalhes se necessário. Trate como obrigatório em qualquer coisa chamada de mais de um lugar, opcional em um auxiliar descartável cujo nome já diz tudo.

Uma docstring é a primeira declaração em um corpo de função, classe ou módulo, e deve dizer o que a função faz e o que retorna, não reafirmar a lista de parâmetros que um leitor já pode ver. Ela ganha seu lugar em qualquer coisa cujo contrato não é visível pela assinatura: os casos extremos, o que ela lança, o que uma entrada vazia faz. Pule o bloco formal argumento-por-argumento quando as anotações de tipo na assinatura já levam os tipos, esse detalhe pertence às dicas, não duplicado em prosa que sai da data. O objetivo é a linha que você gostaria de ler às 2 da manhã enquanto depura uma chamada que você escreveu seis meses atrás.

python
def normalise(value, min_val, max_val):
    """Escale um valor para o intervalo 0-1 dados o min e max conhecidos."""
    return (value - min_val) / (max_val - min_val)
python
def build_url(base, version, resource, *, secure=True):
    """
    Constrói uma URL de endpoint da API.

    Retorna uma string de URL totalmente qualificada. Se secure for False,
    a URL usará http em vez de https.
    """
    scheme = "https" if secure else "http"
    base = base.replace("https://", "").replace("http://", "")
    return f"{scheme}://{base}/{version}/{resource}"

Escreva uma docstring para qualquer função cujo propósito não seja claro pelo seu nome e assinatura.

JunoDocstrings Uma docstring é uma string em aspas triplas bem no topo de uma função, descrevendo o que faz. Seu editor a mostra quando você passa o mouse sobre a função depois, que é quando você ficará feliz que esteja lá. Escreva uma sempre que o nome e entradas não já tornem o propósito claro.
JunoDocstrings Primeira linha do corpo, aspas triplas: help() e seu editor a expõem ao passar o mouse. Uma linha de resumo cobre a maioria das funções, adicione detalhes abaixo apenas quando o comportamento precisar. Obrigatório em qualquer coisa chamada de vários lugares, pulável em um só liner cujo nome o diz tudo.
JunoDocstrings Diga o que a função faz, o que retorna e os casos extremos, não uma lista de parâmetros reafirmada que o leitor já pode ver. Deixe anotações de tipo levarem os tipos para você não estar mantendo os mesmos fatos duas vezes. Aim para a linha que você gostaria à 1 da manhã depurando uma chamada que você escreveu meio ano atrás.

Anotações de tipo

Anotações de tipo permitem que você anote quais tipos uma função espera e retorna. Python não as aplica em tempo de execução, mas editores as usam para pegar erros antes de você executar qualquer coisa. O -> antes dos dois-pontos especifica o tipo de retorno.

Anotações de tipo são documentação que ferramentas verificam. Editores e verificadores de tipo (mypy, pyright) as usam para pegar desvios de tipo antes do tempo de execução. Elas não têm efeito em tempo de execução em Python padrão. -> None é a anotação correta para funções sem valor de retorno. Para recipientes genéricos, use list[int], dict[str, int] (Python 3.9+).

Anotações de tipo não mudam nada em tempo de execução, Python é executado identicamente com elas ou sem elas. Seu valor inteiro é a camada fora do programa: um verificador de tipo (mypy ou pyright) lendo o código antes que seja executado, e seu editor sinalizando uma chamada ruim enquanto você a digita. Então uma dica que mente é pior que nenhuma dica, porque o verificador confia nela. Use list[int], dict[str, int] e str | None diretamente (sem importação de typing em Python moderno), e anote uma função que retorna nada como -> None, que também sinaliza que chamá-la em uma expressão (x = log(...)) é um erro. O pagamento chega em funções chamadas de muitos lugares: o verificador pega um chamador passando o tipo errado em vez disso superficial como uma falha confusa três frames de profundidade em tempo de execução.

python
def greet(name: str, score: int) -> str:
    return f"{name} scored {score}"
python
def log(message: str) -> None:
    print(f"[LOG] {message}")
python
def top_scores(scores: list[int], n: int) -> list[int]:
    return sorted(scores, reverse=True)[:n]

Anotações de tipo são opcionais mas valiosas em qualquer função que será chamada de múltiplos lugares. Elas são documentação que ferramentas podem verificar.

JunoAnotações de tipo Anotações de tipo anotam o que uma função toma e devolve, como name: str e -> str para o retorno. Python não as aplicará quando é executado, mas seu editor as lê e o avisa antes de você acertar executar. Pense nelas como notas que as ferramentas podem verificar.
JunoAnotações de tipo Dicas são documentação que um verificador de tipo pode verificar, sem efeito em tempo de execução. `-> None` é a anotação correta quando uma função retorna nada, e `list[int]` / `dict[str, int]` funcionam sem importação de `typing` em Python moderno. Vale a pena em qualquer coisa chamada de vários lugares.
JunoAnotações de tipo Dicas não fazem nada em tempo de execução, seu valor é o verificador e editor lendo-as, então uma dica que mente é pior que nenhuma. Use `str | None` e `list[int]` diretamente, `-> None` para nenhum retorno. A vitória é pegar um chamador de tipo errado antes de falhar três frames de profundidade.

Funções como valores

Funções em Python são valores, como strings ou números. Você pode atribuí-las a variáveis e passá-las para outras funções. É assim que sorted() aceita uma função key=.

Funções são objetos de primeira classe: elas têm um tipo (function), podem ser armazenadas em variáveis e coleções, e podem ser passadas como argumentos ou retornadas como valores. Essa é a base de funções de ordem superior como sorted(key=...), map() e filter().

Uma função é um objeto que você passa por referência, nunca uma cópia, então entregá-lo a outra função é barato. O padrão que cresce disso é o closure: uma função definida dentro de outra que ainda alcança as variáveis da função externa depois que aquela tem retornado. É como você constrói uma função configurada na mosca, um make_multiplier(3) que retorna uma função multiplicando por 3, com o 3 capturado e mantido. Duas cautelas em código real: um closure captura a variável, não seu valor no momento da captura, que pega pessoas construindo closures em um loop (todos veem o valor final do loop); e empilhar muitas funções retornadas torna caminhos de chamada difíceis de rastrear, então use closures onde leem claramente e alcance uma classe quando o estado cresce.

python
def double(x):
    return x * 2

def apply(func, value):
    return func(value)

apply(double, 5)   # 10

Passar funções como argumentos mostra-se constantemente com sorted(), map() e filter(). Você também verá em capítulo Lambdas e compreensões.

JunoFunções como valores Uma função é um valor, como uma string ou um número, então você pode armazená-lo em uma variável e entregá-lo a outra função. É exatamente o que permite que você passe uma função como `key=` para `sorted()`. Pareceu estranho para mim no começo, depois clicou e apareceu em qualquer lugar.
JunoFunções como valores Funções são objetos de primeira classe: atribua-as, armazene-as em uma lista, passe-as, retorne-as. Esse é o motor por trás de `sorted(key=...)`, `map()` e `filter()`, e por trás de cada callback que você conectará depois.
JunoFunções como valores Passar uma função é uma referência, não uma cópia, e um closure deixa uma função interna continuar alcançando as variáveis da externa depois que ela retorna. Ela captura a variável, não o valor, então construir closures em um loop morde toda vez. Quando o estado capturado cresce, uma classe lê melhor.

Na prática

Duas funções que funcionam juntas: letter_grade converte uma pontuação para uma letra, e summarise a chama para cada pontuação em uma lista:

python
def letter_grade(score: int) -> str:
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    else:
        return "F"

def summarise(scores: list[int]) -> None:
    total = sum(scores)
    avg = total / len(scores)
    grades = []
    for s in scores:
        grades.append(letter_grade(s))
    print(f"Média: {avg:.1f}")
    print(f"Notas: {', '.join(grades)}")

summarise([87, 92, 74, 65, 91])

Um formatador de log e um processador de arquivo que o usa, com um parâmetro padrão dry_run que previne efeitos colaterais a menos que explicitamente desabilitado:

python
def format_log(level: str, message: str) -> str:
    return f"[{level.upper():5}] {message}"

def process_file(path: str, dry_run: bool = True) -> bool:
    print(format_log("info", f"Processando {path}"))
    if dry_run:
        print(format_log("info", "Execução seca, nenhuma mudança feita"))
        return True
    return True

process_file("report.csv")
process_file("report.csv", dry_run=False)

Um normalizador de valor único e um normalizador de coluna construído sobre ele, com anotações de tipo e uma docstring. A função de coluna computa o intervalo uma vez e reutiliza a função escalar para cada item:

python
def normalise(value: float, min_val: float, max_val: float) -> float:
    """Escale valor para o intervalo 0-1 dados min e max conhecidos."""
    if max_val == min_val:
        return 0.0
    return (value - min_val) / (max_val - min_val)

def normalise_column(values: list[float]) -> list[float]:
    """Normalize uma coluna inteira de valores."""
    lo, hi = min(values), max(values)
    return [normalise(v, lo, hi) for v in values]

raw = [10.0, 25.0, 5.0, 40.0, 15.0]
print(normalise_column(raw))

Anotações de tipo servem dois propósitos aqui: elas documentam o que a função espera, e deixam um verificador de tipo pegar chamadores que passam uma lista de strings por engano.