Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Python: encapsulamento com descritores

Python: encapsulamento com descritores

Python não tem campos privados, mas o decorador `property` permite substituir atributos públicos por getters e setters sem quebrar código cliente. E o mecanismo de descritores, usado no Django para declarações de campos de modelos e forms, permite reutilizar a lógica de getters/setters através de composição em vez de herança. Esta palestra demonstra como propriedades e descritores funcionam na refatoração um exemplo prático.

Luciano Ramalho

October 10, 2012
Tweet

More Decks by Luciano Ramalho

Other Decks in Technology

Transcript

  1. Turing.com.br 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
  2. Turing.com.br 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: ➊, ➋, ➌...
  3. Turing.com.br 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
  4. Turing.com.br ➊ 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
  5. Turing.com.br ➊ 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”
  6. >>> 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 Turing.com.br ➊ porém, simples demais isso vai dar problema na hora de cobrar...
  7. >>> 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 Turing.com.br ➊ porém, simples demais isso vai dar problema na hora de cobrar... Jeff Bezos of Amazon: Birth of a Salesman WSJ.com - http://j.mp/VZ5not “We found that customers could order a negative quantity of books! And we would credit their credit card with the price...” Jeff Bezos
  8. >>> 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 Turing.com.br ➊ porém, simples demais isso vai dar problema na hora de cobrar... Jeff Bezos of Amazon: Birth of a Salesman WSJ.com - http://j.mp/VZ5not “Descobrimos que os clientes conseguiam encomendar uma quantidade negativa de livros! E nós creditávamos o valor em seus cartões...” Jeff Bezos
  9. Turing.com.br ➋ 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 Traceback (most recent call last): ... ValueError: valor deve ser > 0 parece uma violação de encapsulamento mas a lógica do negócio está preservada: peso agora é uma property
  10. Turing.com.br ➋ 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')
  11. Turing.com.br ➋ 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') atributo protegido
  12. Turing.com.br ➋ 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
  13. Turing.com.br ➋ 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
  14. Turing.com.br 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') ➋ implementar property e se quisermos a mesma lógica para o preco? teremos que duplicar tudo isso?
  15. Turing.com.br ➌ validação com descriptor peso e preco são atributos

    da classe ItemPedido a lógica fica em __get__ e __set__, podendo ser reutilizada
  16. Turing.com.br ➌ implementação do 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') 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
  17. Turing.com.br ➌ uso do descriptor a classe ItemPedido tem duas

    instâncias de Quantidade associadas a ela
  18. 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 Turing.com.br ➌ uso do descriptor a classe ItemPedido tem duas instâncias de Quantidade associadas a ela
  19. Turing.com.br ➌ 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
  20. Turing.com.br ➌ 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') uma classe com método __get__ é um descriptor
  21. Turing.com.br ➌ 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 ao preco ou ao peso)
  22. Turing.com.br ➌ 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 ao preco ou ao peso) instance é a instância de ItemPedido que está sendo acessada
  23. Turing.com.br ➌ 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') nome_alvo é o nome do atributo da instância de ItemPedido que este descritor (self) controla
  24. Turing.com.br ➌ 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 atributo-alvo na instância de ItemPedido
  25. Turing.com.br ➌ 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
  26. Turing.com.br ➌ 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') temos que inventar um nome para o atributo-alvo onde será armazenado o valor na instância de ItemPedido
  27. Turing.com.br ➌ 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, dificultando a depuração
  28. Turing.com.br ➍ 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 def subtotal(self): return self.peso * self.preco ItemPedido.__new__ invoca «quantidade».set_nome
  29. Turing.com.br ➍ usar nomes descritivos 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') «quantidade».set_nome redefine o nome_alvo
  30. Turing.com.br ➍ 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 def subtotal(self): return self.peso * self.preco Quando __init__ executa, o descritor já está configurado com um nome de atributo-alvo descritivo
  31. Turing.com.br ➍ 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 atributos protegidos de Python
  32. Turing.com.br ➍ 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
  33. Turing.com.br ➍ 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»
  34. Turing.com.br ➍ funciona, mas custa caro 0 375000.0 750000.0 1125000.0

    1500000.0 versão 1 versão 2 versão 3 versão 4 80004 585394 980708 1427386 Número de instâncias de ItemPedido criadas por segundo (MacBook Pro 2011, Intel Core i7) 7.3 ×
  35. Turing.com.br ➎ 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...
  36. Turing.com.br ➎ metaclasses criam classes! type é a metaclasse default

    em Python: a classe que normalmente constroi outras classes metaclasses são classes cujas instâncias são classes ItemPedido é uma instância de type
  37. Turing.com.br ➎ 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__ a cada nova instância
  38. Turing.com.br 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 ItemPedido(object): __metaclass__ = ModeloMeta 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 ➎ nossa metaclasse Assim dizemos que a classe ItemPedido herda de object, mas é uma instância (construida por) ModeloMeta
  39. Turing.com.br 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 ItemPedido(object): __metaclass__ = ModeloMeta 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 Este __init__ invoca «quantidade».set_nome, para cada descritor, uma vez só, na inicialização da classe ItemPedido ➎ nossa metaclasse
  40. Turing.com.br ➎ desempenho melhor 0 375000.0 750000.0 1125000.0 1500000.0 versão

    1 versão 2 versão 3 versão 4 versão 5 585771 80004 585394 980708 1427386 Número de instâncias de ItemPedido criadas por segundo (MacBook Pro 2011, Intel Core i7) mesmo desempenho, + nomes amigáveis
  41. Turing.com.br 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 ➏ o poder da abstração
  42. Turing.com.br 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 ➏ módulo modelo.py
  43. Turing.com.br 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