domingo, 18 de novembro de 2012

Python - Passagem de Parâmetros

Faz tempo que não falo algo de programação aqui no blog não é? Hoje o assunto vai ser algo interessante, que acredito que muita gente nunca se perguntou, e quem já precisou saber não soube responder a uma simples pergunta: Passagem de Parâmetros em Python é por Valor ou por referência? Você sabe? Sim? Não? Então continue a ler!

Um aspecto da programação em Python que engana aqueles programadores que estão vindo de linguagens como C e Java é a forma como argumentos são passados para funções. Em um nível mais fundamental desse problema, a confusão realmente está pela falta de entendimento sobre a forma como Python utiliza um modelo centrado em objetos e na forma que esses objetos são tratados em uma atribuição de variável. Quando você pergunta a um programador experiente em Python se a passagem de parâmetros das funções é por valor ou por referência, a resposta que ele lhe dará é: Nenhuma. Uma definição mais correta que podemos dar e que se aproxima levemente dessas duas formas é que argumentos em python podem ser passados por objetos ou por referência à objetos. Mas, o que significa isso na realidade?


Simples. Em Python, TUDO é objeto. Se você ler meu post sobre Metaclasses em Python cuidado, seu cérebro irá derreter entenderá o signficado disso. O que comumente chamamos de "variáveis" em Python são melhor definidos como "nomes". Da mesma forma, o processo de "atribuir" um valor à uma variável, em Python significa "dar um nome ao objeto". A Atribuição em Python é isso: ligar um nome a um objeto. E cada ligação têm um escopo definido, ou seja, sua visibilidade dentro dos blocos de código da aplicação python.


É um monte de coisa nova ao mesmo tempo pra você assimilar ou não mas, esses termos são a pedra fundamental do Modelo de Execução do Python. Comparado com, por exemplo, C, as diferenças são sutis mas muito importantes. Um exemplo mais concreto talvez possa iluminar sua percepção para essas diferenças.

Imagine o seguinte código em C

int foo = 10;
// ...
variavel = 20;

No exemplo acima, a variável "foo" se refere a um espaço de memória, e o valor "10" foi escrito naquela posição de memória (de fato, nós podemos pegar o endereço de "foo" e determinar a porção da memória a qual ele está se referindo). Mais tarde, o conteúdo daquela posição de memória é alterada para "20". O valor anterior não existe mais, ele foi sobrescrito. Isso com certeza é de fácil entendimento mesmo que você não seja programador.

Agora, vamos olhar um código similar em Python

foo = 10
# ...
foo = 20

Ligando Nomes à Objetos

Na primeira linha, nós criamos uma ligação entre o nome "foo" e o objeto numérico contendo o valor 10. No contexto da execução do nosso programa, o ambiente foi alterado; a ligação entre "foo" e o objeto numérico é criado no escopo do bloco onde a ligação ocorreu. Quando, mais tarde, fazemos "foo = 20", o objeto numérico contento o valor 10 não é afetado. Nós so mudamos a ligação do nome "foo" do objeto que armazena o valor 10 para outro objeto com valor 20. Nós não alteramos de nenhuma forma os objetos representados por 10 e 20. Até onde sabemos, os dois objetos vivem felizes na memória (desconsideremos o coletor de lixo nesse momento).

Com uma única ligação talvez seja difícil enxergar o que estamos querendo falar, mas isso começa a ficar mais evidente quando essas ligações são compartilhadas e chamadas de função são envolvidas na história.

Vamos analisar o seguinte código

pessoa = "Eduardo"
conjunto1 = []
conjunto1.append(pessoa)

conjunto2 = conjunto1
conjunto2.append("Maria")

pessoa = "Sansa"

print(pessoa, conjunto1, conjunto2)

Então ... O que será impresso na última linha? Para começar, a ligação de "pessoa" com o objeto string "Eduardo" é adicionado ao escopo atual de execução. Da mesma forma, "conjunto1" é ligado a um objeto lista vazio. Na próxima linha, um método é invocado no objeto lista, o qual está ligado ao nome "conjunto1". Nesse ponto, só existem dois objetos no nosso ambiente: o objeto string e o objeto lista. Ambos, "pessoa" e "conjunto1[0]" estão referenciando o mesmo objeto (se vc fizer "print(pessoa is conjunto1[0])" verá a relação.

Continuemos nossa demonstração. Na próxima linha, uma nova ligação é feita: "conjunto2". Atribuições entre nomes não criam novos objetos. No entanto, ambos os nomes passam a apontar para o mesmo objeto. Como resultado, o objeto string e o objeto lista continuam a ser os únicos objetos que foram criados pelo interpretador. Na próxima linha, temos outra invocação de método. Como o nome "conjunto2" está ligado ao mesmo objeto que "conjunto1" a operação ocorrerá no mesmo objeto. No final, a linha print irá mostrar os seuguintes valores:

Sansa ["Eduardo", "Maria"] ["Eduardo", "Maria"]

Isso nos trás a uma importante questão: Há somente dois tipos de objetos criados pelo Python. Um tipo de objeto mutável, ou seja, que pode ser alterado com o passar da execução, e um objeto imutável, que não pode ser alterado depois de criado. No entanto, eles podem ser usados para criar novos objetos, como a função string.join() faz. Quando você pensa sobre isso, essa dicotomia é necessária pois, novamente, tudo em Python são objetos. Pense comigo: Se os números inteiros não fossem imutáveis, eu poderia facilmente alterar o valor (e o significado) do número dois, só para ficar nesse exemplo.

No entanto, é incorreto dizer que objetos mutáveis podem ser alterados e objetos imutáveis não podem. Considere o seguinte código:

nomes = ["João", "Maria", "José"]
sobrenomes = ["Souza", "Silva", "Monteiro"]
nomes_completos = (nomes, sobrenomes)

nomes.append("Sebastião")

Tuplas em Python são objetos imutáveis. Nós não podemos mudar os valores do objeto ligado ao nome "nomes_completos". No entanto, nós não podemos dizer o mesmo dos objetos dentro da tupla. Em nosso caso, a tupla contém duas listas e essas listas podem ser alteradas. Esse tipo de sutileza pode frequentemente ser muito útil.

Funções e Parâmetros

Nesse momento, você já deve ter entendido mais ou menos como as chamadas de funções funcionam em Python. Se eu chamo uma função qualquer, "foo(bar)", eu estou meramente criando uma ligação dentro do escopo de foo para o objeto nomeado como "bar" no momento que a função é chamada. Se "bar" fizer referência à um objeto mutável e foo alterar seu valor, então esses valores estarão visíveis fora do escopo da função.

def foo(bar):
    bar.append(42)
    print(bar)
    # >> [42]

respostas = []
foo(respostas)
print(respostas)
# >> [42]

Por outro lado, se "bar" se referir a um objeto imutável, o máximo que a função "foo" poderá fazer é criar um nome "bar" em seu escopo e apontar para outro objeto.

def foo(bar):
    bar = "novo valor"
    print (bar)
    # >> "novo valor"

respostas = "valor antigo"
foo(respostas)
print(respostas)
# >> "valor antigo"

Conclusão

Eu espero que nesse momento você tenha entendido que Python não possui passagem de parâmetros por valor. Em Python uma variável não é um alias para uma posição de memória, é simplesmente uma ligação à um objeto do Python. Enquanto a expressão "Tudo em Python é Objeto" causa um pouco de confusão naqueles que estão acostumados com outras linguagens, esse modo de fazer as coisas permite construções de linguagens muito poderosas e versáteis.

Esse post é meio que uma conclusão de temas mais complexos que eu já abordei aqui sobre Python. Você pode conferir outros posts que falam mais sobre essa característica nos links abaixo.

Links Relacionados

Python Decorators - Uma Introdução
Python Decorators - Argumentos
Metaclasses em Python