DEV Community

Gustavo Soares
Gustavo Soares

Posted on • Updated on

Python: Funções

Não é de hoje que códigos são escritos e reescritos constantemente. Quando programamos, nos vemos numa situação onde o mesmo trecho de código se aplica a diversas situações e, de fato, gostaríamos que assim fosse, sem ter que escrever tudo de novo o tempo todo. Aí entram as funções.

Funções são blocos nomeados de instruções que podem ser chamados e utilizados quantas vezes forem necessários, permitindo à pessoa programadora repetir o mesmo código sem a necessidade de escrevê-lo do zero.

Uma outra explicação para funções em linguagens de programação seria compará-las ao conceito matemático. Para uma função f(x)f(x) , existe um conjunto LL finito de valores tal que

f:LImf,xL f:L\xrightarrow{}Im_f, x\in L

Sendo ImfIm_f o conjunto de possíveis valores que ff retornará.

As funções em Python se comportam da mesma forma e você pode criá-las através da palavra reservada def, seguida do nome da função e seus parênteses (). Além disso, funções podem retornar valores através da palavra reservada return. Nenhuma instrução da função abaixo do após o retorno será lida, então tome cuidado com seu uso.

# def <nome_da_funcao>(<parametro_1>, <parametro_2>, ...)
def faz_algo():
        #conteúdo
Enter fullscreen mode Exit fullscreen mode

No trecho acima, declaramos a função faz_algo, seguida de dois pontos (:). Como Python usa indentação como marcador para distinguir escopos, utilizar dois pontos diz ao interpretador que aqui estamos criando um novo escopo para a função. Portanto, além de indentar corretamente, não se esqueça dos dois pontos.

Funções são popularmente conhecidas como abstrações de código. De fato, elas nos poupam de muita computação desnecessária, como por exemplo a função print(), que escreve uma mensagem no console. E se ela não existisse na linguagem? Eu não gosto nem de pensar nessas coisas.

Dito isso, vamos criar uma função que retorna a soma de 2 mais 2:

# def <nome_da_funcao>()
def soma_dois():
    return 2 + 2

print(soma_dois())
Enter fullscreen mode Exit fullscreen mode

Ao rodar o script, receberemos o inteiro 4 como resposta e assim encapsulamos uma operação de soma do número 2 com ele mesmo. Mas e se quiséssemos criar uma função que somasse qualquer número com outro qualquer?

Passagem de Parâmetros

Comumente nos deparamos com a necessidade de usar valores externos ao escopo da função dentro dele, mas não seria prático criar uma abstração de código que depende de valores externos. Para tal, temos a passagem de parâmetros a funções.

Parâmetros são variáveis locais especificadas entre os parênteses (), no cabeçalho da função, que recebem cópias de valores externos ao escopo da função. Funções podem receber inúmeros parâmetros de diferentes tipos, contanto que sejam separados por vírgula. Como Python é uma linguagem dinamicamente tipada, não há a necessidade de explicitar os tipos na declaração de parâmetros, ou seja, eles serão descobertos em tempo de execução.

Considerando o exemplo de soma acima, façamos com que ele aceite todo tipo de inteiros para somar e não só o número 2;

# parâmetros a e b declarados dentro dos parênteses e
# separados por vírgula
def soma_dois(a, b):
    return a + b

# invocação da função, passando seus dois parâmetros
print(soma_dois(1, 6))
print(soma_dois(10, 2))
print(soma_dois(5, 20))
Enter fullscreen mode Exit fullscreen mode

E o output:

$> python3 <nome_do_arquivo>.py
7
12
25
Enter fullscreen mode Exit fullscreen mode

Como foi dito anteriormente, quando declaramos uma função, também declaramos um escopo. O escopo pode ser definido como um espaço léxico delimitante aos quais instruções estão associadas. Um exemplo disso são os parâmetros há pouco expliccados. Se os parâmetros são variáveis locais de uma função, então estão vinculados ao escopo e deixarão de existir na memória no fim da mesma. O mesmo serve para variáveis definidas dentro da função.

Valores Padronizados para argumentos

Podemos passar um valor base para um parâmetro contando que ele substitua o valor passado na invocação da função, assim, omitindo ele. Veja o exemplo a seguir:

def falar(mensagem, t=1):
    print(mensagem * t)

falar("Olá, Mundo!")
falar("Olá, Mundo!", 5)
Enter fullscreen mode Exit fullscreen mode

A função falar recebe dois parâmetros na sua declaração: uma mensagem a ser escrita e a quantidade de vezes que a mensagem será escrita. No segundo parâmetro há uma atribuição de valores, o que nos indica que, logo na declaração, um valor já é atribuído a esse parâmetro, excluindo a necessidade de passar um valor na invocação da função. Graças a esse fato, as duas invocações da função são válidas e rodam. Veja o output:

$> python3 <nome_do_arquivo>.py
Olá, Mundo!
Olá, Mundo!Olá, Mundo!Olá, Mundo!Olá, Mundo!Olá, Mundo!
Enter fullscreen mode Exit fullscreen mode

Passagem por Nomenclatura

Se você tiver uma função com diversos parâmetros e quiser especificar um ou mais deles, então você invocar a função e especificar esses parâmetros pelo nome deles em vez da ordem de passagem.

Normalmente, quando invocamos uma função, usamos a passagem posicional de parâmetros, que leva em consideração a ordem em que foram declarados no cabeçalho da função. Na passagem por nomenclatura, o que é considerado é o nome do parâmetro. Observe o exemplo abaixo:

def mostra_valor(a, b=5, c=10):
    print('a é', a, 'e b é', b, 'e c é', c)

mostra_valor(3, 7)
mostra_valor(25, c=24)
mostra_valor(c=50, a=100)
Enter fullscreen mode Exit fullscreen mode

E o seu output:

$> python3 <nome_do_arquivo>.py
a é 3 e b é 7 e c é 10
a é 25 e b é 5 e c é 24
a é 100 e b é 5 e c é 50
Enter fullscreen mode Exit fullscreen mode

A função mostra_valor tem somente um parâmetro sem valor padrão, seguido de outros dois com valores padrão. Na primeira invocação mostra_valor(3, 7), o parâmetro a recebe 3, b recebe 7 e c mantém seu valor padrão, pois não foi passado nenhum outro parâmetro. Na segunda invocação mostra_valor(25, c=24), o parâmetro a recebe o valor 25 devido ao fator posicional da passagem de valores, b mantém seu valor padrão e c recebe 24 graças à passagem por nomenclatura. Na terceira invocação mostra_valor(c=50, a=100), o parâmetro c recebe 50 por nomenclatura, a recebe 100 também por nomenclatura e b mantém seu valor padrão.

Recursão

De forma rasa e informal, a recursão na programação pode ser considerada como a repetição de um processo e, portanto, pode ser definida como um processo que chama a si mesmo direta ou indiretamente até que alcance uma condição de término. Mas como que esse processo se repete? É um laço de iteração?

A recursão se aplica diretamente a funções, pois é o único bloco de processamento que pode chamar a si mesmo durante sua execução. Uma função recursiva em sua integridade aplica um algoritmo de Divisão e Conquista (O que é Divisão e Conquista?), que consiste em dividir de forma recursiva um problema grande em problemas menores até que o problema seja resolvido. Ou seja, a função sempre vai retornar ela mesma com uma versão mais simples do problema até chegar na solução.

Consideremos que você, pessoa programadora, tem que determinar a soma dos nn primeiros números naturais ( N\N ). Existem diversas maneiras de fazer isso, mas a mais simples seria sair somando os números de 1 até nn , parecendo com isso:

f(n)=1+2+3+4+...+n f(n)=1+2+3+4+...+n

Imagina se nn for um número na casa dos milhares. Bastante trabalhoso, né? Nesse caso temos a opção recursiva para resolver esse problema. Veja a seguir:

f={f(n)=1se n=1n+f(n1)se n>1 f = \begin{cases} f(n) = 1 & \text{se } n=1 \\ n + f(n-1) & \text{se } n \gt 1 \end{cases}

A única diferença entre os dois métodos é que a função ff está sendo chamada dentro de sua própria função, estabelecendo uma condição de recursão.

Ainda ficou confuso? Observe os dois exemplos abaixo, onde um implementa um for..in loop e o outro, a recursão:

def sequencia_decolar(contagem):
    for numero in range(contagem, -1, -1):
        if numero == 0:
            print("Decolar!!!")
        else:
            print(numero)

sequencia_decolar(5)
Enter fullscreen mode Exit fullscreen mode

No trecho acima, criamos uma contagem regressiva para a decolagem de um foguete com o for e um laço condicional para caso a contagem chegue a 0. Agora veja esse mesmo caso com outros olhos.

def sequencia_decolar(contagem):
    if contagem == 0:
        print("Decolar!!!")
        return
    else:
        print(contagem)
        return sequencia_decolar(contagem - 1)

sequencia_decolar(5)
Enter fullscreen mode Exit fullscreen mode

Onde a variável local contagem representa de onde a contagem deve começar.

No exemplo recursivo vimos que o retorno sequencia_decolar(contagem - 1)
é o mesmo problema, porém simplificado, o que caminha para uma pilha de
funções onde a anterior, mais complexa, não será resolvida até que a
próxima, mais simples, seja. Por exemplo, se a variável contagem for 5, teremos esse comportamento:

## Invocação da função sequencia_decolar(5);

5 é igual a 0? Não.
Então imprima "5" no terminal e retorne sequencia_decolar(5 - 1);

4 é igual a 0? Não.
Então imprima "4" no terminal e retorne sequencia_decolar(4 - 1);

3 é igual a 0? Não.
Então imprima "3" no terminal e retorne sequencia_decolar(3 - 1);

2 é igual a 0? Não.
Então imprima "2" no terminal e retorne sequencia_decolar(2 - 1);

1 é igual a 0? Não.
Então imprima "1" no terminal e retorne sequencia_decolar(1 - 1);

0 é igual a 0? Sim.
Então imprima "Decolar!!!" e retorne vazio;
Enter fullscreen mode Exit fullscreen mode

Tipos de Recursão

Entendemos como a recursão funciona, agora entenderemos onde cada tipo de recursão se encaixa.

Como mencionado anteriormente, a recursividade pode aparecer direta ou indiretamente:

  • Forma direta: É formada pela mesma estrutura de comandos e uma chamada a si mesma durante seu bloco de execução.
  • Forma indireta: Nesse caso podem existir n funções e todas dependem de todas, gerando uma cadeia de dependências
    até que a condição de término seja atingida. Veja o exemplo abaixo:

    def impar(n):
        if n == 0:
            return False
        else:
            return par(n - 1)
    
    def par(n):
        if n == 0:
            return True
        else:
            return impar(n - 1)
    
    print(odd(5))  # Output: True
    

    No exemplo acima, odd() chama even() e even() chama odd(). Isso cria uma recursão indireta, pois a função odd() não chama a si mesma diretamente, mas chama a função even(), que por sua vez chama odd() novamente. A recursão continua até que a condição de parada seja alcançada. Neste caso, a recursão termina quando o valor de n é zero. Tudo que o exemplo faz é dizer se um número é par ou ímpar através de recursão.

Memória na Recursividade

Sabemos que a recursividade aumenta bastante a legibilidade do nosso código, mas nem tudo é um mar de rosas, principalmente quando se trata de memória na recursão. Como será que funciona a recursividade por debaixo dos panos?

Numa função recursiva, enquanto a chamada não atinge a condição de término, empilhamos uma invocação na pilha. Quando a condição de término é atingida, as funções empilhadas são executadas uma por uma, da última empilhada até a primeira, até que não haja mais função a ser executada.

Quando comparadas com laços de iteração, funções recursivas consomem mais memória que laços, visto que cada chamada à função consome mais memória na stack e um loop não requer espaço extra. Dito isso, pense bem antes de aplicar recursividade no seu projeto, principalmente se desempenho for um fator crucial.

Alguns pontos a serem ressaltados ao comparar as duas situações são:

  • Iterações terminam quando uma condição se torna falha. As recursões, quando se chega no caso mais básico;
  • Iterações não alocam espaços extras na stack;
  • Recursões fornecem maior legibilidade ao código. Uma vez que se compreende o processo, fica fácil proceder;

Top comments (0)