Encapsulamento com descritores

57
Descritores de atributos em Python Luciano Ramalho [email protected] novembro/2012

description

 

Transcript of Encapsulamento com descritores

Page 2: Encapsulamento com descritores

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

Page 3: Encapsulamento com descritores

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

Page 4: Encapsulamento com descritores

Turing.com.br

Page 5: Encapsulamento com descritores

➊ 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

Page 6: Encapsulamento com descritores

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”

Page 7: Encapsulamento com descritores

>>> 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 SalesmanWSJ.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

Page 8: Encapsulamento com descritores

➊ a solução clássicaclass 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')

atributoprotegido

mudançasna interface

Page 9: Encapsulamento com descritores

• 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...

➊ porém, a API foi alterada!>>> ervilha.pesoTraceback (most recent call last): ...AttributeError: 'ItemPedido' object has no attribute 'peso'

Page 10: Encapsulamento com descritores

➊ atributos protegidos

• Atributos protegidos em Python são salvaguardas

• servem para evitar atribuição ou sobrescrita acidental

• não para evitar usos (ou abusos) intencionais

Page 11: Encapsulamento com descritores

>>> ervilha._ItemPedido__peso10

• Atributos protegidos em Python são salvaguardas

• servem para evitar atribuição ou sobrescrita acidental

• não para evitar usos (ou abusos) intencionais

➊ atributos protegidos

Page 12: Encapsulamento com descritores

Turing.com.br

Page 13: Encapsulamento com descritores

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

➋ o segundo doctest

parece uma violação de encapsulamento

mas a lógica do negócio é preservada

peso não foi alterado

Page 14: Encapsulamento com descritores

Turing.com.br

➋ validação com property

peso agora é uma 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

Page 15: Encapsulamento com descritores

Turing.com.br

➋ implementar propertyclass 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

Page 16: Encapsulamento com descritores

Turing.com.br

➋ implementar propertyclass 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

Page 17: Encapsulamento com descritores

Turing.com.br

➋ implementar propertyclass 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

Page 18: Encapsulamento com descritores

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ógicapara o preco?

teremos que duplicar tudo isso?

Page 19: Encapsulamento com descritores

Turing.com.br

Page 20: Encapsulamento com descritores

➌ os atributos gerenciados (managed attributes)

usaremos descritores para gerenciar o acesso aos atributos peso e preco, preservando a lógica de negócio__init__

subtotal

descricaopeso {descriptor}preco {descriptor}

ItemPedido

Page 21: Encapsulamento com descritores

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

Page 22: Encapsulamento com descritores

implementaçãodo descritor

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

classenew-style

Page 23: Encapsulamento com descritores

Turing.com.br

➊ a classe produz instâncias

classeinstâncias

Page 24: Encapsulamento com descritores

Turing.com.br

➌ a classe descriptor

classe

instâncias

Page 25: Encapsulamento com descritores

Turing.com.br

➌ uso do descriptor

a classe ItemPedidotem duas instâncias de Quantidade associadas a ela

Page 26: Encapsulamento com descritores

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

Turing.com.br

implementaçãodo descriptor

Page 27: Encapsulamento com descritores

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 ItemPedidotem duas instâncias de Quantidade associadas a ela

Page 28: Encapsulamento com descritores

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

Page 29: Encapsulamento com descritores

➌ 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 precopassam pelos descritores

Page 30: Encapsulamento com descritores

Turing.com.br

➌ implementar o descriptorclass 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

Page 31: Encapsulamento com descritores

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')

Turing.com.br

➌ implementar o descriptor

self é a instância do descritor (associadaao preco ou ao peso)

Page 32: Encapsulamento com descritores

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')

Turing.com.br

➌ implementar o descriptor

self é a instância do descritor (associadaao preco ou ao peso)

instance é a instância de ItemPedido que está sendo acessada

Page 33: Encapsulamento com descritores

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')

Turing.com.br

➌ implementar o descriptornome_alvo é o nome do atributo da instância de ItemPedido que este descritor (self) controla

Page 34: Encapsulamento com descritores

Turing.com.br

➌ implementar o descriptor

__get__ e __set__ manipulam o atributo-alvo no objeto ItemPedido

Page 35: Encapsulamento com descritores

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')

Turing.com.br

➌ implementar o descriptor

__get__ e __set__ usam getattr e setattr para manipular o atributo-alvo na instância de ItemPedido

Page 36: Encapsulamento com descritores

➌ 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

Page 37: Encapsulamento com descritores

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

Page 38: Encapsulamento com descritores

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')

Turing.com.br

➌ implementar o descriptor

temos que gerar um nome para o atributo-alvo onde será armazenado o valor na instância de ItemPedido

Page 39: Encapsulamento com descritores

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

Page 40: Encapsulamento com descritores

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_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

Page 41: Encapsulamento com descritores

➌ os atributos alvo

Quantidade_0 armazena o valor de peso

__init____get____set__

nome_alvo

«descriptor»Quantidade

__init__subtotal

descricaoQuantidade_0Quantidade_1

ItemPedido«peso»

«preco»

«get/set atributo alvo»

Quantidade_1 armazena o valor de preco

Page 42: Encapsulamento com descritores

➌ os atributos gerenciados

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!

__init__subtotal

descricaopeso {descriptor}preco {descriptor}

ItemPedido

Page 43: Encapsulamento com descritores

➌ 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

Page 44: Encapsulamento com descritores

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 quando cada descritor é instanciado, a classe ItemPedido não existe, e nem os atributos gerenciados

Page 45: Encapsulamento com descritores

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 desafiopor exemplo, o atributo peso só é criado depois que Quantidade() é instanciada

Page 46: Encapsulamento com descritores

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 controlara construção da classe gerenciadacom uma...

Page 47: Encapsulamento com descritores

Turing.com.br

Acelerando...

Page 48: Encapsulamento com descritores

Turing.com.br

Page 49: Encapsulamento com descritores

Turing.com.br

➎ metaclasses criam classes!metaclasses são

classes cujas instâncias são

classes

Page 50: Encapsulamento com descritores

Turing.com.br

➏ simplicidade aparente

Page 51: Encapsulamento com descritores

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

Page 52: Encapsulamento com descritores

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

Page 53: Encapsulamento com descritores

Turing.com.br

➏ esquema final +

Page 54: Encapsulamento com descritores

Referências

• Raymond Hettinger’s Descriptor HowTo Guide (part of the official Python docs.)

• Alex Martelli’sPython in a Nutshell, 2e. (Python 2.5 but still excellent, includes the complete attribute access algorithm)

Page 55: Encapsulamento com descritores

Referências

• David Beazley’sPython Essential Reference, 4th edition(covers Python 2.6)

• Source code and doctests for this talk:http://github.com/ramalho/talks/tree/master/encap(will move to: https://github.com/oturing/encapy)

Page 56: Encapsulamento com descritores

A revelação final

• Funções em Python são descritores!

• implementam __get__

• É assim que uma função, quando acessada como atributo de uma instância se torna um bound method (método vinculado)

• fn.__get__ devolve uma aplicação parcial de fn, vinculando self à instância-alvo

Elegante, hein?

Page 57: Encapsulamento com descritores

Turing.com.br

Oficinas Turing:computação para programadores

• Próximos lançamentos:

• 1ª turma de Python para quem usa Django

• 3ª turma de Objetos Pythonicos

• 4ª turma de Python para quem sabe Python

Para saber mais sobre estes cursos ONLINE escreva para:

[email protected]