Skip to content

Latest commit

 

History

History
1346 lines (1153 loc) · 55.7 KB

File metadata and controls

1346 lines (1153 loc) · 55.7 KB

Referências, mutabilidade, e memória

“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.‘”

— Adaptado de “Alice Através do Espelho e o que Ela Encontrou Lá”
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.

Novidades neste capítulo

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.

Variáveis não são caixas

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”.

Example 1. As variáveis 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]
  1. Cria uma lista [1, 2, 3] e a vincula à variável a.

  2. Vincula a variável b ao mesmo valor referenciado por a.

  3. Modifica a lista referenciada por a, anexando um novo item.

  4. É possível ver o efeito através da variável b. Se você pensar em b como uma caixa que guardava uma cópia de [1, 2, 3] da caixa a, 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.

Boxes and labels diagram
Figure 1. 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.

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.

Example 2. 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']
  1. A saída Gizmo id: … é um efeito colateral da criação de uma instância de Gizmo.

  2. Multiplicar uma instância de Gizmo levanta uma exceção.

  3. Aqui está a prova de que um segundo Gizmo foi de fato instanciado antes que a multiplicação fosse tentada.

  4. Mas a variável y nunca 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.

Identidade, igualdade e apelidos

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.

Example 3. 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}
  1. lewis é um apelido para charles.

  2. O operador is e a função id confirmam essa afirmação.

  3. Adicionar um item a lewis é o mesmo que adicionar um item a charles.

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.

Alias x copy diagram
Figure 2. charles e lewis estão vinculados ao mesmo objeto; alex está vinculado a um objeto diferente de valor igual.
Example 4. 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
  1. alex é uma referência a um objeto que é uma réplica do objeto vinculado a charles.

  2. Os objetos são iguais quando comparados devido à implementação de __eq__ na classe dict.

  3. 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 is compara a identidade de dois objetos; a função id() 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 id() ocorre durante o processo de debugging, quando o repr() de dois objetos são semelhantes, mas você precisa saber se duas referências são apelidos ou apontam para objetos diferentes. Se as referências estão em contextos diferentes—​por exemplo, em stack frames diferentes—​pode não ser viável usar is.

Escolhendo Entre == e is

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 None

E a forma apropriada de escrever sua negação é:

x is not None

None é 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 == é mais utilizado que is. O caso mais comum para uso de is é comparar com None. O is também é útil para testar valores de classes derivadas de enum.Enum. Se não estiver seguro, use ==. Em geral, é o que você quer, e ele também funciona com None e valores de Enum, ainda que seja um pouco mais lento.

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.

A imutabilidade relativa das tuplas

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.

Example 5. 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
  1. t1 é imutável, mas t1[-1] é mutável.

  2. Cria a tupla t2, cujos itens são iguais àqueles de t1.

  3. Apesar de serem objetos distintos,t1 e t2 são iguais quando comparados, como esperado.

  4. Obtém o id da lista na posição t1[-1].

  5. Modifica diretamente a lista t1[-1].

  6. O id de t1[-1] não mudou, apenas seu valor.

  7. t1 e t2 agora 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 princípio, cópias são rasas

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
  1. list(l1) cria uma cópia de l1.

  2. As cópias são iguais…​

  3. …​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.

Example 6. 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
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)
  1. l2 é uma cópia rasa de l1. Este estado está representado em Estado do programa imediatamente após a atribuição 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).

  2. Concatenar 100 a l1 não tem qualquer efeito sobre l2.

  3. Aqui removemos 55 da lista interna l1[1]. Isso afeta l2, pois l2[1] está associado à mesma lista em l1[1].

  4. Para um objeto mutável como a lista referida por l2[1], o operador += altera a lista diretamente. Essa mudança é visível em l1[1], que é um apelido para l2[1].

  5. += em uma tupla cria uma nova tupla e reassocia a variável l2[2] a ela. Isso é equivalente a fazer l2[2] = l2[2] + (10, 11). Agora as tuplas na última posição de l1 e l2 não são mais o mesmo objeto. Veja a 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.).

References diagram
Figure 3. Estado do programa imediatamente após a atribuição 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)
Example 7. Saída de [ex_shallow_copy]
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)]
References diagram
Figure 4. 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.)

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.

Cópias profundas e cópias rasas

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.

Example 8. Bus pega ou deixa passageiros
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.

Example 9. Os efeitos do uso de 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)
  1. Usando copy e deepcopy, criamos três instâncias distintas de Bus.

  2. Após bus1 deixar 'Bill', ele também desaparece de bus2.

  3. A inspeção do atributo passengers mostra que bus1 e bus2 compartilham o mesmo objeto lista, pois bus2 é uma cópia rasa de bus1.

  4. bus3 é uma cópia profunda de bus1, então seu atributo passengers se 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..

Example 10. Referências cíclicas: 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.

Parâmetros de função como referências

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ó:

Example 11. Uma função pode mudar qualquer objeto mutável que receba
>>> 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))
  1. O número x não se altera.

  2. A lista a é alterada.

  3. A tupla t não se altera.

Outra questão relacionada a parâmetros de função é o uso de valores mutáveis como defaults, discutida a seguir.

Porque evitar tipos mutáveis como default em parâmetros

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.

Example 12. Uma classe simples ilustrando o perigo de um default mutável
link:../code/06-obj-ref/haunted_bus.py[role=include]
  1. Quando não passamos o argumento passengers, esse parâmetro é vinculado ao objeto lista default, que inicialmente está vazia.

  2. Essa atribuição torna self.passengers um apelido de passengers, que por sua vez é um apelido para a lista default, quando um argumento passengers não é passado para a função.

  3. Quando os métodos .remove() e .append() são usados com self.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.

Example 13. Ônibus assombrados por passageiros fantasmas
>>> 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']
  1. bus1 começa com uma lista de dois passageiros.

  2. Até aqui, tudo bem: nenhuma surpresa em bus1.

  3. bus2 começa vazio, então a lista vazia default é vinculada a self.passengers.

  4. bus3 também começa vazio, e novamente a lista default é atribuída.

  5. A lista default não está mais vazia!

  6. Agora Dave, pego pelo bus3, aparece no bus2.

  7. O problema: bus2.passengers e bus3.passengers se referem à mesma lista.

  8. 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
True

O 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.

Programação defensiva com argumentos mutáveis

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.

Example 14. Passageiros desaparecem quando são deixados por um 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']
  1. basketball_team contém o nome de cinco estudantes.

  2. Um TwilightBus é carregado com o time.

  3. O bus deixa uma estudante, depois outra.

  4. 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.

Example 15. Uma classe simples mostrando os perigos de mudar argumentos recebidos
link:../code/06-obj-ref/twilight_bus.py[role=include]
  1. Aqui cuidadosamente criamos uma lista vazia quando passengers é None.

  2. Entretanto, esta atribuição transforma self.passengers em um apelido para passengers, que por sua vez é um apelido para o argumento passado para __init__ (i.e. basketball_team em Passageiros desaparecem quando são deixados por um TwilightBus).

  3. Quando os métodos .remove() e .append() são usados com self.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)
  1. Cria uma cópia da lista passengers, ou converte o argumento para list se 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.

del e coleta de lixo

Os objetos nunca são destruídos explicitamente; no entanto, quando eles se tornam inacessíveis, eles podem ser coletados como lixo.

— “Modelo de Dados” capítulo de A Referência da Linguagem Python

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)
  1. Cria o objeto [1, 2] e vincula a a ele.

  2. Vincula b ao mesmo objeto [1, 2].

  3. Apaga a referência a.

  4. [1, 2] não é afetado, pois b ainda aponta para ele.

  5. Reassociar b a 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 __del__, mas ele não causa a remoção de uma instância e não deve ser invocado em seu código. O método __del__ é invocado pelo interpretador Python quando a instância está prestes a ser destruída, para dar a ela a chance de liberar recursos externos. É muito raro ser preciso implementar __del__ em seu código, mas ainda assim alguns programadores Python perdem tempo codando este método sem necessidade. O uso correto de __del__ é bastante complexo. Consulte __del__ no capítulo "Modelo de Dados" em A Referência da Linguagem Python.

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.

Example 16. Detectando o fim de um objeto quando não resta nenhuma referência apontando para ele
>>> 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
  1. s1 e s2 são apelidos do mesmo conjunto, {1, 2, 3}.

  2. Para essa demonstração, a função bye não deve ser um método vinculado ao objeto prestes a ser destruído, nem manter uma referência para o objeto.

  3. Registra o callback bye no objeto referenciado por s1.

  4. O atributo .alive é True antes do objeto finalize ser chamado.

  5. Como vimos, del não apaga o objeto, apenas a referência s1 a ele.

  6. Reassociar a última referência, s2, torna {1, 2, 3} inacessível. Ele é destruído, o callback bye é invocado, e ender.alive se torna False.

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.

Peças que Python prega com imutáveis

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 is de forma incorreta, então acho que vale a pena mencionar esses detalhes.

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]

Example 17. Uma tupla construída a partir de outra é, na verdade, exatamente a mesma tupla.
>>> t1 = (1, 2, 3)
>>> t2 = tuple(t1)
>>> t2 is t1  (1)
True
>>> t3 = t1[:]
>>> t3 is t1  (2)
True
  1. t1 e t2 estão vinculadas ao mesmo objeto

  2. 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:

Example 18. Strings e inteiros literais podem criar objetos compartilhados.
>>> 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
  1. Criando duas str com o mesmo valor.

  2. Surpresa: a e b se referem ao mesmo objeto str!

  3. Alguns inteiros pequenos são compartilhados.

  4. 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 str ou int! Sempre use == em vez de is para verificar a igualdade de strings ou inteiros. A internalização é uma otimização para uso interno do interpretador Python.

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]

Resumo do capítulo

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.

Para saber mais

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."

Ponto de vista

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."


1. Lynn Andrea Stein é uma aclamada educadora de ciências da computação. Ela atualmente leciona na Olin College of Engineering (EN).
2. Ao contrário de sequências planas de tipo único, como 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.
4. Isso está claramente documentado. Digite 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.
5. Essa mentirinha inofensiva, do método 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.
6. Um péssimo uso dessas informações seria perguntar sobre elas quando entrevistando candidatos a emprego ou criando perguntas para exames de "certificação". Há inúmeros fatos mais importantes e úteis para testar conhecimentos de Python.
7. Na verdade, o tipo de um objeto pode ser modificado, bastando para isso atribuir uma classe diferente ao atributo __class__ do objeto. Mas isso é uma perversão, e eu me arrependo de ter escrito essa nota de rodapé.