18. Modelo de Dados e de Objetos do Python: Referências, Comparações e Cópias
Entender como o Python armazena e gerencia dados é crucial para escrever programas corretos. Neste capítulo, vamos explorar o modelo de objetos (object model) do Python — o sistema fundamental que governa como todos os dados funcionam no Python. Você vai aprender por que algumas atribuições criam cópias independentes enquanto outras criam referências compartilhadas, como comparar objetos corretamente e como evitar armadilhas comuns ao trabalhar com coleções.
Esse conhecimento vai ajudar você a entender comportamentos surpreendentes que talvez já tenha encontrado, como por que modificar uma lista às vezes afeta outra, ou por que comparar duas listas com == dá resultados diferentes de comparar com is.
18.1) Tudo É um Objeto no Python
No Python, todo pedaço de dado é um objeto (object). Isso não é apenas um conceito teórico — tem implicações práticas sobre como seus programas funcionam.
Quando você cria um número, uma string, uma lista, ou qualquer outro valor, o Python cria um objeto (object) na memória. Um objeto é um contêiner que contém:
- Os dados em si (o valor (value))
- Informações sobre que tipo de dado ele é (o tipo (type))
- Um identificador único (a identidade (identity))
Vamos ver isso na prática:
# Criando diferentes tipos de objetos
number = 42
text = "Hello"
items = [1, 2, 3]
# Cada uma dessas variáveis se refere a um objeto na memória
print(number) # Output: 42
print(text) # Output: Hello
print(items) # Output: [1, 2, 3]Até valores simples como inteiros são objetos. Isso significa que eles têm capacidades além de apenas armazenar um número:
# Inteiros são objetos com métodos
number = 42
print(number.bit_length()) # Output: 6
# Strings são objetos com métodos
text = "hello"
print(text.upper()) # Output: HELLO
# Listas são objetos com métodos
items = [3, 1, 2]
items.sort()
print(items) # Output: [1, 2, 3]Por que isso importa? Porque quando você atribui uma variável ou passa dados para uma função (function), você não está copiando o objeto — você está criando uma referência (reference) ao mesmo objeto. Isso é fundamentalmente diferente de como algumas outras linguagens de programação funcionam, e entender essa distinção vai evitar muitos bugs confusos.
# Criando um objeto lista
original = [1, 2, 3]
# Isso não cria uma nova lista - cria outra referência
# para o MESMO objeto lista
another_name = original
# Modificar por uma referência afeta a outra
another_name.append(4)
print(original) # Output: [1, 2, 3, 4]
print(another_name) # Output: [1, 2, 3, 4]Tanto original quanto another_name se referem ao mesmo objeto lista na memória. Quando modificamos a lista via another_name, vemos a mudança via original porque as duas estão olhando para o mesmo objeto.
Esse comportamento é chamado de semântica de referência (reference semantics), e é um dos conceitos mais importantes na programação em Python. Vamos explorá-lo em profundidade ao longo deste capítulo.
18.2) Identidade, Tipo e Valor de Objetos
Todo objeto no Python tem três características fundamentais que o definem: identidade (identity), tipo (type) e valor (value). Entender essas características ajuda você a raciocinar sobre como os objetos se comportam e como compará-los corretamente.
18.2.1) Identidade de Objetos com id()
A identidade (identity) de um objeto é um número único que o Python atribui quando o objeto é criado. Essa identidade nunca muda durante o tempo de vida do objeto — é como um endereço permanente na memória.
Você pode obter a identidade de um objeto usando a função id():
# Criando objetos e conferindo suas identidades
x = [1, 2, 3]
y = [1, 2, 3]
z = x
print(id(x)) # Output: 140234567890123 (example - actual number varies)
print(id(y)) # Output: 140234567890456 (different from x)
print(id(z)) # Output: 140234567890123 (same as x)Os números reais que você verá serão diferentes cada vez que rodar o programa, mas o padrão permanece: x e y têm identidades diferentes porque são objetos diferentes, mesmo que contenham os mesmos valores. Enquanto isso, z tem a mesma identidade que x porque z é apenas outro nome para o mesmo objeto.
Aqui vai um exemplo prático mostrando por que identidade importa:
# Dois alunos com as mesmas notas
student1_grades = [85, 90, 92]
student2_grades = [85, 90, 92]
# Estes são objetos diferentes (identidades diferentes)
print(id(student1_grades)) # Output: 140234567890123 (example)
print(id(student2_grades)) # Output: 140234567890456 (different)
# Modificar um não afeta o outro
student1_grades.append(88)
print(student1_grades) # Output: [85, 90, 92, 88]
print(student2_grades) # Output: [85, 90, 92]Agora considere um cenário diferente:
# As notas de um aluno acompanhadas por duas variáveis
original_grades = [85, 90, 92]
backup_reference = original_grades
# Estes se referem ao MESMO objeto (mesma identidade)
print(id(original_grades)) # Output: 140234567890123 (example)
print(id(backup_reference)) # Output: 140234567890123 (same!)
# Modificar por qualquer nome afeta ambos
backup_reference.append(88)
print(original_grades) # Output: [85, 90, 92, 88]
print(backup_reference) # Output: [85, 90, 92, 88]Insight chave: Quando duas variáveis têm a mesma identidade, elas se referem ao mesmo objeto exato na memória. Mudanças feitas por uma variável ficam visíveis pela outra porque existe apenas um objeto sendo modificado.
18.2.2) Tipo de Objetos com type()
O tipo (type) de um objeto determina que tipo de dado ele armazena e quais operações você pode executar nele. Como aprendemos no Capítulo 3, você pode verificar o tipo de um objeto usando a função type():
# Tipos diferentes de objetos
number = 42
text = "Hello"
items = [1, 2, 3]
mapping = {"name": "Alice"}
print(type(number)) # Output: <class 'int'>
print(type(text)) # Output: <class 'str'>
print(type(items)) # Output: <class 'list'>
print(type(mapping)) # Output: <class 'dict'>O tipo de um objeto nunca muda depois da criação. Você não consegue transformar um inteiro em uma string — você só pode criar um novo objeto string com base no valor do inteiro:
# O tipo é fixo na criação
x = 42
print(type(x)) # Output: <class 'int'>
# Isso não muda o tipo de x - cria um NOVO objeto string
# e faz x referenciar esse novo objeto em vez disso
x = str(x)
# O objeto inteiro original (42) ainda existe na memória até ser coletado pelo garbage collector
# x agora aponta para um objeto completamente diferente: a string "42"
print(type(x)) # Output: <class 'str'>
print(x) # Output: 42 (agora é uma string, não um inteiro)Entender tipos é crucial porque tipos diferentes suportam operações diferentes:
# Listas suportam append
grades = [85, 90]
grades.append(92)
print(grades) # Output: [85, 90, 92]
# Strings não têm append - elas são imutáveis
text = "Hello"
# text.append(" World") # AttributeError: 'str' object has no attribute 'append'
# Mas strings suportam concatenação
text = text + " World"
print(text) # Output: Hello World18.2.3) Valor do Objeto
O valor (value) de um objeto é o dado real que ele contém. Diferente da identidade e do tipo, o valor pode mudar para objetos mutáveis (mutable) (como listas e dicionários) mas não pode mudar para objetos imutáveis (immutable) (como inteiros e strings).
# Para objetos mutáveis, o valor pode mudar
shopping_cart = ["milk", "bread"]
print(shopping_cart) # Output: ['milk', 'bread']
shopping_cart.append("eggs")
print(shopping_cart) # Output: ['milk', 'bread', 'eggs']
# Mesmo objeto (mesma identidade), valor diferente
# Para objetos imutáveis, o valor não pode mudar
count = 5
print(count) # Output: 5
count = count + 1
print(count) # Output: 6
# Isso criou um NOVO objeto com uma nova identidadeAqui está um exemplo completo mostrando as três características:
# Criando um objeto lista
data = [10, 20, 30]
print("Identity:", id(data)) # Output: Identity: 140234567890123 (example)
print("Type:", type(data)) # Output: Type: <class 'list'>
print("Value:", data) # Output: Value: [10, 20, 30]
# Modificando o valor (identidade e tipo permanecem os mesmos)
data.append(40)
print("Identity:", id(data)) # Output: Identity: 140234567890123 (unchanged)
print("Type:", type(data)) # Output: Type: <class 'list'> (unchanged)
print("Value:", data) # Output: Value: [10, 20, 30, 40] (changed)Entender essas três características ajuda você a prever como os objetos vão se comportar nos seus programas. A identidade diz se duas variáveis se referem ao mesmo objeto, o tipo diz quais operações são permitidas e o valor diz quais dados o objeto contém no momento.
18.3) Tipos Mutáveis e Imutáveis
Uma das distinções mais importantes no Python é entre tipos mutáveis (mutable) e imutáveis (immutable). Essa distinção afeta como os objetos se comportam quando você tenta alterá-los, e entender isso evita muitos erros comuns de programação.
18.3.1) Tipos Imutáveis: Valores Que Não Podem Mudar
Um objeto imutável (immutable) é aquele cujo valor não pode ser alterado após a criação. Quando você executa uma operação que parece modificar um objeto imutável, o Python na verdade cria um novo objeto com o valor modificado.
Os tipos imutáveis do Python incluem:
- Inteiros (
int) - Números de ponto flutuante (
float) - Strings (
str) - Tuplas (
tuple) - Booleanos (
bool) - None (
NoneType)
Vamos ver a imutabilidade em ação com inteiros:
# Criando um inteiro
x = 100
print("Original x:", x) # Output: Original x: 100
print("Identity of x:", id(x)) # Output: Identity of x: 140234567890123 (example)
# Isto parece que estamos modificando x, mas na verdade estamos criando um novo objeto
x = x + 1
print("Modified x:", x) # Output: Modified x: 101
print("Identity of x:", id(x)) # Output: Identity of x: 140234567890456 (different!)A identidade mudou porque x = x + 1 criou um objeto inteiro completamente novo com o valor 101. O objeto original com valor 100 ainda existe (até o garbage collector do Python removê-lo), mas x agora se refere a um objeto diferente.
Strings demonstram imutabilidade ainda mais claramente:
# Criando uma string
message = "Hello"
print("Original:", message) # Output: Original: Hello
print("Identity:", id(message)) # Output: Identity: 140234567890789 (example)
# Métodos de string não modificam o original - eles retornam novas strings
uppercase = message.upper()
print("Original:", message) # Output: Original: Hello (unchanged)
print("Uppercase:", uppercase) # Output: Uppercase: HELLO
print("Identity of original:", id(message)) # Output: Identity of original: 140234567890789 (same)
print("Identity of uppercase:", id(uppercase)) # Output: Identity of uppercase: 140234567891012 (different)Até operações que parecem estar modificando uma string na verdade criam novos objetos string:
# Construindo uma string com concatenação
text = "Python"
print("Before:", text, "- ID:", id(text)) # Output: Before: Python - ID: 140234567891234 (example)
text = text + " Programming"
print("After:", text, "- ID:", id(text)) # Output: After: Python Programming - ID: 140234567891567 (different)Por que imutabilidade importa: Objetos imutáveis são seguros para compartilhar entre partes diferentes do seu programa porque nenhuma parte pode modificá-los acidentalmente. Isso torna seu código mais previsível e mais fácil de entender.
18.3.2) Tipos Mutáveis: Valores Que Podem Mudar
Um objeto mutável (mutable) é aquele cujo valor pode ser alterado após a criação sem criar um novo objeto. A identidade do objeto permanece a mesma, mas seu conteúdo pode ser modificado.
Os tipos mutáveis do Python incluem:
- Listas (
list) - Dicionários (
dict) - Conjuntos (
set)
Vamos ver mutabilidade com listas:
# Criando uma lista
numbers = [1, 2, 3]
print("Original:", numbers) # Output: Original: [1, 2, 3]
print("Identity:", id(numbers)) # Output: Identity: 140234567892345 (example)
# Modificando a lista - mesmo objeto, valor diferente
numbers.append(4)
print("Modified:", numbers) # Output: Modified: [1, 2, 3, 4]
print("Identity:", id(numbers)) # Output: Identity: 140234567892345 (same!)A identidade não mudou porque modificamos o objeto lista existente em vez de criar um novo. Isso é fundamentalmente diferente de como tipos imutáveis funcionam.
Dicionários e conjuntos também são mutáveis:
# Exemplo de dicionário
student = {"name": "Alice", "grade": 85}
print("Before:", student, "- ID:", id(student)) # Output: Before: {'name': 'Alice', 'grade': 85} - ID: 140234567893012 (example)
student["grade"] = 90 # Modificando o dicionário
print("After:", student, "- ID:", id(student)) # Output: After: {'name': 'Alice', 'grade': 90} - ID: 140234567893012 (same)
# Exemplo de conjunto
unique_numbers = {1, 2, 3}
print("Before:", unique_numbers, "- ID:", id(unique_numbers)) # Output: Before: {1, 2, 3} - ID: 140234567893345 (example)
unique_numbers.add(4) # Modificando o conjunto
print("After:", unique_numbers, "- ID:", id(unique_numbers)) # Output: After: {1, 2, 3, 4} - ID: 140234567893345 (same)18.3.3) Por Que a Mutabilidade Importa na Prática
A diferença entre tipos mutáveis e imutáveis se torna crítica quando múltiplas variáveis se referem ao mesmo objeto:
# Exemplo imutável - compartilhamento seguro
x = "Hello"
y = x # y se refere ao mesmo objeto string
# "Modificar" x cria um novo objeto
x = x + " World"
print(x) # Output: Hello World
print(y) # Output: Hello (unchanged - y still refers to the original)# Exemplo mutável - modificações compartilhadas
list1 = [1, 2, 3]
list2 = list1 # list2 se refere ao MESMO objeto lista
# Modificar via list1 afeta list2
list1.append(4)
print(list1) # Output: [1, 2, 3, 4]
print(list2) # Output: [1, 2, 3, 4] (also changed!)Entender mutabilidade é essencial para:
- Prever comportamento: Saber se uma operação cria um novo objeto ou modifica um objeto existente
- Evitar bugs: Prevenir modificações não intencionais quando objetos são compartilhados
- Escrever código eficiente: Escolher o tipo certo para o seu caso de uso
- Entender o comportamento de funções: Saber quando parâmetros de função podem ser modificados
Nas próximas seções, vamos explorar como atribuição funciona com esses tipos diferentes e como criar cópias independentes quando necessário.
18.4) Como a Atribuição Funciona com Objetos
A atribuição no Python não copia objetos — ela cria referências (references) a objetos. Entender essa distinção é crucial para escrever programas corretos, especialmente ao trabalhar com tipos mutáveis.
18.4.1) Atribuição Cria Referências, Não Cópias
Quando você escreve x = y, o Python não cria uma cópia do objeto ao qual y se refere. Em vez disso, ele faz x se referir ao mesmo objeto ao qual y se refere. As duas variáveis viram nomes para o mesmo objeto na memória.
Vamos ver isso primeiro com objetos imutáveis:
# Atribuição com inteiros (imutáveis)
a = 100
b = a # b agora se refere ao mesmo objeto inteiro que a
print("a:", a) # Output: a: 100
print("b:", b) # Output: b: 100
print("Same object?", id(a) == id(b)) # Output: Same object? True
# "Modificar" a cria um novo objeto
a = a + 1
print("a:", a) # Output: a: 101
print("b:", b) # Output: b: 100 (unchanged)
print("Same object?", id(a) == id(b)) # Output: Same object? FalseCom objetos imutáveis, esse comportamento normalmente é seguro porque você não consegue modificar o objeto original. Quando você faz uma operação que muda o valor, o Python cria um novo objeto.
No entanto, com objetos mutáveis, o comportamento é bem diferente:
# Atribuição com listas (mutáveis)
list1 = [1, 2, 3]
list2 = list1 # list2 se refere ao MESMO objeto lista que list1
print("list1:", list1) # Output: list1: [1, 2, 3]
print("list2:", list2) # Output: list2: [1, 2, 3]
print("Same object?", id(list1) == id(list2)) # Output: Same object? True
# Modificar via list1 afeta list2
list1.append(4)
print("list1:", list1) # Output: list1: [1, 2, 3, 4]
print("list2:", list2) # Output: list2: [1, 2, 3, 4] (also changed!)
print("Same object?", id(list1) == id(list2)) # Output: Same object? TrueTanto list1 quanto list2 são nomes para o mesmo objeto lista. Quando você modifica a lista por qualquer nome, você vê a mudança por ambos os nomes porque existe apenas uma lista.
Aqui vai um exemplo prático que mostra por que isso importa:
# Gerenciando notas de um aluno
alice_grades = [85, 90, 92]
backup_grades = alice_grades # Tentando criar um backup
print("Original:", alice_grades) # Output: Original: [85, 90, 92]
print("Backup:", backup_grades) # Output: Backup: [85, 90, 92]
# Adicionando uma nova nota
alice_grades.append(88)
# O "backup" também foi modificado!
print("Original:", alice_grades) # Output: Original: [85, 90, 92, 88]
print("Backup:", backup_grades) # Output: Backup: [85, 90, 92, 88]Isso não é backup nenhum — as duas variáveis se referem à mesma lista. Para criar um backup de verdade, você precisa criar uma cópia (o que veremos na Seção 18.8).
18.4.2) Atribuição em Chamadas de Função
Quando você passa um argumento para uma função (function), o Python usa a mesma semântica de referência. O parâmetro vira outro nome para o mesmo objeto:
# Função com parâmetro imutável
def increment(number):
number = number + 1 # Cria um novo objeto
return number
value = 5
result = increment(value)
print("Original value:", value) # Output: Original value: 5 (unchanged)
print("Returned result:", result) # Output: Returned result: 6O parâmetro number inicialmente se refere ao mesmo objeto inteiro que value. Quando fazemos number = number + 1, criamos um novo objeto inteiro e fazemos number se referir a ele. O objeto original (e value) permanecem inalterados.
Com objetos mutáveis, o comportamento é diferente:
# Função com parâmetro mutável
def add_item(items, new_item):
items.append(new_item) # Modifica a lista original
shopping_list = ["milk", "bread"]
add_item(shopping_list, "eggs")
print("Original list:", shopping_list) # Output: Original list: ['milk', 'bread', 'eggs']O parâmetro items se refere ao mesmo objeto lista que shopping_list. Quando modificamos a lista por items, estamos modificando a lista original.
Aqui vai um erro comum e como evitá-lo:
# ERRO: Modificando o original sem querer
def process_grades(grades):
grades.append(100) # Modifica o original!
return grades
student_grades = [85, 90, 92]
processed = process_grades(student_grades)
print("Original:", student_grades) # Output: Original: [85, 90, 92, 100] (modified!)
print("Processed:", processed) # Output: Processed: [85, 90, 92, 100]
# CORRETO: Crie uma cópia se você não quiser modificar o original
def process_grades_safely(grades):
# Cria uma nova lista com os mesmos elementos
result = grades + [100] # Concatenação cria uma nova lista
return result
student_grades = [85, 90, 92]
processed = process_grades_safely(student_grades)
print("Original:", student_grades) # Output: Original: [85, 90, 92] (unchanged)
print("Processed:", processed) # Output: Processed: [85, 90, 92, 100]Observação importante sobre argumentos padrão mutáveis: Uma armadilha comum relacionada envolve usar objetos mutáveis como valores padrão de parâmetros (como def func(items=[]):). Parâmetros padrão são criados uma vez quando a função é definida, não a cada vez que ela é chamada, o que pode levar a um comportamento inesperado em que a lista padrão acumula valores ao longo de múltiplas chamadas da função. Vamos explorar isso em detalhes no Capítulo 20, mas fique atento: essa é uma fonte frequente de bugs ao trabalhar com parâmetros mutáveis.
18.5) Semântica de Referência e Aliasing de Objetos
Semântica de referência (reference semantics) significa que variáveis no Python são nomes que se referem a objetos, não contêineres que guardam valores. Quando múltiplas variáveis se referem ao mesmo objeto, chamamos isso de aliasing. Entender aliasing é essencial para prever como seus programas se comportam.
18.5.1) O Que É Aliasing?
Aliasing ocorre quando duas ou mais variáveis se referem ao mesmo objeto na memória. As variáveis são "aliases" uma da outra — nomes diferentes para a mesma coisa.
Vamos ver aliasing com um exemplo simples:
# Criando uma lista e um alias
original = [1, 2, 3]
alias = original # alias se refere à mesma lista que original
print("Original:", original) # Output: Original: [1, 2, 3]
print("Alias:", alias) # Output: Alias: [1, 2, 3]
print("Same object?", id(original) == id(alias)) # Output: Same object? True
# Modificando via o alias
alias.append(4)
# A mudança fica visível pelos dois nomes
print("Original:", original) # Output: Original: [1, 2, 3, 4]
print("Alias:", alias) # Output: Alias: [1, 2, 3, 4]Existe apenas um objeto lista na memória, mas ele tem dois nomes: original e alias. Qualquer modificação feita por qualquer nome afeta o mesmo objeto subjacente.
Aqui vai um exemplo mais realista com registros de alunos:
# Banco de dados de alunos com aliasing
students = {
"alice": {"name": "Alice", "grade": 85},
"bob": {"name": "Bob", "grade": 90}
}
# Criando um alias para o registro da Alice
alice_record = students["alice"]
print("Alice's grade:", alice_record["grade"]) # Output: Alice's grade: 85
# Modificando via o alias
alice_record["grade"] = 95
# A mudança fica visível no dicionário original
print("Updated grade:", students["alice"]["grade"]) # Output: Updated grade: 95A variável alice_record é um alias para o dicionário armazenado em students["alice"]. Quando modificamos alice_record, estamos modificando o mesmo dicionário que está armazenado no dicionário students.
18.5.2) Detectando Aliasing com o Operador is
Você pode verificar se duas variáveis são aliases (se referem ao mesmo objeto) usando o operador is:
# Verificando aliasing
list1 = [1, 2, 3]
list2 = list1 # Alias
list3 = [1, 2, 3] # Objeto diferente com o mesmo valor
print("list1 is list2:", list1 is list2) # Output: list1 is list2: True (aliases)
print("list1 is list3:", list1 is list3) # Output: list1 is list3: False (different objects)
print("list1 == list3:", list1 == list3) # Output: list1 == list3: True (same value)O operador is verifica identidade (se duas variáveis se referem ao mesmo objeto), enquanto == verifica valor (se dois objetos têm o mesmo conteúdo). Vamos explorar essa distinção em detalhes na Seção 18.6.
18.5.3) Aliasing em Coleções
Aliasing fica mais complexo quando objetos são armazenados em coleções:
# Criando uma lista de listas
row = [0, 0, 0]
grid = [row, row, row] # Todos os três elementos são aliases da mesma lista!
print("Grid:")
for r in grid:
print(r)
# Output:
# [0, 0, 0]
# [0, 0, 0]
# [0, 0, 0]
# Modificar um elemento afeta todas as linhas
grid[0][0] = 1
print("\nAfter modification:")
for r in grid:
print(r)
# Output:
# [1, 0, 0]
# [1, 0, 0]
# [1, 0, 0]Esse é um erro comum ao tentar criar uma grade 2D. As três linhas são aliases da mesma lista, então modificar uma linha modifica todas elas.
A forma correta de criar linhas independentes:
# Criando linhas independentes
grid = [[0, 0, 0], [0, 0, 0], [0, 0, 0]] # Cada linha é uma lista separada
print("Grid:")
for r in grid:
print(r)
# Output:
# [0, 0, 0]
# [0, 0, 0]
# [0, 0, 0]
# Agora modificar um elemento só afeta aquela linha
grid[0][0] = 1
print("\nAfter modification:")
for r in grid:
print(r)
# Output:
# [1, 0, 0]
# [0, 0, 0]
# [0, 0, 0]18.6) Igualdade, Identidade e Pertencimento (==, is, e in) entre Tipos
O Python fornece três operadores fundamentais para comparar e verificar relações entre objetos: == para igualdade, is para identidade e in para pertencimento. Entender quando usar cada operador é crucial para escrever programas corretos.
18.6.1) Igualdade com == (Comparando Valores)
O operador == verifica se dois objetos têm o mesmo valor. Não importa se eles são o mesmo objeto na memória — importa apenas se seus conteúdos são iguais.
# Comparando valores com ==
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
print(list1 == list2) # Output: True (same values)
print(list1 == list3) # Output: True (same values)Mesmo que list1 e list2 sejam objetos diferentes na memória, eles têm o mesmo valor, então == retorna True.
Veja como == funciona com tipos diferentes:
# Igualdade entre tipos diferentes
print(42 == 42) # Output: True (same integer value)
print(42 == 42.0) # Output: True (integer equals float with same value)
print("hello" == "hello") # Output: True (same string value)
print([1, 2] == [1, 2]) # Output: True (same list contents)
print({"a": 1} == {"a": 1}) # Output: True (same dictionary contents)
# Valores diferentes
print(42 == 43) # Output: False
print("hello" == "Hello") # Output: False (case-sensitive)
print([1, 2] == [2, 1]) # Output: False (order matters)Para coleções, == faz uma comparação profunda (deep comparison) — ele verifica se todos os elementos são iguais:
# Comparação profunda com estruturas aninhadas
list1 = [[1, 2], [3, 4]]
list2 = [[1, 2], [3, 4]]
print(list1 == list2) # Output: True (all nested elements are equal)
# Mesmo que as listas internas sejam objetos diferentes
print(id(list1[0]) == id(list2[0])) # Output: False (different objects)
print(list1[0] == list2[0]) # Output: True (same values)18.6.2) Identidade com is (Comparando Identidade do Objeto)
O operador is verifica se duas variáveis se referem ao mesmo objeto na memória. Ele compara identidades, não valores.
# Comparando identidades com is
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
print(list1 is list2) # Output: False (different objects)
print(list1 is list3) # Output: True (same object)
# Confirmando com id()
print(id(list1) == id(list2)) # Output: False
print(id(list1) == id(list3)) # Output: TrueQuando usar is: O uso mais comum de is é para verificar None:
# Checando por None (do jeito correto)
def find_student(name, students):
"""Return student record or None if not found."""
for student in students:
if student["name"] == name:
return student
return None
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 90}
]
result = find_student("Charlie", students)
# Use 'is' para checar None
if result is None:
print("Student not found") # Output: Student not found
else:
print(f"Found: {result}")18.6.3) Pertencimento com in (Checando Contenção)
O operador in verifica se um valor está contido em uma coleção. Ele funciona com strings, listas, tuplas, conjuntos e dicionários:
# Pertencimento em diferentes tipos
print(2 in [1, 2, 3]) # Output: True
print("hello" in "hello world") # Output: True
print("x" in {"x": 10, "y": 20}) # Output: True (checks keys)
print(5 in {1, 2, 3, 4, 5}) # Output: TruePara dicionários, in verifica se uma chave existe:
# Checando pertencimento em dicionário
student = {"name": "Alice", "grade": 85, "age": 20}
print("name" in student) # Output: True (key exists)
print("Alice" in student) # Output: False (value, not key)
print("grade" in student) # Output: True (key exists)
# Checar valores exige acessar .values()
print("Alice" in student.values()) # Output: TrueO operador not in verifica ausência:
# Checando ausência
shopping_list = ["milk", "bread", "eggs"]
if "butter" not in shopping_list:
print("Don't forget to buy butter!") # Output: Don't forget to buy butter!Resumo de quando usar cada operador:
- Use
==quando você quiser verificar se dois objetos têm o mesmo valor - Use
isquando você quiser verificar se duas variáveis se referem ao mesmo objeto (mais comumente comNone, ou ao fazer debugging de aliasing) - Use
inquando você quiser verificar se um valor está contido em uma coleção
Entender essas distinções ajuda você a escrever comparações mais precisas e corretas nos seus programas.
18.7) Comparando Objetos Que Contêm Outros Objetos
Quando objetos contêm outros objetos (como listas dentro de listas, ou dicionários contendo listas), comparações ficam mais sutis. Entender como o Python compara estruturas aninhadas é essencial para trabalhar com dados complexos.
18.7.1) Como == Funciona com Estruturas Aninhadas
O operador == faz uma comparação recursiva (recursive comparison) para estruturas aninhadas. Ele compara não só o contêiner externo, mas também todos os objetos aninhados:
# Comparando listas aninhadas
list1 = [[1, 2], [3, 4]]
list2 = [[1, 2], [3, 4]]
print(list1 == list2) # Output: True
# Mesmo que as listas internas sejam objetos diferentes
print(id(list1[0]) == id(list2[0])) # Output: False
print(list1[0] == list2[0]) # Output: TrueO Python compara cada elemento recursivamente. Para list1 == list2 ser True, cada elemento correspondente precisa ser igual, incluindo elementos aninhados.
Aqui vai um exemplo mais complexo:
# Estrutura aninhada com múltiplos níveis
data1 = {
"students": [
{"name": "Alice", "grades": [85, 90, 92]},
{"name": "Bob", "grades": [88, 91, 87]}
],
"class": "Python 101"
}
data2 = {
"students": [
{"name": "Alice", "grades": [85, 90, 92]},
{"name": "Bob", "grades": [88, 91, 87]}
],
"class": "Python 101"
}
print(data1 == data2) # Output: TrueO Python compara:
- Chaves e valores do dicionário no nível superior ("students" e "class")
- A lista de alunos
- Cada dicionário de aluno (com as chaves "name" e "grades")
- A lista de notas de cada aluno
- Cada número de nota individual
Todos os níveis precisam bater para a comparação retornar True.
18.7.2) Ordem Importa para Sequências
Para sequências (listas e tuplas), a ordem dos elementos importa:
# Ordem importa em listas
list1 = [[1, 2], [3, 4]]
list2 = [[3, 4], [1, 2]]
print(list1 == list2) # Output: False (different order)
# Mas ordem não importa em conjuntos
set1 = {frozenset([1, 2]), frozenset([3, 4])}
set2 = {frozenset([3, 4]), frozenset([1, 2])}
print(set1 == set2) # Output: True (sets are unordered)18.7.3) Comparando Coleções de Tipos Diferentes
Tipos diferentes de coleção (list, tuple, set) nunca são iguais entre si, mesmo que contenham os mesmos elementos:
# Comparando tipos diferentes
print([1, 2, 3] == (1, 2, 3)) # Output: False (list vs tuple)
print([1, 2, 3] == {1, 2, 3}) # Output: False (list vs set)
# Mesmo com os mesmos elementos
list_version = [1, 2, 3]
tuple_version = (1, 2, 3)
set_version = {1, 2, 3}
print(list_version == tuple_version) # Output: False
print(list_version == set_version) # Output: False
print(tuple_version == set_version) # Output: False18.8) Cópias Rasas de Listas, Dicionários e Conjuntos
Ao trabalhar com objetos mutáveis, você muitas vezes precisa criar cópias independentes para evitar modificações não intencionais. Por exemplo, ao fazer backup de dados antes de processá-los, criar cenários de teste sem afetar dados de produção, ou passar dados para funções que não deveriam modificar o original. Entender como os mecanismos de cópia do Python funcionam ajuda você a criar cópias realmente independentes quando necessário.
No entanto, nem todos os métodos de cópia criam cópias completamente independentes. Entender a diferença entre cópias rasas (shallow copies) e cópias profundas (deep copies) é crucial para evitar bugs sutis.
18.8.1) O Que É uma Cópia Rasa?
Uma cópia rasa (shallow copy) cria um novo objeto, mas não cria cópias dos objetos contidos dentro dele. Em vez disso, o novo objeto contém referências aos mesmos objetos aninhados que o original.
Vamos ver isso com uma lista simples:
# Criando uma cópia rasa de uma lista simples
original = [1, 2, 3]
copy = original.copy() # Cria uma cópia rasa
print("Original:", original) # Output: Original: [1, 2, 3]
print("Copy:", copy) # Output: Copy: [1, 2, 3]
# Elas são objetos diferentes
print("Same object?", original is copy) # Output: Same object? False
# Modificar a cópia não afeta o original
copy.append(4)
print("Original:", original) # Output: Original: [1, 2, 3]
print("Copy:", copy) # Output: Copy: [1, 2, 3, 4]Para listas simples contendo objetos imutáveis (como inteiros), uma cópia rasa funciona perfeitamente. A cópia é independente do original.
Mas o que acontece com estruturas aninhadas? Vamos ver onde cópias rasas mostram suas limitações:
# Cópia rasa com listas aninhadas
original = [[1, 2], [3, 4]]
copy = original.copy()
print("Original:", original) # Output: Original: [[1, 2], [3, 4]]
print("Copy:", copy) # Output: Copy: [[1, 2], [3, 4]]
# As listas externas são objetos diferentes
print("Same outer list?", original is copy) # Output: Same outer list? False
# Mas as listas aninhadas são os MESMOS objetos
print("Same nested list?", original[0] is copy[0]) # Output: Same nested list? True
# Modificar uma lista aninhada afeta ambas
copy[0].append(99)
print("Original:", original) # Output: Original: [[1, 2, 99], [3, 4]]
print("Copy:", copy) # Output: Copy: [[1, 2, 99], [3, 4]]18.8.2) Criando Cópias Rasas de Listas
Existem várias formas de criar uma cópia rasa de uma lista:
# Método 1: Usando o método copy()
original = [[1, 2], [3, 4]]
copy1 = original.copy()
# Método 2: Usando slicing de lista
copy2 = original[:]
# Método 3: Usando o construtor list()
copy3 = list(original)
# Todos os três criam cópias rasas
print(copy1) # Output: [[1, 2], [3, 4]]
print(copy2) # Output: [[1, 2], [3, 4]]
print(copy3) # Output: [[1, 2], [3, 4]]
# A lista externa é diferente
print(original is copy1) # Output: False
print(original is copy2) # Output: False
print(original is copy3) # Output: False
# Mas as listas internas são COMPARTILHADAS
print(original[0] is copy1[0]) # Output: True
print(original[0] is copy2[0]) # Output: True
print(original[0] is copy3[0]) # Output: True18.8.3) Criando Cópias Rasas de Dicionários
Dicionários também suportam cópia rasa:
# Método 1: Usando o método copy()
original = {"name": "Alice", "grade": 85}
copy1 = original.copy()
# Método 2: Usando o construtor dict()
copy2 = dict(original)
# Ambos criam cópias rasas
print(copy1) # Output: {'name': 'Alice', 'grade': 85}
print(copy2) # Output: {'name': 'Alice', 'grade': 85}
# Eles são objetos diferentes
print(original is copy1) # Output: False
print(original is copy2) # Output: False
# Modificar a cópia não afeta o original
copy1["grade"] = 90
print("Original:", original) # Output: Original: {'name': 'Alice', 'grade': 85}
print("Copy:", copy1) # Output: Copy: {'name': 'Alice', 'grade': 90}No entanto, com estruturas aninhadas, a mesma limitação de cópia rasa se aplica:
# Cópia rasa com dicionário aninhado
original = {
"name": "Alice",
"grades": [85, 90, 92]
}
copy = original.copy()
print("Original:", original) # Output: Original: {'name': 'Alice', 'grades': [85, 90, 92]}
print("Copy:", copy) # Output: Copy: {'name': 'Alice', 'grades': [85, 90, 92]}
# Os dicionários são objetos diferentes
print("Same dict?", original is copy) # Output: Same dict? False
# Mas a lista grades é o MESMO objeto
print("Same grades list?", original["grades"] is copy["grades"]) # Output: Same grades list? True
# Modificar a lista grades afeta ambos
copy["grades"].append(88)
print("Original:", original) # Output: Original: {'name': 'Alice', 'grades': [85, 90, 92, 88]}
print("Copy:", copy) # Output: Copy: {'name': 'Alice', 'grades': [85, 90, 92, 88]}