Cuidado com parâmetros mutáveis em funções em Python

Mutabilidade em Python

Python tem objetos mutáveis e imutáveis. Os mutáveis contêm estado interno, como atributos, que podem ser alterados durante sua existência. Já os imutáveis não podem ser alterados e seu estado pode ser definido somente em sua inicialização.

Exemplos

Mutáveis Imutáveis
list tuple
dict str, unicode, bytes
set frozenset
quaisquer outros objetos que permitem alteração de atributos int, float, complex

Abaixo seguem alguns exemplos de código mostrando sobre a imutabilidade de strings e inteiros.

# Objetos imutáveis
>> name = 'Alan'
>> print(id(name))
140075435684056
>> name = 'Alan' + ' Turing'
>> print(id(name))
140075435675376
>> number1 = 666
>> number2 = number1
>> print(id(number1))
140594481506000
>> print(id(number2))
140594481506000
>> number1 += 1
>> print(id(number1))
140594481506384
>> print(id(number2))
140594481506000

# Objetos mutáveis
>> l = []
>> print(id(l))
140594479909384
>> l.append(789)
>> print(id(l))
140594479909384
>> l
[789]

Note que o endereço de memória foi alterado para os objetos imutáveis, quando alterei os seus valores, eles se tornaram novos objetos.

O perigo de definir funções com parâmetros mutáveis como padrão

Para entender o problema vamos definir uma classe chamada Instrutor, que receberá o nome do instrutor e uma lista de preços dos cursos do instrutor, caso não seja fornecido será definido uma lista vazia como padrão.

from typing import List


class Instrutor:
     def __init__(self, nome: str, precos_cursos: List[float] = []):
         self.nome = nome
         self.precos_cursos = precos_cursos

     def adiciona_preco(self, novo_preco: float):
         self.precos_cursos.append(novo_preco)

A classe também tem um método para adicionarmos preços de novos cursos. Vamos criar um instrutor, chamado Alan Turing e adicionar alguns preços de cursos para ele.

>> paizao = Instrutor('Alan Turing')
>> paizao.adiciona_preco(999999.99)
>> paizao.adiciona_preco(77777)
>> paizao.precos_cursos
[999999.99, 77777]

Ótimo, tudo funcionando até agora, vamos criar uma instrutora chamada Grace Hopper, e assim como fizemos para o nosso senhor Alan, vamos adicionar alguns preços para ela.

>> gracinha = Instrutor('Grace Hopper')
>> gracinha.adiciona_preco(8888)
>> gracinha.adiciona_preco(5555555)

Vamos ver a lista de preços dela.

>> gracinha.precos_cursos
[999999.99, 77777, 8888, 5555555]

Acontece que os preços do Alan estão juntos com os da Grace. Vamos ver os endereços de memória.

>> print(id(paizao.precos_cursos))
139726721872392
>> print(id(gracinha.precos_cursos))
139726721872392

Pelo endereço de memória nota-se que os dois preços são o mesmo objeto. Isso ocorre porque os parâmetros default são criados quando a função é definida e não quando ela é chamada. Se criarmos um terceiro instrutor passando uma lista como parâmetro, não teremos esse problema.

>> alonzo = Instrutor('Alonzo Church', [1111])
>> print(alonzo.precos_cursos)
[1111]
>> print(id(alonzo.precos_cursos))
139726721393928
>> print(id(paizao.precos_cursos))
139726721872392
>> print(id(gracinha.precos_cursos))
139726721872392

Resolvemos isso não utilizando parâmetros mutáveis como padrão, basta fazer uma pequena alteração na nossa classe inicial.

from typing import List, Optional


class Instrutor:
     def __init__(self, nome: str, precos_cursos: Optional[List[float]] = None):
         self.nome = nome
         self.precos_cursos = precos_cursos or []

     def adiciona_preco(self, novo_preco: float):
         self.precos_cursos.append(novo_preco)

Agora definimos o padrão como None e dentro do método __init__ e se vier um None atribuimos uma lista vazia. Assim para qualquer objeto que criarmos será atribuída uma lista diferente.

Esse tipo de bug pode passar despercebido e é difícil encontrar depois. Essa semana eu estava fazendo um teste de integração em NodJS para um repositório. Várias funções recebiam um mesmo tipo de objeto então eu criei um objeto de exemplo e o passei para as diversas funções, alterando somente os campos necessários. Um dos campos era uma lista, onde vários métodos adicionavam elementos nela. E eu usava tal lista para fazer alguns asserts, meus testes quebravam e demorei cerca de 2/3 horas para descobrir o porquê. Tinha me esquecido que em JS os objetos são passados por referência e não por cópia, o que alterava o objeto original.

Esse tipo de problema pode acontecer em qualquer linguagem de programação e conforme você vai ficando mais proficiente na sua determinada linguagem você vai pegando essas "manhas". Já teve algum problema parecido com esse? Comente aí em baixo!

Grande abraço!

Créditos e referências