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
- Orientação a objetos - Luciano Ramalho
- Imagem do post: Joe Roberts
- Gif Lhama