Funções e módulos
Parâmetros, retorno, âmbito, argumentos e validação de funções pequenas.
Uma função recebe dados, calcula um resultado e pode devolvê-lo ao código que a chamou. Usa-a quando queres dar nome a um cálculo e aplicá-lo a entradas diferentes.
Devolver um resultado
Vamos calcular o preço de uma compra com desconto. O desconto é uma fração entre 0 e 1; 0.10 significa 10 por cento.
def total_compra(quantidade, preco, desconto=0):
if quantidade < 0 or preco < 0:
raise ValueError("Quantidade e preço devem ser não negativos")
if not 0 <= desconto <= 1:
raise ValueError("Desconto fora do intervalo [0, 1]")
return quantidade * preco * (1 - desconto)
print(total_compra(3, 2.5))
print(total_compra(3, 2.5, desconto=0.10))Dados de entrada
A saída é 7.5 e 6.75. O return termina a chamada e entrega o valor. Se uma função chega ao fim sem return, devolve None.
print mostra algo, mas não devolve esse valor. Uma função que apenas escreve 7.5 não permite somar o resultado a outra compra. Mantém o cálculo separado da apresentação quando precisas de reutilizar o valor.
Parâmetros e argumentos
Os nomes na definição, quantidade, preco e desconto, são parâmetros. Os valores fornecidos na chamada são argumentos.
total_compra(3, 2.5) usa posições. total_compra(preco=2.5, quantidade=3) usa nomes. O valor por defeito só se aplica quando omites o argumento. Não pode haver dois argumentos diferentes para o mesmo parâmetro.
Argumentos posicionais devem aparecer antes dos argumentos por nome. *args pode recolher argumentos posicionais adicionais num tuplo e **kwargs argumentos por nome num dicionário. Usa-os quando a quantidade variável faz parte do problema, não para evitar definir os dados necessários.
Número variável de argumentos e lambda
*valores recolhe os argumentos posicionais num tuplo. Uma chamada com uma lista só passa um argumento; *lista na chamada expande os seus elementos.
def somar(*valores):
return sum(valores)
print(somar(2, 4, 6)) # 12
print(somar(*[2, 4, 6])) # 12
**opcoes recolhe argumentos por nome num dicionário. A expansão **dicionario fornece argumentos pelos nomes das chaves. Usa nomes explícitos quando a função precisa sempre dos mesmos dados.
Uma expressão lambda x: x * x cria uma função que devolve uma única expressão. Pode servir como chave de ordenação ou argumento de map. map produz um iterador; list(map(...)) materializa os resultados.
quadrados = list(map(lambda x: x * x, [2, 3, 4]))
print(quadrados) # [4, 9, 16]
Para várias instruções, validação ou um cálculo que precisa de um nome claro, usa def.
Âmbito e mutabilidade
Um nome atribuído dentro da função é normalmente local à chamada. Não altera automaticamente um nome com o mesmo nome fora dela. Mas uma função que recebe uma lista pode alterar essa lista:
def acrescentar_zero(valores):
valores.append(0)
O chamador observa a alteração porque partilha o objeto. Se queres devolver outra lista, escreve return valores + [0]. Documenta qual das duas operações prometes.
Valores por defeito são criados quando a definição da função é executada. Por isso, evita uma lista como argumento por defeito:
def juntar(valor, valores=None):
if valores is None:
valores = []
valores.append(valor)
return valores
Agora cada chamada que omite valores cria uma lista nova. def juntar(valor, valores=[]): reutilizaria a mesma lista entre chamadas.
Ler e alterar um nome global
Uma função pode ler um nome definido no módulo. Uma atribuição dentro da função cria normalmente um nome local, mesmo quando existe um global com o mesmo nome. global declara que a atribuição deve alterar o nome do módulo.
limite = 10
def novo_limite():
limite = 20
return limite
print(novo_limite(), limite) # 20 10
Se acrescentares global limite antes da atribuição na função, o segundo valor passa a 20. Ler um nome local antes da sua atribuição pode causar UnboundLocalError. Passar o limite como parâmetro e devolver o novo valor torna a dependência visível e evita depender da ordem de chamadas.
Testar um contrato pequeno
Um contrato diz que entradas são válidas e que resultado deve ser devolvido. Para a média, vamos exigir uma sequência não vazia de números:
def media(valores):
if not valores:
raise ValueError("A média precisa de uma observação")
return sum(valores) / len(valores)
assert media([4]) == 4
assert media([2, 4, 6]) == 4
assert media([-2, 2]) == 0
try:
media([])
except ValueError:
print("Caso vazio rejeitado")
else:
raise AssertionError("O caso vazio devia causar ValueError")
print("Casos verificados")Dados de entrada
Os testes verificam uma observação, vários valores, sinais diferentes e o caso vazio. Não tratam a média de uma lista vazia como zero, porque zero seria um resultado numérico inventado.
Importar um módulo
Um módulo reúne nomes relacionados. import math permite usar math.sqrt(9). from math import sqrt permite usar sqrt(9). import numpy as np dá um nome curto ao módulo, não copia os seus dados.
Não chames ao teu ficheiro pandas.py ou math.py, porque pode esconder o módulo que queres importar. Num projeto com vários ficheiros, executa o programa a partir da pasta indicada e mantém os imports reproduzíveis.
Rever chamadas e retorno
Exercícios
Distinguir saída de retorno
Uma função executa print(5), sem return. Que valor recebe resultado = funcao()?
Primeira pista
Mais uma pista
Ver solução
resultado recebe None. A consola mostra 5 como efeito da execução.
- None. A função não devolveu explicitamente um valor.
- 5. 5 é escrito na saída, não devolvido.
- "5". A representação impressa não é o valor de retorno.
Erros frequentes
Confundir o que aparece no ecrã com o que pode ser usado no cálculo seguinte.
Justificar entradas inválidas
Define uma média que rejeita uma sequência vazia. Explica por que devolver zero no caso vazio altera o significado da função.
Primeira pista
Mais uma pista
Ver solução
Verifica a entrada e lança ValueError se estiver vazia. Para entradas válidas, devolve sum(valores) / len(valores). Zero seria a média real de alguns conjuntos e confundiria essas situações com dados inexistentes.
Confere a tua resposta:
- Defini a entrada válida como uma sequência não vazia de números.
- Verifiquei o caso vazio antes da divisão.
- Expliquei que zero é um resultado numérico, não uma ausência de observações.
Erros frequentes
Ignorar o contrato, dividir por zero ou acrescentar um elemento fictício.