Luciano Ramalho @ramalhoorg

Propaganda
Produtividade e qualidade
em Python através da
metaprogramação
ou
a “visão radical” na prática
Luciano Ramalho
[email protected]
@ramalhoorg
Fluent Python (O’Reilly)
• Early Release: out/2014
• First Edition: jul/2015
• ~ 700 páginas
•
•
•
•
•
Data structures
Functions as objects
Classes and objects
Control flow
Metaprogramming
PythonPro: escola virtual
• Instrutores: Renzo Nuccitelli e Luciano Ramalho
• Na sua empresa ou online ao vivo com Adobe Connect: http://python.pro.br
• Python Prático
• Python Birds
• Objetos Pythônicos
• Python para quem sabe Python
Simply Scheme: preface
Nosso objetivo
• Expandir o vocabulário com uma idéia
poderosa:
descritores de atributos
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
➊
➊ o primeiro doctest
=======
Passo 1
=======
!
Um pedido de alimentos a granel é uma coleção de ``ItemPedido``.
Cada item possui campos para descrição, peso e preço::
!
>>> from granel import ItemPedido
>>> ervilha = ItemPedido('ervilha partida', 10, 3.95)
>>> ervilha.descricao, ervilha.peso, ervilha.preco
('ervilha partida', 10, 3.95)
!
Um método ``subtotal`` fornece o preço total de cada item::
!
>>> ervilha.subtotal()
39.5
➊ mais simples, impossível
class ItemPedido(object):!
!
o método
inicializador é
conhecido como
“dunder init”
def __init__(self, descricao, peso, preco):!
self.descricao = descricao!
self.peso = peso!
self.preco = preco!
!
def subtotal(self):!
return self.peso * self.preco
➊ 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
isso vai dar
>>> ervilha.subtotal()
problema na
-79.5
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
WSJ.com - http://j.mp/VZ5not
➊ a solução clássica
class ItemPedido(object):!
!
def __init__(self, descricao, peso, preco):!
self.descricao = descricao!
self.set_peso(peso)!
mudanças
self.preco = preco!
!
na
def subtotal(self):!
interface
return self.get_peso() * self.preco!
!
def get_peso(self):!
return self.__peso!
atributo
!
protegido
def set_peso(self, valor):!
if valor > 0:!
self.__peso = valor!
else:!
raise ValueError('valor deve ser > 0')
➊ porém, a API foi alterada!
>>> ervilha.peso
Traceback (most recent call last):
...
AttributeError: 'ItemPedido' object has no attribute 'peso'
• Antes podíamos acessar o peso de um item
escrevendo apenas item.peso, mas agora não...
• Isso quebra código das classes clientes
• Python oferece outro caminho...
➋
➋ o segundo doctest
O peso de um ``ItemPedido`` deve ser maior que zero::
!
>>> ervilha.peso = 0
parece uma
Traceback (most recent call last):
violação de
...
ValueError: valor deve ser > 0
encapsulamento
>>> ervilha.peso
10
mas a lógica do negócio é
preservada
peso não foi alterado
➋ validação com property
O peso de um ``ItemPedido`` deve ser maior que zero::
!
>>> ervilha.peso = 0
Traceback (most recent call last):
...
ValueError: valor deve ser > 0
>>> ervilha.peso
10
peso agora é uma property
➋ 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!
!
atributo
protegido
@peso.setter!
def peso(self, valor):!
if valor > 0:!
self.__peso = valor!
else:!
raise ValueError('valor deve ser > 0')
➋ 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!
no __init__ a
property já
está em uso
!
@peso.setter!
def peso(self, valor):!
if valor > 0:!
self.__peso = valor!
else:!
raise ValueError('valor deve ser > 0')
➋ 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!
!
o atributo
protegido
__peso só é
acessado nos
métodos da
property
@peso.setter!
def peso(self, valor):!
if valor > 0:!
self.__peso = valor!
else:!
raise ValueError('valor deve ser > 0')
➋ implementar property
class ItemPedido(object):!
!
def __init__(self, descricao, peso, preco):!
self.descricao = descricao!
self.peso = peso!
e
self.preco = preco!
!
def subtotal(self):!
return self.peso * self.preco!
!
@property!
def peso(self):!
return self.__peso!
!
se quisermos
a mesma lógica
para o preco?
teremos que
duplicar tudo
isso?
@peso.setter!
def peso(self, valor):!
if valor > 0:!
self.__peso = valor!
else:!
raise ValueError('valor deve ser > 0')
➌
➌ os atributos gerenciados
(managed attributes)
ItemPedido
descricao
peso {descriptor}
preco {descriptor}
__init__
subtotal
usaremos descritores
para gerenciar o acesso
aos atributos peso e
preco, preservando a
lógica de negócio
➌ validação com descriptor
peso e preco
são atributos
da classe
ItemPedido
a lógica fica em __get__ e __set__,
podendo ser reutilizada
➌
class Quantidade(object):!
__contador = 0!
!
def __init__(self):!
prefixo = self.__class__.__name__!
chave = self.__class__.__contador!
self.nome_alvo = '%s_%s' % (prefixo, chave)!
self.__class__.__contador += 1!
!
def __get__(self, instance, owner):!
return getattr(instance, self.nome_alvo)!
implementação
do descritor
classe
new-style
!
def __set__(self, instance, value):!
if value > 0:!
setattr(instance, self.nome_alvo, value)!
else:!
raise ValueError('valor deve ser > 0')!
!
!
class ItemPedido(object):!
peso = Quantidade()!
preco = Quantidade()!
!
def __init__(self, descricao, peso, preco):!
self.descricao = descricao!
self.peso = peso!
self.preco = preco!
!
def subtotal(self):!
return self.peso * self.preco
➊ a classe produz instâncias
classe
instâncias
➌ a classe descriptor
instâncias
classe
➌ uso do descriptor
a classe
ItemPedido
tem duas
instâncias de
Quantidade
associadas a ela
➌
class Quantidade(object):!
__contador = 0!
!
def __init__(self):!
prefixo = self.__class__.__name__!
chave = self.__class__.__contador!
self.nome_alvo = '%s_%s' % (prefixo, chave)!
self.__class__.__contador += 1!
!
def __get__(self, instance, owner):!
return getattr(instance, self.nome_alvo)!
implementação
do descriptor
!
def __set__(self, instance, value):!
if value > 0:!
setattr(instance, self.nome_alvo, value)!
else:!
raise ValueError('valor deve ser > 0')!
!
!
class ItemPedido(object):!
peso = Quantidade()!
preco = Quantidade()!
!
def __init__(self, descricao, peso, preco):!
self.descricao = descricao!
self.peso = peso!
self.preco = preco!
!
def subtotal(self):!
return self.peso * self.preco
➌ uso do descriptor
class ItemPedido(object):!
peso = Quantidade()!
preco = Quantidade()!
!
def __init__(self, descricao, peso,
self.descricao = descricao!
self.peso = peso!
self.preco = preco!
!
def subtotal(self):!
return self.peso * self.preco
a classe
ItemPedido
tem duas
instâncias
de
preco):!
Quantidade
associadas a ela
➌ uso do descriptor
class ItemPedido(object):!
peso = Quantidade()!
preco = Quantidade()!
!
def __init__(self, descricao, peso, preco):!
self.descricao = descricao!
self.peso = peso!
self.preco = preco!
!
def subtotal(self):!
return self.peso * self.preco
cada instância
da classe
Quantidade
controla um
atributo de
ItemPedido
➌ uso do descriptor
class ItemPedido(object):!
peso = Quantidade()!
preco = Quantidade()!
todos os acessos
a peso e preco
passam pelos
descritores
!
def __init__(self, descricao, peso, preco):!
self.descricao = descricao!
self.peso = peso!
self.preco = preco!
!
def subtotal(self):!
return self.peso * self.preco
➌ implementar o descriptor
class Quantidade(object):!
__contador = 0!
!
def __init__(self):!
prefixo = self.__class__.__name__!
chave = self.__class__.__contador!
self.nome_alvo = '%s_%s' % (prefixo, chave)!
self.__class__.__contador += 1!
!
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')
uma classe
com método
__get__ é um
descriptor
➌ implementar o descriptor
class Quantidade(object):!
__contador = 0!
!
def __init__(self):!
prefixo = self.__class__.__name__!
chave = self.__class__.__contador!
self.nome_alvo = '%s_%s' % (prefixo, chave)!
self.__class__.__contador += 1!
!
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
ao preco ou ao peso)
➌ implementar o descriptor
class Quantidade(object):!
__contador = 0!
!
def __init__(self):!
prefixo = self.__class__.__name__!
chave = self.__class__.__contador!
self.nome_alvo = '%s_%s' % (prefixo, chave)!
self.__class__.__contador += 1!
!
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')
instance é a instância
de ItemPedido que está
self é a instância sendo acessada
do descritor (associada
ao preco ou ao peso)
➌ implementar o descriptor
class Quantidade(object):!
__contador = 0!
!
def __init__(self):!
prefixo = self.__class__.__name__!
chave = self.__class__.__contador!
self.nome_alvo = '%s_%s' % (prefixo, chave)!
self.__class__.__contador += 1!
!
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')
nome_alvo é
o nome do
atributo da
instância de
ItemPedido
que este
descritor
(self)
controla
➌ implementar o descriptor
__get__ e __set__
manipulam o atributo-alvo
no objeto ItemPedido
➌ implementar o descriptor
class Quantidade(object):!
__contador = 0!
!
def __init__(self):!
prefixo = self.__class__.__name__!
chave = self.__class__.__contador!
self.nome_alvo = '%s_%s' % (prefixo, chave)!
self.__class__.__contador += 1!
!
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
atributo-alvo na instância de ItemPedido
➌ descriptor implementation
cada instância de descritor
gerencia um atributo
específico das instâncias de
ItemPedido e precisa de um
nome_alvo específico
➌ inicialização do descritor
class ItemPedido(object):!
peso = Quantidade()!
preco = Quantidade()!
quando um descritor é
instanciado, o atributo ao
qual ele será vinculado
ainda não existe!
!
def __init__(self, descricao, peso, preco):!
self.descricao = descricao!
self.peso = peso!
self.preco = preco!
!
def subtotal(self):!
return self.peso * self.preco
exemplo: o
atributo preco
só passa a
existir após a
atribuição
➌ implementar o descriptor
class Quantidade(object):!
__contador = 0!
!
def __init__(self):!
prefixo = self.__class__.__name__!
chave = self.__class__.__contador!
self.nome_alvo = '%s_%s' % (prefixo, chave)!
self.__class__.__contador += 1!
!
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')
temos que gerar um nome
para o atributo-alvo onde
será armazenado o valor
na instância de
ItemPedido
➌ implementar o descriptor
class Quantidade(object):!
__contador = 0!
!
def __init__(self):!
prefixo = self.__class__.__name__!
chave = self.__class__.__contador!
self.nome_alvo = '%s_%s' % (prefixo, chave)!
self.__class__.__contador += 1!
!
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')
cada instância
de Quantidade
precisa criar e
usar um
nome_alvo
diferente
➌ implementar o descriptor
>>> ervilha = ItemPedido('ervilha partida', .5, 3.95)
>>> ervilha.descricao, ervilha.peso, ervilha.preco
('ervilha partida', .5, 3.95)
>>> dir(ervilha)
['Quantidade_0', 'Quantidade_1', '__class__',
'__delattr__', '__dict__', '__doc__', '__format__',
'__getattribute__', '__hash__', '__init__',
'__module__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__',
'__weakref__', 'descricao', 'peso', 'preco',
'subtotal']
Quantidade_0 e Quantidade_1 são
os atributos-alvo
➌ os atributos alvo
ItemPedido
descricao
Quantidade_0
Quantidade_1
__init__
subtotal
«descriptor»
«peso»
«preco»
Quantidade
nome_alvo
__init__
__get__
__set__
«get/set atributo alvo»
Quantidade_0 armazena o valor de peso
Quantidade_1 armazena o valor de preco
➌ os atributos gerenciados
ItemPedido
descricao
peso {descriptor}
preco {descriptor}
__init__
subtotal
clientes da classe
ItemPedido não precisam
saber como peso e preco
são gerenciados
E nem precisam saber que
Quantidade_0 e
Quantidade_1 existem!
➌ próximos passos
• Seria melhor se os atributos-alvo fossem
atributos protegidos
•
_ItemPedido__peso
_Quantitade_0
em vez de
• Para fazer isso, precisamos descobrir o nome do
atributo gerenciado (ex. peso)
• isso não é tão simples quanto parece
• pode ser que não valha a pena complicar mais
➌ o desafio
quando cada
descritor é
instanciado, a
classe ItemPedido
não existe, e nem
os atributos
gerenciados
class ItemPedido(object):!
peso = Quantidade()!
preco = Quantidade()!
!
def __init__(self, descricao, peso, preco):!
self.descricao = descricao!
self.peso = peso!
self.preco = preco!
!
def subtotal(self):!
return self.peso * self.preco
➌ o desafio
por exemplo, o
atributo peso só é
criado depois que
Quantidade() é
instanciada
class ItemPedido(object):!
peso = Quantidade()!
preco = Quantidade()!
!
def __init__(self, descricao, peso, preco):!
self.descricao = descricao!
self.peso = peso!
self.preco = preco!
!
def subtotal(self):!
return self.peso * self.preco
Próximos passos
•
Se o descritor precisar saber o nome do atributo
gerenciado
(talvez para salvar o valor em um banco de dados, usando
nomes de colunas descritivos, como faz o Django)
•
...então você vai precisar controlar
a construção da classe gerenciada
com uma...
Acelerando...
➏
➎ metaclasses criam classes!
metaclasses são
classes cujas
instâncias são
classes
➏ simplicidade aparente
➏ o poder da abstração
from modelo import Modelo, Quantidade!
!
class ItemPedido(Modelo):!
!
peso = Quantidade()!
preco = Quantidade()!
!
def __init__(self, descricao, peso, preco):!
self.descricao = descricao!
self.peso = peso!
self.preco = preco
➏ módulo modelo.py
class Quantidade(object):!
!
!
!
!
!
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')!
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)!
class Modelo(object):!
__metaclass__ = ModeloMeta
➏ esquema final
+
➏ o poder da abstração
Referências
• Raymond Hettinger’s Descriptor HowTo Guide • Alex Martelli’s
Python in a Nutshell, 2e.
• David Beazley’s
Python Essential Reference, 4th edition
(covers Python 2.6)

Download