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) Release: out/2014 • Early Release: • 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 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 descritor es 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):
!
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):
(self, descricao, peso, preco): def __init__ self.descricao = descricao self.set_peso(peso) mudanças self.preco = preco def subtotal(self): return self.get_peso() * self.preco def get_peso(self): __peso return self.
na interface
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 Traceback (most recent call last): ... ValueError: valor deve ser > 0 >>> ervilha.peso 10
parece uma violação de encapsulamento
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 self.preco = preco def subtotal(self): return self.peso * self.preco @property def peso(self): return self. __peso
e 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
(self): def __init__ prefixo = self. __class__ __name__ . chave = self. __class__ __contador . self.nome_alvo = '%s _ %s' % (prefixo, chave) self. __class__ __contador += 1 . (self, instance, owner): def __get__ return getattr(instance, self.nome_alvo)
implementação do descritor
classe new-style
(self, instance, value): def __set__ if value > 0: setattr(instance, self.nome_alvo, value) else: raise ValueError('valor deve ser > 0')
class ItemPedido (object): peso = Quantidade() preco = Quantidade()
(self, descricao, peso, preco): def __init__ self.descricao = descricao self.peso = peso self.preco = preco def subtotal(self):
!
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
(self): def __init__ prefixo = self. __class__ __name__ . chave = self. __class__ __contador . self.nome_alvo = '%s _ %s' % (prefixo, chave) self. __class__ __contador += 1 . (self, instance, owner): def __get__ return getattr(instance, self.nome_alvo)
implementação do descriptor
(self, instance, value): def __set__ if value > 0: setattr(instance, self.nome_alvo, value) else: raise ValueError('valor deve ser > 0')
class ItemPedido (object): peso = Quantidade() preco = Quantidade()
(self, descricao, peso, preco): def __init__ self.descricao = descricao self.peso = peso self.preco = preco def subtotal(self):
#
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
a classe ItemPedido tem duas instâncias de 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
(self): def __init__ prefixo = self. __class__ __name__ . chave = self. __class__ __contador . self.nome_alvo = '%s _ %s' % (prefixo, chave) self. __class__ __contador += 1 . (self, instance, owner): def __get__ return getattr(instance, self.nome_alvo) (self, instance, value): def __set__ 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
(self): def __init__ prefixo = self. __class__ __name__ . chave = self. __class__ __contador . self.nome_alvo = '%s _ %s' % (prefixo, chave) self. __class__ __contador += 1 . (self, instance, owner): def __get__ return getattr(instance, self.nome_alvo) (self, instance, value): def __set__ 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
(self): def __init__ prefixo = self. __class__ __name__ . chave = self. __class__ __contador . self.nome_alvo = '%s _ %s' % (prefixo, chave) self. __class__ __contador += 1 . (self, instance, owner): def __get__ return getattr(instance, self.nome_alvo) (self, instance, value): def __set__ 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
(self): def __init__ prefixo = self. __class__ __name__ . chave = self. __class__ __contador . self.nome_alvo = '%s _ %s' % (prefixo, chave) self. __class__ __contador += 1 . (self, instance, owner): def __get__ return getattr(instance, self.nome_alvo) (self, instance, value): def __set__ 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
(self): def __init__ prefixo = self. __class__ __name__ . chave = self. __class__ __contador . self.nome_alvo = '%s _ %s' % (prefixo, chave) self. __class__ __contador += 1 . (self, instance, owner): def __get__ return getattr(instance, self.nome_alvo) (self, instance, value): def __set__ 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
(self): def __init__ prefixo = self. __class__ __name__ . chave = self. __class__ __contador . self.nome_alvo = '%s _ %s' % (prefixo, chave) self. __class__ __contador += 1 . (self, instance, owner): def __get__ return getattr(instance, self.nome_alvo) (self, instance, value): def __set__ 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
(self): def __init__ prefixo = self. __class__ __name__ . chave = self. __class__ __contador . self.nome_alvo = '%s _ %s' % (prefixo, chave) self. __class__ __contador += 1 . (self, instance, owner): def __get__ return getattr(instance, self.nome_alvo) (self, instance, value): def __set__ 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
«peso» «preco»
«descriptor» 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 em vez de _Quantitade_0
• 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
class ItemPedido(object): peso = Quantidade() preco = Quantidade()
quando cada descritor é instanciado, a classe ItemPedido não existe, e nem os atributos gerenciados
(self, descricao, peso, preco): def __init__ self.descricao = descricao self.peso = peso self.preco = preco def subtotal(self): return self.peso * self.preco
#
o desafio
class ItemPedido(object): peso = Quantidade() preco = Quantidade()
por exemplo, o atributo peso só é criado depois que Quantidade() é instanciada
(self, descricao, peso, preco): def __init__ 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() (self, descricao, peso, preco): def __init__ self.descricao = descricao self.peso = peso self.preco = preco
$
módulo modelo.py class Quantidade(object):
(self): def __init__ self.set_nome(self. __class__ __name__, id(self)) . def set_nome(self, prefix, key): self.nome_alvo = '%s _ %s' % (prefix, key)
(self, instance, owner): def __get__ return getattr(instance, self.nome_alvo) (self, instance, value): def __set__ if value > 0: setattr(instance, self.nome_alvo, value) else: raise ValueError('valor deve ser > 0' ) class ModeloMeta(type):
(cls, nome, bases, dic): def __init__ 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