O documento descreve as etapas de desenvolvimento de uma classe em Python para representar itens de pedidos, com o objetivo de controlar o acesso aos atributos das instâncias de forma a garantir a integridade dos dados. Inicialmente é implementada uma classe simples, porém com acesso irrestrito aos atributos (➊). Posteriormente são acrescentadas propriedades para validar os valores atribuídos (➋) e descritores de classe para reutilizar a lógica de validação (➌). Por fim, são definidos nomes descritivos para
3. Pré-requistos da palestra
• Para acompanhar os slides a seguir, é preciso saber
como funciona o básico de orientação a objetos em
Python. Especificamente:
• contraste entre atributos de classe e de instância
• herança de atributos de classe (métodos e campos)
• atributos protegidos: como e quando usar
• como e quando usar a função super
Turing.com.br
4. Roteiro da palestra
• A partir de um cenário inicial, implementamos uma
classe muito simples
• A partir daí, evoluímos a implementação em seis
etapas para controlar o acesso aos campos das
instâncias, usando propriedades, descritores e
finalmente uma metaclasse
• Nos slides, as etapas são identificadas por
números: ➊, ➋, ➌...
Turing.com.br
5. O cenário
• Comércio de alimentos a granel
• Um pedido tem vários itens
• Cada item tem descrição,
peso (kg), preço unitário (p/ kg)
e sub-total
Turing.com.br
7. ➊ a classe produz instâncias
classe
instâncias
Turing.com.br
8. ➊ mais simples, impossível
o método
inicializador é
conhecido como
class ItemPedido(object): “dunder init”
def __init__(self, descricao, peso, preco):
self.descricao = descricao
self.peso = peso
self.preco = preco
def subtotal(self):
Turing.com.br self.peso * self.preco
return
9. ➊ porém, simples demais
>>> ervilha = ItemPedido('ervilha partida', .5, 7.95)
>>> ervilha.descricao, ervilha.peso, ervilha.preco
('ervilha partida', .5, 7.95)
>>> ervilha.peso = -10
>>> ervilha.subtotal()
isso vai dar
-79.5 problema na
hora de cobrar...
Turing.com.br
10. ➊ porém, simples demais
>>> ervilha = ItemPedido('ervilha partida', .5, 7.95)
>>> ervilha.descricao, ervilha.peso, ervilha.preco
('ervilha partida', .5, 7.95)
>>> ervilha.peso = -10
>>> ervilha.subtotal()
isso vai dar
-79.5 problema na
hora de cobrar...
“We found that customers could
order a negative quantity of books!
And we would credit their credit
card with the price...” Jeff Bezos
Jeff Bezos of Amazon: Birth of a Salesman
Turing.com.br WSJ.com - http://j.mp/VZ5not
11. ➊ porém, simples demais
>>> ervilha = ItemPedido('ervilha partida', .5, 7.95)
>>> ervilha.descricao, ervilha.peso, ervilha.preco
('ervilha partida', .5, 7.95)
>>> ervilha.peso = -10
>>> ervilha.subtotal()
isso vai dar
-79.5 problema na
hora de cobrar...
“Descobrimos que os clientes
conseguiam encomendar uma
quantidade negativa de livros!
E nós creditávamos o valor em
seus cartões...” Jeff Bezos
Jeff Bezos of Amazon: Birth of a Salesman
Turing.com.br WSJ.com - http://j.mp/VZ5not
12. ➋ validação com property
>>> ervilha = ItemPedido('ervilha partida', .5, 7.95)
>>> ervilha.descricao, ervilha.peso, ervilha.preco
('ervilha partida', .5, 7.95)
>>> ervilha.peso = -10 parece uma
Traceback (most recent call last):
... violação de
ValueError: valor deve ser > 0 encapsulamento
mas a lógica do negócio
está preservada:
peso agora é uma property
Turing.com.br
13. ➋ implementar property
class ItemPedido(object):
def __init__(self, descricao, peso, preco):
self.descricao = descricao
self.peso = peso
self.preco = preco
def subtotal(self):
return self.peso * self.preco
@property
def peso(self):
return self.__peso
@peso.setter
def peso(self, valor):
if valor > 0:
self.__peso = valor
else:
raise ValueError('valor deve ser > 0')
Turing.com.br
14. ➋ implementar property
class ItemPedido(object):
def __init__(self, descricao, peso, preco):
self.descricao = descricao
self.peso = peso
self.preco = preco
def subtotal(self):
return self.peso * self.preco
@property
def peso(self): atributo
return self.__peso
protegido
@peso.setter
def peso(self, valor):
if valor > 0:
self.__peso = valor
else:
raise ValueError('valor deve ser > 0')
Turing.com.br
15. ➋ implementar property
class ItemPedido(object):
def __init__(self, descricao, peso, preco):
self.descricao = descricao
self.peso = peso
self.preco = preco
def subtotal(self):
return self.peso * self.preco
no __init__ a
property já
@property
def peso(self): está em uso
return self.__peso
@peso.setter
def peso(self, valor):
if valor > 0:
self.__peso = valor
else:
raise ValueError('valor deve ser > 0')
Turing.com.br
16. ➋ implementar property
class ItemPedido(object):
def __init__(self, descricao, peso, preco):
self.descricao = descricao
self.peso = peso
self.preco = preco
def subtotal(self):
return self.peso * self.preco o atributo
@property protegido
def peso(self): __peso só é
return self.__peso
acessado nos
@peso.setter métodos da
def peso(self, valor):
if valor > 0: property
self.__peso = valor
else:
raise ValueError('valor deve ser > 0')
Turing.com.br
17. ➋ implementar property
class ItemPedido(object):
def __init__(self, descricao, peso, preco):
self.descricao = descricao
self.peso = peso
self.preco = preco ese quisermos
def subtotal(self):
a mesma lógica
return self.peso * self.preco para o preco?
@property
def peso(self):
return self.__peso
teremos que
@peso.setter duplicar tudo
def peso(self, valor):
if valor > 0:
isso?
self.__peso = valor
else:
raise ValueError('valor deve ser > 0')
Turing.com.br
18. ➌ validação com descriptor
peso e preco
são atributos
da classe a lógica fica em
ItemPedido __get__ e __set__,
podendo ser reutilizada
Turing.com.br
21. ➌ uso do descriptor
a classe
ItemPedido
tem duas
instâncias de
Quantidade
associadas a ela
Turing.com.br
22. ➌ uso do descriptor
a classe
class ItemPedido(object):
peso = Quantidade()
ItemPedido
preco = Quantidade() tem duas
def __init__(self, descricao, peso, preco):
instâncias de
self.descricao = descricao Quantidade
self.peso = peso
self.preco = preco
associadas a ela
def subtotal(self):
return self.peso * self.preco
Turing.com.br
23. ➌ uso do descriptor
class ItemPedido(object):
peso = Quantidade() cada instância
preco = Quantidade()
da classe
def __init__(self, descricao, peso, preco): Quantidade
self.descricao = descricao
self.peso = peso controla um
self.preco = preco atributo de
def subtotal(self): ItemPedido
return self.peso * self.preco
Turing.com.br
24. ➌ implementar o descriptor
class Quantidade(object):
def __init__(self):
prefixo = self.__class__.__name__
chave = id(self)
self.nome_alvo = '%s_%s' % (prefixo, chave)
def __get__(self, instance, owner):
return getattr(instance, self.nome_alvo)
uma classe
def __set__(self, instance, value):
if value > 0:
com método
setattr(instance, self.nome_alvo, value) __get__ é um
else:
raise ValueError('valor deve ser > 0') descriptor
Turing.com.br
25. ➌ implementar o descriptor
class Quantidade(object):
def __init__(self):
prefixo = self.__class__.__name__
chave = id(self)
self.nome_alvo = '%s_%s' % (prefixo, chave)
def __get__(self, instance, owner):
return getattr(instance, self.nome_alvo)
def __set__(self, instance, value):
if value > 0:
setattr(instance, self.nome_alvo, value)
else:
raise ValueError('valor deve ser > 0')
self é a instância
do descritor (associada
Turing.com.br ao preco ou ao peso)
26. ➌ implementar o descriptor
class Quantidade(object):
def __init__(self):
prefixo = self.__class__.__name__
chave = id(self)
self.nome_alvo = '%s_%s' % (prefixo, chave)
def __get__(self, instance, owner):
return getattr(instance, self.nome_alvo)
def __set__(self, instance, value):
if value > 0:
setattr(instance, self.nome_alvo, value)
else: instance é a instância
de ItemPedido que está
raise ValueError('valor deve ser > 0')
self é a instância sendo acessada
do descritor (associada
Turing.com.br ao preco ou ao peso)
27. ➌ implementar o descriptor
class Quantidade(object): nome_alvo é
def __init__(self):
o nome do
prefixo = self.__class__.__name__ atributo da
chave = id(self)
self.nome_alvo = '%s_%s' % (prefixo, chave) instância de
def __get__(self, instance, owner):
ItemPedido
return getattr(instance, self.nome_alvo) que este
def __set__(self, instance, value): descritor
if value > 0:
setattr(instance, self.nome_alvo, value)
(self)
else: controla
raise ValueError('valor deve ser > 0')
Turing.com.br
28. ➌ implementar o descriptor
__get__ e __set__
manipulam o atributo-alvo
Turing.com.br no objeto ItemPedido
29. ➌ implementar o descriptor
class Quantidade(object):
def __init__(self):
prefixo = self.__class__.__name__
chave = id(self)
self.nome_alvo = '%s_%s' % (prefixo, chave)
def __get__(self, instance, owner):
return getattr(instance, self.nome_alvo)
def __set__(self, instance, value):
if value > 0:
setattr(instance, self.nome_alvo, value)
else:
raise ValueError('valor deve ser > 0')
__get__ e __set__ usam
getattr e setattr para manipular o
Turing.com.br atributo-alvo na instância de ItemPedido
30. ➌ inicialização do descritor
quando um descritor é
instanciado, o atributo ao
class ItemPedido(object): qual ele será vinculado
peso = Quantidade()
preco = Quantidade() ainda não existe!
def __init__(self, descricao, peso, preco):
self.descricao = descricao
self.peso = peso
self.preco = preco
def subtotal(self): exemplo: o
return self.peso * self.preco
atributo preco
só passa a
existir após a
atribuição
Turing.com.br
31. ➌ implementar o descriptor
class Quantidade(object):
def __init__(self):
prefixo = self.__class__.__name__
chave = id(self)
self.nome_alvo = '%s_%s' % (prefixo, chave)
def __get__(self, instance, owner):
return getattr(instance, self.nome_alvo)
def __set__(self, instance, value):
if value > 0:
temos que inventar um
setattr(instance, self.nome_alvo, value)
else:
nome para o atributo-alvo
raise ValueError('valor deve ser > 0')
onde será armazenado o
valor na instância de
ItemPedido
Turing.com.br
32. ➌ implementar o descriptor
>>> ervilha = ItemPedido('ervilha partida', .5, 3.95)
>>> ervilha.descricao, ervilha.peso, ervilha.preco
('ervilha partida', .5, 3.95)
>>> dir(ervilha)
['Quantidade_4299545872', 'Quantidade_4299546064',
'__class__', '__delattr__', '__dict__', '__doc__',
'__format__', '__getattribute__', '__hash__',
'__init__', '__module__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__',
'__weakref__', 'descricao', 'peso', 'preco',
'subtotal']
nesta implementação, os nomes dos
atributos-alvo não são descritivos,
Turing.com.br
dificultando a depuração
33. ➍ usar nomes descritivos
ItemPedido.__new__ invoca
«quantidade».set_nome
Turing.com.br para redefinir o nome_alvo
34. ➍ usar nomes descritivos
ItemPedido.__new__ invoca «quantidade».set_nome
«quantidade».set_nome
Turing.com.br redefine o nome_alvo
36. ➍ usar nomes descritivos
«quantidade».set_nome
class Quantidade(object):
redefine o nome_alvo
def __init__(self):
self.set_nome(self.__class__.__name__, id(self))
def set_nome(self, prefix, key):
self.nome_alvo = '%s_%s' % (prefix, key)
def __get__(self, instance, owner):
return getattr(instance, self.nome_alvo)
def __set__(self, instance, value):
if value > 0:
setattr(instance, self.nome_alvo, value)
else:
raise ValueError('valor deve ser > 0')
Turing.com.br
37. ➍ usar nomes descritivos
class ItemPedido(object):
peso = Quantidade()
preco = Quantidade()
def __new__(cls, *args, **kwargs):
for chave, atr in cls.__dict__.items():
if hasattr(atr, 'set_nome'):
atr.set_nome('__' + cls.__name__, chave)
return super(ItemPedido, cls).__new__(cls, *args, **kwargs)
def __init__(self, descricao, peso, preco):
self.descricao = descricao
self.peso = peso
self.preco = preco Quando __init__
def subtotal(self): executa, o descritor
return self.peso * self.preco
já está configurado
com um nome de
atributo-alvo
Turing.com.br
descritivo
38. ➍ usar nomes descritivos
ItemPedido.__new__ invoca «quantidade».set_nome
Turing.com.br
39. ➍ usar nomes descritivos
«quantidade».set_nome
Turing.com.br redefine o nome_alvo
40. ➍ nomes descritivos
>>> ervilha = ItemPedido('ervilha partida', .5, 3.95)
>>> ervilha.descricao, ervilha.peso, ervilha.preco
('ervilha partida', 0.5, 3.95)
>>> dir(ervilha)
['__ItemPedido_peso', '__ItemPedido_preco',
'__class__', '__delattr__', '__dict__', '__doc__',
'__format__', '__getattribute__', '__hash__',
'__init__', '__module__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__',
'__weakref__', 'descricao', 'peso', 'preco',
'subtotal']
nesta implementação e nas
próximas, os nomes dos atributos-
alvo seguem a convenção de
Turing.com.br atributos protegidos de Python
41. ➍ funciona, mas custa caro
• ItemPedido aciona __new__
para construir cada nova
instância
• Porém a associação dos
descritores é com a classe
ItemPedido: o nome do
atributo-alvo nunca vai mudar,
uma vez definido corretamente
Turing.com.br
42. ➍ funciona, mas custa caro
• Isso significa que para cada
nova instância de
ItemPedido que é criada,
«quantidade».set_nome
é invocado duas vezes
• Mas o nome do atributo-alvo
não tem porque mudar na vida
de uma «quantidade»
Turing.com.br
43. ➍ funciona, mas custa caro
1500000.0 1427386
7.3 ×
1125000.0
980708
750000.0
585394
375000.0
80004
0
versão 1 versão 2 versão 3 versão 4
Número de instâncias de ItemPedido criadas
por segundo (MacBook Pro 2011, Intel Core i7)
Turing.com.br
44. ➎ como evitar trabalho inútil
• ItemPedido.__new__ resolveu
mas de modo ineficiente.
• Cada «quantidade» deve receber
o nome do seu atributo-alvo apenas
uma vez, quando a própria classe
ItemPedido for criada
• Para isso precisamos de uma...
Turing.com.br
45. ➎ metaclasses criam classes!
metaclasses são
classes cujas
instâncias são
classes
Turing.com.br
46. ➎ metaclasses criam classes!
metaclasses são
classes cujas
instâncias são
classes
ItemPedido é uma
instância de type
type é a metaclasse default
em Python: a classe que
normalmente constroi
Turing.com.br outras classes
48. ➎ nossa metaclasse
ModeloMeta é a
metaclasse que vai
construir a classe
ItemPedido
ModeloMeta.__init__
fará apenas uma vez o
que antes era feito em
ItemPedido.__new__
Turing.com.br a cada nova instância
49. ➎ nossa metaclasse
class ModeloMeta(type):
def __init__(cls, nome, bases, dic):
super(ModeloMeta, cls).__init__(nome, bases, dic)
for chave, atr in dic.items():
if hasattr(atr, 'set_nome'):
atr.set_nome('__' + nome, chave)
Assim dizemos que a
class ItemPedido(object):
__metaclass__ = ModeloMeta classe ItemPedido herda
peso = Quantidade() de object, mas é uma
preco = Quantidade()
instância (construida por)
def __init__(self, descricao, peso, preco):
self.descricao = descricao ModeloMeta
self.peso = peso
self.preco = preco
def subtotal(self):
return self.peso * self.preco
Turing.com.br
50. ➎ nossa metaclasse
class ModeloMeta(type):
def __init__(cls, nome, bases, dic):
super(ModeloMeta, cls).__init__(nome, bases, dic)
for chave, atr in dic.items():
if hasattr(atr, 'set_nome'):
atr.set_nome('__' + nome, chave)
Este __init__ invoca
class ItemPedido(object):
__metaclass__ = ModeloMeta «quantidade».set_nome,
peso = Quantidade()
para cada descritor, uma
preco = Quantidade()
vez só, na inicialização da
def __init__(self, descricao, peso, preco):
self.descricao = descricao
classe ItemPedido
self.peso = peso
self.preco = preco
def subtotal(self):
return self.peso * self.preco
Turing.com.br
54. ➎ desempenho melhor
1500000.0 1427386
mesmo desempenho,
+ nomes amigáveis
1125000.0
980708
750000.0
585394 585771
375000.0
80004
0
versão 1 versão 2 versão 3 versão 4 versão 5
Número de instâncias de ItemPedido criadas
por segundo (MacBook Pro 2011, Intel Core i7)
Turing.com.br
60. Oficinas Turing:
computação para programadores
• Próximos lançamentos:
• 4ª turma de Python para quem sabe Python
• 3ª turma de Objetos Pythonicos
• 1ª turma de Aprenda Python com um Pythonista
Turing.com.br