2. 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
Turing.com.br
3. 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
Turing.com.br
5. ➊ 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
6. ➊ mais simples, impossível
o método
inicializador é
conhecido como
class ItemPedido(object): “dunder init”
def __init__(self, descricao, peso, preco):
self.descricao = descricao
self.peso = peso
self.preco = preco
def subtotal(self):
Turing.com.br self.peso * self.preco
return
7. ➊ 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()
isso vai dar
-79.5 problema na
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
Turing.com.br WSJ.com - http://j.mp/VZ5not
8. ➊ a solução clássica
class ItemPedido(object):
def __init__(self, descricao, peso, preco):
self.descricao = descricao
self.set_peso(peso)
self.preco = preco mudanças
na
def subtotal(self):
return self.get_peso() * self.preco
interface
def get_peso(self):
return self.__peso atributo
def set_peso(self, valor): protegido
if valor > 0:
self.__peso = valor
else:
raise ValueError('valor deve ser > 0')
9. ➊ 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...
10. ➊ 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
11. ➊ 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
>>> ervilha._ItemPedido__peso
10
13. ➋ 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
14. ➋ 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
Turing.com.br
15. ➋ 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): atributo
return self.__peso
protegido
@peso.setter
def peso(self, valor):
if valor > 0:
self.__peso = valor
else:
raise ValueError('valor deve ser > 0')
Turing.com.br
16. ➋ 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
no __init__ a
property já
@property
def peso(self): está em uso
return self.__peso
@peso.setter
def peso(self, valor):
if valor > 0:
self.__peso = valor
else:
raise ValueError('valor deve ser > 0')
Turing.com.br
17. ➋ 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 o atributo
@property protegido
def peso(self): __peso só é
return self.__peso
acessado nos
@peso.setter métodos da
def peso(self, valor):
if valor > 0: property
self.__peso = valor
else:
raise ValueError('valor deve ser > 0')
Turing.com.br
18. ➋ implementar property
class ItemPedido(object):
def __init__(self, descricao, peso, preco):
self.descricao = descricao
self.peso = peso
self.preco = preco ese quisermos
def subtotal(self):
a mesma lógica
return self.peso * self.preco para o preco?
@property
def peso(self):
return self.__peso
teremos que
@peso.setter duplicar tudo
def peso(self, valor):
if valor > 0:
isso?
self.__peso = valor
else:
raise ValueError('valor deve ser > 0')
Turing.com.br
20. ➌ os atributos gerenciados
(managed attributes)
ItemPedido usaremos descritores
descricao para gerenciar o acesso
peso {descriptor} aos atributos peso e
preco {descriptor} preco, preservando a
__init__ lógica de negócio
subtotal
21. ➌ validação com descriptor
peso e preco
são atributos
da classe a lógica fica em
ItemPedido __get__ e __set__,
podendo ser reutilizada
Turing.com.br
27. ➌ uso do descriptor
a classe
class ItemPedido(object):
peso = Quantidade()
ItemPedido
preco = Quantidade() tem duas
def __init__(self, descricao, peso, preco):
instâncias de
self.descricao = descricao Quantidade
self.peso = peso
self.preco = preco
associadas a ela
def subtotal(self):
return self.peso * self.preco
Turing.com.br
28. ➌ uso do descriptor
class ItemPedido(object):
peso = Quantidade() cada instância
preco = Quantidade()
da classe
def __init__(self, descricao, peso, preco): Quantidade
self.descricao = descricao
self.peso = peso controla um
self.preco = preco atributo de
def subtotal(self): ItemPedido
return self.peso * self.preco
Turing.com.br
29. ➌ uso do descriptor
todos os acessos
a peso e preco
class ItemPedido(object): passam pelos
peso = Quantidade()
preco = Quantidade() descritores
def __init__(self, descricao, peso, preco):
self.descricao = descricao
self.peso = peso
self.preco = preco
def subtotal(self):
return self.peso * self.preco
30. ➌ 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) uma classe
def __set__(self, instance, value): com método
if value > 0:
setattr(instance, self.nome_alvo, value)
__get__ é um
else: descriptor
raise ValueError('valor deve ser > 0')
Turing.com.br
31. ➌ 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
Turing.com.br ao preco ou ao peso)
32. ➌ 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:
instance é a instância
setattr(instance, self.nome_alvo, value)
else:
de ItemPedido que está
raise ValueError('valor deve ser > 0')
self é a instância sendo acessada
do descritor (associada
Turing.com.br ao preco ou ao peso)
33. ➌ implementar o descriptor
class Quantidade(object):
__contador = 0 nome_alvo é
def __init__(self):
o nome do
prefixo = self.__class__.__name__ atributo da
chave = self.__class__.__contador
self.nome_alvo = '%s_%s' % (prefixo, chave) instância de
self.__class__.__contador += 1
ItemPedido
def __get__(self, instance, owner): que este
return getattr(instance, self.nome_alvo)
descritor
def __set__(self, instance, value):
if value > 0:
(self)
setattr(instance, self.nome_alvo, value) controla
else:
raise ValueError('valor deve ser > 0')
Turing.com.br
34. ➌ implementar o descriptor
__get__ e __set__
manipulam o atributo-alvo
Turing.com.br no objeto ItemPedido
35. ➌ 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
Turing.com.br atributo-alvo na instância de ItemPedido
36. ➌ 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
37. ➌ inicialização do descritor
quando um descritor é
instanciado, o atributo ao
class ItemPedido(object): qual ele será vinculado
peso = Quantidade()
preco = Quantidade() ainda não existe!
def __init__(self, descricao, peso, preco):
self.descricao = descricao
self.peso = peso
self.preco = preco
def subtotal(self): exemplo: o
return self.peso * self.preco
atributo preco
só passa a
existir após a
atribuição
Turing.com.br
38. ➌ 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:
temos que gerar um nome
setattr(instance, self.nome_alvo, value)
else: para o atributo-alvo onde
raise ValueError('valor deve ser > 0')
será armazenado o valor
na instância de
ItemPedido
Turing.com.br
39. ➌ implementar o descriptor
class Quantidade(object):
__contador = 0
def __init__(self):
cada instância
prefixo = self.__class__.__name__ de Quantidade
chave = self.__class__.__contador
self.nome_alvo = '%s_%s' % (prefixo, chave) precisa criar e
self.__class__.__contador += 1
usar um
def __get__(self, instance, owner): nome_alvo
return getattr(instance, self.nome_alvo)
diferente
def __set__(self, instance, value):
if value > 0:
setattr(instance, self.nome_alvo, value)
else:
raise ValueError('valor deve ser > 0')
41. ➌ os atributos alvo
ItemPedido «descriptor»
descricao «peso» Quantidade
Quantidade_0 nome_alvo
Quantidade_1 «preco» __init__
__init__ __get__
subtotal __set__
«get/set atributo alvo»
Quantidade_0 armazena o valor de peso
Quantidade_1 armazena o valor de preco
42. ➌ os atributos gerenciados
clientes da classe
ItemPedido ItemPedido não precisam
descricao
saber como peso e preco
peso {descriptor}
preco {descriptor}
são gerenciados
__init__
subtotal
E nem precisam saber que
Quantidade_0 e
Quantidade_1 existem!
43. ➌ 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
44. ➌ o desafio quando cada
descritor é
instanciado, a
classe ItemPedido
não existe, e nem
class ItemPedido(object): os atributos
peso = Quantidade()
preco = Quantidade() gerenciados
def __init__(self, descricao, peso, preco):
self.descricao = descricao
self.peso = peso
self.preco = preco
def subtotal(self):
return self.peso * self.preco
45. ➌ o desafio
por exemplo, o
atributo peso só é
criado depois que
Quantidade() é
class ItemPedido(object): instanciada
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
46. 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...
54. Referências
• Raymond Hettinger’s
Descriptor HowTo Guide
(part of the official Python docs.)
• Alex Martelli’s
Python in a Nutshell, 2e.
(Python 2.5 but still excellent,
includes the complete attribute
access algorithm)
55. Referências
• David Beazley’s
Python 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)
56. 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?
57. 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:
ramalho@turing.com.br
Turing.com.br