Funções

Em linguagens de programação, funções se comportam como black boxes. Nós não precisamos de saber como elas funcionam, basta saber o que elas esperam de entrada, e o que elas nos dão como saída/ resultado. Pense um carro; não precisamos saber como ele funciona para dirigí-lo. O mesmo vale para um computador ou quase qualquer outra ferramenta mais complexa que um martelo. Nós abstraímos o objeto, o que importa é sua função (o que ele faz) e como usá-lo, e não como ele funciona ou é criado.

black-box.png
Figura 1: Uma blackbox (Imagem por Krauss, sob licença creative commons)

Além de tornar objetos mais fáceis de se usar (imagine precisar saber como um computador funciona para usá-lo!), abstrair também facilita a evolução tecnológica. Dado que você já sabe dirigir, a fabricante pode mudar o modelo de um mesmo carro para usar um motor elétrico ao invés de um de combustão interna, e você não precisa de aprender nada para adquirir e usar o modelo novo.

Em linguagens de programação, funções também são formas de separar problemas. Tomemos novamente o exemplo de um carro: ele é formado por diversos componentes (peças, sistemas, etc.) Em uma fabricante de veículos há responsáveis pelo carro em geral, mas a maior parte do trabalho é feito por times especializados nas suas partes/componentes. Um engenheiro trabalhando em um motor mais eficiente não precisa de conversar com um que esteja desenhando uma carroceria mais aerodinâmica, desde que o trabalho de cada um seja bem limitado/especificado – afinal, o motor precisa caber na carroceria.

Separar um problema/produto/programa em componentes menores não só tem o potencial de melhorar a eficiência de sua resolução/produção/escrita, mas às vezes é o que a viabiliza.

1. Definindo funções

No nosso aprendizado Python até aqui, já usamos diversas funções: print, range, input. Imagine se todas as vezes que quiséssemos mostrar alguma coisa na tela, nosso código precisasse de reescrever (reimplementar) a função print. Felizmente, basta sabermos que print existe e como usá-la; não precisamos repetir código (e evitamos todos os problemas que isso acarreta).

Ao contrário do caso de print, todo o código que escrevemos até aqui precisaria ser repetido se quiséssemos utilizá-lo novamente. Usando nosso código anterior, se quisermos calcular dois números da sequência de Fibonacci e multiplicá-los, precisamos de repetir o código duas vezes, colocando os números em variáveis diferentes, e então multiplicá-las. Para facilitar o processo e não repetir código, bastaria definir uma função nthfibo (de n-ésimo Fibonacci) e chamá-la duas vezes:

def nthfibo(n):
    # calcula o n-ésimo número da sequência de Fibonacci
    pass # pass é uma keyword que não faz nada; é só um placeholder
         # para o código que ainda vamos escrever

nthfibo(4) * nthfibo(5)

O exemplo acima já nos dá uma ideia da sintaxe usada para definir funções. A keyword def inicia a definição; em seguida vem o nome da função (a nomenclatura deve seguir as mesmas regras de nomes de variáveis), e em seguida os nomes de seus valores de entrada (opcionais, também chamados de argumentos ou parâmetros) entre parênteses e separados por vírgulas, e então dois pontos (:). Na linha seguinte vem um bloco de código indentado, que é o corpo da função. Esse corpo é executado quando chamamos a função.

No exemplo nthfibo, o nome da função é (obviamente) nthfibo, a função possui somente um argumento n que devemos fornecer ao chamar a função. Para chamar uma função escrevemos seu nome seguido de parênteses, com seus argumentos entre os parênteses e separados por vírgula. Note que no exemplo as chamadas de nthfibo(4) e nthfibo(5) estão fora do corpo da função.

1.1. Exercício: função nthfibo

Escreva função nthfibo (pode usar o código do exercício anterior), e calcule o décimo número da sequência fazendo a soma do resultado de duas chamadas de nthfibo; Lembre-se: \[F_n = F_{n-1} + F_{n-2} \qquad (n > 2)\]

2. ‘Retornando’ resultados

Você provavelmente teve problemas respondendo o exercício anterior. Em Python (e em quase todas as outras linguagens de programação), é preciso especificar o resultado da função usando a keyword return. Compare o resultado de chamar as duas funções abaixo:

def nothing():
    pass
def two():
    return 2
print(nothing())
print(two())

Quando uma função não possui a keyword return especificando seu resultado/saída, ela não retorna nada. Em Python, nada é representado por None. Poderíamos escrever a função nothing mais explicitamente:

def nothing():
    # equivalente à definição anterior
    return None

None não parece muito útil, mas na verdade tem seus usos. Por exemplo, você provalmente já tentou logar em algum site em que você não se lembrava do seu nome de usuário ou o email que usou; ao entrar os seus dados e receber a mensagem de que aquele usuário/email não estava castrado no sistema, algum código no servidor do site provavelmente retornou None (ou null, como costuma se chamar em outras linguagens).

Importante: uma função só retorna uma vez. Após retornar, ela não faz mais nada. Você pode até escrever código depois de um return, mas ele não será executado (tente!).

2.1. Exercício: função nthfibo (second time is the charm!)

Defina a função nthfibo usando return. Note como você usa mais de um return, correspondendo aos ‘casos possíveis’ da função.

3. Escopo

Até aqui nós temos escrito programas pequenos, mas programas reais podem ser muito longos (SQLite — uma base de dados que provavelmente está presente no seu computador e no seu celular — tem por volta de 238 mil linhas de código; o programa que implementa a linguagem Python está se aproximando de um milhão de linhas de código). Em programas desta escala, até mesmo não repetir nomes de variáveis passa a ser um problema. Felizmente, esse é um não-problema, dado que a maioria das variáveis tem um escopo definido.

Por exemplo, ao definir nthfibo, escolhemos n como o nome de seu único argumento. Ao invocarmos nthfibo(10), a variável n assume o valor 10 dentro do corpo da função nthfibo, mas seu valor fora dela (se houver) continua o mesmo. Veja:

n = 42
print(n)
nthfibo(10)
print(n)

A lógica é a seguinte: apesar de já existir uma variável n ‘fora’ de nthfibo, a definição de nthfibo redeclara uma variável n com escopo local; para todos efeitos, esta é uma nova variável, que nada interfere com a outra variável declarada no escopo de fora de nthfibo.

Apesar de não ser possível (normalmente) modificar variáveis de um escopo exterior de uma função, a função pode acessar estas variáveis (desde que seus nomes não conflitem com nomes internos, como é o caso do exemplo anterior entre n e nthfibo). Veja:

pi = 3.14159

def sphere_area(r):
    return 4 * pi * r**2

def sphere_volume(r):
    return 4/3 * pi * r**3

Se precisarmos de modificar uma variável de um escopo externo com um valor calculado por uma função, basta retornar o valor e fazer o assignment.

def increment(n):
    return n + 1

n = 5
increment(n)
print(n)
n = increment(n)
print(n)

4. Funções como argumentos de funções

Note que nada impede que funções sejam argumentos de outras funções. Podemos definir uma função map_sum:

def map_sum(f, n):
    total = 0
    for i in range(1, n + 1):
        total = total + f(i)
    return total

def add_one(n):
    return n + 1

map_sum(add_one, 10)

4.1. Exercício: funções de alta ordem

É importante treinar a leitura de código tanto quanto sua escrita. O que faz a função map_sum? Qual é o resultado de map_sum(nthfibo, 10) e o que ele representa?

5. Recursão

5.1. Exercício: Fibonacci, recursivamente

Até aqui, calculamos a sequência de Fibonacci usando loops. Mas a definição matemática da sequência sugere uma implementação mais simples e direta, mas que só é possível com chamadas de funções. Tente escrever essa versão, chamando-a de nthfibo_rec. Para todo inteiro \(n\) maior que zero, deve valer que nthfibo(n) == nthfibo_rec(n).

droste.jpg
Figura 2: “To understand recursion, first you have to understand recursion”

Note que enquanto blackboxes, tanto a versão recursiva quando a versão que usa loops (chamada iterativa) são equivalentes, pois tem as mesmas entradas e saídas. Mas do ponto de vista computacional, existe uma diferença entre elas: compare a execução das duas versões para \(n = 35\). E para \(n = 10000\).

6. Retornos múltiplos

Uma função pode retornar mais de um valor, basta separar os valores a serem retornados por vírgulas:

def nthfibo_rec(n):
    if n == 1:
        return 0, 0
    elif n == 2:
        return 1, 0
    elif n > 2:
        prev, prevprev = nthfibo_rec(n - 1)
        return prev + prevprev, prev

Teste esta versão de nthfibo_rec(n) com \(n = 35\) e \(n = 10000\). (Ignore o fato de que ela retorna tanto o n-ésimo e o n-1-ésimo números da sequência.)

7. Argumentos opcionais

Como vimos com range, uma função Python pode ter argumentos opcionais. Isto é útil quando (assim como no caso de range!) há um valor ‘padrão’ para um argumento, que mesmo assim por vezes queremos alterar. Por exemplo, ao definir uma função que calcula o logaritmo de um número, podemos ter dois argumentos: o primeiro (obrigatório) seria o número do qual queremos o logaritmo, e o segundo (opcional) seria a base do logaritmo (cujo valor padrão pode ser a base natural \(e\) se quisermos agradar matemáticos, ou \(2\) se quisermos agradar programadores).

def logaritmo(n, b=2):
    pass

logaritmo(8) # 3
logaritmo(8, 8) # 1

7.1. Exercício: logaritmo

Podemos usar uma versão do método de Newton para implementar uma função que calcule o logaritmo de um número maior que 1 em uma certa base. Para podermos usar o método, precisamos de:

  1. ter um limite inferior e um superior para valor que queremos calcular;
  2. dada uma possível solução (um chute), ter uma forma de verificar o quão bom é esse chute;

Por exemplo, para o caso da raiz quadrada de um número maior que 1, temos:

  1. a raiz de um número está entre 1 (limite inferior para a solução) e o número (limite superior para a solução);
  2. para ver o quão bom é um chute \(c\), basta calcular a distância de entre \(c^2\) e o número em questão (\(|n - c^2|\));

Preenchidos esses dois requisitos, e dado um chute/solução potencial, basta verificar se o chute é bom ou não. Se ele for muito próximo do resultado real, paramos e propomos o chute como solução final. Caso contrário, iremos refinar o chute até que ele fique próximo o suficiente do valor que queremos.

Uma forma de fazer isso é a seguinte: para obter um chute, usamos a média do limite inferior e superior. Se o chute for menor do que o valor que queremos, definimos o chute como novo limite inferior, e tentemos de novo; se o chute for maior, definimos o chute como novo limite superior, e tentemos de novo. Paramos quando o chute for arbitrariamente próximo do valor que buscamos (por exemplo, a diferença entre a raiz verdadeira e o nosso chute é menor do que 0.001).

Por exemplo, para o caso da raiz quadrada de um número \(n\) maior que 1, nossos limites iniciais são \(1\) e \(n\). O nosso primeiro chute \(c\) será \(\frac{n + 1}{2}\). Verificamos se o chute é bom: se \(c^2\) for muito próximo de \(n\) (por exemplo, se \(|n - c^2| < 0.05\)), retornamos \(c\) como solução. Caso contrário, tomamos \(c\) como novo limite (inferior ou superior, a depender de \(c^2\) ser maior ou menor que \(n\)), e calculamos um novo chute \(c'\) usando o mesmo método (média aritmética dos limites inferior e superior). Fazemos com \(c'\) a mesma coisa que com \(c\), até que cheguemos num chute bom o suficiente.

Neste exercício, implemente este método (ou um melhor) para calcular o logaritmo de um número maior do que 1. A função logarithm deve ter três argumentos, n para o número do qual queremos calcular o algoritmo, b para a base do logaritmo (opcional, valor padrão 2), e tol (opcional, valor pardrão 0.01). tol é a tolerância, isto é, o quão próximo o chute deve ser para julgarmos que ele serve de resposta. Use a função abs (que retorna o valor absoluto de um número) para facilitar o cálculo da proximidade do chute.

Antes de começar a programar, pense sobre o quê seriam os passos 1 e 2 para logaritmo (e compare com os passos 1 e 2 para a raiz quadrada). Se achar mais fácil, implemente a raiz quadrada antes de implementar o logaritmo, pois há muitas partes comuns entre os dois exercícios. Note que há várias respostas possíveis, qualquer uma delas é válida, mas algumas respostas são mais rápidas de calcular ou mais fáceis de programar.

7.2. Exercício: taxa de performance funcional

Calcule a taxa de performance a ser paga caso um fundo de investimento bata seu benchmark e o valor final líquido do investimento. (Você pode usar a resposta do exercício anterior como base.) A função deve receber 4 argumentos: o valor inicial do investimento, o valor final bruto do investimento, a valorização percentual do benchmark (4.5 é 4.5% de valorização), e (como argumento opcional) o percentual da taxa de performance (o padrão deve ser 20, significando uma taxa de 20%). Se o fundo não superou o benchmark (ou teve perdas), a taxa de performance a ser paga deve ser zero, e o valor final líquido deve ser igual ao valor final bruto.

Bruno Cuconato / 2024-08-07
TOP | HOME
Computação FGV/EPGE