# Funções e módulos

Parâmetros, retorno, âmbito, argumentos e validação de funções pequenas.

Página: https://resumos.rgo.pt/cadeiras/ct-iadp/funcoes/

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.

```python
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))
```

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:

```python
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")
```

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

Uma função pode produzir saída e ter outro retorno.

Mais uma pista

Sem return explícito, Python devolve None.

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.

[Voltar à explicação](https://resumos.rgo.pt/cadeiras/ct-iadp/funcoes/#devolver-um-resultado)

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

A média divide a soma pelo número de observações.

Mais uma pista

Sem observações, o denominador seria zero.

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.

[Voltar à explicação](https://resumos.rgo.pt/cadeiras/ct-iadp/funcoes/#testar-um-contrato-pequeno)
