Publicado em 20 de Maio de 2020 às 16:02

Última atualização em 3 de Novembro de 2020 às 18:39

Funções com valores padrão mutáveis: Qual é o problema?

python
Acompanhe esse conteúdo pelo YouTube

Observe a célula abaixo. O que você acha que esse bloco vai exibir como output?

def acrescenta_1(lista=[]):
    lista.append(1)
    return lista

print(acrescenta_1(), acrescenta_1())

O output é:

[1, 1] [1, 1]

Era o que você esperava? Quer entender o motivo de o resultado ser esse? Então é só seguir com a leitura.

Aproveitando que tenho a sua atenção já vou dizer que essa aula só existe por causa desse tweet. Recomendo entrar nesse link e acompanhar a discussão.

Agora vamos seguir com a aula.

A primeira coisa que vamos entender é por que as respostas erradas estão erradas. Como você deve ter reparado no tweet que inspira a aula de hoje, 52% das pessoas acharam que o output desse código seria [1] [1]. Uma possível linha de raciocínio que nos permite chegar nessa resposta é a seguinte:

  1. Chamo a função pela primeira vez e, com isso, uma lista vazia é criada;
  2. Adiciono um novo item a essa lista, no caso esse item é o número 1;
  3. Nesse exato momento minha lista está assim: [1], então a função print vai exibir exatamente isso;
  4. Chamo a função pela segunda vez e, com isso, uma nova lista vazia é criada;
  5. Adiciono um novo item a essa lista, no caso esse item é o número 1;
  6. Nesse exato momento minha lista está assim: [1], então a função print vai exibir exatamente isso;

Não se sinta mal por ter pensado dessa forma. O raciocínio faz sentido, mas chega na resposta errada, porque não é assim que o Python funciona!

30% das pessoas acharam que a resposta seria [1] [1, 1]. Uma possível linha de raciocínio que nos permite chegar nessa resposta é a seguinte:

  1. Chamo a função pela primeira vez e, com isso, uma lista vazia é criada;
  2. Adiciono um novo item a essa lista, no caso esse item é o número 1;
  3. Nesse exato momento minha lista está assim: [1], então a função print vai exibir exatamente isso;
  4. Chamo a função pela segunda vez e, dessa vez, ela vai usar a mesma lista criada na primeira chamada;
  5. Adiciono um novo item a essa lista, no caso esse item é o número 1;
  6. Nesse exato momento minha lista está assim: [1, 1], então a função print vai exibir exatamente isso;

Quase lá! Esse raciocínio chega muito perto da resposta correta, e mostra que entendemos como funcionam os objetos mutáveis no Python. Mas faltou um detalhe pra acertar…

Vamos discutir agora alguns fundamentos de Python necessários para chegar na resposta correta (e entendê-la). Não é bruxaria, não é o Python nos sacaneando. É simplesmente uma questão de entender como o Python funciona. Vamos lá:

Objetos mutáveis e suas armadilhas

Isso aqui é um clássico do Python e já pegou muita gente desprevenida.

Observe esse bloco de código e pense: Qual vai ser o valor das variáveis numeros_do_fabio e numeros_da_josi?

numeros_do_fabio = [1, 2, 3]
numeros_da_josi = numeros_do_fabio

numeros_da_josi.append(4)
print(f"Numeros escolhidos pela Josi: {numeros_da_josi}")
print(f"Numeros escolhidos pelo Fabio: {numeros_do_fabio}")

Output:

Numeros escolhidos pela Josi: [1, 2, 3, 4]
Numeros escolhidos pelo Fabio: [1, 2, 3, 4]

O resultado surpreendeu novamente, ou por essa você já esperava?

Para entender o que aconteceu nesse bloco de código precisamos entender como as variáveis funcionam no Python. No Python, variáveis não são caixas. Variáveis são nomes, rótulos, etiquetas, apelidos.

Nós aprendemos em algum momento da nossa vida a famosa analogia de que variáveis são caixas que armazenam objetos. Essa analogia é ótima e funciona muito bem para várias linguagens de programação. No Python, se variáveis fossem caixas que armazenam um objeto, o bloco de código acima retornaria algo diferente. A operação numeros_da_josi = numeros_do_fabio poderia ser entendida como a criação de uma nova caixa, e essa caixa armazenaria o valor atual da variável numeros_do_fabio, então teríamos duas caixas diferentes, e, portanto, alterações no conteúdo de uma não deveria afetar o conteúdo de outra.

Mas não é assim que as variáveis funcionam no Python. É melhor pensar em variáveis como nomes que damos para objetos. Ou apelidos. Ou rótulos. Ou etiquetas. Uma imagem exemplifica isso muito bem:

Variáveis no Python não são caixas:

Variáveis no Python são nomes, rótulos, etiquetas, apelidos:

Imagens retiradas deste site. É um artigo excelente. Recomendo estudá-lo quando possível.

Observações:

  1. É claro que variáveis são apenas variáveis. Quando dizemos que elas são nomes, ou caixas, ou qualquer outra coisa, é apenas uma analogia.
  2. Esse caso é especial porque estamos lidando com listas, que são objetos mutáveis! O exemplo abaixo não deve causar nenhuma estranheza ou confusão, e também não tem nenhuma pegadinha:
objeto_nao_mutavel_do_fabio = 42
objeto_nao_mutavel_da_josi = objeto_nao_mutavel_do_fabio

objeto_nao_mutavel_da_josi = 43
print(f"Objeto da Josi: {objeto_nao_mutavel_da_josi}")
print(f"Objeto do Fabio: {objeto_nao_mutavel_do_fabio}")

Output:

Objeto da Josi: 43
Objeto do Fabio: 42

Esse bloco de código também não deve causar nenhuma estranheza. A lista numeros_do_fabio não é modificada. Isso acontece porque apesar de as duas listas serem visualmente idênticas, elas são objetos diferentes, iniciados em momentos diferentes, ocupando lugares (endereços) diferentes na memória do computador.

numeros_do_fabio = [1, 2, 3]
numeros_da_josi = [1, 2, 3]

numeros_da_josi.append(4)
print(f"Numeros escolhidos pela Josi: {numeros_da_josi}")
print(f"Numeros escolhidos pelo Fabio: {numeros_do_fabio}")

Output:

Numeros escolhidos pela Josi: [1, 2, 3, 4]
Numeros escolhidos pelo Fabio: [1, 2, 3]

Nosso objetivo inicial com o código numeros_da_josi = numeros_do_fabio era ter dois objetos diferentes, apontando para duas listas diferentes (listas com valores iguais, mas objetos diferentes), podemos fazer isso com uma pequena modificação no código, basta utilizar o método copy:

numeros_do_fabio = [1, 2, 3]
numeros_da_josi = numeros_do_fabio.copy()

numeros_da_josi.append(4)
print(f"Numeros escolhidos pela Josi: {numeros_da_josi}")
print(f"Numeros escolhidos pelo Fabio: {numeros_do_fabio}")

Output:

Numeros escolhidos pela Josi: [1, 2, 3, 4]
Numeros escolhidos pelo Fabio: [1, 2, 3]

Até aqui nós entendemos como variáveis funcionam no Python (“nomes” em vez de “caixas”), e entendemos que saber esse fundamento nos permite não sermos surpreendidos pelo bloco que iniciou essa seção (onde equivocadamente achamos que estamos modificando apenas uma lista e acabamos modificando as duas, porque na verdade não tem duas listas, é só uma mesmo!)

Isso nos permite voltar para a função do início da aula:

def acrescenta_1(lista=[]):
    lista.append(1)
    return lista

Quando chamamos uma função sem parêntesis, o Python nos retorna uma referência a essa função:

>>> acrescenta_1
<function __main__.acrescenta_1(lista=[])>

Repare que a lista vazia é criada exatamente uma vez, no momento em que a função é definida.

Todas as vezes que executarmos essa função, a mesma lista será utilizada:

>>> acrescenta_1()
[1]
>>> acrescenta_1
<function __main__.acrescenta_1(lista=[1])>
>>> acrescenta_1()
[1, 1]
>>> acrescenta_1
<function __main__.acrescenta_1(lista=[1, 1])>

Como as funções funcionam

Agora temos o último fundamento que nos permite entender o comportamento do código que iniciou a aula:

Quando escrevemos print(acrescenta_1(), acrescenta_1()), estamos chamando a função print, certo?

O pulo do gato aqui é o seguinte: Antes de executar a função print, o Python primeiro executa as duas chamadas dentro do parêntesis! Isso não é uma particularidade da função print, e sim uma particularidade de funções no Python.

Na prática é como se isso aqui estivesse acontecendo:

lista_1 = acrescenta_1()
lista_2 = acrescenta_1()
print(lista_1, lista_2)

Como as duas variáveis lista_1 e lista_2 apontam para o mesmo objeto, é natural que o resultado do print seja esse, afinal, estamos printando duas vezes o mesmo objeto!

É exatamente isso: Estamos exibindo na tela duas vezes o mesmo objeto, acontece que esse objeto possui dois nomes/apelidos/rótulos.

Lembre-se disso:

Curiosidade: O pylint nos alerta sobre isso!

Faça esse teste! Crie um arquivo python com o código da função acrescenta_1. Você receberá o alerta Dangerous default value [] as argument pylint(dangerous-default-value).

Mas eu preciso usar uma lista vazia na minha função, qual é o procedimento correto?

Basta fazer essa pequena modificação na função:

def acrescenta_1(lista=None):
    if lista is None:
        lista = []
    lista.append(1)
    return lista

Agora sim, com o código escrito dessa forma, uma lista nova será criada todas as vezes que chamarmos a função (e não passarmos uma lista como argumento do parâmetro lista)!

Então, com essa modificação, você consegue adivinhar o output do código abaixo?

print(acrescenta_1(), acrescenta_1())

É [1] [1].

Existe algum caso em que usar um valor mutável padrão como parâmetro seja útil?

Sim! Já ouviu falar de memoization? Observe esse exemplo:

def enesimo_elemento_fibonacci(n, fibonacci_cached={0: 0, 1: 1}):
    if n not in fibonacci_cached:
        fibonacci_cached[n] = (
            enesimo_elemento_fibonacci(n - 1)
            + enesimo_elemento_fibonacci(n - 2)
        )
    return fibonacci_cached[n]

>>> enesimo_elemento_fibonacci(10)
55
>>> enesimo_elemento_fibonacci
<function __main__.enesimo_elemento_fibonacci(n, fibonacci_cached={0: 0, 1: 1, 2: 1, 3: 2, 4: 3, 5: 5, 6: 8, 7: 13, 8: 21, 9: 34, 10: 55})>

Observações:

  1. Embora escrever uma função que calcule o enésimo elemento da sequência de Fibonacci de forma recursiva seja muito elegante, obtemos uma performance melhor quando escrevemos de forma iterativa. Isso é assunto para outro artigo;
  2. O Python tem um decorator (@lru_cache) que pode ser usado para memoization;
  3. Esse exemplo foi feito com um dicionário em vez de uma lista, mas isso não é um problema: Listas e dicionários são objetos mutáveis no Python;
  4. Os códigos dessa seção e da anterior são inspirados neste artigo da documentação do Python.

Lição de casa

  1. Crie uma função parecida com a função inicial, avalie o seu comportamento conforme ela vai sendo chamada. Faça ela com listas, sets (conjuntos) e dicionários.

  2. Você conhece a função id()? Crie objetos mutáveis e “brinque” com essa função.


Compartilhe

Gostou deste artigo? Compartilhe para esse conteúdo chegar a mais pessoas!

Não perca nenhum conteúdo!

Se você quiser receber um e-mail sempre que eu postar um artigo novo, basta se inscrever aqui:


Artigos que podem ser do seu interesse

15 de Novembro de 2020

Python + VS Code + Black: Como formatar o seu código Python no VS Code automaticamente usando Black

Um passo a passo ensinando a configurar o VS Code para formatar código Python automaticamente.

python

11 de Setembro de 2020

Tutorial Django - Cadastro e login de usuários apenas com e-mail e senha

Aprenda a fazer o cadastro e login de usuários apenas com e-mail e senha.

python, django

11 de Setembro de 2020

Tutorial Django - Criando um modelo de usuário personalizado (Custom User Model)

Aprenda a criar um modelo de usuário personalizado nos seus projetos Django.

python, django

20 de Abril de 2020

Resenha - Think Python (Pense em Python)

Meus comentários sobre esse livro.

python, resenhas