Por que Python usa 'métodos mágicos'?

99

Tenho brincado com Python recentemente e uma coisa que estou achando um pouco estranho é o uso extensivo de 'métodos mágicos', por exemplo, para tornar seu comprimento disponível, um objeto implementa um método def __len__(self), e então é chamado quando você escrevelen(obj) .

Eu estava me perguntando por que os objetos simplesmente não definem um len(self)método e o chamam diretamente como um membro do objeto, por exemplo obj.len()? Tenho certeza de que deve haver bons motivos para o Python agir dessa maneira, mas, como um novato, ainda não descobri quais são.

Greg Beech
fonte
4
Acho que a razão geral é a) histórica eb) algo como len()ou reversed()se aplica a muitos tipos de objetos, mas um método como append()se aplica apenas a sequências, etc.
Grant Paul

Respostas:

64

AFAIK lené especial a este respeito e tem raízes históricas.

Aqui está uma citação do FAQ :

Por que o Python usa métodos para algumas funcionalidades (por exemplo, list.index ()), mas funções para outras (por exemplo, len (lista))?

O principal motivo é a história. As funções eram usadas para aquelas operações que eram genéricas para um grupo de tipos e que deveriam funcionar mesmo para objetos que não tinham métodos (por exemplo, tuplas). Também é conveniente ter uma função que pode ser prontamente aplicada a uma coleção amorfa de objetos quando você usa os recursos funcionais do Python (map (), apply () et al).

Na verdade, implementar len (), max (), min () como uma função interna é, na verdade, menos código do que implementá-los como métodos para cada tipo. Pode-se discutir sobre casos individuais, mas é uma parte do Python e é tarde demais para fazer essas mudanças fundamentais agora. As funções devem permanecer para evitar a quebra massiva do código.

Os outros "métodos mágicos" (na verdade chamados de método especial no folclore Python) fazem muito sentido e existem funcionalidades semelhantes em outras linguagens. Eles são usados ​​principalmente para código que é chamado implicitamente quando a sintaxe especial é usada.

Por exemplo:

  • operadores sobrecarregados (existem em C ++ e outros)
  • construtor / destruidor
  • ganchos para acessar atributos
  • ferramentas para metaprogramação

e assim por diante...

Eli Bendersky
fonte
2
Python e o princípio da menor surpresa é uma boa leitura para algumas das vantagens de Python ser assim (embora eu admita que o inglês precisa ser melhorado). O ponto básico: permite que a biblioteca padrão implemente uma tonelada de código que se torna muito, muito reutilizável, mas ainda substituível.
jpmc26
20

Do Zen do Python:

Diante da ambigüidade, recuse a tentação de adivinhar.
Deve haver uma - e de preferência apenas uma - maneira óbvia de fazer isso.

Esta é uma das razões - com métodos personalizados, os desenvolvedores estaria livre para escolher um nome método diferente, como getLength(), length(), getlength()ou qualquer. Python impõe nomenclatura estrita para que a função comum len()possa ser usada.

Todas as operações comuns a muitos tipos de objetos são colocadas em métodos mágicos, como __nonzero__, __len__ou __repr__. Eles são principalmente opcionais, no entanto.

A sobrecarga de operador também é feita com métodos mágicos (por exemplo __le__), portanto, faz sentido usá-los para outras operações comuns também.

AndiDog
fonte
Este é um argumento convincente. Mais satisfatório que "Guido realmente não acreditava em OO" ... (como já vi afirmado em outro lugar).
Andy Hayden
15

Python usa a palavra "métodos mágicos" , porque esses métodos realmente fazem mágica para seu programa. Uma das maiores vantagens de usar os métodos mágicos do Python é que eles fornecem uma maneira simples de fazer os objetos se comportarem como tipos embutidos. Isso significa que você pode evitar maneiras feias, contra-intuitivas e não padronizadas de realizar operações básicas.

Considere o seguinte exemplo:

dict1 = {1 : "ABC"}
dict2 = {2 : "EFG"}

dict1 + dict2
Traceback (most recent call last):
  File "python", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'dict' and 'dict'

Isso dá um erro, porque o tipo de dicionário não suporta adição. Agora, vamos estender a classe de dicionário e adicionar o método mágico "__add__" :

class AddableDict(dict):

    def __add__(self, otherObj):
        self.update(otherObj)
        return AddableDict(self)


dict1 = AddableDict({1 : "ABC"})
dict2 = AddableDict({2 : "EFG"})

print (dict1 + dict2)

Agora, ele fornece a seguinte saída.

{1: 'ABC', 2: 'EFG'}

Assim, ao adicionar este método, de repente a mágica aconteceu e o erro que você estava obtendo antes desapareceu.

Espero que isso deixe as coisas claras para você. Para obter mais informações, consulte:

A Guide to Python's Magic Methods (Rafe Kettler, 2012)

Mangu Singh Rajpurohit
fonte
9

Algumas dessas funções fazem mais do que um único método seria capaz de implementar (sem métodos abstratos em uma superclasse). Por exemplo, bool()age mais ou menos assim:

def bool(obj):
    if hasattr(obj, '__nonzero__'):
        return bool(obj.__nonzero__())
    elif hasattr(obj, '__len__'):
        if obj.__len__():
            return True
        else:
            return False
    return True

Você também pode ter 100% de certeza de que bool()sempre retornará True ou False; se você confiasse em um método, não poderia ter certeza do que receberia de volta.

Algumas outras funções que têm implementações relativamente complicadas (mais complicadas do que os métodos mágicos subjacentes provavelmente são) são iter()e cmp(), e todos os métodos de atributo ( getattr, setattre delattr). Coisas como inttambém acessar métodos mágicos ao fazer coerção (você pode implementar __int__), mas têm dupla função como tipos. len(obj)é na verdade o único caso em que não acredito que seja diferente obj.__len__().

Ian Bicking
fonte
2
Em vez de, hasattr()eu usaria try:/ except AttributeError:e em vez de, if obj.__len__(): return True else: return Falseeu apenas diria, return obj.__len__() > 0mas essas são apenas coisas estilísticas.
Chris Lutz,
No python 2.6 (ao qual btw se bool(x)refere x.__nonzero__()), seu método não funcionaria. As instâncias de bool têm um método __nonzero__()e seu código continuaria chamando a si mesmo assim que obj fosse um bool. Talvez bool(obj.__bool__())deva ser tratado da mesma maneira que você tratou __len__? (Ou este código realmente funciona para Python 3?)
Ponkadoodle
A natureza circular de bool () era um tanto intencionalmente absurda, para refletir a natureza peculiarmente circular da definição. Há um argumento de que deveria ser simplesmente considerado um primitivo.
Ian Bicking,
A única diferença (atualmente) entre len(x)e x.__len__()é que o primeiro aumentará OverflowError para comprimentos que excedam sys.maxsize, enquanto o último geralmente não para tipos implementados em Python. No entanto, isso é mais um bug do que um recurso (por exemplo, o objeto de intervalo do Python 3.2 pode lidar com intervalos arbitrariamente grandes, mas usar lencom eles pode falhar. __len__Porém, eles também falham, já que são implementados em C em vez de Python)
ncoghlan
4

Eles não são realmente "nomes mágicos". É apenas a interface que um objeto deve implementar para fornecer um determinado serviço. Nesse sentido, eles não são mais mágicos do que qualquer definição de interface predefinida que você precise reimplementar.

Stefano Borini
fonte
1

Embora o motivo seja principalmente histórico, existem algumas peculiaridades no Python lenque tornam apropriado o uso de uma função em vez de um método.

Algumas operações em Python são implementadas como métodos, por exemplo list.indexe dict.append, enquanto outras são implementadas como chamáveis ​​e métodos mágicos, por exemplo stre itere reversed. Os dois grupos diferem o suficiente para que a abordagem diferente seja justificada:

  1. Eles são comuns.
  2. str, inte amigos são tipos. Faz mais sentido chamar o construtor.
  3. A implementação difere da chamada de função. Por exemplo, iterpode chamar __getitem__se __iter__não estiver disponível e oferece suporte a argumentos adicionais que não cabem em uma chamada de método. Pelo mesmo motivo it.next()foi alterado paranext(it) em versões recentes do Python - faz mais sentido.
  4. Alguns deles são parentes próximos dos operadores. Existe uma sintaxe para chamar __iter__e __next__- é chamado de forloop. Para consistência, uma função é melhor. E torna-o melhor para certas otimizações.
  5. Algumas das funções são simplesmente muito semelhantes ao resto de alguma forma - repratuam como o strfaz. Ter str(x)contra x.repr()seria confuso.
  6. Alguns deles raramente usam o método de implementação real, por exemplo isinstance.
  7. Alguns deles são operadores reais, getattr(x, 'a')é outra forma de fazer x.ae getattrcompartilha muitas das qualidades acima mencionadas.

Eu pessoalmente chamo o primeiro grupo de método e o segundo grupo de operador. Não é uma distinção muito boa, mas espero que ajude de alguma forma.

Dito isso, lennão se encaixa exatamente no segundo grupo. Está mais próximo das operações do primeiro, com a única diferença de que é muito mais comum do que quase qualquer uma delas. Mas a única coisa que faz é ligar __len__, e está muito perto de L.index. No entanto, existem algumas diferenças. Por exemplo, __len__pode ser chamado para a implementação de outros recursos, como bool, se o método foi chamado, lenvocê pode quebrar bool(x)com o lenmétodo personalizado que faz coisas completamente diferentes.

Em suma, você tem um conjunto de recursos muito comuns que as classes podem implementar e que podem ser acessados ​​por meio de um operador, por meio de uma função especial (que geralmente faz mais do que a implementação, como um operador faria), durante a construção do objeto, e todos eles compartilham alguns traços comuns. Todo o resto é um método. E lené uma espécie de exceção a essa regra.

Rosh Oxymoron
fonte
0

Não há muito a acrescentar às duas postagens acima, mas todas as funções "mágicas" não são realmente mágicas. Eles fazem parte do módulo __ builtins__ que é importado implícita / automaticamente quando o interpretador é iniciado. Ie:

from __builtins__ import *

acontece sempre antes do início do programa.

Sempre achei que seria mais correto se o Python fizesse isso apenas para o shell interativo e exigisse scripts para importar as várias partes internas de que precisavam. Provavelmente, um manuseio diferente de __ main__ seria bom em shells vs interativo. Enfim, confira todas as funções e veja como é sem elas:

dir (__builtins__)
...
del __builtins__
user318904
fonte