Os métodos mágicos de Python

Obs.: Códigos testados com a versão 2.7 do Python.

Quem está chegando em Python normalmente fica um pouco confuso ao ler o código de uma classe e perceber um monte de métodos com underscores (__) no nome. Para entender o que são esses métodos, vamos ver um exemplo.

Uma classe para números binários

Suponha que, por algum motivo, você receba a tarefa de implementar uma classe para representação de números em base binária.

class Binario(object):

    def __init__(self, valor_dec):
        self.valor_dec = valor_dec
        self.valor_bin = bin(self.valor_dec)  #bin() é uma função builtin

b = Binario(5)
print b

Se executarmos o código acima, teremos em b um objeto do tipo Binario, que representa o valor 5 em base-2. 5 em base binária é 101. Porém, a execução da linha print b mostrou a seguinte saída na tela:

<__main__.Binario object at 0x28286d0>


Isso porque o print executa a função str() no objeto recebido. Essa função, por sua vez, procura por um método chamado __str__() no objeto a ser impresso. Como não definimos um método com esse nome em nossa classe, o interpretador continua sua busca pelo método na classe que está acima de Binario na hierarquia de classes, que é object. Lá ele encontra o método __str__, que então retorna o texto <__main__.Binario object at 0x28286d0>, contendo o endereço do objeto na memória.

O método __str__() deve retornar uma representação em forma de string do valor do objeto. Para personalizar essa mensagem, ou seja, para fazer com que o print em objetos do tipo Binario mostre uma sequência de 0s e 1s representando o número binário em questão, vamos adicionar um método __str__() à nossa classe:

class Binario(object):

    def __init__(self, valor_dec):
        self.valor_dec = valor_dec
        self.valor_bin = bin(self.valor_dec)

    def __str__(self):
        return "%s" % (self.valor_bin)

b = Binario(5)
print b

Agora, o resultado da execução do código acima é o seguinte:


0b101

Que é o formato retornado pela função bin() quando chamada. O prefixo 0b é adicionado para indicar que se trata de um número binário. Podemos facilmente nos livrar desse prefixo para representar o número binário na tela usando operadores de slicing:

def __str__(self):
    return "%s" % (self.valor_bin[2:])

Beleza! Agora nosso número binário pode ser impresso corretamente na tela! 🙂

Sem perceber e sem chamá-los em lugar algum, já utilizamos dois métodos mágicos de Python:

  • __init__: método chamado para inicialização do objeto, logo após a sua construção;
  • __str__: método chamado pela função str() para obter o valor do objeto em forma de string;

Chamamos eles de métodos mágicos porque eles resolvem o nosso problema sem sequer termos que chamá-los. Quem os chama são códigos de outras classes/programas, que esperam que nossos objetos forneçam tais métodos.

E se agora quisermos comparar objetos do tipo Binario em operações relacionais como >, <, >=, <=, != e ==? Se tentarmos comparar duas instâncias de Binario usando algum desses operadores, podemos ter respostas inesperadas, visto que eles não irão fazer o que esperamos. O esperado é que a > b retorne True se o valor de a for maior do que o valor de b. Porém, onde definimos qual valor será usado para comparação dos objetos? Como não fizemos isso, o interpretador irá usar o id de ambos os objetos para a comparação.

Para definir como os objetos de nossa classe serão comparados, podemos implementar o método mágico __cmp__. Na documentação oficial, vemos instruções sobre como implementar esse método para que nossos objetos possam ser comparados e usados em operações relacionais:


object.__cmp__(self, other)
Deve retornar um inteiro negativo se self < other, zero se self == other, ou um número positivo se self > other.

Vamos então implementar o nosso método __cmp__. Podemos, para isso, usar o valor em decimal, que armazenamos internamente na variável self.valor_dec:

def __cmp__(self, other):
    if self.valor_dec > other.valor_dec:
        return 1
    elif self.valor_dec < other.valor_dec:
        return -1
    else:
        return 0

Que poderia também ser escrito como:

def __cmp__(self, other):
    return self.valor_dec - other.valor_dec

Tendo adicionado o código acima à classe Binario, agora podemos utilizar nossos objetos em operações relacionais:

b = Binario(1)
c = Binario(2)
if b < c:
    print "OK"

Mais uma vez, nosso método é executado sem que o chamemos explicitamente. Além dos métodos que vimos aqui, existem vários outros métodos mágicos que podemos implementar em nossos objetos para que o comportamento deles se pareça mais com o comportamento de objetos nativos da linguagem. Vou listar alguns deles a seguir:

  • __add__(self, other): para adicionarmos a possibilidade de aplicação do operador + aos nossos objetos. Para os outros operadores, também existem métodos mágicos (subtração(-): __sub__; multiplicação(*): __mul__, divisão(/): __div__, módulo(%): __mod__, potência(**): __pow__);
  • __call__(self): faz com que o objeto seja chamável (executável), assim como uma função é;
  • __len__: retorna o comprimento do objeto (se for um container);
  • __getitem__(self, key): para containers, retorna o elemento correspondente à chave key;

São muitos os métodos. Se você quiser conhecê-los melhor, sugiro dar uma olhada nesse texto (em inglês): http://www.rafekettler.com/magicmethods.html.

Os métodos mágicos (magic methods), também chamados de métodos dunderscore (double-underscore) ou de métodos especiais, são muito úteis pois permitem que objetos de nossas classes possuam uma interface de acesso semelhante aos objetos nativos da linguagem. A função sorted(), por exemplo, ordena os elementos de um iterável de acordo com o valor dos objetos que a compõe. Se definirmos nosso método de comparação, a função sorted() irá usá-lo para fazer a ordenação dos elementos da lista. Assim, é possível que códigos de terceiros lidem com nosso código sem sequer conhecê-lo. Veja mais sobre esse conceito em: Polimorfismo.

Entendendo “tudo é objeto em Python”

Nessa última semana, li um excelente post sobre o modelo de execução do Python escrito pelo Jeff Knupp e tive vontade de escrever sobre um assunto que é tratado por ele no post: muita gente fala que em Python tudo é um objeto, mas nem todos entendem o que isso realmente significa. Vou tentar ilustrar aqui com um exemplo que acho bem interessante, que é a declaração de classes.

A palavra reservada class

Antes de ler o livro Learning Python, eu tinha uma ideia um pouco distorcida sobre classes em Python. Para mim, a palavra class tinha um quê de mágica envolvida. Com a leitura, aprendi que class é um construtor de objetos, e não somente uma declaração existente no código. Isso porque class é executada pelo interpretador quando encontrada no código, e o resultado dessa execução é um novo objeto existente no escopo do módulo. A diferença deste para um construtor qualquer, é que este constrói objetos do tipo type, permitindo assim que criemos objetos desse tipo recém criado. Vamos ver um exemplo rápido no interpretador:

>>> class Teste(object): pass
>>> print type(Teste)
<type 'type'>

Talvez você esteja pensando: “grande coisa, é a mesma coisa que em outras linguagens orientadas a objetos que já usei…”. Mas se acalme aí, pois o melhor ainda está por vir. Tendo criado o objeto Teste com a declaração class, agora podemos acessá-lo como qualquer outro objeto. Podemos acrescentar atributos à classe:

>>> Teste.x = 0
>>> print Teste.x
0
>>> t1 = Teste()
>>> print t1.x
0

Perceba que acrescentamos o atributo x ao objeto Teste, e que isso faz com que instâncias dessa classe passem a possuir esse atributo. E aí, você fazia isso em Java? Isso permite que, por exemplo, adicionemos métodos a uma classe já existente:

>>> def funcao(self): print 'valor de x:', self.x
>>> Teste.f = funcao
>>> t2 = Teste()
>>> t2.f()
valor de x: 0

Como a palavra def também cria objetos (do tipo function), podemos manipular funções como se fossem outros objetos quaisquer. Preste atenção à linha 2 do trecho acima (Teste.f = funcao) e perceba que não estamos fazendo uma chamada à função funcao. O que fizemos foi criar um atributo f em Teste, e então fizemos com que esse atributo referencie o objeto funcao, que incidentalmente é um objeto “executável”, um objeto function. Assim, todas as instâncias de Teste passam a ter também uma função f em seu escopo.

Feito isso, peço que releia os trechos de código acima e depois, diga se a seguinte linha de código irá funcionar ou não:

>>> t1.f()

O que você acha? A resposta é: a linha acima é executada com sucesso e a saída na tela de sua execução é mostrada abaixo:


valor de x: 0

Algo estranho? Perceba que t1 foi instanciado antes de adicionarmos o método f à classe Teste. Como ele incorporou esse método? Para entender isso, é preciso entender o modelo de resolução de nomes e hierarquia de objetos em Python.

Hierarquia de classes

Quando criamos uma classe, como fizemos com o objeto Teste, estamos criando um objeto que descende de object, que é um objeto do tipo type e que possui vários atributos e métodos que são herdados pelas classes descendentes. Ao criarmos uma instância de Teste, como t1 e t2, estamos criando objetos que descendem de Teste, e por transitividade, descendem também de object. Veja a imagem abaixo:

mro

Essa é a hierarquia existente em tempo de execução em Python. Sempre que tentamos acessar um atributo de um objeto, o interpretador faz o seguinte: verifica se o objeto em questão possui tal atributo; caso não possua, verifica se o objeto que está imediatamente acima na hierarquia possui tal atributo, e faz isso sucessivamente até encontrar ou chegar a object. Se chegar em object e nem ele possuir o atributo procurado, ocorre um AtributeError. Isso é feito para cada acesso a atributos, em tempo de execução. Isso explica o porquê de t1.f() ter funcionado corretamente, mesmo t1 tendo sido criado antes de termos adicionado f a Teste. O que ocorre é bem simples: ao tentar acessar t1.f(), o interpretador busca por f em t1 e não encontra. Assim, busca por f no objeto imediatamente acima de t1, que é Teste, o encontra lá e executa o código do objeto.

Interessante, não? 🙂

Leia mais:

Propriedades em Python

Em algumas situações, ao criamos uma classe, não desejamos que os atributos que compõem um objeto dessa classe sejam alterados sem prévia validação. Para isso, costumamos definir os atributos como privados e então escrever métodos get_NOME_ATRIBUTO() e set_NOME_ATRIBUTO(), onde são colocados os testes antes de modificar o objeto. Podemos usar properties em Python para tornar nosso código independente de montes de métodos get e set. Antes de vermos as properties, vamos dar uma passada rápida sobre atributos privados.

Atributos Privados em Python

Antes de mais nada, Python não possui um mecanismo explícito para que definamos um atributo como privado, ao contrário de outras linguagens como Java. O que existe é uma espécie de truque, onde o nome dos atributos privados é precedido por dois underscores (“__”). Veja o exemplo abaixo:

>>> class Ponto:
...     def __init__(self, x, y):
...         self.__x = x
...         self.__y = y
...
>>> p = Ponto(2,3)
>>> print p.__x
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
AttributeError: Ponto instance has no attribute '__x'

No código acima, criamos uma classe com dois atributos (__x e __y). Veja que ao tentar acessar o atributo __x de fora da classe, obtemos uma mensagem de erro, dizendo que tal atributo não existe. Isso mesmo, para fora da classe ele não é visível (ao menos não como __x). O que acontece, na realidade, é que o interpretador Python altera o nome dos atributos que se iniciam com “__”, colocando “_NomeClasse” na frente do nome do atributo. Por exemplo, o código acima ao ser interpretado pelo Python, tem  suas variáveis __x e __y transformadas em _Ponto__x e _Ponto__y. Veja:

>>> dir(p)
['_Ponto__x', '_Ponto__y', '__doc__', '__init__', '__module__']

Se tentarmos acessar os atributos através desses nomes, conseguimos acessar seus valores normalmente:

>>> print p._Ponto__x
2
>>> print p._Ponto__y
3

Ou seja, os atributos não são realmente privados. O que acontece é que o interpretador modifica os nomes deles.

Properties

Python oferece um mecanismo builtin para construção de propriedades para uma classe. Propriedades são elementos acessados externamente como se fossem atributos, mas que internamente (à classe), são manipulados por funções. Vamos a um exemplo:

class Ponto:

    def __init__(self, x, y):
        self.__x = x
        self.__y = y

    def get_x(self):
        return self.__x
    def get_y(self):
        return self.__y

    def set_x(self, x):
        if x > 0:
            self.__x = x

    def set_y(self, y):
        if y > 0:
            self.__y = y

p = Ponto(2, 3)
print p.get_x()
p.set_x(10)

Cada acesso à variável __x deve ser feito através das funções get e set definidas para essa variável. Mas, também podemos fazer isso de modo que o acesso à variável (de fora da classe) seja mais “elegante”. Vamos montar uma property chamada x, de forma que qualquer acesso a p.x irá chamar as funções atribuídas à variável.

class Ponto(object):

    def __init__(self, x, y):
        self.__x = x
        self.__y = y

    def get_x(self):
        return self.__x

    def get_y(self):
        return self.__y

    def set_x(self, x):
        if x > 0:
            self.__x = x

    def set_y(self, y):
        if y > 0:
            self.__y = y

    x = property(fget=get_x, fset=set_x)

p = Ponto(2, 3)
p.x = -1000
print p.x

O resultado da execução do código acima irá mostrar na tela o número 2, que foi o valor atribuído a x na construção do objeto. Ao executar a penúltima linha (p.x = -1000), o método set_x() será chamado automaticamente, pois foi configurado como a função fset da propriedade x. Como o método set_x() não permite a alteração da variável interna __x caso o novo valor seja menor ou igual a zero, então esse valor não é alterado. Ao executar a última linha (print p.x), é chamado o método get_x(), configurado como o método fget na construção da propriedade. Ele irá retornar o valor de __x, que permanece ainda sendo o valor 2.

É importante observar a linha (x = property(get_x, set_x)). É ela que cria essa propriedade x que pode ser acessada externamente. Como parâmetros, passamos as duas funções que vão ser chamadas ao ser feita alguma operação sobre essa propriedade. Assim, de fora de nossa classe, os acessos são feitos à property x, quando na verdade estamos acessando internamente o atributo __x. Ou seja, para quem está utilizando nossa classe, instanciando objetos dela, x é como se fosse um atributo público, visível de fora da classe. Mas, isso não impede que sejam feitos acessos externos à classe às variáveis _Ponto__x e _Ponto__y sem passar por método algum.

A assinatura da função que cria a propriedade é:

property([fget[, fset[, fdel[, doc]]]])

Mais informações em: http://docs.python.org/library/functions.html#property