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çãostr()
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 à chavekey
;
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.
Republicou isso em debugging on the table.
Publica um livro que eu compro uma dúzia!
Mais um ótimo artigo, parabéns!