O senso estético de Guido para o design de linguagens é incrível. Conheci muitos projetistas capazes de criar linguagens teoricamente lindas, que ninguém jamais usaria. Mas Guido é uma daquelas raras pessoas capaz de criar uma linguagem só um pouco menos teoricamente linda que, por isso mesmo, é uma delícia para programar.
Jim Hugunin, criador do Jython, co-criador do AspectJ, arquiteto do DLR (Dynamic Language Runtime) do .Net. "Story of Jython" (_A História do Jython_) (EN), escrito como prefácio ao Jython Essentials (EN), de Samuele Pedroni e Noel Rappin (O'Reilly).
Uma das melhores qualidades do Python é sua consistência. Após trabalhar com Python por algum tempo é possível intuir, de uma maneira informada e correta, o funcionamento de recursos que você acabou de conhecer.
Entretanto, se você aprendeu outra linguagem orientada a objetos antes do Python, pode achar estranho usar len(collection)
em vez de collection.len()
.
Essa aparente esquisitice é a ponta de um iceberg que, quando compreendido de forma apropriada, é a chave para tudo aquilo que chamamos de pythônico.
O iceberg se chama o Modelo de Dados do Python, e é a API que usamos para fazer nossos objetos lidarem bem com os aspectos mais idiomáticos da linguagem.
É possível pensar no modelo de dados como uma descrição do Python na forma de um framework. Ele formaliza as interfaces dos elementos constituintes da própria linguagem, como sequências, funções, iteradores, corrotinas, classes, gerenciadores de contexto e assim por diante.
Quando usamos um framework, gastamos um bom tempo programando métodos que são chamados por ela. O mesmo acontece quando nos valemos do Modelo de Dados do Python para criar novas classes. O interpretador do Python invoca métodos especiais para realizar operações básicas sobre os objetos, muitas vezes acionados por uma sintaxe especial.
Os nomes dos métodos especiais são sempre precedidos e seguidos de dois sublinhados.
Por exemplo, a sintaxe obj[key]
está amparada no método especial __getitem__
.
Para resolver my_collection[key]
, o interpretador chama my_collection.__getitem__(key)
.
Implementamos métodos especiais quando queremos que nossos objetos suportem e interajam com elementos fundamentais da linguagem, tais como:
-
Coleções
-
Acesso a atributos
-
Iteração (incluindo iteração assíncrona com
async for
) -
Sobrecarga (overloading) de operadores
-
Invocação de funções e métodos
-
Representação e formatação de strings
-
Programação assíncrona usando
await
-
Criação e destruição de objetos
-
Contextos gerenciados usando as instruções
with
ouasync with
Note
|
Mágica e o "dunder"
O termo método mágico é uma gíria usada para se referir aos métodos especiais, mas como falamos de um método específico, por exemplo |
Esse capítulo sofreu poucas alterações desde a primeira edição, pois é uma introdução ao Modelo de Dados do Python, que é muito estável. As mudanças mais significativas foram:
-
Métodos especiais que suportam programação assíncrona e outras novas funcionalidades foram acrescentados às tabelas em Visão geral dos métodos especiais.
-
A Figura 2, mostrando o uso de métodos especiais em A API de Collection, incluindo a classe base abstrata
collections.abc.Collection
, introduzida no Python 3.6.
Além disso, aqui e por toda essa segunda edição, adotei a sintaxe f-string, introduzida no Python 3.6,
que é mais legível e muitas vezes mais conveniente que as notações de formatação de strings mais antigas:
o método str.format()
e o operador %
.
Tip
|
Existe ainda uma razão para usar |
O Exemplo 1 é simples, mas demonstra as possibilidades que se abrem com a implementação de apenas dois métodos especiais, __getitem__
e __len__
.
link:code/01-data-model/frenchdeck.py[role=include]
A primeira coisa a se observar é o uso de collections.namedtuple
para construir uma classe simples representando cartas individuais.
Usamos namedtuple
para criar classes de objetos que são apenas um agrupamento de atributos, sem métodos próprios, como um registro de banco de dados. Neste exemplo, a utilizamos para fornecer uma boa representação para as cartas em um baralho, como mostra a sessão no console:
>>> beer_card = Card('7', 'diamonds')
>>> beer_card
Card(rank='7', suit='diamonds')
Mas a parte central desse exemplo é a classe FrenchDeck
.
Ela é curta, mas poderosa.
Primeiro, como qualquer coleção padrão do Python, uma instância de FrenchDeck
responde à função len()
, devolvendo o número de cartas naquele baralho:
>>> deck = FrenchDeck()
>>> len(deck)
52
Ler cartas específicas do baralho é fácil, graças ao método __getitem__
.
Por exemplo, a primeira e a última carta:
>>> deck[0]
Card(rank='2', suit='spades')
>>> deck[-1]
Card(rank='A', suit='hearts')
Deveríamos criar um método para obter uma carta aleatória? Não é necessário.
O Python já tem uma função que devolve um item aleatório de uma sequência: random.choice
.
Podemos usá-la em uma instância de FrenchDeck
:
>>> from random import choice
>>> choice(deck)
Card(rank='3', suit='hearts')
>>> choice(deck)
Card(rank='K', suit='spades')
>>> choice(deck)
Card(rank='2', suit='clubs')
Acabamos de ver duas vantagens de usar os métodos especiais no contexto do Modelo de Dados do Python.
-
Os usuários de suas classes não precisam memorizar nomes arbitrários de métodos para operações comuns ("Como descobrir o número de itens? Seria
.size()
,.length()
ou alguma outra coisa?") -
É mais fácil de aproveitar a rica biblioteca padrão do Python e evitar reinventar a roda, como no caso da função
random.choice
.
Mas fica melhor.
Como nosso __getitem__
usa o
operador []
de self._cards
,
nosso baralho suporta fatiamento automaticamente.
Podemos olhar as três primeiras cartas no topo de um novo baralho,
e depois pegar apenas os ases, iniciando com o índice 12 e pulando 13 cartas por vez:
>>> deck[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'),
Card(rank='4', suit='spades')]
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'),
Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]
E como já temos o método especial __getitem__
, nosso baralho é um objeto iterável,
ou seja, pode ser percorrido em um laço for
:
>>> for card in deck: # doctest: +ELLIPSIS
... print(card)
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
...
Também podemos iterar sobre o baralho na ordem inversa:
>>> for card in reversed(deck): # doctest: +ELLIPSIS
... print(card)
Card(rank='A', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='Q', suit='hearts')
...
Note
|
Reticências nos doctests
Sempre que possível, extraí as listagens do console do Python usadas neste livro com o
Nesse casos, usei a diretiva |
A iteração muitas vezes é implícita.
Se uma coleção não fornecer um método __contains__
, o operador in
realiza uma busca sequencial.
No nosso caso, in
funciona com nossa classe FrenchDeck
porque ela é iterável.
Veja a seguir:
>>> Card('Q', 'hearts') in deck
True
>>> Card('7', 'beasts') in deck
False
E o ordenamento?
Um sistema comum de ordenar cartas é por seu valor numérico (ases sendo os mais altos) e depois por naipe, na ordem espadas (o mais alto), copas, ouros e paus (o mais baixo).
Aqui está uma função que ordena as cartas com essa regra,
devolvendo 0
para o 2 de paus e 51
para o às de espadas.
suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)
def spades_high(card):
rank_value = FrenchDeck.ranks.index(card.rank)
return rank_value * len(suit_values) + suit_values[card.suit]
Podemos agora listar nosso baralho em ordem crescente de usando spades_high
como critério de ordenação:
>>> for card in sorted(deck, key=spades_high): # doctest: +ELLIPSIS
... print(card)
Card(rank='2', suit='clubs')
Card(rank='2', suit='diamonds')
Card(rank='2', suit='hearts')
... (46 cards omitted)
Card(rank='A', suit='diamonds')
Card(rank='A', suit='hearts')
Card(rank='A', suit='spades')
Apesar da FrenchDeck
herdar implicitamente da classe object
,
a maior parte de sua funcionalidade não é herdada, vem do uso do modelo de dados e de composição.
Ao implementar os métodos especiais __len__
e __getitem__
,
nosso FrenchDeck
se comporta como uma sequência Python padrão,
podendo assim se beneficiar de recursos centrais da linguagem (por exemplo, iteração e fatiamento),
e da biblioteca padrão, como mostramos nos exemplos usando random.choice
,
reversed
, e sorted
.
Graças à composição,
as implementações de __len__
e __getitem__
podem delegar todo o trabalho para um objeto list
, especificamente self._cards
.
Note
|
E como embaralhar as cartas?
Como foi implementado até aqui, um |
A primeira coisa para se saber sobre os métodos especiais é que eles foram feitos para serem chamados pelo interpretador Python, e não por você.
Você não escreve my_object.__len__()
.
Escreve len(my_object)
e, se my_object
é uma instância de uma classe definida pelo usuário, então o Python chama o método __len__
que você implementou.
Mas o interpretador pega um atalho quando está lidando com um tipo embutido como list
, str
, bytearray
, ou extensões como os arrays do NumPy.
As coleções de tamanho variável do Python escritas em C incluem uma struct[1]
chamada PyVarObject
, com um campo ob_size
que mantém o número de itens na coleção. Então, se my_object
é uma instância de algum daqueles tipos embutidos, len(my_object)
lê o valor do campo ob_size
, e isso é muito mais rápido que chamar um método.
Na maior parte das vezes, a chamada a um método especial é implícita.
Por exemplo, o comando for i in x:
na verdade gera uma invocação de iter(x)
,
que por sua vez pode chamar x.__iter__()
se esse método estiver disponível,
ou usar x.__getitem__()
, como no exemplo do FrenchDeck
.
Em condições normais, seu código não deveria conter muitas chamadas diretas a métodos especiais. A menos que você esteja fazendo muita metaprogramação, implementar métodos especiais deve ser muito mais frequente que invocá-los explicitamente. O único método especial que é chamado frequentemente pelo seu código é __init__
,
para invocar o método de inicialização da superclasse na implementação do seu próprio __init__
.
Geralmente, se você precisa invocar um método especial, é melhor chamar a função embutida relacionada (por exemplo, len
, iter
, str
, etc.).
Essas funções chamam o método especial correspondente,
mas também fornecem outros serviços e—para tipos embutidos—são mais rápidas que chamadas a métodos.
Veja, por exemplo, [iter_closer_look] no [iterables2generators].
Na próxima seção veremos alguns dos usos mais importantes dos métodos especiais:
-
Emular tipos numéricos
-
Representar objetos na forma de strings
-
Determinar o valor booleano de um objeto
-
Implementar coleções
Vários métodos especiais permitem que objetos criados pelo usuário respondam a operadores como +
.
Vamos tratar disso com mais detalhes no [operator_overloading].
Aqui nosso objetivo é continuar ilustrando o uso dos métodos especiais, através de outro exemplo simples.
Vamos implementar uma classe para representar vetores bi-dimensionais—isto é, vetores euclidianos como aqueles usados em matemática e física (veja a Figura 1).
Tip
|
O tipo embutido |
Vamos começar a projetar a API para essa classe escrevendo em uma sessão de console simulada, que depois podemos usar como um doctest. O trecho a seguir testa a adição de vetores ilustrada na Figura 1:
>>> v1 = Vector(2, 4)
>>> v2 = Vector(2, 1)
>>> v1 + v2
Vector(4, 5)
Observe como o operador +
produz um novo objeto Vector(4, 5)
.
A função embutida abs
devolve o valor absoluto de números inteiros e de ponto flutuante, e a magnitude de números complex
. Então, por consistência, nossa API também usa abs
para calcular a magnitude de um vetor:
>>> v = Vector(3, 4)
>>> abs(v)
5.0
Podemos também implementar o operador *
, para realizar multiplicação escalar (isto é, multiplicar um vetor por um número para obter um novo vetor de mesma direção e magnitude multiplicada):
>>> v * 3
Vector(9, 12)
>>> abs(v * 3)
15.0
O Exemplo 2 é uma classe Vector
que implementa as operações descritas acima, usando os métodos especiais __repr__
, __abs__
, __add__
, e __mul__
.
link:code/01-data-model/vector2d.py[role=include]
Implementamos cinco métodos especiais, além do costumeiro __init__
.
Veja que nenhum deles é chamado diretamente dentro da classe ou durante seu uso normal, ilustrado pelos doctests.
Como mencionado antes, o interpretador Python é o único usuário frequente da maioria dos métodos especiais.
O Exemplo 2 implementa dois operadores: +
e *
,
para demonstrar o uso básico de __add__
e __mul__
.
No dois casos, os métodos criam e devolvem uma nova instância de Vector
,
e não modificam nenhum dos operandos: self
e other
são apenas lidos.
Esse é o comportamento esperado de operadores infixos: criar novos objetos e não tocar em seus operandos.
Vou falar muito mais sobre esse tópico no [operator_overloading].
Warning
|
Da forma como está implementado, o Exemplo 2 permite multiplicar um |
Nas seções seguintes vamos discutir os outros métodos especiais em Vector
.
O método especial __repr__
é chamado pelo repr
embutido para obter a representação do objeto como string, para inspeção. Sem um __repr__
personalizado, o console do Python mostraria uma instância de Vector
como <Vector object at 0x10e100070>
.
O console iterativo e o depurador chamam repr
para exibir o resultado das expressões.
O repr
também é usado:
-
Pelo marcador posicional
%r
na formatação clássica com o operador%
. Ex.:'%r' % my_obj
-
Pelo sinalizador de conversão
!r
na nova sintaxe de strings de formato usada nas f-strings e no métodostr.format
. Ex:f'{my_obj!r}'
Note que a f-string no nosso __repr__
usa !r
para obter a representação padrão dos atributos a serem exibidos.
Isso é uma boa prática, pois durante uma seção de depuração podemos ver a diferença entre Vector(1, 2)
e Vector('1', '2')
. Este segundo objeto não funcionaria no contexto desse exemplo, porque nosso código espera que os argumentos do construtor sejam números, não str
.
A string devolvida por __repr__
não deve ser ambígua e, se possível, deve corresponder ao código-fonte necessário para recriar o objeto representado. É por isso que nossa representação de Vector
se parece com uma chamada ao construtor da classe, por exemplo Vector(3, 4)
.
Por outro lado, __str__
é chamado pelo método embutido str()
e usado implicitamente pela função print
.
Ele deve devolver uma string apropriada para ser exibida aos usuários finais.
Algumas vezes a própria string devolvida por __repr__
é adequada para exibir ao usuário,
e você não precisa programar __str__
, porque a implementação herdada da classe object
chama
__repr__
como alternativa.
O [coord_tuple_ex] é um dos muitos exemplos neste livro com um __str__
personalizado.
Tip
|
Programadores com experiência anterior em linguagens que contém o método "What is the difference between |
Apesar do Python ter um tipo bool
, ele aceita qualquer objeto em um contexto booleano, tal como as expressões controlando uma instrução if
ou while
, ou como operandos de and
, or
e not
.
Para determinar se um valor x
é verdadeiro ou falso, o Python invoca bool(x)
,
que devolve True
ou False
.
Por default, instâncias de classes definidas pelo usuário são consideradas verdadeiras, a menos que __bool__
ou __len__
estejam implementadas.
Basicamente, bool(x)
chama x.__bool__()
e usa o resultado.
Se __bool__
não está implementado, o Python tenta invocar x.__len__()
, e se esse último devolver zero, bool
devolve False
.
Caso contrário, bool
devolve True
.
Nossa implementação de __bool__
é conceitualmente simples:
ela devolve False
se a magnitude do vetor for zero, caso contrário devolve True
.
Convertemos a magnitude para um valor booleano usando bool(abs(self))
, porque espera-se que
__bool__
devolva um booleano.
Fora dos métodos __bool__
, raramente é necessário chamar bool()
explicitamente,
porque qualquer objeto pode ser usado em um contexto booleano.
Observe que o método especial __bool__
permite que seus objetos sigam as regras de teste do valor verdade definidas no capítulo "Tipos Embutidos" da documentação da Biblioteca Padrão do Python.
Note
|
Essa é uma implementação mais rápida de def __bool__(self):
return bool(self.x or self.y) Isso é mais difícil de ler, mas evita a jornada através de |
A Figura 2 documenta as interfaces dos tipos de coleções essenciais na linguagem. Todas as classes no diagrama são ABCs—classes base abstratas (ABC é a sigla para a mesma expressão em inglês, Abstract Base Classes). As ABCs e o módulo collections.abc
são tratados no [ifaces_prot_abc].
O objetivo dessa pequena seção é dar uma visão panorâmica das interfaces das coleções mais importantes do Python, mostrando como elas são criadas a partir de métodos especiais.
list
e dict
. O restante dos métodos tem implementações concretas, então as subclasses podem herdá-los.Cada uma das ABCs no topo da hierarquia tem um único método especial.
A ABC Collection
(introduzida no Python 3.6) unifica as três interfaces essenciais, que toda coleção deveria implementar:
-
Iterable
, para suportarfor
, desempacotamento, e outras formas de iteração -
Sized
para suportar a função embutidalen
-
Container
para suportar o operadorin
Na verdade, o Python não exige que classes concretas herdem de qualquer dessas ABCs.
Qualquer classe que implemente __len__
satisfaz a interface Sized
.
Três especializações muito importantes de Collection
são:
-
Sequence
, formalizando a interface de tipos embutidos comolist
estr
-
Mapping
, implementado pordict
,collections.defaultdict
, etc. -
Set
, a interface dos tipos embutidosset
efrozenset
Apenas Sequence
é Reversible
, porque sequências suportam o ordenamento arbitrário de seu conteúdo, ao contrário de mapeamentos(mappings) e conjuntos(sets).
Note
|
Desde o Python 3.7, o tipo |
Todos os métodos especiais na ABC Set
implementam operadores infixos.
Por exemplo, a & b
calcula a intersecção entre os conjuntos a
e b
,
e é implementada no método especial __and__
.
Os próximos dois capítulos vão tratar em detalhes das sequências, mapeamentos e conjuntos da biblioteca padrão.
Agora vamos considerar as duas principais categorias dos métodos especiais definidos no Modelo de Dados do Python.
O capítulo "Modelo de Dados" de A Referência da Linguagem Python lista mais de 80 nomes de métodos especiais. Mais da metade deles implementa operadores aritméticos, bit a bit, ou de comparação. Para ter uma visão geral do que está disponível, veja tabelas a seguir.
A Tabela 1 mostra nomes de métodos especiais, excluindo aqueles usados para implementar operadores infixos ou funções matemáticas fundamentais como abs
. A maioria desses métodos será tratado ao longo do livro, incluindo as adições mais recentes:
métodos especiais assíncronos como __anext__
(acrescentado no Python 3.5), e o método de personalização de classes, __init_subclass__
(do Python 3.6).
Categoria | Nomes dos métodos |
---|---|
Representação de string/bytes |
|
Conversão para número |
|
Emulação de coleções |
|
Iteração |
|
Execução de chamável ou corrotina |
|
Gerenciamento de contexto |
|
Criação e destruição de instâncias |
|
Gerenciamento de atributos |
|
Descritores de atributos |
|
Classes base abstratas |
|
Metaprogramação de classes |
|
Operadores infixos e numéricos são suportados pelos métodos especiais listados na
Tabela 2.
Aqui os nomes mais recentes são __matmul__
, __rmatmul__
, e __imatmul__
, adicionados no Python 3.5 para suportar o uso de @
como um operador infixo de multiplicação de matrizes, como veremos no [operator_overloading].
Categoria do operador | Símbolos | Nomes de métodos |
---|---|---|
Unário numérico |
|
|
Comparação rica |
|
|
Aritmético |
|
|
Aritmética reversa |
operadores aritméticos com operandos invertidos) |
|
Atribuição aritmética aumentada |
|
|
Bit a bit |
|
|
Bit a bit reversa |
(operadores bit a bit com os operandos invertidos) |
|
Atribuição bit a bit aumentada |
|
|
Note
|
O Python invoca um método especial de operador reverso no segundo argumento quando o método especial correspondente não pode ser usado no primeiro operando.
Atribuições aumentadas são atalho combinando um operador infixo com uma atribuição de variável, por exemplo O [operator_overloading] explica em detalhes os operadores reversos e a atribuição aumentada. |
Em 2013, fiz essa pergunta a Raymond Hettinger, um dos desenvolvedores principais do Python, e o núcleo de sua resposta era uma citação do "The Zen of Python" (O Zen do Python) (EN): "a praticidade vence a pureza."
Em Como os métodos especiais são utilizados, descrevi como len(x)
roda muito rápido quando x
é uma instância de um tipo embutido.
Nenhum método é chamado para os objetos embutidos do CPython: o tamanho é simplesmente lido de um campo em uma struct C.
Obter o número de itens em uma coleção é uma operação comum, e precisa funcionar de forma eficiente para tipos tão básicos e diferentes como str
, list
, memoryview
, e assim por diante.
Em outras palavras, len
não é chamado como um método porque recebe um tratamento especial como parte do Modelo de Dados do Python, da mesma forma que abs
.
Mas graças ao método especial __len__
, também é possível fazer len
funcionar com nossos objetos personalizados.
Isso é um compromisso justo entre a necessidade de objetos embutidos eficientes e a consistência da linguagem.
Também de "O Zen do Python": "Casos especiais não são especiais o bastante para quebrar as regras."
Note
|
Pensar em |
Ao implementar métodos especiais, seus objetos podem se comportar como tipos embutidos, permitindo o estilo de programação expressivo que a comunidade considera pythônico.
Uma exigência básica para um objeto Python é fornecer strings representando a si mesmo que possam ser usadas, uma para depuração e registro (log), outra para apresentar aos usuários finais. É para isso que os métodos especiais __repr__
e __str__
existem no modelo de dados.
Emular sequências, como mostrado com o exemplo do FrenchDeck
, é um dos usos mais comuns dos métodos especiais.
Por exemplo, bibliotecas de banco de dados frequentemente devolvem resultados de consultas na forma de coleções similares a sequências.
Tirar o máximo proveito dos tipos de sequências existentes é o assunto do [sequences].
Como implementar suas próprias sequências será visto na [user_defined_sequences],
onde criaremos uma extensão multidimensional da classe Vector
.
Graças à sobrecarga de operadores, o Python oferece uma rica seleção de tipos numéricos, desde os tipos embutidos até decimal.Decimal
e fractions.Fraction
,
todos eles suportando operadores aritméticos infixos.
As bibliotecas de ciência de dados NumPy suportam operadores infixos com matrizes e tensores. A implementação de operadores—incluindo operadores reversos e atribuição aumentada—será vista no [operator_overloading], usando melhorias do exemplo Vector
.
Também veremos o uso e a implementação da maioria dos outros métodos especiais do Modelo de Dados do Python ao longo deste livro.
O capítulo "Modelo de Dados" em A Referência da Linguagem Python é a fonte canônica para o assunto desse capítulo e de uma boa parte deste livro.
Python in a Nutshell, 3rd ed. (EN), de Alex Martelli, Anna Ravenscroft, e Steve Holden (O’Reilly) tem uma excelente cobertura do modelo de dados. Sua descrição da mecânica de acesso a atributos é a mais competente que já vi, perdendo apenas para o próprio código-fonte em C do CPython. Martelli também é um contribuidor prolífico do Stack Overflow, com mais de 6200 respostas publicadas. Veja seu perfil de usuário no Stack Overflow.
David Beazley tem dois livros tratando do modelo de dados em detalhes, no contexto do Python 3: Python Essential Reference (EN), 4th ed. (Addison-Wesley), e Python Cookbook, 3rd ed. (EN) (O’Reilly), com a co-autoria de Brian K. Jones.
O The Art of the Metaobject Protocol (EN) (MIT Press) de Gregor Kiczales, Jim des Rivieres, e Daniel G. Bobrow explica o conceito de um protocolo de metaobjetos, do qual o Modelo de Dados do Python é um exemplo.
Modelo de dados ou modelo de objetos?
Aquilo que a documentação do Python chama de "Modelo de Dados do Python", a maioria dos autores diria que é o "Modelo de objetos do Python"
O Python in a Nutshell, 3rd ed. de Martelli, Ravenscroft, e Holden, e o Python Essential Reference, 4th ed., de David Beazley são os melhores livros sobre o Modelo de Dados do Python, mas se referem a ele como o "modelo de objetos." Na Wikipedia, a primeira definição de "modelo de objetos" (EN) é: "as propriedades dos objetos em geral em uma linguagem de programação de computadores específica." É disso que o Modelo de Dados do Python trata. Neste livro, usarei "modelo de dados" porque a documentação prefere este termo ao se referir ao modelo de objetos do Python, e porque esse é o título do capítulo de A Referência da Linguagem Python mais relevante para nossas discussões.
Métodos de "trouxas"
O The Original Hacker’s Dictionary (Dicionário Hacker Original) (EN) define mágica como "algo ainda não explicado ou muito complicado para explicar" ou "uma funcionalidade, em geral não divulgada, que permite fazer algo que de outra forma seria impossível."
A comunidade Ruby chama o equivalente dos métodos especiais naquela linguagem de métodos mágicos. Muitos integrantes da comunidade Python também adotam esse termo. Eu acredito que os métodos especiais são o contrário de mágica. O Python e o Ruby oferecem a seus usuários um rico protocolo de metaobjetos integralmente documentado, permitindo que "trouxas" como você e eu possam emular muitas das funcionalidades disponíveis para os desenvolvedores principais que escrevem os interpretadores daquelas linguagens.
Por outro lado, pense no Go. Alguns objetos naquela linguagem tem funcionalidades que são mágicas, no sentido de não poderem ser emuladas em nossos próprios objetos definidos pelo usuário.
Por exemplo, os arrays, strings e mapas do Go suportam o uso de colchetes para acesso a um item, na forma a[i]
.
Mas não há como fazer a notação []
funcionar com um novo tipo de coleção definida por você.
Pior ainda, o Go não tem o conceito de uma interface iterável ou um objeto iterador ao nível do usuário, daí sua sintaxe para for/range
estar limitada a suportar cinco tipos "mágicos" embutidos, incluindo arrays, strings e mapas.
Talvez, no futuro, os projetistas do Go melhorem seu protocolo de metaobjetos. Em 2021, ele ainda é muito mais limitado do que Python, Ruby, e JavaScript oferecem.
Metaobjetos
The Art of the Metaobject Protocol (AMOP) (A Arte do protocolo de metaobjetos) é meu título favorito entre livros de computação. Mas o menciono aqui porque o termo protocolo de metaobjetos é útil para pensar sobre o Modelo de Dados do Python, e sobre recursos similares em outras linguagens. A parte metaobjetos se refere aos objetos que são os componentes essenciais da própria linguagem. Nesse contexto, protocolo é sinônimo de interface. Assim, um protocolo de metaobjetos é um sinônimo chique para modelo de objetos: uma API para os elementos fundamentais da linguagem.
Um protocolo de metaobjetos rico permite estender a linguagem para suportar novos paradigmas de programação. Gregor Kiczales, o primeiro autor do AMOP, mais tarde se tornou um pioneiro da programação orientada a aspecto, e o autor inicial do AspectJ, uma extensão de Java implementando aquele paradigma. A programação orientada a aspecto é muito mais fácil de implementar em uma linguagem dinâmica como Python, e alguns frameworks fazem exatamente isso. O exemplo mais importante é a zope.interface (EN), parte do framework sobre a qual o sistema de gerenciamento de conteúdo Plone é construído.