Variáveis, valores e referências

Já vi muita gente tendo problemas em código Python simplesmente por não entender direito como Python lida com os conceitos de variáveis. Considere o exemplo abaixo:

>>> x = [1, 2, 3]
>>> y = x
>>> x.append(4)
>>> print y
************

Qual o resultado esperado pela execução da última linha (print y)? Se você ficou na dúvida, mesmo que por um curto período de tempo, leia este post até o fim, que você irá entender melhor.

Nome

Grave o seguinte:

Em Python, uma variável é apenas um NOME que REFERENCIA a um OBJETO.

Veja o exemplo abaixo:

>>> x = 42

O código acima é muitas vezes lido como “atribui o valor 42 à variável x”. Mas, o que Python faz é o seguinte: cria um objeto do tipo int que possui 42 como valor, cria o nome x e faz com que o nome x referencie o objeto (do tipo int) 42. Assim, toda vez que o nome x for usado em seu código, ele será automaticamente substituído pelo valor do objeto que este nome referencia (42). A imagem abaixo ilustra melhor a relação entre x e 42.

var1

Continuando o exemplo anterior, o que acontece se fizermos o seguinte?

>>> x = x + 1

É simples, o nome x passa a fazer referência a um novo objeto do tipo int, cujo valor é 43.

A imagem a seguir dá uma idéia melhor sobre o que acontece.

var2

Objetos do tipo int são imutáveis. x = x + 1 cria um novo objeto do tipo int (cujo valor é determinado pela soma de x com 1) e faz com que x passe a referenciar esse novo objeto. Se você observar a imagem acima, verá que não há mais seta alguma apontando para o valor 42, isto é, não há mais nenhum nome fazendo referência àquele objeto. Normalmente, um objeto que não possui nome algum o referenciando vira candidato a coleta de lixo, que é um mecanismo que elimina da memória objetos que não são mais necessários. Mas, o interpretador Python não realiza esse processo em objetos do tipo int e do tipo str (quando pequenos). Ao invés disso, ele mantém esses objetos em uma espécie de cache, para não ter que recriá-los em um futuro próximo e a todo momento em que forem necessários. Se quiser confirmar isso:

>>> 42 is 42  # ambos são o mesmo objeto
True
>>> 'ola' is 'ola'
True
>>> [] is []  # o mesmo já não vale para listas
False

Como comentei anteriormente, toda vez que um nome de variável aparece em uma expressão, esse nome é substituído pelo valor do objeto ao qual ele faz referência. Sabendo disso, considere a expressão abaixo:

>>> y = x

O interpretador cria um novo nome y e faz com que ele referencie o objeto referenciado por x, como mostra a figura abaixo:

var3

Agora, o que acontece se fizermos o seguinte?

>>> x = 10

É criado um objeto int com valor 10, e x então passa a referenciar a esse novo objeto.

var4

Tá, e daí?

E daí que entendendo isso tudo, você achará mais natural alguns comportamentos em Python. Por exemplo, teste o seguinte código e tente entender o que acontece:

>>> x = [1, 2, 3]
>>> y = x
>>> x.append(4)
>>> print x
[1, 2, 3, 4]
>>> print y
[1, 2, 3, 4]

Como mostra a imagem abaixo, y = x faz com que y passe a referenciar o mesmo objeto que x referencia.

var5

Assim, x.append(4) tem efeito sobre o objeto referenciado agora pelas duas variáveis (x e y).

Isso ocorre porque listas, em Python, são objetos mutáveis. O método append() modifica a lista de modo in-place, isto é, as modificações são feitas no próprio objeto, sem a necessidade de criação de uma nova lista, como ocorreria com objetos imutáveis, como strings ou ints, por exemplo.

Mutável vs Imutável

Vamos ver agora um exemplo da diferença entre um objeto mutável (lista) e um objeto imutável (string). Temos dois objetos, l e s:

>>> l = [1, 2, 3]
>>> s = 'abc'

Queremos adicionar um novo elemento ao fim de cada um deles. Com a lista podemos usar o método append():

>>> l.append(4)

Que adiciona o valor 4 ao final de l, modificando-a. Com a string, não temos esse método disponível, então vamos usar o operador de concatenação:

>>> s = s + 'd'

O lado direito da expressão acima cria uma nova string com o conteúdo de s acrescido do caractere 'd' e faz com que o nome s passe a referenciar essa nova string. Ou seja, ao invés de modificar, foi criado um novo objeto. O antigo valor de s ('abc') passa então a ficar sem referência alguma a ele.

Quem é quem?

Para confirmar se duas variáveis referenciam o mesmo objeto, podemos usar o operador de identidade is que verifica se duas variáveis possuem como valor o mesmo objeto.

>>> x = [1, 2, 3]
>>> y = x
>>> x is y
True

Outra forma de verificar se duas variáveis se referem ao mesmo objeto é usando o comando id(), que retorna o identificador do objeto, que nada mais é do que um número inteiro que cada objeto possui para ser unicamente identificado:

>>> print id(x)
30008456
>>> print id(y)
30008456

Tanto x quanto y se referem ao objeto com o identificador 30008456, isto é, ao mesmo objeto ([1, 2, 3]).

Agora, se o que você deseja é fazer uma cópia do objeto lista, de modo que uma modificação na cópia não interfira no objeto original, existem algumas formas de fazer isso: usando o operador de fatiamento (slicing) ou o método copy(), disponível no módulo copy.

>>> x = [1, 2, 3]
>>> y = x[:] # cria uma nova lista com todo conteúdo de x e atribui a y
>>> x is y
False
>>> import copy
>>> y = copy.copy(x)
>>> x is y
False

Perceba que o método copy.copy() faz apenas o que chamamos de cópia rasa da lista, pois se a lista em questão possuir outras listas aninhadas, estas não serão copiadas, sendo somente suas referências copiadas. Para cópias profundas, use copy.deepcopy().

Passagem de parâmetros

Outra confusão muito comum é a passagem de parâmetros para funções. Veja o exemplo abaixo e tente descobrir o resultado da execução:

def func(x, y):
    x = x + 1
    y.append(4)

x = 1
y = [1, 2, 3]
func(x, y)
print 'x:', x
print 'y:', y

(Pare por um momento se for necessário antes de seguir a leitura e analise o código acima para descobrir o resultado.)

O resultado pode ser visto abaixo:

x: 1
y: [1, 2, 3, 4]

A função func recebeu dois argumentos. De forma leiga, poderíamos dizer que ambos sofreram alterações dentro da função, mas somente na lista a alteração persistiu fora do escopo da função. Isso tem uma explicação bem clara: para qualquer objeto passado para uma função, é feita uma cópia da referência do objeto para o escopo local da função. Assim, se o objeto for mutável, uma operação como append(), por exemplo, vai afetar o objeto referenciado pela variável e assim a alteração persiste fora do escopo da função. Com um objeto imutável, não é possível alterar o objeto em si. O que se faz é alterar a referência (x = x + 1, por exemplo). Como a referência é apenas uma cópia, qualquer alteração feita sobre ela não irá ter efeito na referência do escopo de fora. Por isso, x permanece referenciando o mesmo objeto (1) no escopo global após a função ter terminado.

Resumindo: para entender o comportamento da passagem de parâmetros em Python, basta entender a diferença entre objetos mutáveis e imutáveis, e também lembrar que toda variável em Python nada mais é do que uma referência para algum objeto da memória.

Além disso, é conveniente deixar de lado um pouco aquela noção de que x = x + 1 (ou operações parecidas) altera(m) o objeto. Lembre sempre que o lado direito da expressão (x + 1) cria um novo objeto e que o lado esquerdo da expressão (x =) faz com que o nome x passe a referenciar o novo objeto. Mais nada. 🙂

Pronto!

O conceito de variáveis e objetos em Python é bem simples e consistente. Fique atento ao seu código, principalmente quando você precisa realizar cópias de objetos, o que em Python não é feito usando o operador =. Ah, não se esqueça outras linguagens implementam esses conceitos de formas diferentes. Veja: Other languages have “variables”… Python has “names” .

Anúncios