Introdução
O sistema de objetos Python é amplamente baseado em uma implementação que envolve dicionários. Esta seção discute isso.
This tutorial is from open-source community. Access the source code
O sistema de objetos Python é amplamente baseado em uma implementação que envolve dicionários. Esta seção discute isso.
Lembre-se que um dicionário é uma coleção de valores nomeados.
stock = {
'name' : 'GOOG',
'shares' : 100,
'price' : 490.1
}
Dicionários são comumente usados para estruturas de dados simples. No entanto, eles são usados para partes críticas do interpretador e podem ser o tipo de dado mais importante em Python.
Dentro de um módulo, um dicionário armazena todas as variáveis e funções globais.
## foo.py
x = 42
def bar():
...
def spam():
...
Se você inspecionar foo.__dict__ ou globals(), você verá o dicionário.
{
'x' : 42,
'bar' : <function bar>,
'spam' : <function spam>
}
Objetos definidos pelo usuário também usam dicionários tanto para dados de instância quanto para classes. De fato, todo o sistema de objetos é, em sua maior parte, uma camada extra que é colocada em cima de dicionários.
Um dicionário armazena os dados da instância, __dict__.
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{'name' : 'GOOG', 'shares' : 100, 'price': 490.1 }
Você preenche este dict (e instância) ao atribuir a self.
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
Os dados da instância, self.__dict__, se parecem com isto:
{
'name': 'GOOG',
'shares': 100,
'price': 490.1
}
Cada instância recebe seu próprio dicionário privado.
s = Stock('GOOG', 100, 490.1) ## {'name' : 'GOOG','shares' : 100, 'price': 490.1 }
t = Stock('AAPL', 50, 123.45) ## {'name' : 'AAPL','shares' : 50, 'price': 123.45 }
Se você criasse 100 instâncias de alguma classe, haveria 100 dicionários armazenando dados.
Um dicionário separado também armazena os métodos.
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def cost(self):
return self.shares * self.price
def sell(self, nshares):
self.shares -= nshares
O dicionário está em Stock.__dict__.
{
'cost': <function>,
'sell': <function>,
'__init__': <function>
}
Instâncias e classes são ligadas. O atributo __class__ se refere de volta à classe.
>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name': 'GOOG', 'shares': 100, 'price': 490.1 }
>>> s.__class__
<class '__main__.Stock'>
>>>
O dicionário da instância armazena dados exclusivos para cada instância, enquanto o dicionário da classe armazena dados compartilhados coletivamente por todas as instâncias.
Quando você trabalha com objetos, você acessa dados e métodos usando o operador ..
x = obj.name ## Getting
obj.name = value ## Setting
del obj.name ## Deleting
Essas operações estão diretamente ligadas aos dicionários subjacentes.
Operações que modificam um objeto atualizam o dicionário subjacente.
>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name':'GOOG', 'shares': 100, 'price': 490.1 }
>>> s.shares = 50 ## Setting
>>> s.date = '6/7/2007' ## Setting
>>> s.__dict__
{ 'name': 'GOOG', 'shares': 50, 'price': 490.1, 'date': '6/7/2007' }
>>> del s.shares ## Deleting
>>> s.__dict__
{ 'name': 'GOOG', 'price': 490.1, 'date': '6/7/2007' }
>>>
Suponha que você leia um atributo em uma instância.
x = obj.name
O atributo pode existir em dois lugares:
Ambos os dicionários devem ser verificados. Primeiro, verifique no __dict__ local. Se não for encontrado, procure em __dict__ da classe através de __class__.
>>> s = Stock(...)
>>> s.name
'GOOG'
>>> s.cost()
49010.0
>>>
Este esquema de busca é como os membros de uma classe são compartilhados por todas as instâncias.
Classes podem herdar de outras classes.
class A(B, C):
...
As classes base são armazenadas em uma tupla em cada classe.
>>> A.__bases__
(<class '__main__.B'>, <class '__main__.C'>)
>>>
Isso fornece um link para as classes pai.
Logicamente, o processo de encontrar um atributo é o seguinte. Primeiro, verifique no __dict__ local. Se não for encontrado, procure em __dict__ da classe. Se não for encontrado na classe, procure nas classes base através de __bases__. No entanto, existem alguns aspectos sutis disso que serão discutidos a seguir.
Em hierarquias de herança, os atributos são encontrados percorrendo a árvore de herança em ordem.
class A: pass
class B(A): pass
class C(A): pass
class D(B): pass
class E(D): pass
Com herança simples, há um único caminho para o topo. Você para com a primeira correspondência.
O Python pré-calcula uma cadeia de herança e a armazena no atributo MRO na classe. Você pode visualizá-la.
>>> E.__mro__
(<class '__main__.E'>, <class '__main__.D'>,
<class '__main__.B'>, <class '__main__.A'>,
<type 'object'>)
>>>
Esta cadeia é chamada de Ordem de Resolução de Métodos (Method Resolution Order). Para encontrar um atributo, o Python percorre o MRO em ordem. A primeira correspondência vence.
Com herança múltipla, não há um único caminho para o topo. Vamos dar uma olhada em um exemplo.
class A: pass
class B: pass
class C(A, B): pass
class D(B): pass
class E(C, D): pass
O que acontece quando você acessa um atributo?
e = E()
e.attr
Um processo de busca de atributo é realizado, mas qual é a ordem? Esse é um problema.
O Python usa herança múltipla cooperativa (cooperative multiple inheritance), que obedece a algumas regras sobre a ordenação de classes.
O MRO é computado ordenando todas as classes em uma hierarquia de acordo com essas regras.
>>> E.__mro__
(
<class 'E'>,
<class 'C'>,
<class 'A'>,
<class 'D'>,
<class 'B'>,
<class 'object'>)
>>>
O algoritmo subjacente é chamado de "Algoritmo de Linearização C3" (C3 Linearization Algorithm). Os detalhes precisos não são importantes, desde que você se lembre que uma hierarquia de classes obedece às mesmas regras de ordenação que você pode seguir se sua casa estivesse pegando fogo e você tivesse que evacuar - primeiro os filhos, seguidos pelos pais.
Considere dois objetos completamente não relacionados:
class Dog:
def noise(self):
return 'Bark'
def chase(self):
return 'Chasing!'
class LoudDog(Dog):
def noise(self):
## Code commonality with LoudBike (below)
return super().noise().upper()
E
class Bike:
def noise(self):
return 'On Your Left'
def pedal(self):
return 'Pedaling!'
class LoudBike(Bike):
def noise(self):
## Code commonality with LoudDog (above)
return super().noise().upper()
Há uma similaridade de código na implementação de LoudDog.noise() e LoudBike.noise(). Na verdade, o código é exatamente o mesmo. Naturalmente, código como esse está fadado a atrair engenheiros de software.
O padrão Mixin é uma classe com um fragmento de código.
class Loud:
def noise(self):
return super().noise().upper()
Esta classe não é utilizável isoladamente. Ela se mistura com outras classes via herança.
class LoudDog(Loud, Dog):
pass
class LoudBike(Loud, Bike):
pass
Milagrosamente, a intensidade agora foi implementada apenas uma vez e reutilizada em duas classes completamente não relacionadas. Esse tipo de truque é um dos principais usos da herança múltipla em Python.
super()Sempre use super() ao substituir métodos.
class Loud:
def noise(self):
return super().noise().upper()
super() delega para a próxima classe na MRO (Method Resolution Order - Ordem de Resolução de Métodos).
A parte complicada é que você não sabe qual é essa classe. Especialmente, você não sabe qual é se a herança múltipla estiver sendo usada.
A herança múltipla é uma ferramenta poderosa. Lembre-se que com poder vem responsabilidade. Frameworks / bibliotecas às vezes a usam para recursos avançados envolvendo a composição de componentes. Agora, esqueça que você viu isso.
Na Seção 4, você definiu uma classe Stock que representava uma posse de ações. Neste exercício, usaremos essa classe. Reinicie o interpretador e crie algumas instâncias:
>>> ================================ RESTART ================================
>>> from stock import Stock
>>> goog = Stock('GOOG',100,490.10)
>>> ibm = Stock('IBM',50, 91.23)
>>>
No shell interativo, inspecione os dicionários subjacentes das duas instâncias que você criou:
>>> goog.__dict__
... veja a saída ...
>>> ibm.__dict__
... veja a saída ...
>>>
Tente definir um novo atributo em uma das instâncias acima:
>>> goog.date = '6/11/2007'
>>> goog.__dict__
... veja a saída ...
>>> ibm.__dict__
... veja a saída ...
>>>
Na saída acima, você notará que a instância goog tem um atributo date, enquanto a instância ibm não tem. É importante notar que o Python realmente não impõe nenhuma restrição aos atributos. Por exemplo, os atributos de uma instância não se limitam àqueles configurados no método __init__().
Em vez de definir um atributo, tente colocar um novo valor diretamente no objeto __dict__:
>>> goog.__dict__['time'] = '9:45am'
>>> goog.time
'9:45am'
>>>
Aqui, você realmente percebe o fato de que uma instância é apenas uma camada sobre um dicionário. Observação: deve-se enfatizar que a manipulação direta do dicionário é incomum - você sempre deve escrever seu código para usar a sintaxe (.).
As definições que compõem uma definição de classe são compartilhadas por todas as instâncias dessa classe. Observe que todas as instâncias têm um link de volta para sua classe associada:
>>> goog.__class__
... veja a saída ...
>>> ibm.__class__
... veja a saída ...
>>>
Tente chamar um método nas instâncias:
>>> goog.cost()
49010.0
>>> ibm.cost()
4561.5
>>>
Observe que o nome 'cost' não está definido em goog.__dict__ nem em ibm.__dict__. Em vez disso, ele está sendo fornecido pelo dicionário da classe. Tente isto:
>>> Stock.__dict__['cost']
... veja a saída ...
>>>
Tente chamar o método cost() diretamente através do dicionário:
>>> Stock.__dict__['cost'](goog)
49010.0
>>> Stock.__dict__['cost'](ibm)
4561.5
>>>
Observe como você está chamando a função definida na definição da classe e como o argumento self recebe a instância.
Tente adicionar um novo atributo à classe Stock:
>>> Stock.foo = 42
>>>
Observe como este novo atributo agora aparece em todas as instâncias:
>>> goog.foo
42
>>> ibm.foo
42
>>>
No entanto, observe que ele não faz parte do dicionário da instância:
>>> goog.__dict__
... veja a saída e observe que não há atributo 'foo' ...
>>>
A razão pela qual você pode acessar o atributo foo nas instâncias é que o Python sempre verifica o dicionário da classe se não conseguir encontrar algo na própria instância.
Observação: Esta parte do exercício ilustra algo conhecido como variável de classe (class variable). Suponha, por exemplo, que você tenha uma classe como esta:
class Foo(object):
a = 13 ## Variável de classe (Class variable)
def __init__(self,b):
self.b = b ## Variável de instância (Instance variable)
Nesta classe, a variável a, atribuída no corpo da própria classe, é uma "variável de classe". Ela é compartilhada por todas as instâncias que são criadas. Por exemplo:
>>> f = Foo(10)
>>> g = Foo(20)
>>> f.a ## Inspecione a variável de classe (a mesma para ambas as instâncias)
13
>>> g.a
13
>>> f.b ## Inspecione a variável de instância (difere)
10
>>> g.b
20
>>> Foo.a = 42 ## Mude o valor da variável de classe
>>> f.a
42
>>> g.a
42
>>>
Uma característica sutil do Python é que invocar um método realmente envolve duas etapas e algo conhecido como um método vinculado (bound method). Por exemplo:
>>> s = goog.sell
>>> s
<bound method Stock.sell of Stock('GOOG', 100, 490.1)>
>>> s(25)
>>> goog.shares
75
>>>
Métodos vinculados (bound methods) realmente contêm todas as partes necessárias para chamar um método. Por exemplo, eles mantêm um registro da função que implementa o método:
>>> s.__func__
<function sell at 0x10049af50>
>>>
Este é o mesmo valor encontrado no dicionário Stock.
>>> Stock.__dict__['sell']
<function sell at 0x10049af50>
>>>
Métodos vinculados (bound methods) também registram a instância, que é o argumento self.
>>> s.__self__
Stock('GOOG',75,490.1)
>>>
Quando você invoca a função usando (), todas as partes se juntam. Por exemplo, chamar s(25) realmente faz isso:
>>> s.__func__(s.__self__, 25) ## Mesmo que s(25)
>>> goog.shares
50
>>>
Crie uma nova classe que herda de Stock.
>>> class NewStock(Stock):
def yow(self):
print('Yow!')
>>> n = NewStock('ACME', 50, 123.45)
>>> n.cost()
6172.50
>>> n.yow()
Yow!
>>>
A herança (inheritance) é implementada estendendo o processo de busca por atributos. O atributo __bases__ possui uma tupla dos pais imediatos:
>>> NewStock.__bases__
(<class 'stock.Stock'>,)
>>>
O atributo __mro__ possui uma tupla de todos os pais, na ordem em que serão pesquisados os atributos.
>>> NewStock.__mro__
(<class '__main__.NewStock'>, <class 'stock.Stock'>, <class 'object'>)
>>>
Veja como o método cost() da instância n acima seria encontrado:
>>> for cls in n.__class__.__mro__:
if 'cost' in cls.__dict__:
break
>>> cls
<class '__main__.Stock'>
>>> cls.__dict__['cost']
<function cost at 0x101aed598>
>>>
Parabéns! Você concluiu o laboratório Dictionaries Revisited. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.