“Você está triste,” disse o Cavaleiro em um tom de voz ansioso: “deixe eu cantar para você uma canção reconfortante. […] O nome da canção se chama ‘OLHOS DE HADOQUE’.”
“Oh, esse é o nome da canção?,” disse Alice, tentando parecer interessada.
“Não, você não entendeu,” retorquiu o Cavaleiro, um pouco irritado. “É assim que o nome É CHAMADO. O nome na verdade é ‘O ENVELHECIDO HOMEM VELHO.‘”
de Lewis Caroll
Alice e o Cavaleiro dão o tom do que veremos nesse capítulo. O tema é a distinção entre objetos e seus nomes: o nome não é o objeto, o nome é outra coisa.
Começamos o capítulo apresentando uma metáfora para variáveis em Python: variáveis são rótulos, não caixas. Mesmo que você já domine variáveis de referência, a analogia pode ainda ser útil para ilustrar questões de aliasing (“apelidamento”) para outra pessoa.
Depois discutimos os conceitos de identidade, valor e apelidamento de objetos. Uma característica surpreendente das tuplas é revelada: elas são imutáveis, mas seus valores podem mudar. Isso leva a uma discussão sobre cópias rasas e profundas. Referências e parâmetros de funções são o tema seguinte: o problema do parâmetro com default mutável e formas seguras de lidar com argumentos mutáveis passados para nossas funções por clientes.
As últimas seções do capítulo tratam de coleta de lixo (garbage collection),
a instrução del e de algumas otimizações com com objetos imutáveis em Python.
É um capítulo bastante árido, mas os tópicos tratados podem explicar muitos bugs sutis em programas reais em Python, além de boas práticas para evitá-los.
Os tópicos tratados aqui são muito estáveis e fundamentais. Não foi introduzida nenhuma mudança digna de nota nesta segunda edição.
Acrescentei um exemplo usando is para testar a existência de um objeto sentinela,
e um aviso sobre o mau uso do operador is no final da Escolhendo Entre == e is.
Este capítulo estava na Parte IV, mas decidi abordar esses temas mais cedo, pois eles funcionam melhor como o encerramento da Parte II, “Estruturas de Dados”, que como abertura de “Práticas de Orientação a Objetos"
|
Note
|
A seção sobre “Referências Fracas” da primeira edição deste livro agora é um post em https://fluentpython.com. |
Vamos começar desaprendendo que uma variável é como uma caixa onde você guarda dados.
Em 1997, fiz um curso de verão sobre Java no MIT. A professora, Lynn Stein[1] apontou que a metáfora comum, de “variáveis como caixas”, na verdade atrapalha o entendimento de variáveis de referência em linguagens orientadas a objetos. As variáveis em Python são como variáveis de referência em Java; uma metáfora melhor é pensar em uma variável como uma etiqueta que dá nome a um objeto. O exemplo e a figura a seguir ajudam a entender o motivo disso.
O As variáveis a e b referem-se à mesma lista, não a cópias da lista. é uma interação simples que não pode ser explicada por “variáveis como caixas”.
a e b referem-se à mesma lista, não a cópias da lista.>>> a = [1, 2, 3] (1)
>>> b = a (2)
>>> a.append(4) (3)
>>> b (4)
[1, 2, 3, 4]-
Cria uma lista [1, 2, 3] e a vincula à variável
a. -
Vincula a variável
bao mesmo valor referenciado pora. -
Modifica a lista referenciada por
a, anexando um novo item. -
É possível ver o efeito através da variável
b. Se você pensar embcomo uma caixa que guardava uma cópia de[1, 2, 3]da caixaa, este comportamento não faz sentido.
A Se você imaginar variáveis como caixas, não é possível entender a atribuição em Python; por outro lado, imagine variáveis como etiquetas autocolantes e o [ex_a_b_refs] é facilmente explicável. explica por que a metáfora da caixa está errada em Python, enquanto etiquetas apresentam uma imagem mais útil para entender como variáveis funcionam.
Assim, a instrução b = a não copia o conteúdo de uma caixa a para uma caixa b.
Ela cola uma nova etiqueta b no objeto que já tem a etiqueta a.
A professora Stein também falava sobre atribuição de uma maneira bastante específica. Por exemplo, quando discutia sobre um objeto representando uma gangorra em uma simulação, ela dizia: “A variável g foi atribuída à gangorra”, mas nunca “A gangorra foi atribuída à variável g”. Com variáveis de referência, faz mais sentido dizer que a variável é atribuída a um objeto, não o contrário. Afinal, o objeto é criado antes da atribuição. O Variáveis são vinculadas a objetos somente após os objetos serem criados prova que o lado direito de uma atribuição é processado primeiro.
Já que o verbo “atribuir” é usado de diferentes maneiras,
“vincular” é uma alternativa melhor:
a declaração de atribuição em Python x = … vincula o nome x
ao objeto criado ou referenciado no lado direito.
E o objeto precisa existir antes que um nome possa ser vinculado a ele,
como demonstra o Variáveis são vinculadas a objetos somente após os objetos serem criados.
>>> class Gizmo:
... def __init__(self):
... print(f'Gizmo id: {id(self)}')
...
>>> x = Gizmo()
Gizmo id: 4301489152 (1)
>>> y = Gizmo() * 10 (2)
Gizmo id: 4301489432 (3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for *: 'Gizmo' and 'int'
>>>
>>> dir() (4)
['Gizmo', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'x']-
A saída
Gizmo id: …é um efeito colateral da criação de uma instância deGizmo. -
Multiplicar uma instância de
Gizmolevanta uma exceção. -
Aqui está a prova de que um segundo
Gizmofoi de fato instanciado antes que a multiplicação fosse tentada. -
Mas a variável
ynunca foi criada, porque a exceção aconteceu enquanto a parte direita da atribuição estava sendo executada.
|
Tip
|
Para entender uma atribuição em Python, leia primeiro o lado direito: é ali que o objeto é criado ou recuperado. Depois disso, a variável do lado esquerdo é vinculada ao objeto, como uma etiqueta colada a ele. Esqueça as caixas. |
Como variáveis são apenas meras etiquetas, nada impede que um objeto tenha várias etiquetas vinculadas a si. Quando isso acontece, você tem apelidos (aliases), nosso próximo tópico.
Lewis Carroll
é o pseudônimo literário do Prof.
Charles Lutwidge Dodgson.
O Sr.
Carroll não é apenas igual ao Prof.
Dodgson,
eles são exatamente a mesma pessoa. charles e lewis se referem ao mesmo objeto
expressa essa ideia em Python.
charles e lewis se referem ao mesmo objeto>>> charles = {'name': 'Charles L. Dodgson', 'born': 1832}
>>> lewis = charles (1)
>>> lewis is charles
True
>>> id(charles), id(lewis) (2)
(4300473992, 4300473992)
>>> lewis['balance'] = 950 (3)
>>> charles
{'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}-
lewisé um apelido paracharles. -
O operador
ise a funçãoidconfirmam essa afirmação. -
Adicionar um item a
lewisé o mesmo que adicionar um item acharles.
Entretanto, suponha que um impostor—vamos chamá-lo de Dr.
Alexander Pedachenko—diga
que é o verdadeiro Charles L. Dodgson, nascido em 1832.
Suas credenciais podem ser as mesmas,
mas o Dr.
Pedachenko não é o Prof.
Dodgson. charles e lewis estão vinculados ao mesmo objeto; alex está vinculado a um objeto diferente de valor igual. ilustra esse cenário.
charles e lewis estão vinculados ao mesmo objeto; alex está vinculado a um objeto diferente de valor igual.O alex e charles são iguais quando comparados, mas alex não é charles constrói e testa o objeto alex como apresentado em charles e lewis estão vinculados ao mesmo objeto; alex está vinculado a um objeto diferente de valor igual..
alex e charles são iguais quando comparados, mas alex não é charles>>> alex = {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950} (1)
>>> alex == charles (2)
True
>>> alex is not charles (3)
True-
alexé uma referência a um objeto que é uma réplica do objeto vinculado acharles. -
Os objetos são iguais quando comparados devido à implementação de
__eq__na classedict. -
Mas são objetos distintos. Essa é a forma pythônica de escrever a negação de uma comparação de identidade:
a is not b.
charles e lewis se referem ao mesmo objeto é um exemplo de apelidamento (aliasing).
Naquele código, lewis e charles são apelidos: duas variáveis vinculadas ao mesmo objeto.
Por outro lado, alex não é um apelido para charles:
essas variáveis estão vinculadas a objetos diferentes.
Os
objetos vinculados a alex e charles tem o mesmo
valor — é isso que == compara — mas têm identidades diferentes.
Na Referência da Linguagem Python, está escrito:
A identidade de um objeto nunca muda após ele ter sido criado; você pode pensar nela como o endereço do objeto na memória. O operador
iscompara a identidade de dois objetos; a funçãoid()retorna um inteiro representando essa identidade.
O verdadeiro significado do id de um objeto depende da implementação da linguagem.
Em CPython, id() retorna o endereço de memória do objeto,
mas outro interpretador Python pode retornar algo diferente.
O ponto fundamental é que o id será sempre um valor numérico único,
e ele nunca mudará durante a vida do objeto.
Na prática, raramente usamos a função id() quando programamos.
A verificação de identidade é feita, na maior parte das vezes, com o operador is,
que compara os IDs dos objetos, então nosso código não precisa chamar id()
explicitamente.
A seguir falamos sobre is versus ==.
|
Tip
|
Para o revisor técnico Leonardo Rochael,
o uso mais frequente de |
O operador == compara os valores de objetos (os dados que eles contêm),
enquanto is compara suas identidades.
Quando estamos programando, em geral, nos preocupamos mais com os valores do
que com as identidades dos objetos,
então == aparece com mais frequência que is em programas Python.
Entretanto, se você estiver comparando uma variável com um singleton (um objeto único)
faz mais sentido usar is.
O caso mais comum é checar se a variável está vinculada a None.
Esta é a forma recomendada de fazer isso:
x is NoneE a forma apropriada de escrever sua negação é:
x is not NoneNone é o singleton mais comum que testamos com is.
Objetos sentinela são outro exemplo de singletons que testamos com is.
Veja um modo de criar e testar um objeto sentinela:
END_OF_DATA = object()
# ... many lines
def traverse(...):
# ... more lines
if node is END_OF_DATA:
return
# etc.O operador is é mais rápido que ==, pois não pode ser sobrecarregado.
Daí Python não precisa encontrar e invocar métodos especiais para calcular seu resultado
e o processamento é tão simples quanto comparar dois IDs, que são números inteiros.
Por outro lado, a == b é açúcar sintático para a.__eq__(b).
O método __eq__, herdado de object, compara os IDs dos objetos,
então produz o mesmo resultado de is.
Mas a maioria dos tipos embutidos sobrescreve __eq__ com implementações mais úteis,
que levam em consideração os valores dos atributos dos objetos.
A determinação da igualdade pode envolver muito processamento—por exemplo,
quando se comparam coleções grandes ou estruturas aninhadas com muitos níveis.
|
Warning
|
Normalmente estamos mais interessados na igualdade que na identidade de objetos,
por isso o operador |
Para concluir essa discussão de identidade versus igualdade,
vamos ver como o tipo notoriamente imutável tuple não é assim tão invariável quanto você poderia supor.
As tuplas, como a maioria das coleções em Python — lists, dicts, sets, etc..— são contêineres: armazenam referências para objetos.[2]
Se os itens referenciados forem mutáveis, eles podem mudar, mesmo que tupla em si não mude.
Em outras palavras, a imutabilidade das tuplas, refere-se apenas ao conteúdo interno da
estrutura de dados tuple (isto é, as referências que ela armazena),
e não se estende aos objetos referenciados.
O t1 e t2 inicialmente são iguais, mas a mudança em um item mutável dentro da tupla t1 as torna diferentes ilustra uma situação em que o valor de uma tupla muda
como resultado de mudanças em um objeto mutável ali referenciado.
O que não pode nunca mudar em uma tupla é a identidade dos itens que ela contém.
t1 e t2 inicialmente são iguais, mas a mudança em um item mutável dentro da tupla t1 as torna diferentes>>> t1 = (1, 2, [30, 40]) (1)
>>> t2 = (1, 2, [30, 40]) (2)
>>> t1 == t2 (3)
True
>>> id(t1[-1]) (4)
4302515784
>>> t1[-1].append(99) (5)
>>> t1
(1, 2, [30, 40, 99])
>>> id(t1[-1]) (6)
4302515784
>>> t1 == t2 (7)
False-
t1é imutável, mast1[-1]é mutável. -
Cria a tupla
t2, cujos itens são iguais àqueles det1. -
Apesar de serem objetos distintos,
t1et2são iguais quando comparados, como esperado. -
Obtém o
idda lista na posiçãot1[-1]. -
Modifica diretamente a lista
t1[-1]. -
O
iddet1[-1]não mudou, apenas seu valor. -
t1et2agora são diferentes
Essa imutabilidade relativa das tuplas está por trás do enigma da [tuple_puzzler]. Essa também é razão pela qual não é possível gerar o hash de algumas tuplas, como vimos na [what_is_hashable_sec].
A distinção entre igualdade e identidade tem outras implicações quando você precisa copiar um objeto.
Uma cópia é um objeto igual com um id diferente.
Mas se um objeto contém outros objetos,
é preciso que a cópia duplique os objetos internos ou eles podem ser compartilhados?
Não há uma resposta única.
A seguir discutimos esse ponto.
A forma mais fácil de copiar uma lista (ou a maioria das coleções mutáveis nativas) é usando o construtor padrão do próprio tipo. Por exemplo:
>>> l1 = [3, [55, 44], (7, 8, 9)]
>>> l2 = list(l1) (1)
>>> l2
[3, [55, 44], (7, 8, 9)]
>>> l2 == l1 (2)
True
>>> l2 is l1 (3)
False-
list(l1)cria uma cópia del1. -
As cópias são iguais…
-
…mas se referem a dois objetos diferentes.
Para listas e outras sequências mutáveis, o atalho l2 = l1[:] também cria uma cópia.
Contudo, tanto o construtor quanto [:] produzem uma cópia rasa (shallow copy).
Isto é, o contêiner externo é duplicado, mas a cópia é preenchida com referências
para os mesmos itens contidos no contêiner original.
Isso economiza memória e não causa qualquer problema se todos os itens forem imutáveis.
Mas se existirem itens mutáveis, isso pode gerar surpresas desagradáveis.
No Criando uma cópia rasa de uma lista contendo outra lista; copie e cole esse código para vê-lo animado no Online Python Tutor criamos uma lista contendo outra lista e uma tupla, e então fazemos algumas mudanças para ver como isso afeta os objetos referenciados.
|
Tip
|
Se você tem um computador conectado à internet disponível, recomendo fortemente que você assista à animação interativa do Criando uma cópia rasa de uma lista contendo outra lista; copie e cole esse código para vê-lo animado no Online Python Tutor em Online Python Tutor. No momento em que escrevo, o link direto para um exemplo pronto no pythontutor.com não estava funcionando de forma estável. Mas a ferramenta é ótima, então vale a pena gastar seu tempo copiando e colando o código. |
l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1) # (1)
l1.append(100) # (2)
l1[1].remove(55) # (3)
print('l1:', l1)
print('l2:', l2)
l2[1] += [33, 22] # (4)
l2[2] += (10, 11) # (5)
print('l1:', l1)
print('l2:', l2)-
l2é uma cópia rasa del1. Este estado está representado em Estado do programa imediatamente após a atribuiçãol2 = list(l1)em [ex_shallow_copy].l1el2se referem a listas diferentes, mas as listas compartilham referências para os mesmos objetos internos, a lista[66, 55, 44]e para a tupla(7, 8, 9). (Diagrama gerado pelo Online Python Tutor). -
Concatenar
100al1não tem qualquer efeito sobrel2. -
Aqui removemos
55da lista internal1[1]. Isso afetal2, poisl2[1]está associado à mesma lista eml1[1]. -
Para um objeto mutável como a lista referida por
l2[1], o operador+=altera a lista diretamente. Essa mudança é visível eml1[1], que é um apelido paral2[1]. -
+=em uma tupla cria uma nova tupla e reassocia a variávell2[2]a ela. Isso é equivalente a fazerl2[2] = l2[2] + (10, 11). Agora as tuplas na última posição del1el2não são mais o mesmo objeto. Veja a Estado final del1el2: elas ainda compartilham referências para o mesmo objeto lista, que agora contém[66, 44, 33, 22], mas a operaçãol2[2] += (10, 11)criou uma nova tupla com conteúdo(7, 8, 9, 10, 11), sem relação com a tupla(7, 8, 9)referenciada porl1[2]. (Diagram generated by the Online Python Tutor.).
l2 = list(l1) em [ex_shallow_copy]. l1 e l2 se referem a listas diferentes, mas as listas compartilham referências para os mesmos objetos internos, a lista [66, 55, 44] e para a tupla (7, 8, 9). (Diagrama gerado pelo Online Python Tutor)A saída de Criando uma cópia rasa de uma lista contendo outra lista; copie e cole esse código para vê-lo animado no Online Python Tutor é Saída de [ex_shallow_copy],
e o estado final dos objetos está representado em Estado final de l1 e l2: elas ainda compartilham referências para o mesmo objeto lista, que agora contém [66, 44, 33, 22], mas a operação l2[2] += (10, 11) criou uma nova tupla com conteúdo (7, 8, 9, 10, 11), sem relação com a tupla (7, 8, 9) referenciada por l1[2]. (Diagram generated by the Online Python Tutor.).
l1: [3, [66, 44], (7, 8, 9), 100]
l2: [3, [66, 44], (7, 8, 9)]
l1: [3, [66, 44, 33, 22], (7, 8, 9), 100]
l2: [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]l1 e l2: elas ainda compartilham referências para o mesmo objeto lista, que agora contém [66, 44, 33, 22], mas a operação l2[2] += (10, 11) criou uma nova tupla com conteúdo (7, 8, 9, 10, 11), sem relação com a tupla (7, 8, 9) referenciada por l1[2]. (Diagram generated by the Online Python Tutor.)Já deve estar claro que cópias rasas são fáceis de criar, mas podem ou não ser o que você quer. Nosso próximo tópico é a criação de cópias profundas.
Trabalhar
com cópias rasas nem sempre é um problema,
mas algumas vezes você vai precisar criar cópias profundas
(isto é, cópias que não compartilham referências de objetos internos).
O módulo copy oferece as funções deepcopy e copy,
que retornam cópias profundas e rasas de objetos arbitrários.
Para ilustrar o uso de copy() e deepcopy(), Bus pega ou deixa passageiros define uma classe simples,
Bus, representando um ônibus escolar que é carregado com passageiros,
e então pega ou deixa passageiros ao longo de sua rota.
link:../code/06-obj-ref/bus.py[role=include]Agora, no Os efeitos do uso de copy versus deepcopy interativo, vamos criar um objeto bus1 e
dois clones: uma cópia rasa (bus2) e uma cópia profunda
(bus3). Então vemos o que acontece quando o bus1 deixa um passageiro.
copy versus deepcopy>>> import copy
>>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
>>> bus2 = copy.copy(bus1)
>>> bus3 = copy.deepcopy(bus1)
>>> id(bus1), id(bus2), id(bus3)
(4301498296, 4301499416, 4301499752) (1)
>>> bus1.drop('Bill')
>>> bus2.passengers
['Alice', 'Claire', 'David'] (2)
>>> id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
(4302658568, 4302658568, 4302657800) (3)
>>> bus3.passengers
['Alice', 'Bill', 'Claire', 'David'] (4)-
Usando
copyedeepcopy, criamos três instâncias distintas deBus. -
Após
bus1deixar'Bill', ele também desaparece debus2. -
A inspeção do atributo
passengersmostra quebus1ebus2compartilham o mesmo objeto lista, poisbus2é uma cópia rasa debus1. -
bus3é uma cópia profunda debus1, então seu atributopassengersse refere a outra lista.
Em geral, criar cópias profundas não é uma questão simples.
Objetos podem conter referências cíclicas que fariam um algoritmo
ingênuo entrar em um laço infinito.
A função deepcopy memoriza os objetos já copiados,
e trata referências cíclicas corretamente.
Isso é demonstrado no Referências cíclicas: b tem uma referência para a e então é concatenado a a; ainda assim, deepcopy consegue copiar a..
b tem uma referência para a e então é concatenado a a; ainda assim, deepcopy consegue copiar a.>>> a = [10, 20]
>>> b = [a, 30]
>>> a.append(b)
>>> a
[10, 20, [[...], 30]]
>>> from copy import deepcopy
>>> c = deepcopy(a)
>>> c
[10, 20, [[...], 30]]Além disso, algumas vezes uma cópia profunda pode ser profunda demais.
Por exemplo, objetos podem ter referências para recursos externos ou para
singletons (objetos únicos) que não devem ser copiados.
Você pode controlar o comportamento de copy e de deepcopy
implementando os métodos especiais __copy__ e __deepcopy__,
como descrito na
documentação do módulo copy
O compartilhamento de objetos através de apelidos também explica como a passagens de parâmetros funciona em Python, e o problema do uso de tipos mutáveis como parâmetros default. Vamos falar sobre essas questões a seguir.
O único modo de passagem de parâmetros em Python é a chamada por compartilhamento (call by sharing). É o mesmo modo usado na maioria das linguagens orientadas a objetos, incluindo JavaScript, Ruby e Java (em Java isso se aplica aos tipos de referência; tipos primitivos usam a chamada por valor). Chamada por compartilhamento significa que cada parâmetro formal da função recebe uma cópia de cada referência nos argumentos. Em outras palavras, os parâmetros dentro da função se tornam apelidos dos argumentos passados.
O resultado desse esquema é que a função pode modificar qualquer objeto mutável
passado a ela como parâmetro, mas não pode mudar a identidade daqueles objetos
(isto é, ela não pode substituir integralmente um objeto por outro).
O Uma função pode mudar qualquer objeto mutável que receba mostra uma função simples usando += com um de seus parâmetros.
Quando passamos números, listas e tuplas para a função,
os argumentos originais são afetados de maneiras diferentes.
Veja só:
>>> def f(a, b):
... a += b
... return a
...
>>> x = 1
>>> y = 2
>>> f(x, y)
3
>>> x, y (1)
(1, 2)
>>> a = [1, 2]
>>> b = [3, 4]
>>> f(a, b)
[1, 2, 3, 4]
>>> a, b (2)
([1, 2, 3, 4], [3, 4])
>>> t = (10, 20)
>>> u = (30, 40)
>>> f(t, u) (3)
(10, 20, 30, 40)
>>> t, u
((10, 20), (30, 40))-
O número
xnão se altera. -
A lista
aé alterada. -
A tupla
tnão se altera.
Outra questão relacionada a parâmetros de função é o uso de valores mutáveis como defaults, discutida a seguir.
Parâmetros opcionais com valores default são um ótimo recurso para definição de funções em Python, permitindo que nossas APIs evoluam mantendo a compatibilidade com versões anteriores. Entretanto, evite usar objetos mutáveis como valores default em parâmetros.
Para ilustrar o motivo, no Uma classe simples ilustrando o perigo de um default mutável
modificamos o método __init__ da classe Bus do Bus pega ou deixa passageiros para criar HauntedBus.
Tentamos ser espertos: em vez do valor default passengers=None,
temos passengers=[], para evitar o if do __init__ anterior.
Essa "esperteza" causa problemas.
link:../code/06-obj-ref/haunted_bus.py[role=include]-
Quando não passamos o argumento
passengers, esse parâmetro é vinculado ao objeto lista default, que inicialmente está vazia. -
Essa atribuição torna
self.passengersum apelido depassengers, que por sua vez é um apelido para a lista default, quando um argumentopassengersnão é passado para a função. -
Quando os métodos
.remove()e.append()são usados comself.passengers, estamos, na verdade, mudando a lista default, que é um atributo do objeto-função.
Ônibus assombrados por passageiros fantasmas mostra o comportamento misterioso de HauntedBus.
>>> bus1 = HauntedBus(['Alice', 'Bill']) (1)
>>> bus1.passengers
['Alice', 'Bill']
>>> bus1.pick('Charlie')
>>> bus1.drop('Alice')
>>> bus1.passengers (2)
['Bill', 'Charlie']
>>> bus2 = HauntedBus() (3)
>>> bus2.pick('Carrie')
>>> bus2.passengers
['Carrie']
>>> bus3 = HauntedBus() (4)
>>> bus3.passengers (5)
['Carrie']
>>> bus3.pick('Dave')
>>> bus2.passengers (6)
['Carrie', 'Dave']
>>> bus2.passengers is bus3.passengers (7)
True
>>> bus1.passengers (8)
['Bill', 'Charlie']-
bus1começa com uma lista de dois passageiros. -
Até aqui, tudo bem: nenhuma surpresa em
bus1. -
bus2começa vazio, então a lista vazia default é vinculada aself.passengers. -
bus3também começa vazio, e novamente a lista default é atribuída. -
A lista default não está mais vazia!
-
Agora
Dave, pego pelobus3, aparece nobus2. -
O problema:
bus2.passengersebus3.passengersse referem à mesma lista. -
Mas
bus1.passengersé uma lista diferente.
O problema é que instâncias de HauntedBus que não recebem uma lista de passageiros
inicial acabam todas compartilhando a mesma lista de passageiros entre si.
Este tipo de bug pode ser muito sutil.
Como o Ônibus assombrados por passageiros fantasmas demonstra,
quando HauntedBus recebe uma lista com passageiros como parâmetro,
ele funciona como esperado.
Coisas estranhas acontecem somente quando HauntedBus começa vazio,
pois aí self.passengers se torna um apelido para o valor default do parâmetro passengers.
O problema é que cada valor default é processado quando a função é definida—normalmente
quando o módulo é carregado—e os valores default se tornam atributos do objeto-função.
Assim, se o valor default é um objeto mutável e você o altera,
a alteração vai afetar todas as futuras chamadas da função.
Após executar as linhas do Ônibus assombrados por passageiros fantasmas,
você pode inspecionar o objeto HauntedBus.__init__
e ver os estudantes fantasma assombrando o atributo __defaults__:
>>> dir(HauntedBus.__init__) # doctest: +ELLIPSIS
['__annotations__', '__call__', ..., '__defaults__', ...]
>>> HauntedBus.__init__.__defaults__
(['Carrie', 'Dave'],)Por fim, podemos verificar que bus2.passengers é um apelido vinculado
ao primeiro elemento do atributo HauntedBus.__init__.__defaults__:
>>> HauntedBus.__init__.__defaults__[0] is bus2.passengers
TrueO problema com defaults mutáveis explica porque None é normalmente usado como
valor default para parâmetros que podem receber valores mutáveis.
No Bus pega ou deixa passageiros, __init__ checa se o argumento passengers é None.
Se for, self.passengers é vinculado a uma nova lista vazia.
Se passengers não for None,
a implementação correta vincula uma cópia daquele argumento a self.passengers.
A próxima seção explica porque copiar o argumento é uma boa prática.
Ao escrever uma função que recebe um argumento mutável, você deve considerar com cuidado se o cliente que chama sua função espera que o argumento passado seja modificado.
Por exemplo, se sua função recebe um dict e precisa modificá-lo durante seu processamento,
esse efeito colateral deve ou não ser visível fora da função?
A resposta, na verdade, depende do contexto.
É tudo uma questão de alinhar as expectativas do autor da função com as do cliente da função.
O último exemplo com ônibus neste capítulo mostra como o TwilightBus viola as expectativas
ao compartilhar sua lista de passageiros com seus clientes.
Antes de estudar a implementação, veja como a classe TwilightBus funciona pela
perspectiva de um cliente daquela classe, em Passageiros desaparecem quando são deixados por um TwilightBus.
TwilightBus>>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat'] (1)
>>> bus = TwilightBus(basketball_team) (2)
>>> bus.drop('Tina') (3)
>>> bus.drop('Pat')
>>> basketball_team (4)
['Sue', 'Maya', 'Diana']-
basketball_teamcontém o nome de cinco estudantes. -
Um
TwilightBusé carregado com o time. -
O
busdeixa uma estudante, depois outra. -
As passageiras desembarcadas desapareceram do time de basquete!
TwilightBus viola o "Princípio da Menor Surpresa", uma boa prática do design de
interfaces.[3]
Com certeza, é surpreendente que quando o ônibus deixa uma estudante,
seu nome seja removido da escalação do time de basquete.
Uma classe simples mostrando os perigos de mudar argumentos recebidos é a implementação de TwilightBus e uma explicação do problema.
link:../code/06-obj-ref/twilight_bus.py[role=include]-
Aqui cuidadosamente criamos uma lista vazia quando
passengerséNone. -
Entretanto, esta atribuição transforma
self.passengersem um apelido parapassengers, que por sua vez é um apelido para o argumento passado para__init__(i.e.basketball_teamem Passageiros desaparecem quando são deixados por umTwilightBus). -
Quando os métodos
.remove()e.append()são usados comself.passengers, estamos, na verdade, modificando a lista original recebida como argumento pelo construtor.
O problema aqui é que o ônibus está apelidando a lista passada para o construtor.
Ao invés disso, ele deveria manter sua própria lista de passageiros.
A solução é simples: em __init__, quando o parâmetro passengers é fornecido,
self.passengers deveria ser inicializado com uma cópia daquela lista,
como fizemos, de forma correta, em Bus pega ou deixa passageiros:
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers) (1)-
Cria uma cópia da lista
passengers, ou converte o argumento paralistse ele não for uma lista.
Agora nossa manipulação interna da lista de passageiros não afetará
o argumento usado para inicializar o ônibus.
E com uma vantagem adicional, essa solução é mais flexível:
agora o argumento passado no parâmetro passengers pode ser
uma tupla ou qualquer outro tipo iterável,
como set ou mesmo resultados de uma consulta a um banco de dados,
pois o construtor de list aceita qualquer iterável.
Ao criar nossa própria lista, estamos também assegurando que ela suporta os métodos necessários,
.remove() e .append(), operações que usamos nos métodos .pick() e .drop().
|
Tip
|
A menos que um método tenha o objetivo explícito de alterar um objeto recebido como argumento, você deveria pensar bem antes de apelidar tal objeto e simplesmente vinculá-lo a uma variável interna de sua classe. Quando em dúvida, crie uma cópia. Os clientes de sua classe ficarão mais felizes. Claro, criar uma cópia não é grátis: há custos de memória e processamento. Entretanto, uma API que causa bugs sutis é um problema bem maior que uma que seja um pouco mais lenta ou que use mais recursos. |
Agora vamos conversar sobre uma das instruções mais incompreendidas em Python:
del.
Os objetos nunca são destruídos explicitamente; no entanto, quando eles se tornam inacessíveis, eles podem ser coletados como lixo.
A primeira surpresa de del é não ser uma função,
mas uma instrução (statement).
Escrevemos del x e não del(x)—apesar dessa última forma funcionar também,
mas apenas porque as expressões x e (x) em geral tem o mesmo significado em Python.
O segundo aspecto surpreendente é que del apaga referências, não objetos.
A coleta de lixo pode eliminar um objeto da memória como resultado indireto de del,
se a variável apagada for a última referência ao objeto.
Reassociar uma variável também pode reduzir a zero o número de referências a um objeto,
causando sua destruição.
>>> a = [1, 2] (1)
>>> b = a (2)
>>> del a (3)
>>> b (4)
[1, 2]
>>> b = [3] (5)-
Cria o objeto
[1, 2]e vinculaaa ele. -
Vincula
bao mesmo objeto[1, 2]. -
Apaga a referência
a. -
[1, 2]não é afetado, poisbainda aponta para ele. -
Reassociar
ba um objeto diferente remove a última referência restante a[1, 2]. Agora o coletor de lixo pode descartar aquele objeto.
|
Warning
|
Existe um método especial |
No CPython, o algoritmo primário de coleta de lixo é a contagem de referências.
Essencialmente, cada objeto mantém uma contagem do número de referências apontando para si.
Assim que a contagem chega a zero, o objeto é imediatamente destruído:
CPython invoca o método __del__ no objeto (se definido)
e daí libera a memória alocada para aquele objeto.
No CPython 2.0, um algoritmo de coleta de lixo geracional foi acrescentado,
para detectar grupos de objetos envolvidos em referências cíclicas—grupos que
podem ser inacessíveis mesmo que existam referências restantes,
quando todas as referências mútuas estão contidas dentro daquele grupo.
Outras implementações de Python tem coletores de lixo mais sofisticados,
que não se baseiam na contagem de referências,
o que significa que o método __del__ pode não ser chamado
imediatamente quando não existem mais referências ao objeto.
Veja "PyPy, Garbage Collection, and a Deadlock" (EN)
de A. Jesse Jiryu Davis para uma discussão sobre os usos próprios e impróprios de __del__.
Para demonstrar o fim da vida de um objeto, Detectando o fim de um objeto quando não resta nenhuma referência apontando para ele usa weakref.finalize
para registrar uma função callback a ser chamada quando o objeto é destruído.
>>> import weakref
>>> s1 = {1, 2, 3}
>>> s2 = s1 (1)
>>> def bye(): (2)
... print('...like tears in the rain.')
...
>>> ender = weakref.finalize(s1, bye) (3)
>>> ender.alive (4)
True
>>> del s1
>>> ender.alive (5)
True
>>> s2 = 'spam' (6)
...like tears in the rain.
>>> ender.alive
False-
s1es2são apelidos do mesmo conjunto,{1, 2, 3}. -
Para essa demonstração, a função
byenão deve ser um método vinculado ao objeto prestes a ser destruído, nem manter uma referência para o objeto. -
Registra o callback
byeno objeto referenciado pors1. -
O atributo
.aliveéTrueantes do objetofinalizeser chamado. -
Como vimos,
delnão apaga o objeto, apenas a referências1a ele. -
Reassociar a última referência,
s2, torna{1, 2, 3}inacessível. Ele é destruído, o callbackbyeé invocado, eender.alivese tornaFalse.
O ponto principal de Detectando o fim de um objeto quando não resta nenhuma referência apontando para ele é mostrar explicitamente que del não apaga objetos,
mas que objetos podem ser apagados como uma consequência de
ficarem inacessíveis após o uso de del.
Você pode estar se perguntando porque o objeto {1, 2, 3} foi destruído em Detectando o fim de um objeto quando não resta nenhuma referência apontando para ele.
Afinal, a referência s1 foi passada para a função finalize,
que precisa tê-la mantido para conseguir monitorar o objeto e invocar o callback.
Isso funciona porque finalize mantém uma
referência fraca (weak reference) para {1, 2, 3}.
Referências fracas não aumentam a contagem de referências de um objeto.
Assim, uma referência fraca não evita que o objeto alvo seja removido pelo coletor de lixo.
Referências fracas são úteis em cenários de caching,
pois não queremos que os objetos "cacheados" sejam mantidos vivos apenas
por terem uma referência no cache.
|
Note
|
Referências fracas são um tópico muito especializado, então decidi retirá-lo dessa segunda edição. Em vez disso, publiquei a nota "Weak References" em https://fluentpython.com. |
|
Note
|
Esta seção opcional discute alguns detalhes que,
na verdade, não são muito importantes para usuários de Python,
e que podem não se aplicar a outras implementações da linguagem ou mesmo a futuras versões de CPython.
Entretanto, já vi muita gente tropeçar nesses casos laterais e daí passar a usar o
operador |
Fiquei surpreso ao descobrir que, dada uma tupla t,
a chamada t[:] não cria uma cópia, mas devolve uma referência para o mesmo objeto.
Da mesma forma, tuple(t) também retorna uma referência para a mesma
tupla.[4]
Uma tupla construída a partir de outra é, na verdade, exatamente a mesma tupla. demonstra esse fato.
>>> t1 = (1, 2, 3)
>>> t2 = tuple(t1)
>>> t2 is t1 (1)
True
>>> t3 = t1[:]
>>> t3 is t1 (2)
True-
t1et2estão vinculadas ao mesmo objeto -
Assim como
t3.
Podemos observar o mesmo comportamento com instâncias de str, bytes e frozenset.
Note que frozenset não é uma sequência, então fs[:]
não funciona se fs é um frozenset.
Mas fs.copy() tem o mesmo efeito: ele trapaceia e retorna uma referência ao mesmo objeto,
e não uma cópia.[5]
O Strings e inteiros literais podem criar objetos compartilhados. mostra outra otimização, relacionada aos tipos str e int:
>>> s1 = 'ABC'
>>> s2 = 'ABC' # (1)
>>> s2 is s1 # (2)
True
>>> n1 = 10
>>> n2 = 10
>>> n1 is n2 # (3)
True
>>> n3 = 1729
>>> n4 = 1729
>>> n3 is n4 # (4)
False-
Criando duas
strcom o mesmo valor. -
Surpresa:
aebse referem ao mesmo objetostr! -
Alguns inteiros pequenos são compartilhados.
-
Outros inteiros não são compartilhados.
O compartilhamento de strings literais é uma técnica de otimização chamada internalização (interning). O CPython usa uma técnica similar com inteiros pequenos, para evitar a duplicação desnecessária de números que aparecem com muita frequência em programas, como 0, 1, -1, 10, etc. Observe que o CPython não internaliza todas as strings e inteiros, e o critério pelo qual ele faz isso é um detalhe de implementação não documentado.
|
Warning
|
Nunca dependa da internalização de |
Os truques discutidos nessa seção, incluindo o comportamento de frozenset.copy(),
são mentiras inofensivas que economizam memória e tornam o interpretador mais rápido.
Não se preocupe, elas não trarão nenhum problema, pois se aplicam apenas a tipos imutáveis.
Provavelmente, o melhor uso para esse tipo de detalhe é ganhar apostas contra outros
Pythonistas.[6]
Todo objeto em Python tem uma identidade, um tipo e um valor. Apenas o valor do objeto pode mudar ao longo do tempo.[7]
Se duas variáveis se referem a objetos imutáveis de valor igual (quando a == b é True),
na prática, dificilmente importa se elas se referem a cópias de mesmo valor
ou são apelidos do mesmo objeto, porque o valor de objeto imutável não muda,
com uma exceção.
A exceção são as tuplas: se ela contém referências para itens mutáveis,
então seu valor mudará se o valor de um item mutável for alterado.
Na prática, esse cenário não é tão comum.
O que nunca muda numa coleção imutável são as identidades dos objetos mantidos ali.
A classe frozenset não sofre desse problema,
porque ela só pode conter elementos hashable,
e o valor de um objeto hashable não pode mudar, por definição.
O fato de variáveis conterem referências tem muitas consequências práticas para a programação em Python:
-
Uma atribuição simples não cria cópias.
-
Uma atribuição composta com
+=ou*=cria novos objetos se a variável à esquerda da atribuição estiver vinculada a um objeto imutável, mas pode modificar um objeto mutável diretamente. -
Atribuir um novo valor a uma variável existente não muda o objeto previamente vinculado à variável. Isso se chama rebinding (re-vinculação); a variável passa a se referir a um objeto diferente. Se aquela variável era a última referência ao objeto anterior, aquele objeto será eliminado pela coleta de lixo.
-
Parâmetros de função são passados como apelidos, o que significa que a função pode alterar qualquer objeto mutável recebido como argumento. Não há como evitar isso, exceto criando cópias locais ou usando objetos imutáveis (i.e., passando uma tupla em vez de uma lista)
-
Usar objetos mutáveis como valores default de parâmetros de função é perigoso, pois se os parâmetros forem modificados pela função, o default muda, afetando chamadas posteriores que usem o default.
Em CPython, um objeto é descartado assim que o número de referências a ele chega a zero. Objetos também podem ser descartados se formarem grupos com referências cíclicas sem nenhuma referência externa ao grupo.
Em algumas situações, pode ser útil manter uma referência para um objeto que
não vai, por si só, manter o objeto vivo.
Um exemplo é uma classe que queira manter o registro de todas as suas instâncias atuais.
Isso pode ser feito com referências fracas,
um mecanismo de baixo nível encontrado nas coleções WeakValueDictionary,
WeakKeyDictionary, WeakSet, e na função finalize do módulo weakref.
Leia "Weak References" em https://fluentpython.com
para mais detalhes sobre weakref.
O capítulo "Modelo de Dados" de A Referência da Linguagem Python inicia com uma explicação bastante clara sobre identidades e valores de objetos.
Wesley Chun, autor da série Core Python, apresentou Understanding Python’s Memory Model, Mutability, and Methods (EN) na EuroPython 2011, discutindo não apenas o tema desse capítulo como também o uso de métodos especiais.
Doug Hellmann escreveu os posts "copy — Duplicate Objects" (EN) e "weakref — Garbage-Collectable References to Objects" (EN), cobrindo alguns dos tópicos que acabamos de tratar.
Você pode encontrar mais informações sobre o coletor de lixo geracional do CPython em gc — Interface para o coletor de lixo, que começa com a frase "Este módulo fornece uma interface para o coletor de lixo opcional." O adjetivo "opcional" aqui pode ser surpreendente, mas o capítulo "Modelo de Dados" também afirma:
Uma implementação tem permissão para adiar a coleta de lixo ou omiti-la completamente — é um detalhe de implementação como a coleta de lixo é implementada, desde que nenhum objeto que ainda esteja acessível seja coletado.
Pablo Galindo escreveu um texto mais aprofundado sobre o Coletor de Lixo em Python, em "Design of CPython’s Garbage Collector" (EN) no Python Developer’s Guide, voltado para contribuidores novos e experientes da implementação CPython.
O coletor de lixo do CPython 3.4 aperfeiçoou o tratamento de objetos contendo um método __del__,
como descrito em PEP 442—Safe object finalization (EN).
A Wikipedia tem um artigo sobre string interning (EN), que menciona o uso desta técnica em várias linguagens, incluindo Python.
A Wikipedia também tem um artigo sobre "Haddocks' Eyes", a canção de Lewis Carroll que mencionei no início deste capítulo. Os editores da Wikipedia escreveram que a letra é usada em trabalhos de lógica e filosofia "para elaborar o status simbólico do conceito de 'nome': um nome como um marcador de identificação pode ser atribuído a qualquer coisa, incluindo outro nome, introduzindo assim níveis diferentes de simbolização."
Tratamento igual para todos os objetos
Aprendi
Java antes de conhecer Python.
O operador == em Java sempre me pareceu equivocado.
É mais comum que programadores estejam preocupados com a igualdade que com a identidade.
Mas para objetos (não tipos primitivos), o == em Java compara referências, não valores dos objetos.
Mesmo para algo tão básico quanto comparar strings,
Java obriga você a usar o método .equals.
E mesmo assim, há outro problema: se você escrever a.equals(b) e a for null,
você causa uma null pointer exception (exceção de ponteiro nulo).
Os projetistas de Java sentiram necessidade de sobrecarregar + para strings;
por que não mantiveram essa ideia e sobrecarregaram == também?
Python faz melhor.
O operador == compara valores de objetos; is compara referências.
E como Python permite sobrecarregar operadores,
== funciona de forma sensata com todos os objetos na biblioteca padrão,
incluindo None, que é um objeto de verdade, ao contrário do null de Java.
E claro, você pode definir __eq__ nas suas próprias classes para controlar
o que == significa para suas instâncias.
Se você não sobrecarregar __eq__, o método herdado de object compara os IDs dos objetos,
então a regra básica é que cada instância de uma classe definida pelo usuário é considerada diferente.
Estas são algumas das coisas que me fizeram mudar de Java para Python assim que terminei de ler The Python Tutorial em uma tarde de setembro de 1998.
Mutabilidade
Este capítulo não seria necessário se todos os objetos em Python fossem imutáveis. Quando você está lidando com objetos imutáveis, não faz diferença se as variáveis guardam os objetos em si ou referências para objetos compartilhados.
Se a == b é verdade, e nenhum dos dois objetos pode mudar,
eles podem perfeitamente ser o mesmo objeto.
Por isso a internalização de strings é segura.
A identidade dos objetos so é importante quando esses objetos podem mudar.
Em programação funcional "pura", todos os dados são imutáveis: concatenar algo a uma coleção, na verdade, cria uma nova coleção. Elixir é uma linguagem funcional prática e fácil de aprender, na qual todos os tipos nativos são imutáveis, incluindo as listas.
Python, por outro lado, não é uma linguagem funcional,
muito menos uma linguagem funcional pura.
Instâncias de classes definidas pelo usuário são mutáveis por padrão em Python—como
na maioria das linguagens orientadas a objetos.
Ao criar seus próprios objetos, você precisa tomar o cuidado adicional de torná-los imutáveis,
se este for um requisito.
Cada atributo do objeto precisa ser também imutável,
senão você termina criando algo como uma tupla:
imutável quanto ao id do objeto,
mas seu valor pode mudar se a tupla contiver um objeto mutável.
Objetos mutáveis também são a razão pela qual programar com threads é tão difícil: threads modificando objetos sem uma sincronização apropriada podem corromper dados. Sincronização excessiva, por outro lado, causa deadlocks. A linguagem e a plataforma Erlang—que inclui Elixir—foi projetada para maximizar o tempo de execução em aplicações distribuídas de alta concorrência, como aplicações de controle de telecomunicações. Naturalmente, eles escolheram tornar os dados imutáveis por default.
Destruição de objetos e coleta de lixo
Não existe em Python uma forma de destruir um objeto diretamente. E essa omissão é uma grande qualidade: se você pudesse destruir um objeto a qualquer momento, o que aconteceria com as referências que apontam para ele?
A coleta de lixo em CPython é feita principalmente por contagem de referências, que é fácil de implementar, mas vulnerável a vazamentos de memória (memory leaks) quando existem referências cíclicas. Assim, com a versão 2.0 (de outubro de 2000), um coletor de lixo geracional foi implementado, e ele consegue descartar objetos inatingíveis que foram mantidos vivos por ciclos de referências.
Mas a contagem de referências ainda está lá como mecanismo básico, e ela causa a destruição imediata de objetos com zero referências. Isso significa que, em CPython — pelo menos por hora — é seguro escrever:
open('test.txt', 'wt', encoding='utf-8').write('1, 2, 3')Este código é seguro porque a contagem de referências do objeto arquivo será zero
após o método write retornar, e o arquivo será fechado quando o objeto for descartado.
Entretanto, a mesma linha não é segura em Jython ou IronPython,
que usam o coletor de lixo dos runtimes de seus ambientes
(a Java VM e a .NET CLR, respectivamente), que são mais sofisticados,
mas não se baseiam em contagem de referências,
e podem demorar mais para destruir o objeto e fechar o arquivo.
Em todos os casos, incluindo em CPython, a melhor prática é fechar o arquivo explicitamente,
e a forma mais confiável de fazer isso é usando a instrução with,
que garante o fechamento do arquivo mesmo se acontecerem
exceções enquanto ele estiver aberto.
Usando with, a linha anterior se torna:
with open('test.txt', 'wt', encoding='utf-8') as fp:
fp.write('1, 2, 3')Se você tiver interesse no assunto de coletores de lixo,
você talvez queira ler o artigo de Thomas Perl,
"Python Garbage Collector Implementations: CPython, PyPy and GaS"
(EN), onde eu aprendi esses detalhes sobre a segurança de open().write() em CPython.
Passagem de parâmetros: chamada por compartilhamento
Uma maneira popular de explicar como a passagem de parâmetros funciona em Python é a frase: "Parâmetros são passados por valor, mas os valores são referências." Isso não está errado, mas causa confusão porque os modos mais comuns de passagem de parâmetros em linguagens tradicionais são chamada por valor (a função recebe uma cópia dos argumentos) e chamada por referência (a função recebe um ponteiro para o argumento).
Em Python, a função recebe uma cópia dos argumentos, mas os argumentos são sempre referências. Então o valor dos objetos referenciados podem ser alterados pela função, se eles forem mutáveis, mas sua identidade não. Além disso, como a função recebe uma cópia da referência em um argumento, reassociar essa referência no corpo da função não tem qualquer efeito fora da função. Adotei o termo chamada por compartilhamento depois de encontrar a definição de call by sharing no livro Programming Language Pragmatics, 3rd ed., de Michael L. Scott (Morgan Kaufmann), seção "8.3.1: Parameter Modes."
str, byte e array.array, que não contêm referências e sim seu conteúdo — caracteres, bytes e números — armazenado em um espaço contíguo de memória.
help(tuple) no console de Python e leia: "Se o argumento é uma tupla, o valor de retorno é o mesmo objeto." Pensei que sabia tudo sobre tuplas antes de escrever esse livro.
copy não copiar nada, é justificável pela compatibilidade da interface: torna frozenset mais compatível com set. De qualquer forma, não faz diferença para o usuário final se dois objetos imutáveis idênticos são o mesmo ou são cópias.
__class__ do objeto. Mas isso é uma perversão, e eu me arrependo de ter escrito essa nota de rodapé.



