Exercise 6.1

Objectives:

  • Learn more about how objects are represented.

  • Learn how attribute assignment and lookup works.

  • Better understand the role of a class definition

Files Created: None

Files Modified: None

Introduction

In Exercise 5.1, you defined a class Stock that represented a holding of stock. In this exercise, we will use that class. So, restart the Python shell and perform these steps:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> goog = Stock('GOOG',100,490.10)
>>> ibm  = Stock('IBM',50, 91.23)
>>>

(a) Representation of Instances

At the interactive shell, inspect the underlying dictionaries of the two instances you created:

>>> goog.__dict__
... look at the output ...
>>> ibm.__dict__
... look at the output ...
>>>

(b) Modification of Instance Data

Try setting a new attribute on one of the above instances:

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

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

(c) 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:

>>> goog.__class__
... look at output ...
>>> ibm.__class__
... look at output ...
>>>

Try calling a method on the instances:

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

>>> Stock.__dict__['cost']
... look at output ...
>>>

Try calling the cost() method directly through the dictionary:

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

>>> Stock.foo = 42
>>>

Notice how this new attribute now shows up on all of the instances:

>>> goog.foo
42
>>> ibm.foo
42
>>>

However, notice that it is not part of the instance dictionary:

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

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:

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

(d) 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:

>>> s = goog.sell
>>> s
<bound method Stock.sell of Stock('GOOG',100,490.1)>
>>> s(25)
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:

>>> s.__func__
<function sell at 0x10049af50>
>>>

This is the same value as found in the Stock dictionary.

>>> Stock.__dict__['sell']
<function sell at 0x10049af50>
>>>

Bound methods also record the instance, which is the self argument.

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

>>> s.__func__(s.__self__, 25)    # Same as s(25)
50
>>>
Links