Files
practical-python/Notes/05_Object_model/01_Dicts_revisited.md
David Beazley a608739313 Fix typo
2020-05-30 07:54:35 -05:00

660 lines
14 KiB
Markdown

[Contents](../Contents.md) \| [Previous (4.4 Exceptions)](../04_Classes_objects/04_Defining_exceptions.md) \| [Next (5.2 Encapsulation)](02_Classes_encapsulation.md)
# 5.1 Dictionaries Revisited
The Python object system is largely based on an implementation
involving dictionaries. This section discusses that.
### Dictionaries, Revisited
Remember that a dictionary is a collection of names values.
```python
stock = {
'name' : 'GOOG',
'shares' : 100,
'price' : 490.1
}
```
Dictionaries are commonly used for simple data structures. However,
they are used for critical parts of the interpreter and may be the
*most important type of data in Python*.
### Dicts and Modules
Within a module, a dictionary holds all of the global variables and
functions.
```python
# foo.py
x = 42
def bar():
...
def spam():
...
```
If you inspect `foo.__dict__` or `globals()`, you'll see the dictionary.
```python
{
'x' : 42,
'bar' : <function bar>,
'spam' : <function spam>
}
```
### Dicts and Objects
User defined objects also use dictionaries for both instance data and
classes. In fact, the entire object system is mostly an extra layer
that's put on top of dictionaries.
A dictionary holds the instance data, `__dict__`.
```python
>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{'name' : 'GOOG','shares' : 100, 'price': 490.1 }
```
You populate this dict (and instance) when assigning to `self`.
```python
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
```
The instance data, `self.__dict__`, looks like this:
```python
{
'name': 'GOOG',
'shares': 100,
'price': 490.1
}
```
**Each instance gets its own private dictionary.**
```python
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 }
```
If you created 100 instances of some class, there are 100 dictionaries
sitting around holding data.
### Class Members
A separate dictionary also holds the methods.
```python
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
```
The dictionary is in `Stock.__dict__`.
```python
{
'cost': <function>,
'sell': <function>,
'__init__': <function>
}
```
### Instances and Classes
Instances and classes are linked together. The `__class__` attribute
refers back to the class.
```python
>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name': 'GOOG', 'shares': 100, 'price': 490.1 }
>>> s.__class__
<class '__main__.Stock'>
>>>
```
The instance dictionary holds data unique to each instance, whereas
the class dictionary holds data collectively shared by *all*
instances.
### Attribute Access
When you work with objects, you access data and methods using the `.` operator.
```python
x = obj.name # Getting
obj.name = value # Setting
del obj.name # Deleting
```
These operations are directly tied to the dictionaries sitting underneath the covers.
### Modifying Instances
Operations that modify an object update the underlying dictionary.
```python
>>> 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' }
>>>
```
### Reading Attributes
Suppose you read an attribute on an instance.
```python
x = obj.name
```
The attribute may exist in two places:
* Local instance dictionary.
* Class dictionary.
Both dictionaries must be checked. First, check in local `__dict__`.
If not found, look in `__dict__` of class through `__class__`.
```python
>>> s = Stock(...)
>>> s.name
'GOOG'
>>> s.cost()
49010.0
>>>
```
This lookup scheme is how the members of a *class* get shared by all instances.
### How inheritance works
Classes may inherit from other classes.
```python
class A(B, C):
...
```
The base classes are stored in a tuple in each class.
```python
>>> A.__bases__
(<class '__main__.B'>, <class '__main__.C'>)
>>>
```
This provides a link to parent classes.
### Reading Attributes with Inheritance
Logically, the process of finding an attribute is as follows. First,
check in local `__dict__`. If not found, look in `__dict__` of the
class. If not found in class, look in the base classes through
`__bases__`. However, there are some subtle aspects of this discussed next.
### Reading Attributes with Single Inheritance
In inheritance hierarchies, attributes are found by walking up the
inheritance tree in order.
```python
class A: pass
class B(A): pass
class C(A): pass
class D(B): pass
class E(D): pass
```
With single inheritance, there is single path to the top.
You stop with the first match.
### Method Resolution Order or MRO
Python precomputes an inheritance chain and stores it in the *MRO* attribute on the class.
You can view it.
```python
>>> E.__mro__
(<class '__main__.E'>, <class '__main__.D'>,
<class '__main__.B'>, <class '__main__.A'>,
<type 'object'>)
>>>
```
This chain is called the **Method Resolution Order**. The find an
attribute, Python walks the MRO in order. The first match wins.
### MRO in Multiple Inheritance
With multiple inheritance, there is no single path to the top.
Let's take a look at an example.
```python
class A: pass
class B: pass
class C(A, B): pass
class D(B): pass
class E(C, D): pass
```
What happens when you access an attribute?
```python
e = E()
e.attr
```
A attribute search process is carried out, but what is the order? That's a problem.
Python uses *cooperative multiple inheritance* which obeys some rules
about class ordering.
* Children are always checked before parents
* Parents (if multiple) are always checked in the order listed.
The MRO is computed by sorting all of the classes in a hierarchy
according to those rules.
```python
>>> E.__mro__
(
<class 'E'>,
<class 'C'>,
<class 'A'>,
<class 'D'>,
<class 'B'>,
<class 'object'>)
>>>
```
The underlying algorithm is called the "C3 Linearization Algorithm."
The precise details aren't important as long as you remember that a
class hierarchy obeys the same ordering rules you might follow if your
house was on fire and you had to evacuate--children first, followed by
parents.
### An Odd Code Reuse (Involving Multiple Inheritance)
Consider two completely unrelated objects:
```python
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()
```
And
```python
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()
```
There is a code commonality in the implementation of `LoudDog.noise()` and
`LoudBike.noise()`. In fact, the code is exactly the same. Naturally,
code like that is bound to attract software engineers.
### The "Mixin" Pattern
The *Mixin* pattern is a class with a fragment of code.
```python
class Loud:
def noise(self):
return super().noise().upper()
```
This class is not usable in isolation.
It mixes with other classes via inheritance.
```python
class LoudDog(Loud, Dog):
pass
class LoudBike(Loud, Bike):
pass
```
Miraculously, loudness was now implemented just once and reused
in two completely unrelated classes. This sort of trick is one
of the primary uses of multiple inheritance in Python.
### Why `super()`
Always use `super()` when overriding methods.
```python
class Loud:
def noise(self):
return super().noise().upper()
```
`super()` delegates to the *next class* on the MRO.
The tricky bit is that you don't know what it is. You especially don't
know what it is if multiple inheritance is being used.
### Some Cautions
Multiple inheritance is a powerful tool. Remember that with power
comes responsibility. Frameworks / libraries sometimes use it for
advanced features involving composition of components. Now, forget
that you saw that.
## Exercises
In Section 4, you defined a class `Stock` that represented a holding of stock.
In this exercise, we will use that class. Restart the interpreter and make a
few instances:
```python
>>> ================================ RESTART ================================
>>> from stock import Stock
>>> goog = Stock('GOOG',100,490.10)
>>> ibm = Stock('IBM',50, 91.23)
>>>
```
### Exercise 5.1: Representation of Instances
At the interactive shell, inspect the underlying dictionaries of the
two instances you created:
```python
>>> goog.__dict__
... look at the output ...
>>> ibm.__dict__
... look at the output ...
>>>
```
### Exercise 5.2: Modification of Instance Data
Try setting a new attribute on one of the above instances:
```python
>>> goog.date = '6/11/2007'
>>> goog.__dict__
... look at output ...
>>> ibm.__dict__
... look at output ...
>>>
```
In the above output, you'll notice that the `goog` instance has a
attribute `date` whereas the `ibm` instance does not. It is important
to note that Python really doesn't place any restrictions on
attributes. For example, the attributes of an instance are not
limited to those set up in the `__init__()` method.
Instead of setting an attribute, try placing a new value directly into
the `__dict__` object:
```python
>>> goog.__dict__['time'] = '9:45am'
>>> goog.time
'9:45am'
>>>
```
Here, you really notice the fact that an instance is just a layer on
top of a dictionary. Note: it should be emphasized that direct
manipulation of the dictionary is uncommon--you should always write
your code to use the (.) syntax.
### Exercise 5.3: The role of classes
The definitions that make up a class definition are shared by all
instances of that class. Notice, that all instances have a link back
to their associated class:
```python
>>> goog.__class__
... look at output ...
>>> ibm.__class__
... look at output ...
>>>
```
Try calling a method on the instances:
```python
>>> goog.cost()
49010.0
>>> ibm.cost()
4561.5
>>>
```
Notice that the name 'cost' is not defined in either `goog.__dict__`
or `ibm.__dict__`. Instead, it is being supplied by the class
dictionary. Try this:
```python
>>> Stock.__dict__['cost']
... look at output ...
>>>
```
Try calling the `cost()` method directly through the dictionary:
```python
>>> Stock.__dict__['cost'](goog)
49010.0
>>> Stock.__dict__['cost'](ibm)
4561.5
>>>
```
Notice how you are calling the function defined in the class
definition and how the `self` argument gets the instance.
Try adding a new attribute to the `Stock` class:
```python
>>> Stock.foo = 42
>>>
```
Notice how this new attribute now shows up on all of the instances:
```python
>>> goog.foo
42
>>> ibm.foo
42
>>>
```
However, notice that it is not part of the instance dictionary:
```python
>>> goog.__dict__
... look at output and notice there is no 'foo' attribute ...
>>>
```
The reason you can access the `foo` attribute on instances is that
Python always checks the class dictionary if it can't find something
on the instance itself.
Note: This part of the exercise illustrates something known as a class
variable. Suppose, for instance, you have a class like this:
```python
class Foo(object):
a = 13 # Class variable
def __init__(self,b):
self.b = b # Instance variable
```
In this class, the variable `a`, assigned in the body of the
class itself, is a "class variable." It is shared by all of the
instances that get created. For example:
```python
>>> f = Foo(10)
>>> g = Foo(20)
>>> f.a # Inspect the class variable (same for both instances)
13
>>> g.a
13
>>> f.b # Inspect the instance variable (differs)
10
>>> g.b
20
>>> Foo.a = 42 # Change the value of the class variable
>>> f.a
42
>>> g.a
42
>>>
```
### Exercise 5.4: Bound methods
A subtle feature of Python is that invoking a method actually involves
two steps and something known as a bound method. For example:
```python
>>> s = goog.sell
>>> s
<bound method Stock.sell of Stock('GOOG', 100, 490.1)>
>>> s(25)
>>> goog.shares
75
>>>
```
Bound methods actually contain all of the pieces needed to call a
method. For instance, they keep a record of the function implementing
the method:
```python
>>> s.__func__
<function sell at 0x10049af50>
>>>
```
This is the same value as found in the `Stock` dictionary.
```python
>>> Stock.__dict__['sell']
<function sell at 0x10049af50>
>>>
```
Bound methods also record the instance, which is the `self`
argument.
```python
>>> s.__self__
Stock('GOOG',75,490.1)
>>>
```
When you invoke the function using `()` all of the pieces come
together. For example, calling `s(25)` actually does this:
```python
>>> s.__func__(s.__self__, 25) # Same as s(25)
>>> goog.shares
50
>>>
```
### Exercise 5.5: Inheritance
Make a new class that inherits from `Stock`.
```
>>> class NewStock(Stock):
def yow(self):
print('Yow!')
>>> n = NewStock('ACME', 50, 123.45)
>>> n.cost()
6172.50
>>> n.yow()
Yow!
>>>
```
Inheritance is implemented by extending the search process for attributes.
The `__bases__` attribute has a tuple of the immediate parents:
```python
>>> NewStock.__bases__
(<class 'stock.Stock'>,)
>>>
```
The `__mro__` attribute has a tuple of all parents, in the order that
they will be searched for attributes.
```python
>>> NewStock.__mro__
(<class '__main__.NewStock'>, <class 'stock.Stock'>, <class 'object'>)
>>>
```
Here's how the `cost()` method of instance `n` above would be found:
```python
>>> for cls in n.__class__.__mro__:
if 'cost' in cls.__dict__:
break
>>> cls
<class '__main__.Stock'>
>>> cls.__dict__['cost']
<function cost at 0x101aed598>
>>>
```
[Contents](../Contents.md) \| [Previous (4.4 Exceptions)](../04_Classes_objects/04_Defining_exceptions.md) \| [Next (5.2 Encapsulation)](02_Classes_encapsulation.md)