Produtividade e qualidade em Python através da metaprogramação ou a visão radical na prática Luciano Ramalho ramalho@python.pro.br @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): def init (self, descricao, peso, preco): self.descricao = descricao self.peso = peso self.preco = preco def subtotal(self): return self.peso * self.preco o método inicializador é conhecido como dunder init
➊ 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() -79.5 Descobrimos que os clientes conseguiam encomendar uma quantidade negativa de livros E nós creditávamos o valor em seus cartões... Jeff Bezos isso vai dar problema na hora de cobrar... 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) self.preco = preco def subtotal(self): return self.get_peso() * self.preco def get_peso(self): return self. peso def set_peso(self, valor): if valor > 0: self. peso = valor else: raise ValueError('valor deve ser > 0') mudanças na interface atributo protegido
➊ 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 peso não foi alterado parece uma violação de encapsulamento mas a lógica do negócio é preservada
➋ 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 @peso.setter def peso(self, valor): if valor > 0: self. peso = valor else: raise ValueError('valor deve ser > 0') no init a property já está em uso
➋ 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') o atributo protegido peso só é acessado nos métodos da 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 @peso.setter def peso(self, valor): if valor > 0: self. peso = valor else: raise ValueError('valor deve ser > 0') e se quisermos a mesma lógica para o preco? teremos que duplicar tudo isso?
➌
➌ 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
➌ implementação do descritor classe new-style 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') 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
➌ implementação do 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') 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, 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() def init (self, descricao, peso, preco): self.descricao = descricao self.peso = peso self.preco = preco def subtotal(self): return self.peso * self.preco todos os acessos a peso e preco passam pelos descritores
➌ 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') self é a instância do descritor (associada ao preco ou ao peso) instance é a instância de ItemPedido que está sendo acessada
➌ 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() def init (self, descricao, peso, preco): self.descricao = descricao self.peso = peso self.preco = preco def subtotal(self): return self.peso * self.preco quando um descritor é instanciado, o atributo ao qual ele será vinculado ainda não existe 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 «peso» «preco» «get/set atributo alvo» «descriptor» Quantidade nome_alvo init get set 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 quando cada descritor é instanciado, a 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 classe ItemPedido não existe, e nem os atributos gerenciados
➌ o desafio 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 por exemplo, o atributo peso só é criado depois que Quantidade() é instanciada
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, 4 th edition (covers Python 2.6)