O que você vai aprender nesta aula?
Após o término da aula você terá aprendido:
Este material usou o Capítulo 1 (Modelo de dados do Python) do livro Python Fluente do Luciano Ramalho
Nesta aula vamos falar sobre como funciona o modelo de dados do Python.
O Python é uma linguagem conhecida por sua consistência. Isso permite que, após trabalhar certo tempo com a linguagem, você consiga ter palpiters corretos sobre recursos do Python que você ainda não domina.
Um exemplo da consistência da linguagem se dá pela função len()
, que apesar de parecer estranho de se usar - len(collection)
ao invés de collection.len()
como é feito em outras linguagens - sabemos, conforme visto no curso, que podemos usá-la para qualquer coleção, enquanto outras linguagens possuem métodos de nomes diferentes para realizar essa mesma operação.
O responsável por consistência (e estranheza) é o Python data model (modelo de dados do Python) que descreve a API que pode ser usada para fazer que seus próprios objetos interajam bem com os recursos mais idiomáticos da linguagem. Ele descreve os objetos e como estes interagem entre si.
O modelo de dados formaliza as interfaces dos blocos de construção da própria linguagem, por exemplo, as sequências, os iteradores, as funções, as classes, os gerenciadores de contexto e assim por diante.
O python faz isso usando os métodos especiais: o interpretador do Python chama esses métodos para realizar operações básicas em objetos, geralmente acionados por uma sintaxe especial.
Os métodos especiais são sempre escritos com underscores duplos no início e no fim (como __getitem__
). Por exemplo a sintaxe especial obj[chave]
é tratada pelo método especial __getitem__
. Quando o interpretador avalia colecao[chave]
ele chama colecao.__getitem__(chave)
.
Vamos mostrar um exemplo de como podemos usar o modelo de dados do python a nosso favor. Vamos criar um baralho pythônico:
In [1]:
from exemplos.baralho import Baralho
baralho = Baralho()
Podemos acessar as cartas do baralho por índice:
In [2]:
baralho[0]
Out[2]:
In [3]:
baralho[-1]
Out[3]:
Também podemos realizar slicing no baralho:
In [4]:
baralho[:5]
Out[4]:
In [5]:
baralho[15:20]
Out[5]:
In [6]:
baralho[-5:]
Out[6]:
E iterá-lo:
In [7]:
for carta in baralho:
print(carta)
Iterá-lo de trás para frente:
In [8]:
for carta in reversed(baralho):
print(carta)
Enumerá-lo!!!111!!!onze!!11!
In [9]:
for carta in enumerate(baralho):
print(carta)
Sorteio de cartas usando o módulo random
:
In [10]:
from random import choice
choice(baralho)
Out[10]:
In [11]:
choice(baralho)
Out[11]:
In [12]:
choice(baralho)
Out[12]:
Sorteando 5 cartas (pode haver repetição):
In [13]:
mao = [choice(baralho) for _ in range(5)]
mao
Out[13]:
Também podemos verificar se uma carta específica está no baralho:
In [14]:
from exemplos.baralho import Carta
Carta('10', 'espadas') in baralho
Out[14]:
In [15]:
Carta('3', 'alabardas') in baralho
Out[15]:
E se saber quantas cartas há no baralho:
In [16]:
len(baralho)
Out[16]:
Você deve estar se perguntando quanto custou para implementar tudo isso? Respota: muito pouco.
""" Arquivo: 02-python-oo/aula-03/exemplos/baralho.py """
from collections import namedtuple
Carta = namedtuple('Carta', ['valor', 'naipe'])
class Baralho:
valores = [str(n) for n in range(2, 11)] + list('AJQK')
naipes = 'copas ouros paus espadas'.split()
def __init__(self):
self.cartas = [Carta(v, n) for v in self.valores for n in self.naipes]
def __len__(self):
return len(self.cartas)
def __getitem__(self, pos):
return self.cartas[pos]
Vimos duas vantagens de usar os métodos especiais para tirar proveito do modelo de dados do Python:
Os usuarios de suas classes não precisarão memorizar nomes arbitrários de métodos para realizar operações comuns (Como
obter a quantidade de itens? Uso .size()
, .length()
, ou o quê?)
Podemos se beneficiar da biblioteca-padrão do Python e não reinventar a roda, como visto no uso das funções random.choice
e reversed
.
Os métodos especiais foram criados para serem chamados pelo interpretador Python e não diretamente. Não usamos objeto.__len__
, para obter a quantidade de elementos, mas sim len(objeto)
. Se objeto
for a instância de uma classe definida pelo usuário (programador), o Python chamará o método __len__
da instância.
Na grande maioria das vezes a chamada aos métodos especiais será feita de forma implícita. Por exemplo, a construção de for i in x
invoca iter(x)
, que poderá chamar x.__iter__()
se existir.
Um exemplo comum de implementação e chamada de métodos especiais diretamente é o __init__
para sobrescrever o inicilizador da superclasse. Também é comum invocar o inicializador da superclasse diretamente com, por exemplo, super().__init__()
ao implementar seu próprio inicializador.
Caso precise chamar um método especial, em geral é muito melhor chamar a função embutida relacionada ou a sintaxe especial (obj[chave]
, len
, iter
, str
etc.). Essas funções embutidas invocam o método especiail correspondente, porém, com frequência, oferecem outros serviços e - para os tipos embutidos - são mais rápidas que chamadas de métodos.
Nós podemos fazer todas essas operações no Baralho
sem herdar de alguma classe espeicial, pois implementamos o protocolo de sequência como definido no modelo de dados do Python. Agora ficam duas dúvidas: o que é exatamente um protocolo e uma sequência?
No contexto de programação orientada a objetos um protocolo é uma interface informal definida somente na documentação e não no código. Por exemplo, o protocolo de sequência em Python implica somente os métodos __len__
e __getitem__
. Qualquer classe que implemente esses métodos poderá ser usada em qualquer lugar em que se espera uma sequência.
Esse tipo de programação ficou conhecida como Duck Typing e é muito comum em linguagens dinâmicas como Python e Ruby.
"Não verifique se é um pato: verifique se faz quack como um pato, anda como um pato etc., de acordo com o subconjunto exatao de comportamento de pato de que você precisa para usar a linguagem." (Alex Martelli, 2000)
Essa técnica consiste em não verificar se uma classe é, por exemplo, uma sequência e sim se ela se comporta como uma sequência.
É importante notar que, como os protocolos são informais e não impostos. Geralmente você pode implementar somente a parte de um protocolo que faz sentido a sua aplicação sem que haja problemas. Por exemplo, para dar suporte a iteração é necessário implementar somente o método __getitem__
e não é necessário o __len__
Agora que sabemos como funcionam os protocolos em Python, vamos falar sobre o protocolo de sequência.
O python data model define sequências como conjuntos finitos indexados por números não negativos. Sendo n
o tamanho da sequência, os índices vão de 0 a n
- 1 e são acessados por a[i]
.
Falaremos mais sobre o protocolo de sequência futuramente. Caso queira entender mais sobre o assunto consulte sua documentação.
Vamos ver como utilizar os métodos especiais para emular tipos numéricos.
O python data model diz que números são criados por números declará-los em sua forma literal (como por exemplo a = 3
, 3.4
etc.) e resultados de operações aritméticos e funções aritméticas embutidas.
Implementaremos uma classe para representar vetores bidimensionais (vetores euclidianos) usados na matemática e na física.
In [17]:
from exemplos.vetor1 import Vetor
v1 = Vetor(1, -2)
v2 = Vetor(3, 4)
Podemos somar vetores usando o operador +
:
In [18]:
v1 + v2
Out[18]:
Usar o operador de subtração:
In [19]:
v1 - v2
Out[19]:
Multiplicação por escalar:
In [20]:
v1 * 3
Out[20]:
In [21]:
v2 * -4
Out[21]:
Valor absoluto (distância do vetor até a origem):
In [22]:
abs(v2)
Out[22]:
Comparação de vetores por valor:
In [23]:
v1 == v2
Out[23]:
In [24]:
v1 == Vetor(1, -2)
Out[24]:
In [25]:
v2 == Vetor(3, 4)
Out[25]:
Podemos fazer verificações booleanas com o vetor:
In [26]:
if v1:
print('v1 existe e possui valor')
In [27]:
if not Vetor(0, 0):
print('vetor não possui valor')
else:
print('alguma coisa deu errado')
Esse exemplo usou a classe Vetor
demonstrada a seguir, que implementa as operações demonstradas por meio dos métodos especiais __repr__
, __abs__
, __add__
, __bool__
, __eq__
, __sub__
e __mul__
:
"""
Arquivo: 02-python-oo/aula-03/exemplos/vetor.py
Implementa um vetor bidimensional
"""
import math
class Vetor:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
return 'Vetor({!r}, {!r})'.format(self.x, self.y)
def __abs__(self):
return math.hypot(self.x, self.y)
def __add__(self, v2):
return Vetor(self.x + v2.x, self.y + v2.y)
def __bool__(self):
return bool(self.x or self.y)
def __eq__(self, v2):
return self.x == v2.x and self.y == v2.y
def __sub__(self, v2):
return Vetor(self.x - v2.x, self.y - v2.y)
def __mul__(self, scalar):
return Vetor(self.x * scalar, self.y * scalar)
O método __repr__
é responsável por retornar a representação do objeto para inspeção. Esse valor é usado no modo interativo e em debugers. Caso esse método não seja sobrescrito será exibido algo como <Vetor object at 0x123e9230>
.
A representação do objeto é obtido a partir da função embutida repr()
. É uma boa prática usar !r
para obter a representação dos atributos do objeto, pois mostra a diferença fundamental entre Vector(1, 2)
e Vector('1', '2')
- a última não funcionará, pois os argumentos do construtor devem ser número e não str
.
Também há o método __str__
que é utilizado para exibir o valor do objeto para o usuário final. Para entender melhor a diferença consulte esta thread do stack overflow que foi muito bem respondida pelos pythonistas Alex Martelli e Martijn Peters.
Esse exemplo contém alguns problemas:
In [28]:
v1
Out[28]:
In [29]:
v1 * 5
Out[29]:
In [30]:
5 * v1
No exemplo anterior tentamos multiplicar um int
por um Vetor
, porém foi levantada uma exceção já que o tipo int
não sabe multiplicar por Vetor
. Apenas Vetor
sabe multiplicar por escalar.
Para resolver esse problema precisamos antes entender como funciona x * y
:
x
tiver x.__mul__
, chama x.__mul__(y)
e devolve o resultado a menos que seja NotImplemented
x
não tiver x.__mul__
, ou sua chamada devolver NotImplemented
, verifica se y
possui __rmul__
, chama y.__rmul__(x)
e devolve o resultado, a menos que seja NotImplemented
y
não tiver __rmul__
, ou sua chamada devolver NotImplented
, levanta TypeError
com uma mensagem unsupported operand type(s)O método __rmul__
é chamado de versão refletida, reversa ou direita (do inglês right) de __mul__
.
Para corrigir precisamos adicionar o método __rmul__
à clase Vetor:
import math
class Vetor:
...
def __mul__(self, escalar):
return Vetor(self.x * escalar, self.y * escalar)
def __rmul__(self, outro):
return self * outro
Porém, ao adicionar esse código acontece outro problema:
In [33]:
from exemplos.vetor2 import Vetor
Vetor(1, 2) * Vetor(2, 4)
Esse resultado não faz sentido, não é assim que multiplicação de vetores funciona.
Não vamos implementar aqui a multiplicação de vetores, pois o foco da aula é ensinar programação e não matemática. Portanto, precisamos permitir a multiplicação de vetores apenas por escalares, vamos corrigir a função __mul__
:
import math
from numbers import Number
class Vetor:
...
def __mul__(self, escalar):
if isinstance(escalar, Real):
return Vetor(self.x * escalar, self.y * escalar)
else:
return NotImplemented
Verificamos se o escalar recebido de fato é um número real, se for retornamos o resultado da multiplicação do vetor pelo escalar, caso contrário é retornado NotImplemented
.
Retornamos NotImplemented
ao invés de levantar uma exceção, para permitir que o Python tente executar __rmul__
no escalar
, pois pode ser que seja algum tipo que implemente a operação reversa da multiplicação.
Também há um problema com a comparação de valores quando comparamos vetores com outros tipos:
In [34]:
from exemplos.vetor1 import Vetor
Vetor(1, 3) == [1, 2]
In [35]:
Vetor(2, 4) == 'oi'
Essa comparação deveria retornar False
, não levantar uma exceção. Podemos corrigir esse problema da seguinte maneira:
import math
from numbers import Number
class Vetor:
...
def __eq__(self, outro):
if isinstance(outro, Vetor):
return self.x == outro.x and self.y == outro.y
else:
return NotImplemented
Agora podemos comparar Vetor
com outros tipos:
In [2]:
from exemplos.vetor2 import Vetor
Vetor(1, 3) == 8
Out[2]:
In [3]:
Vetor(-2, 3) == [1, 2, 3]
Out[3]:
Nosso vetor ainda não suporta operações unárias como -v
e +v
:
In [10]:
from exemplos.vetor1 import Vetor
-Vetor(1, 5)
In [11]:
+Vetor(2, 3)
Para que esses operadores funcionem precisamos definir os métodos __neg__
e __pos__
:
from numbers import Real
import math
class Vetor:
...
def __neg__(self):
return self * -1
def __pos__(self):
return self
A função __neg__
simplesmente retornou o vetor por -1
. Já a função __pos__
retorna a própria instância, pois +Vetor(x, y)
é sempre igual a ele mesmo Vetor(x, y)
.
Seria interessante se pudessemos desempacotar os valores de x
e de y
de um vetor para uma tupla. Isso facilitaria nossa vida, pois poderiamos fazer isso:
In [13]:
v = Vetor(3, -1)
In [16]:
x, y = v
x, y
Ao invés disso:
In [17]:
x = v.x
y = v.y
x, y
Out[17]:
O desempacotamento facilita ainda mais nossa vida se tivessemos uma lista de vetores:
In [20]:
from random import randint
lista_vetores = [Vetor(x=randint(-10, 10), y=randint(-10, 10)) for _ in range(5)]
lista_vetores
Out[20]:
Pois poderiamos ter acesso facilitado a x
e y
durante uma iteração usando o desempacotamento de sequências:
In [21]:
for x, y in lista_vetores:
print(x, y)
Ao invés de ter que acessar os atributos diretamente:
In [23]:
for vetor in lista_vetores:
print(vetor.x, vetor.y)
Antes de implementar essa funcionalidade precisamos entender como funciona o desempacotamento de sequências: o objeto a direita é iterado e cada variável a esquerda é atribuída ao item resultante dessa iteração.
In [30]:
(a, b) = (1, 0)
a, b
Out[30]:
In [27]:
[a, b] = [3, 4]
a, b
Out[27]:
O que acontece por trás de tudo isso é: Extraímos o iterador da sequência a direita:
In [33]:
iterador = iter([3, 4])
Ele é iterado uma vez e o resultado da iteração é atribuído a primeira variável:
In [ ]:
a = next(iterador)
E é iterado até chegar ao último elemento:
In [34]:
b = next(iterador)
In [35]:
a, b
Out[35]:
Agora que sabemos disso fica claro que, para nosso vetor ser desempacotado precisamos torná-lo iterável. Para isso podemos definir o método __iter__
que deve retornar um iterador:
...
class Vetor:
...
def __iter__(self):
return iter((self.x, self.y))
Nesse método definimos uma tupla composta pelos atributos x
e y
da instância do Vetor
e retornamos o iterador da dessa tupla.
Essa implementação funciona, porém podemos usar geradores para deixar esse método mais simples e eficiente:
...
class Vetor:
...
def __iter__(self):
yield self.x; yield self.y
Para entender essa implementação é necessário conhecer o funcionamento de geradores, que veremos numa aula futura.
As operações que implementamos para nosso vetor não o alteram, mesmo quando usamos operadores acumulados:
In [36]:
from exemplos.vetor2 import Vetor
v = Vetor(1, 2)
v, id(v)
Out[36]:
Se realizarmos uma soma acumulada com outro vetor:
In [37]:
v += Vetor(2, 3)
v, id(v)
Out[37]:
Um novo objeto é criado, pois o objeto referenciado pela variável v
não é mais o mesmo (a identidade dos objetos são diferentes).
Para que essas operações de fato modifiquem um objeto, como acontecem com objetos mutáveis:
In [38]:
lista = [1, 2, 3, 4]
lista, id(lista)
Out[38]:
In [39]:
lista += [5, 6, 7, 8]
lista, id(lista)
Out[39]:
O objeto permanece o mesmo, seu valor que é alterado.
Matemáticamente não faz muito sentido ter um vetor mutável, mas para entendermos melhor esses conceitos vamos fazer um VetorMutável
, como subclasse de Vetor
, que altere o valor do vetor quanto as operações +=
e *=
forem usadas implementando os métodos __iadd__
e __imul__
:
In [42]:
from exemplos.vetor2 import VetorMutavel
vm = VetorMutavel(2, 3)
vm, id(vm)
Out[42]:
In [43]:
vm += VetorMutavel(-1, 4)
vm, id(vm)
Out[43]:
In [44]:
vm *= -2
vm, id(vm)
Out[44]:
Essa classe VetorMutavel
pode ser implementada da seguinte maneira:
class VetorMutavel(Vetor):
def __iadd__(self, outro):
if isinstance(outro, Vetor):
self.x += outro.x
self.y += outro.y
return self
return NotImplemented
def __imul__(self, outro):
if isinstance(outro, Real):
self.x *= outro
self.y *= outro
return self
return NotImplemented
In [23]:
def chamavel():
print('posso ser chamado')
In [2]:
chamavel()
In [22]:
type(chamavel)
Out[22]:
In [3]:
class Foo:
def bar(self):
print('também posso ser chamado!')
Quando classes são chamadas retornam instâncias:
In [6]:
foo = Foo()
In [7]:
foo.bar()
In [8]:
def gen():
yield 1
Chamar geradores retorna objetos geradores que executam o código definido
In [11]:
gen()
Out[11]:
Para acessar o conteúdo do gerador precisamos iterá-lo, para isso podemos usar a função embutida next()
:
In [21]:
g = gen()
next(g)
Out[21]:
Porém se requisitamos mais valores de um gerador que ele pode gerar uma exceção é levantada:
In [20]:
next(g)
Veremos mais sobre geradores nas próximas aulas. Para saber mais sobre chamáveis consulte o python data model e como chamáveis são expressados
Por fim, objetos que definem um método __call__
também são chamáveis.
Para demonstrar isso vamos implementar uma tombola (gaiola de bingo). A tombola pode:
Vamos ao código:
In [79]:
import random
class Tombola:
def __init__(self, itens=None):
self._itens = []
self.carrega(itens)
def __call__(self):
return self.sorteia()
def carrega(self, itens):
self._itens.extend(itens)
def inspeciona(self):
return tuple(self._itens)
def mistura(self):
random.shuffle(self._itens)
def sorteia(self):
return self._itens.pop()
def vazia(self):
return len(self._itens) == 0
Vamos criar nossa tombola que armazena os números de 1 a 20:
In [26]:
tombola = Tombola(range(1, 21))
Verificaremos seus itens:
In [27]:
tombola.inspeciona()
Out[27]:
Está vazia?
In [28]:
tombola.vazia()
Out[28]:
Misturando:
In [29]:
tombola.mistura()
tombola.inspeciona()
Out[29]:
Sorteando um item da maneira clássica:
In [30]:
tombola.sorteia()
Out[30]:
Aproveitando o método __call__
que definimos podemos sortear chamado o objeto tombola
sem chamar o método tombola.sorteia()
:
In [31]:
tombola()
Out[31]:
Os operadores "aritméticos" (como +
, *
etc.) também podem ser aplicados a outros objetos para realizar operações que fazem sentido a esse objetos. Como por exemplo em listas e strings, em que o operador +
realiza a concatenação:
In [32]:
lista = [1, 2, 3, 4]
lista
Out[32]:
In [33]:
lista + [5, 6, 7, 8]
Out[33]:
In [34]:
[-4, -3, -2, -1, 0] + lista
Out[34]:
In [35]:
pal = "palavra"
pal
Out[35]:
In [37]:
pal + '!!!1!1!11onze!!!1!'
Out[37]:
Para mostrar como isso funciona vamos implementar uma tombola expansível que torne possível juntar os itens dessa tombola com outra tombola ou um iterável.
Vamos definir o método __add__
para permitir a "soma" de tombolas:
In [39]:
class TombolaExpansivel(Tombola):
def __add__(self, other):
if isinstance(other, Tombola):
return TombolaExpansivel(self.inspeciona() + other.inspeciona())
else:
return NotImplemented
Na linha 3 verificamos se o objeto somado é uma instância de Tombola
, isso permite que nossa TombolaExpansivel
seja somada com Tombola
e todas suas subclasses:
In [62]:
tombola_exp = TombolaExpansivel(range(1, 11))
tombola_exp.inspeciona()
Out[62]:
Podemos somar a instância de TombolaExpansivel
com Tombola
e suas subclasses:
In [44]:
outra_tombola = tombola_exp + Tombola(range(11, 21))
outra_tombola.inspeciona()
Out[44]:
In [57]:
mais_tombolas = tombola_exp + TombolaExpansivel(range(11, 16))
mais_tombolas.inspeciona()
Out[57]:
Sobrescrevendo o método __add__
já é possível usar a soma atribuída, porém haverá um problema indesejado:
In [58]:
id(tombola_exp), tombola_exp.inspeciona()
Out[58]:
In [63]:
tombola_exp += Tombola(range(11, 16))
id(tombola_exp), tombola_exp.inspeciona()
Out[63]:
Vemos que as identidades dos objetos atribuidos a tombola_exp são diferentes. Isso por que a atribuição acumulada, por padrão, na verdade faz:
In [64]:
tombola_exp = tombola_exp + Tombola(range(16, 21))
In [65]:
tombola_exp.inspeciona()
Out[65]:
E nossa função o __add__
cria um novo objeto. Como queremos que nossa TombolaExpansivel
seja mutável, precisamos definir o método __iadd__
para modificar a instância.
Aproveitando que vamos mexer no __iadd__
podemos melhorar nossa tombola para receber item de qualquer iterável e não somente de Tombola
e suas subclasses:
In [80]:
class TombolaExpansivel(Tombola):
def __add__(self, other):
if isinstance(other, Tombola):
return TombolaExpansivel(self.inspeciona() + other.inspeciona())
else:
return NotImplemented
def __iadd__(self, outro):
if isinstance(outro, Tombola):
outro_iteravel = outro.inspeciona()
else:
try:
outro_iteravel = iter(outro)
except TypeError:
msg = "operando da direita no += deve ser {!r} ou um iterável"
raise TypeError(msg.format(type(self).__name__))
self.carrega(outro_iteravel)
return self
Linha 9 e 10: se o objeto à direita for uma tombola inspecionamos e "pegamos" seus itens
Linha 12 e 13: tenta extrair um iterável do objeto a direita, isso funcionará se este objeto for iterável, se não uma exceção do tipo TypeError
é levantada.
Linha 14, 15 e 16: Se for levantada uma exceção TypeError
é criada uma outra exceção do tipo TypeError
, porém com uma mensagem de erro mais clara.
Linha 17: carrega os próprios itens e da outra tupla.
Agora podemos, de fato, modificar nossa TombolaExpansível
:
In [81]:
tombola_exp = TombolaExpansivel(range(-10, 1, 1))
id(tombola_exp), tombola_exp.inspeciona()
Out[81]:
In [82]:
tombola_exp += [1, 2, 3, 4]
id(tombola_exp), tombola_exp.inspeciona()
Out[82]:
Para finalizar, vamos ver uma tabela todos os métodos especiais do Python. Algum dos métodos especiais ainda não foram explicados no curso e outros não serão, portanto consulte a documentação caso você precise deles.
Esta tabela foi tirada do livro Fluent Python (Python Fluente)
Na tabela a seguir constam todos os métodos mágicos por tipo. (em inglês, pois não há ebook da versão pt-br)
Fim da aula 03