CptS 481 - Python Software Construction

Unit 9: Objects

In [1]:
from IPython.display import HTML
HTML(open("notes.css", "r").read())
Out[1]:

A Brief Overview of Objects

  • shopping mall example:

    Person

    attributes: name, age, gender, address, email, phoneNumber, parkingSpot

    Customer

    attributes: amountOfCash, purchaseHistory, storesVisited, arrivalTime, departureTime

    Employee

    attributes: employer, schedule, salary, licenseNumber, idNumber

    Q: The same person can be an employee at one shop and a customer at another (or even the same) shop. How can we deal with this?

    maybe create an EmployeeCustomer that inherits from both

  • object properties:

    • instantiation

      class can create new instance

    • encapsulation

      state and methods maintained with each object

    • inheritance

      one class can acquire attributes from another, possibly modifying them

    • abstraction

      share common attributes from a class that is never instanced

Creating Classes in Python

Defining Classes and Methods

class ClassName[(*BaseClasses)]: [ classDocString ] suite

In [2]:
class Thingie:
    pass

thingie = Thingie()
thingie.name = "I Dunno"
thingie.otherStuff = 124
print(thingie.name)
I Dunno
In [3]:
# syntax similar to C structs
#print(dir(thingie))
print(thingie.__dict__)
{'name': 'I Dunno', 'otherStuff': 124}
In [4]:
thingie.__class__
Out[4]:
__main__.Thingie
In [5]:
if 1:
    thingie.otherStuff = 456
else:
    thingie.__dict__['otherStuff'] = 456
print(thingie.otherStuff)
456
In [6]:
thing2 = thingie.__class__()
thing2
Out[6]:
<__main__.Thingie at 0x7fe9f4d5ed90>
In [7]:
dir(thingie.__class__)
Out[7]:
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__']
In [8]:
thingie.name
Out[8]:
'I Dunno'
In [9]:
thing1 = Thingie()
thing1.name = "I dunno, either"
thing1.name
Out[9]:
'I dunno, either'

(Using the __dict__ attribute to assign values is not Pythonic.)

Instance Initialization: The __init__() Method

  • first argument to this and all other instance methods is the new object being created

  • this argument is Pythonically called self

    • similar to C++'s this, except that

      • it's not a pointer

      • it's never implied

In [10]:
class Person:

    def __init__(self, name):
        self.name = name
        self.parkingSpot = None

bob = Person("Bob")
print(bob.name)
print(bob.parkingSpot)
Bob
None
In [11]:
class Person:

    def __init__(self, name=None, parkingSpot=None):
        self.name = name
        self.parkingSpot = parkingSpot

bob = Person()
print(bob.name)
print(bob.parkingSpot)
None
None
  • general rule: __init__() should just set attributes

    • good idea: set all attributes, if only to None

Methods are nested inside the class definition.

  • each class instance has its own name space

  • To do something substantial like parse a file into a new object, use a class method (below).

Defining Attributes

  • instance attributes (you're used to these)
    object.attribute
  • assigning to an attribute adds to the object's namespace

  • you can do this anywhere

In [12]:
class Box:
    """
    Instances of this class are rectangular boxes.
    """
    def __init__(self, w, h):
        """
        w: width of the box
        h: height of the box
        """
        self.w = w
        self.h = h

    def area(self):
        """
        returns the area of the Box
        """
        return self.w * self.h
In [13]:
from pprint import pprint

print('for a Box b...')
b = Box(10,23)
print('box instance:', b)
print('  hex(id(b)):', hex(id(b)))
for a Box b...
box instance: <__main__.Box object at 0x7fe9f452c400>
  hex(id(b)): 0x7fe9f452c400
In [14]:
Box.__doc__
Out[14]:
'\n    Instances of this class are rectangular boxes.\n    '
In [15]:
print('b\'s attributes and methods:')
print('   b.area():', b.area())
b's attributes and methods:
   b.area(): 230
In [16]:
help(Box)
Help on class Box in module __main__:

class Box(builtins.object)
 |  Box(w, h)
 |  
 |  Instances of this class are rectangular boxes.
 |  
 |  Methods defined here:
 |  
 |  __init__(self, w, h)
 |      w: width of the box
 |      h: height of the box
 |  
 |  area(self)
 |      returns the area of the Box
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

In [17]:
help(b)
Help on Box in module __main__ object:

class Box(builtins.object)
 |  Box(w, h)
 |  
 |  Instances of this class are rectangular boxes.
 |  
 |  Methods defined here:
 |  
 |  __init__(self, w, h)
 |      w: width of the box
 |      h: height of the box
 |  
 |  area(self)
 |      returns the area of the Box
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

In [18]:
print('   dir(Box):')
pprint(dir(Box))
   dir(Box):
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'area']
In [19]:
print('     dir(b):')
pprint(dir(b))
     dir(b):
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'area',
 'h',
 'w']
In [20]:
Box.__class__
Out[20]:
type
In [21]:
b.__class__
Out[21]:
__main__.Box

access '__' entities normally. For example:

In [22]:
Box.__doc__
Out[22]:
'\n    Instances of this class are rectangular boxes.\n    '
In [23]:
b.__class__.__doc__
Out[23]:
'\n    Instances of this class are rectangular boxes.\n    '

A More Elaborate Example: geom2d

This package implements a simple 2D object demo, mainly computing areas.

Class Attributes

  • defined in class defn
  • referenced using class name
    Foo.classAttributeName

Hidden Methods and Attributes

  • not intended to be accessed outside the class

  • begin with __ (double underscores) but do not end with them (why?)

  • Python "mangles" the name

In [24]:
class Box:
    """
    Instances of this class are rectangular boxes.
    """
    __all = [] # hidden class variable

    def __init__(self, w, h):
        """
        constructor
        """
        self.w = w
        self.h = h
        Box.__all.append(self)

    @classmethod
    def every(cls):
        return cls.__all

    def area(self):
        """
        returns the area of the Box
        """
        return self.w * self.h

print('for a Box b...')
b = Box(10,23)
b = Box(1,23)
b = Box(10,24)
b = Box(11,2)
print('b\'s attributes and methods:')
print('   b.area():', b.area())
for a Box b...
b's attributes and methods:
   b.area(): 22
In [25]:
print('  Box.all:')
pprint(Box.every())
b2 = Box.every()[1]
  Box.all:
[<__main__.Box object at 0x7fe9f55c18b0>,
 <__main__.Box object at 0x7fe9fba99430>,
 <__main__.Box object at 0x7fe9f452c400>,
 <__main__.Box object at 0x7fe9f55cff40>]
In [26]:
b2.area()
Out[26]:
23
In [27]:
print('  dir(Box):', dir(Box))
  dir(Box): ['_Box__all', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'area', 'every']
In [28]:
for box in Box.every():
    print(box.area())
230
23
240
22
In [29]:
b2.w
Out[29]:
1
In [30]:
#b2.width
In [31]:
b2.width = 19

Magic Attributes: The Unit Class Example

Here is a basic Units class. All it does is keep the name of the units and print in out via the __str__() special method.

In [32]:
class Units:

    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name  # do *not* call print() in __str__()

feet = Units('ft')
print(feet)
ft
In [33]:
str(feet)
Out[33]:
'ft'

But there is no value associated with the units, so the next version attaches a value to the Units and includes it in the printout.

In [34]:
class Units:

    def __init__(self, value, name):
        self.value = value
        self.name  = name

    def __str__(self):
        return str(self.value) + ' ' + self.name

print(Units(12.8, 'ft'))
12.8 ft
In [35]:
print(Units("five", "furlongs"))
five furlongs
In [36]:
print(Units(3, 'm'))
3 m

This version adds the __mul__() special method, to allow Units to be multplied by scalars, but only when the Units is on the left (for now).

In [37]:
class Units:

    def __init__(self, value, name):
        self.name = name
        self.value = value

    def __mul__(self, other):
        # Operator special methods usually return new instances.
        return Units(self.value * other, self.name)

    def __str__(self):
        return str(self.value) + ' ' + self.name


twoFt = Units(2, 'ft')
print(twoFt)
2 ft
In [38]:
# so this works...
print(twoFt*5)
10 ft
In [39]:
# but this doesn't...
if 0:
    print(5*twoFt)

This version adds the __rmul__() special method, to allow scalars to multply Units when the Units is on the right.

In [40]:
class Units:

    def __init__(self, value, name):
        self.name = name
        self.value = value

    def __str__(self):
        return str(self.value) + ' ' + self.name

    def __mul__(self, other):
        return Units(self.value * other, self.name)

    def __rmul__(self, other): # "r" prefix means "self is on the right"
        # Note how we reuse __mul__().
        return self.__mul__(other)

ft = Units(1, 'ft')
print(5.7*ft)
5.7 ft
In [41]:
print(ft*5)
5 ft

Note how we use the constructor within the methods.

Now suppose we want to do 3*ft + 7*ft ... . The next version adds the __add__() special method, which allows us to add two Units (if they have the same names).

In [42]:
class Units:

    def __init__(self, value, name):
        self.name = name
        self.value = value

    def __add__(self, other):
        # We require the same unit name, for now.
        assert self.name == other.name, (self.name, other.name)
        return Units(self.value + other.value, self.name)

    def __mul__(self, other):
        return Units(self.value * other, self.name)

    def __rmul__(self, other):
        return self.__mul__(other)

    def __str__(self):
        return str(self.value) + ' ' + self.name


ft = Units(1, 'ft')
print(5*ft + 3*ft + Units(12,'ft'))
20 ft

This version adds __sub__() and __neg__() special methods, so we can perform more complicated arithmetic with Unitses.

In [43]:
class Units:

    def __init__(self, value, name):
        self.name = name
        self.value = value

    def __add__(self, other):
        assert self.name == other.name
        return Units(self.value + other.value, self.name)

    def __mul__(self, other):
        return Units(self.value * other, self.name)

    def __neg__(self):
        return Units(-self.value, self.name)

    def __rmul__(self, other):
        return self.__mul__(other)

    def __str__(self):
        return str(self.value) + ' ' + self.name

    def __sub__(self, other):
        assert self.name == other.name
        return Units(self.value - other.value, self.name)

print(-Units(10, 'ft'))
-10 ft
In [44]:
ft = Units(1, 'ft')
print(5*ft - 3*ft)
2 ft

Next, we add the __repr__() special method. Notice how this is "lossless".

In [45]:
class Units:

    def __init__(self, value, name):
        self.name = name
        self.value = value

    def __add__(self, other):
        assert self.name == other.name
        return Units(self.value + other.value, self.name)

    def __mul__(self, other):
        return Units(self.value * other, self.name)

    def __negate__(self, other):
        return Units(-self.value, self.name)

    def __repr__(self):
        return 'Units(' + repr(self.value) + ', ' + repr(self.name) + ')'

    def __rmul__(self, other):
        return self.__mul__(other)

    def __str__(self):
        return str(self.value) + ' ' + self.name

    def __sub__(self, other):
        assert self.name == other.name
        return Units(self.value - other.value, self.name)

print(Units(12, 'ft'))
12 ft
In [46]:
ft12 = Units(12, 'ft')
print(repr(ft12))
Units(12, 'ft')
In [47]:
ft12
Out[47]:
Units(12, 'ft')

Sometimes, str() and repr() may return different strings:

In [48]:
aString = "this is a string"
print(aString)
repr(aString)
this is a string
Out[48]:
"'this is a string'"
In [49]:
print(repr(aString))
'this is a string'

Sometimes, they may be the same:

In [50]:
x = 2**0.5
print(str(x))
print(repr(x))
1.4142135623730951
1.4142135623730951

How about dimensions like 3 ft$^2$ and such?

The next version allows us to raise units to powers and multiply them either by scalars or each other.

In [51]:
class Units:

    def __init__(self, value, nameOrNameSeq):
        # arg1 is now a list (of strings), but allow a convenience...
        names = [ nameOrNameSeq ] \
         if isinstance(nameOrNameSeq, str) else nameOrNameSeq
        self.names = sorted(names) # to eliminate ambiguity
        self.value = value

    def __add__(self, other):
        assert self.names == other.names
        return Units(self.value + other.value, self.names)

    def __mul__(self, other):
        # kinda clunky, but we'll clean it up later
        if isinstance(other, Units):
            return Units(self.value * other.value, self.names + other.names)
        else:
            assert self.names
            return Units(self.value * other, self.names)

    def __negate__(self, other):
        return Units(-self.value, self.names)

    def __repr__(self):
        return 'Units(' + repr(self.value) + ', ' + repr(self.names) + ')'

    def __rmul__(self, other):
        return self.__mul__(other)

    def __pow__(self, expo):
        return Units(self.value**expo, expo * self.names)

    def __str__(self):
        return str(self.value) + ' ' + Units.unitPowers(self.names)

    def __sub__(self, other):
        assert self.name == other.name
        return Units(self.value - other.value, self.names)

    @staticmethod
    def unitPowers(seq):
        """convert a list of unit names to a string with exponents, if needed"""
        results = []
        seq = sorted(seq) # just to be on the safe side, need to copy anyway
        while seq:
            unit = seq.pop()
            count = 1
            while seq and seq[0] == unit: # count duplicates
                seq.pop()
                count += 1
            if count > 1: # use exponent only if >1 repeated unit
                results.append("{}**{}".format(unit, count))
            else:
                results.append(unit)
        return " ".join(results)

ft = Units(12, 'ft')
print(ft)
12 ft
In [52]:
x = 4*ft
print(x)
48 ft
In [53]:
print('(' + str(x) + ')**2 =', x**2)
(48 ft)**2 = 2304 ft**2
In [54]:
print('(' + str(x) + ')**3 =', x**3)
(48 ft)**3 = 110592 ft**3
In [55]:
print(x, '*', x, '=', x*x)
48 ft * 48 ft = 2304 ft**2
In [56]:
print(x, '*', x, '*', x, '=', x*x*x)
48 ft * 48 ft * 48 ft = 110592 ft**3

The next version allows us to convert values from one unit to another with the in_() method.

In [57]:
class Units:

    def __init__(self, value, arg1):
        # arg1 is now a list (of strings), but allow a convenience...
        names = [ arg1 ] if isinstance(arg1, str) else arg1
        self.names = sorted(names) # to eliminate ambiguity
        self.value = value

    def __add__(self, other):
        other = other.in_(self.names)
        return Units(self.value + other.value, self.names)

    def __mul__(self, other):
        # kinda clunky, but we'll clean it up later
        if isinstance(other, Units):
            return Units(self.value * other.value, self.names + other.names)
        else:
            return Units(self.value * other, self.names)

    def __negate__(self, other):
        return Units(-self.value, self.names)

    def __repr__(self):
        return 'Units(' + repr(self.value) + ', ' + repr(self.names) + ')'

    def __rmul__(self, other):
        return self.__mul__(other)

    def __pow__(self, expo):
        return Units(self.value**expo, expo * self.names)

    def __str__(self):
        return str(self.value) + ' ' + Units.unitPowers(self.names)

    def __sub__(self, other):
        other = other.in_(self.names)
        return Units(self.value - other.value, self.names)

    def in_(self, names):
        names = [ names ] if isinstance(names, str) else names
        conversions = {
            ('ft', 'in'): 12,
            ('ft', 'cm'): 12 * 2.54,
            ('m',  'cm'): 100,
            ('m',  'in'): 100 * 2.54,
            ('ft', 'm' ): 12 * 2.54 * 0.01,
            ('yd', 'ft'): 3,
            ('in', 'cm'): 2.54,
            ('yd', 'in'): 3 * 12,
            ('yd', 'cm'): 3 * 12 * 2.54,
            # You get the idea...
            }
        factor = 1
        assert len(self.names) == len(names)
        for (nameFrom, nameTo) in zip(self.names, names):
            if (nameFrom, nameTo) in conversions:
                factor *= conversions[nameFrom, nameTo]
            elif (nameTo, nameFrom) in conversions:
                factor *= 1 / conversions[nameTo, nameFrom]
            else:
                raise ValueError(nameTo, nameFrom)
        return Units(self.value * factor, names)

    @staticmethod
    def unitPowers(seq):
        """convert a list of unit names to a string with exponents, if needed"""
        results = []
        seq = sorted(seq) # just to be on the safe side, need to copy anyway
        while seq:
            unit = seq.pop()
            count = 1
            while seq and seq[0] == unit: # count duplicates
                seq.pop()
                count += 1
            if count > 1: # use exponent only if >1 repeated unit
                results.append("{}**{}".format(unit, count))
            else:
                results.append(unit)
        return " ".join(results)


q = Units(12, 'ft')
x = 4*q
print(x, 'is', x.in_('yd'))
48 ft is 16.0 yd
In [58]:
y = x.in_('cm')
z = y.in_('ft')
print(y, 'is', z)
1463.04 cm is 48.0 ft
In [59]:
print(y, '+', z, 'is', y + z)
1463.04 cm + 48.0 ft is 2926.08 cm
In [60]:
print(z, '-', y, 'is', z - y)
48.0 ft - 1463.04 cm is 0.0 ft

To allow Python to sort Units, all we need is the __lt__() (i.e. "$<$") magic method. (We still need __eq__() to compare Units for equality.)

In [61]:
class Units:

    def __init__(self, value, arg1):
        # arg1 is now a list (of strings), but allow a convenience...
        names = [ arg1 ] if isinstance(arg1, str) else arg1
        self.names = sorted(names) # to eliminate ambiguity
        self.value = value

    def __add__(self, other):
        other = other.in_(self.names)
        return Units(self.value + other.value, self.names)

    def __lt__(self, other):
        other = other.in_(self.names)
        return self.value < other.value

    def __eq__(self, other):
        other = other.in_(self.names)
        return self.value == other.value

    def __mul__(self, other):
        # kinda clunky, but we'll clean it up later
        if isinstance(other, Units):
            return Units(self.value * other.value, self.names + other.names)
        else:
            return Units(self.value * other, self.names)

    def __negate__(self, other):
        return Units(-self.value, self.names)

    def __repr__(self):
        return 'Units(' + repr(self.value) + ', ' + repr(self.names) + ')'

    def __rmul__(self, other):
        return self.__mul__(other)

    def __pow__(self, expo):
        return Units(self.value**expo, expo * self.names)

    def __str__(self):
        return str(self.value) + ' ' + Units.unitPowers(self.names)

    def __sub__(self, other):
        other = other.in_(self.names)
        return Units(self.value - other.value, self.names)

    def in_(self, names):
        names = [ names ] if isinstance(names, str) else names
        conversions = {
            ('ft', 'in'): 12,
            ('ft', 'cm'): 12 * 2.54,
            ('m',  'cm'): 100,
            ('m',  'in'): 100 * 2.54,
            ('ft', 'm' ):  12 * 2.54 * 0.01,
            ('yd', 'ft'): 3,
            ('in', 'cm'): 2.54,
            ('yd', 'in'): 3 * 12,
            ('yd', 'cm'): 3 * 12 * 2.54,
            # You get the idea...
            }
        factor = 1
        for (nameFrom, nameTo) in zip(self.names, names):
            if nameFrom == nameTo:
                pass # no conversion needed
            elif (nameFrom, nameTo) in conversions:
                factor *= conversions[nameFrom, nameTo]
            elif (nameTo, nameFrom) in conversions:
                factor *= 1 / conversions[nameTo, nameFrom]
            else:
                raise ValueError(nameTo, nameFrom)
        return Units(self.value * factor, names)

    @staticmethod
    def unitPowers(seq):
        """convert a list of unit names to a string with exponents, if needed"""
        results = []
        seq = sorted(seq) # just to be on the safe side, need to copy anyway
        while seq:
            unit = seq.pop()
            count = 1
            while seq and seq[0] == unit: # count duplicates
                seq.pop()
                count += 1
            if count > 1: # use exponent only if >1 repeated unit
                results.append("{}**{}".format(unit, count))
            else:
                results.append(unit)
        return " ".join(results)


from pprint import pprint

ft = Units(1, 'ft')
cm = Units(1, 'cm')
m = Units(1, 'm')
in_ = Units(1, 'in')
ft3 = 3*ft
ft4 = 4*ft
cm4 = 4*cm
in12 = 12*in_
ft1 = 1*ft
In [62]:
print(f'{ft3} < {ft4}? {ft3 < ft4}')
3 ft < 4 ft? True
In [63]:
print(f'{ft4} < {m}? {ft4 < m}')
4 ft < 1 m? False
In [64]:
print(f'{ft3} < {cm4}? {cm4}')
3 ft < 4 cm? 4 cm
In [65]:
print(f'{ft3} == {cm4}? {ft3 == cm4}')
3 ft == 4 cm? False
In [66]:
print(f'{ft3} > {cm4}? {ft3 > cm4}')
3 ft > 4 cm? True
In [67]:
print(f'{ft1} == {in12}? {ft1 == in12}')
1 ft == 12 in? True
In [68]:
lst = [m, cm, ft, in_, ft3, ft4, cm4, in12, ft1]
print('lst:')
pprint(lst)
lst:
[Units(1, ['m']),
 Units(1, ['cm']),
 Units(1, ['ft']),
 Units(1, ['in']),
 Units(3, ['ft']),
 Units(4, ['ft']),
 Units(4, ['cm']),
 Units(12, ['in']),
 Units(1, ['ft'])]
In [69]:
print('sorted(lst):')
pprint(sorted(lst))
sorted(lst):
[Units(1, ['cm']),
 Units(1, ['in']),
 Units(4, ['cm']),
 Units(1, ['ft']),
 Units(12, ['in']),
 Units(1, ['ft']),
 Units(3, ['ft']),
 Units(1, ['m']),
 Units(4, ['ft'])]

Left to the reader: Allow Units to have a denominator (e.g. "m/s") and allow division.

Inheritance

  • specified in class definition
  • parent constructor must be called explicitly if you want it (and you usually do)
  • think about when you want to call it.

The super() Function: The geom2d Module

super() lets an inheriting class refer to its parent without giving the name explicitly.

In [70]:
from math import sqrt, pi

class Object2D:
    """base 2D object"""
    def describe(self):
        """prints a description of an object"""
        print(self.name, "of area", self.area())

class Circle(Object2D):
    name = "circle" # class variable (or attribute)
    def __init__(self, r):
        super().__init__()
        self.r = r
    def area(self):
        return pi * self.r**2

class Triangle(Object2D):
    name = "triangle" # sets a class attribute
    def __init__(self, a, b, c):
        super().__init__()
        self.a = a # sets an instance attribute 
        self.b = b
        self.c = c
    def area(self):
        s = 0.5 * (self.a + self.b + self.c) # Heron's formula
        return sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))

class RightTriangle(Triangle):
    name = "right triangle"
    def __init__(self, base, height):
        super().__init__(base, height, (base**2+height**2)**0.5)
        self.base = base
        self.height = height
    def area(self):
        return 0.5 * self.base * self.height

class Rectangle(Object2D):
    name = "rectangle"
    def __init__(self, w, h):
        super().__init__()
        self.w = w
        self.h = h
    def area(self):
        return self.w * self.h    

class Square(Rectangle):
    name = "square"
    def __init__(self, w):
        super().__init__(w, w)
In [71]:
rect = Rectangle(3, 4)
rect.describe()
rectangle of area 12
In [72]:
sqr = Square(3)
circ = Circle(8)

for (i,obj) in enumerate((rect, sqr, circ)):
    print()
    print('item {}:'.format(i))
    obj.describe()
item 0:
rectangle of area 12

item 1:
square of area 9

item 2:
circle of area 201.06192982974676

Another Example: The interval Module

In [73]:
ident_iaddsub = [0, 0]

ident_imuldiv = [1, 1]

def add(x, y):
    # what if x[0] > x[1]?
    # what if x or y is a scalar?
    return [ x[0]+y[0], x[1]+y[1] ]

def sub(x, y):
    return [ x[0]-y[0], x[1]-y[1] ]

def mul(x, y):
    extrema = [x[0]*y[0], x[0]*y[1], x[1]*y[0], x[1]*y[1]]
    return [min(extrema), max(extrema)]

def div(x, y):
    # we need to check if b includes 0
    if y[0] <= 0 <= y[1]:
        print("interval divide by 0")
    extrema = [x[0]/y[0], x[0]/y[1], x[1]/y[0], x[1]/y[1]]
    return [min(extrema), max(extrema)]

a = [ 0.1, 0.25 ]
b = [ 0.125, 0.185 ]
print('Test of the interval arithmetic module:')
print('                   a =', a)
print('                   b =', b)
print('            add(a,b) =', add(a,b))
print('add(a,ident_iaddsub) =', add(a,ident_iaddsub))
print('            sub(a,b) =', sub(a,b))
print('            mul(a,b) =', mul(a,b))
print('mul(a,ident_imuldiv) =', mul(a,ident_imuldiv))
print('            div(a,b) =', div(a,b))
Test of the interval arithmetic module:
                   a = [0.1, 0.25]
                   b = [0.125, 0.185]
            add(a,b) = [0.225, 0.435]
add(a,ident_iaddsub) = [0.1, 0.25]
            sub(a,b) = [-0.024999999999999994, 0.065]
            mul(a,b) = [0.0125, 0.04625]
mul(a,ident_imuldiv) = [0.1, 0.25]
            div(a,b) = [0.5405405405405406, 2.0]

Let's create a class Interval to do interval arithmetic in Python.

In [74]:
class Interval:

    def __init__(self, min=0, max=None):
        if max == None:
            max = min
        self.min = min
        self.max = max

    def __add__(self, y):
        # what if self.min > self.max?
        # what if self or y is a scalar?
        return Interval(self.min+y.min, self.max+y.max)

    def __sub__(self, y):
        return Interval(self.min-y.min, self.max-y.max)

    def __mul__(self, y):
        extrema = [self.min*y.min, self.min*y.max,
                self.max*y.min, self.max*y.max]
        return Interval(min(extrema), max(extrema))

    def __truediv__(self, y):
        # we need to check if b includes 0
        if y.min <= 0 <= y.max:
            print("interval divide by 0")
        extrema = [self.min/y.min, self.min/y.max,
                self.max/y.min, self.max/y.max]
        return Interval(min(extrema), max(extrema))

    def __repr__(self):
        return "Interval({}, {})".format(repr(self.min), repr(self.max))

    def __str__(self):
        return "[{} .. {}]".format(self.min, self.max)

a = Interval(0.1, 0.25)
b = Interval(0.125, 0.185)
c = Interval(3)
print('Test of the interval arithmetic module:')
print('          a =', a)
print('          b =', b)
print('          c =', c)
print('        a+b =', a+b)
print('        a-b =', a-b)
print('        a*b =', a*b)
print('        a/b =', a/b)
print('(a*b)/(a+b) =', (a*b)/(a+b))
Test of the interval arithmetic module:
          a = [0.1 .. 0.25]
          b = [0.125 .. 0.185]
          c = [3 .. 3]
        a+b = [0.225 .. 0.435]
        a-b = [-0.024999999999999994 .. 0.065]
        a*b = [0.0125 .. 0.04625]
        a/b = [0.5405405405405406 .. 2.0]
(a*b)/(a+b) = [0.02873563218390805 .. 0.20555555555555555]

Let's modify this so that we can mix Intervals with scalars to some degree.

In [75]:
class Interval:

    def __init__(self, min=0, max=None):
        if max is None:
            max = min
        self.min = min
        self.max = max

    def __add__(self, y):
        "called for <interval> + <anything>"
        if not isinstance(y, Interval):
            y = Interval(y) # attempt coercion
        return Interval(self.min+y.min, self.max+y.max)

    def __radd__(self, y):
        "called for <notAnInterval> + <interval>"
        return self.__add__(y)
        
    def __sub__(self, y):
        if not isinstance(y, Interval):
            y = Interval(y) # attempt coercion
        return Interval(self.min-y.min, self.max-y.max)

    def __mul__(self, y):
        if not isinstance(y, Interval):
            y = Interval(y) # attempt coercion
        extrema = [self.min*y.min, self.min*y.max,
                self.max*y.min, self.max*y.max]
        return Interval(min(extrema), max(extrema))

    def __pow__(self, expo):
        if expo > 0:
            return Interval(self.min**expo, self.max**expo)
        else:
            # use division to do the negative power, in case of zero-spanning
            return Interval(1,1)/Interval(self.min**(-expo), self.max**(-expo))

    def __rmul__(self, y):
        return self.__mul__(y)
        
    def __truediv__(self, y):
        if not isinstance(y, Interval):
            y = Interval(y) # attempt coercion
        # we need to check if b includes 0
        if y.min <= 0 <= y.max:
            print("interval divide by 0")
        extrema = [self.min/y.min, self.min/y.max,
                self.max/y.min, self.max/y.max]
        return Interval(min(extrema), max(extrema))

    def __repr__(self):
        return "Interval({}, {})".format(self.min, self.max)

    def __str__(self):
        return "[{} .. {}]".format(self.min, self.max)

a = Interval(0.1, 0.25)
b = Interval(0.125, 0.185)
c = Interval(3)
print('Test of the interval arithmetic module:')
print('          a =', a)
print('          b =', b)
print('          c =', c)
print('        a+b =', a+b)
print('        a-b =', a-b)
print('        a*b =', a*b)
print('        a/b =', a/b)
print('      5*a/b =', 5*a/b)
print('       b**3 =', b**3)
print('      b**-3 =', b**-3)
print('(a*b)/(a+b) =', (a*b)/(a+b))
Test of the interval arithmetic module:
          a = [0.1 .. 0.25]
          b = [0.125 .. 0.185]
          c = [3 .. 3]
        a+b = [0.225 .. 0.435]
        a-b = [-0.024999999999999994 .. 0.065]
        a*b = [0.0125 .. 0.04625]
        a/b = [0.5405405405405406 .. 2.0]
      5*a/b = [2.7027027027027026 .. 10.0]
       b**3 = [0.001953125 .. 0.006331625]
      b**-3 = [157.9373383610053 .. 512.0]
(a*b)/(a+b) = [0.02873563218390805 .. 0.20555555555555555]

What happens if we try to combine interval with geom2d?

In [76]:
circ = Circle(Interval(10,20))
circ.describe()
circle of area [314.1592653589793 .. 1256.6370614359173]
In [77]:
sqr = Square(Interval(7,9))
sqr.describe()
square of area [49 .. 81]

And if we try to combine interval with geom2d and units?

In [78]:
a = Units(10, 'ft')
b = Units(20, 'ft')
circ = Circle(Interval(a, b))
circ.describe()
circle of area [314.1592653589793 ft**2 .. 1256.6370614359173 ft**2]
In [79]:
sqr = Square(Interval(2*a, 3.5*b))
sqr.describe()
square of area [400 ft**2 .. 4900.0 ft**2]

Class-Bound Methods

Before we talk about these, here's a helper function (which you may find useful elsewhere). This will print all attributes of any Python object. This is a prime example of Python's introspective features.

In [80]:
def dumpObject(obj):
    """
    Print all attributes of `obj` (mainly for debugging).  (This only
    works if `obj` *has* a __dict__ attribute.)
    """
    for key in sorted(obj.__dict__):
        print("{:>20}: {!r}".format(key, obj.__dict__[key]))

print('Units class attributes:')
dumpObject(Units)
Units class attributes:
             __add__: <function Units.__add__ at 0x7fe9f44e3a60>
            __dict__: <attribute '__dict__' of 'Units' objects>
             __doc__: None
              __eq__: <function Units.__eq__ at 0x7fe9f44e35e0>
            __hash__: None
            __init__: <function Units.__init__ at 0x7fe9f44e3af0>
              __lt__: <function Units.__lt__ at 0x7fe9f44e3550>
          __module__: '__main__'
             __mul__: <function Units.__mul__ at 0x7fe9f44e3670>
          __negate__: <function Units.__negate__ at 0x7fe9f44e3700>
             __pow__: <function Units.__pow__ at 0x7fe9f44e3790>
            __repr__: <function Units.__repr__ at 0x7fe9f44e3430>
            __rmul__: <function Units.__rmul__ at 0x7fe9f44e34c0>
             __str__: <function Units.__str__ at 0x7fe9f44e3820>
             __sub__: <function Units.__sub__ at 0x7fe9f44e38b0>
         __weakref__: <attribute '__weakref__' of 'Units' objects>
                 in_: <function Units.in_ at 0x7fe9f44e3b80>
          unitPowers: <staticmethod object at 0x7fe9f44e8af0>
In [81]:
print('Units instance attributes:')
dumpObject(Units(1, 'ft'))
Units instance attributes:
               names: ['ft']
               value: 1

Mind you, it doesn't work all the time. With builtins, for instance:

In [82]:
if 0:
    print('abs function:')
    print(abs.__doc__)
    dumpObject(abs)

Aside: The help() function works because it depends on __doc__, not __dict__.

In [83]:
help(abs)
Help on built-in function abs in module builtins:

abs(x, /)
    Return the absolute value of the argument.

Left to the reader: Fix dumpObject() so that it doesn't complain quite so much.

Class Methods: The classmethod Class Decorator

  • method applies to the whole class

  • first argument is the class

  • first argument is Pythonically named cls (why not class?)

  • especially useful to parse a class instance from a file or input stream

    • the class method can then call the constructor as cls()

Static Methods: The staticmethod Decorator

  • like a class method, but first argument is neither the class nor an instance

  • good way to add a utility function to the class's namespace

Here's how class and static methods used to be declared. (Still supported, but deprecated.)

In [84]:
class MyClass:

    def anInstanceMethod(self):
        # normal instance function
        self.__stat_method()
        return self

    def __stat_method():
        # static method (no "self" involved)
        print('__stat_method called')
    aStaticMethod = staticmethod(__stat_method)

    def __cls_method(cls):
        # class method (act on class, not instance)
        print('__cls_method called')
    aClassMethod = classmethod(__cls_method)

inst = MyClass()

dumpObject(MyClass)
_MyClass__cls_method: <function MyClass.__cls_method at 0x7fe9f44de280>
_MyClass__stat_method: <function MyClass.__stat_method at 0x7fe9f44de0d0>
            __dict__: <attribute '__dict__' of 'MyClass' objects>
             __doc__: None
          __module__: '__main__'
         __weakref__: <attribute '__weakref__' of 'MyClass' objects>
        aClassMethod: <classmethod object at 0x7fe9f44e8f10>
       aStaticMethod: <staticmethod object at 0x7fe9f44e81f0>
    anInstanceMethod: <function MyClass.anInstanceMethod at 0x7fe9f44de1f0>
In [85]:
inst.aStaticMethod()
__stat_method called
In [86]:
inst.aClassMethod()
__cls_method called

It's more Pythonic now to use decorators. The reason for this is another interesting consideration in language design. Hence...

In [87]:
class MyClass:

    count = 0

    def __repr__(self):
        return "I am an instance of 'MyClass'"

    @staticmethod
    def aStaticMethod():
        # static method (no "self" involved)
        print('aStaticMethod() called')

    def anInstanceMethod(self):
        # normal instance function
        self.aStaticMethod()
        print('anInstanceMethod() called')

    @classmethod
    def aClassMethod(cls):
        # class method (act on class, not instance)
        print('aClassMethod() called with cls.__name__: ' + cls.__name__)

inst = MyClass()
print(inst.__dict__)
{}
In [88]:
dumpObject(inst)
In [89]:
dir(inst)
Out[89]:
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'aClassMethod',
 'aStaticMethod',
 'anInstanceMethod',
 'count']

Here's a "normal" instance invocation.

In [90]:
inst.anInstanceMethod()
aStaticMethod() called
anInstanceMethod() called

The class method can be invoked through an instance ...

In [91]:
inst.aClassMethod()
aClassMethod() called with cls.__name__: MyClass

... or through the class itself.

In [92]:
MyClass.aClassMethod()
aClassMethod() called with cls.__name__: MyClass

Likewise for static methods:

In [93]:
inst.aStaticMethod()
aStaticMethod() called
In [94]:
MyClass.aStaticMethod()
aStaticMethod() called

Enhancing Instance Creation: The __new__() Method

Before calling the __init__() method, Python calls an internal class method __new__() to allocate memory (think C++'s new). Like __init__(), you can override it. Here's an example of a class that only allows a single instance.

In [95]:
class OnlyOne:
    def __new__(cls, *args, **kwargs):
        if '__singletonInstance__' in cls.__dict__:
            raise ValueError("There ain't room in this code fer both of us!")
        cls.__singletonInstance__ = object.__new__(cls)
        return cls.__singletonInstance__

first = OnlyOne()
print("So far, so good.")
print(first)
So far, so good.
<__main__.OnlyOne object at 0x7fe9f44dd280>
In [ ]:
 
In [96]:
if 0:
    second = OnlyOne()
In [97]:
# We can use OnlyOne as a "mixin" class.
class Foo(OnlyOne):
    pass

foo0 = Foo()
In [98]:
foo1 = Foo()
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-98-250e884b3a16> in <module>
----> 1 foo1 = Foo()

<ipython-input-95-dd78553d7057> in __new__(cls, *args, **kwargs)
      2     def __new__(cls, *args, **kwargs):
      3         if '__singletonInstance__' in cls.__dict__:
----> 4             raise ValueError("There ain't room in this code fer both of us!")
      5         cls.__singletonInstance__ = object.__new__(cls)
      6         return cls.__singletonInstance__

ValueError: There ain't room in this code fer both of us!

Magic Methods

Special Operator Methods

Just about every operator in Python has a special method associated with it. See Table 10-4 in the textbook for a list.

Rules:

  • Unary "op obj" calls "objClass.__opName__(obj)"

  • Binary "obj0 op obj1" calls

    • "obj0Class.__opName__(obj0, obj1)"

      if "obj0Class.__opName__()" exists, else

    • "obj1Class.__ropName__(obj1, obj0)"

      if "obj1Class.__ropName__()" exists, else

    Otherwise, it raises an exception.

  • There are quite a few ("rich") comparison operators.

Proxy Function Methods

These are for built-in functions like abs(), floor(), len(), and the like. See Table 10-5. The math module also invokes them.

Rule:

  • "funcName(obj)" calls "objClass.__funcName__(obj)"

Special Methods for the Base Classes

There are a series of special methods used by the base classes. Table 10-6 lists them for the Number (pseudo)class and Table 10-7 lists them for the Collection (pseudo)class.

If you have occasion to define your own kind of Number (think: Interval) or Collection, giving it these methods will ensure it works like a base class.

Callable Objects: The __call__() Method

We can overload the () operator, just like C++. We can adapt the interpolator function factory we spoke of earlier to use this syntax.

In [99]:
class LinearFit:

    def __init__(self, x0, y0, x1, y1):
        self.slope = (y1 - y0) / (x1 - x0)
        self.intercept = y0 - self.slope * x0
        
    def __call__(self, t):
        return self.slope*t + self.intercept

    def __str__(self):
        return 'a linear function with slope {} and intercept {}'.format(
            self.slope, self.intercept)

f = LinearFit(1,1, 2,4)
for x in range(-2,6):
    print(x, f(x))
-2 -8.0
-1 -5.0
0 -2.0
1 1.0
2 4.0
3 7.0
4 10.0
5 13.0
In [100]:
print('f is', f)
f is a linear function with slope 3.0 and intercept -2.0

This gives us an ability we didn't have before: A Fit that describes itself.

In [101]:
f.slope
Out[101]:
3.0

Enhanced Attribute Access

The __getattr__() and __setattr__() Methods

Remember that Python does not allow you to do anything about "name = expression". That will always add or replace name in the current (or local or global) namespace, but it does permit you to access "object.attrName = expression" ("set attribute", on the left of an "=") and "object.attrName" when it's part of an expression ("get attribute", perhaps on the right of an "=").

This example shows how you can intercept some Python syntax by overloading certain built-in functions:

In [102]:
class Table:

    def __init__(self, *columnNames):
        self.columnNames = columnNames
        self.rows = []

    def addRow(self, *data):
        assert len(data) == len(self.columnNames)
        row = Row(self, *data)
        self.rows.append(row)
        return row

class Row:

    def __init__(self, table, *data):
        # "self.table = table" won't work: why?
        self.__dict__['table'] = table
        self.__dict__['data'] = list(data)

    def __setattr__(self, name, val):
        """
        Intercept the 'obj.name = val' syntax to set the attribute
        'name' of 'obj' to 'val'.
        """
        assert name in self.table.columnNames
        columnIndex = self.table.columnNames.index(name)
        # "self.data[columnIndex] = val" won't work: why?
        self.__getattribute__('data')[columnIndex] = val

    def __repr__(self):
        # "repr(self.data)" won't work (again)
        return repr(self.__getattribute__('data')) # see below

    def __getattr__(self, name):
        """
        Intercept 'obj.name' when used as part of an expression.
        """
        assert name in self.table.columnNames
        columnIndex = self.table.columnNames.index(name)
        return self.__getattribute__('data')[columnIndex]

table = Table("name", "major")
row0 = table.addRow("Joe", "CptS")
table.addRow("Josi", "Math")
table.addRow("Andre", "EE")

dumpObject(table)
         columnNames: ('name', 'major')
                rows: [['Joe', 'CptS'], ['Josi', 'Math'], ['Andre', 'EE']]

Even though we never set row0's attributes explicitly, we can still access them:

In [103]:
print(row0.name)
Joe
In [104]:
row0.name = "Doug"
dumpObject(table)
         columnNames: ('name', 'major')
                rows: [['Doug', 'CptS'], ['Josi', 'Math'], ['Andre', 'EE']]

Here's another example: The Const class allows us to create constants in Python: values that, once set, cannot be reset.

In [105]:
class Const(OnlyOne):

    class ConstError(TypeError):
        pass

    def __setattr__(self, name, value):
        if name in self.__dict__:
            raise self.ConstError("Can't rebind const ({})".format(name))
        self.__dict__[name] = value

    def __delattr__(self, name):
        if name in self.__dict__:
            raise self.ConstError("Can't unbind const ({})".format(name))
        raise NameError(name)

const = Const()

const.pi = 3.14
const.sqrt2 = 2**0.5
const.e = 2.718
dumpObject(const)
                   e: 2.718
                  pi: 3.14
               sqrt2: 1.4142135623730951
In [106]:
const.pi = 3.15
---------------------------------------------------------------------------
ConstError                                Traceback (most recent call last)
<ipython-input-106-a08db48a1602> in <module>
----> 1 const.pi = 3.15

<ipython-input-105-e3e04decb66a> in __setattr__(self, name, value)
      6     def __setattr__(self, name, value):
      7         if name in self.__dict__:
----> 8             raise self.ConstError("Can't rebind const ({})".format(name))
      9         self.__dict__[name] = value
     10 

ConstError: Can't rebind const (pi)
In [107]:
mypi = const.pi
mypi = 3.0

The __delattr__() Method

Deletes an attribute.

The __getattribute__() Method

__getattribute__() access takes precedence over __getattr__ and can access attributes that aren't in the object's __dict__. We use it above.

Properties: The property() Function

As a more controlled alternative to __getattr__() and __setattr__(), Python lets us define a class attribute (note the syntax) as a 'property' of a class through which we intercept the getting, setting, deletion (rarely used), and documenting of that attribute.

In [112]:
class Knight:

    permittedFavoriteColors = ('red', 'green', 'blue')

    def __init__(self, name):
        self.name = name
        # could set a default favoriteColor here if we had one.

    def getFavoriteColor(self):
        print("(getFavoriteColor called)", end=' ') # so you'll know
        return self._favoriteColor

    def setFavoriteColor(self, color):
        # we can check for illegal colors here
        if color not in Knight.permittedFavoriteColors:
            raise ValueError(
                "'{}' is not allowed as a favorite color".format(color))
        self._favoriteColor = color

    favoriteColor = property(getFavoriteColor, # becomes propName.__get__()
                             setFavoriteColor, # becomes propName.__set__()
                             None,             # becomes propName.__delete__()
                             "The knight's favorite color."
                                               # becomes propName.__doc__
                            )


galahad = Knight('Sir Galahad of Camelot')
print("setting Galahad's favorite color to 'blue' ...")
galahad.favoriteColor = 'blue'
setting Galahad's favorite color to 'blue' ...
In [113]:
print("let's check:")
print("galahad.favoriteColor:", galahad.favoriteColor)
let's check:
(getFavoriteColor called) galahad.favoriteColor: blue
In [115]:
print("Now trying 'yellow'.")
galahad.favoriteColor = 'yellow'
Now trying 'yellow'.
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-115-3d99117c9ae4> in <module>
      1 print("Now trying 'yellow'.")
----> 2 galahad.favoriteColor = 'yellow'

<ipython-input-112-4aec8ca5b1d2> in setFavoriteColor(self, color)
     14         # we can check for illegal colors here
     15         if color not in Knight.permittedFavoriteColors:
---> 16             raise ValueError(
     17                 "'{}' is not allowed as a favorite color".format(color))
     18         self._favoriteColor = color

ValueError: 'yellow' is not allowed as a favorite color

Descriptor Classes: Implementing the __get__(), __set__(), and __delete__() Methods

Descriptor classes provide attributes, methods, and properties to other classes as a kind of non-hierarchical inheritance. Any class that defines __get__(), __set__(), or __delete__() can be a descriptor: Simply use it to instance a class variable. Overloading __call__() effectively lets you use the attribute as a method.

A descriptor instance works just like a property, except that the access methods of the property become the above methods of the class. Effectively, several different classes can all share the same property and access methods.

For more details, see the book.

Slots

This example shows the use of the __slots__ global built-in variable to restrict the assignment of attributes to objects. Note how this restricts Python's usually permissive attribute assignment, which might sometimes be a good idea.

In [117]:
class KnightWithSlots:
    __slots__ = [ 'name', 'quest', 'favouriteColour' ]

class KnightWithoutSlots:
    pass

def tryObject(kn, label):
    print(label + '...')
    kn.name = 'Lancelot'
    kn.quest = 'To seek the Grail.'
    try:
        kn.favoriteColor = 'Yellow'
    except AttributeError:
        print("    Tried to set 'favoriteColor' (US spelling) and failed. -- continuing")
    try:
        dumpObject(kn)
    except AttributeError:
        print("    __slots__ attribute precludes __dict__, so dumpObject() failed")
In [118]:
tryObject(KnightWithoutSlots(), 'knight without slots')
knight without slots...
       favoriteColor: 'Yellow'
                name: 'Lancelot'
               quest: 'To seek the Grail.'
In [119]:
print()
tryObject(KnightWithSlots(), 'knight with slots')
knight with slots...
    Tried to set 'favoriteColor' (US spelling) and failed. -- continuing
    __slots__ attribute precludes __dict__, so dumpObject() failed

Introspection

Introspecting with the Built-in Functions and Attributes

In [ ]:
class Lister:

    def __repr__(self):
        return '\n'.join(self._attrLines())

    def _attrLines(self, nameTag=''):
        if nameTag:
            nameTag += ': '
        result = [ "( {}{}, id: 0x{:08x}".format(
            nameTag, self.__class__.__name__, id(self)) ]
        try:
            for attr in sorted(self.__dict__):
                try:
                    for line in self.__dict__[attr]._attrLines(attr):
                        result.append('  ' + line)
                except AttributeError:
                    # __dict__[attr] has no _attrLines() method
                    value = self.__dict__[attr]
                    # 10 should allow a pretty long attribute name
                    result.append("|{:>10}: {!r}".format(attr, value))
        except AttributeError:
            # self has no __dict__ attribute: treat as "terminal"
            result.append(object.repr(self))
        result.append(')')
        return result
In [121]:
class ChildClass(Lister):

    def __init__(self, val):
        self.val = val
        self.thingie = 99
        #self.func = self.method

    def method(self):
        return True
In [122]:
class ParentClass(Lister):

    def __init__(self, val):
        self.aObj = ChildClass('a')
        self.bObj = ChildClass(('b', 'c'))
        self.cObj = ChildClass(val)
        self.aList = [ 1, 3, 7 ]

m = ParentClass('foo')
m
Out[122]:
( ParentClass, id: 0x7fe9f44dda30
|     aList: [1, 3, 7]
  ( aObj: ChildClass, id: 0x7fe9f44dd940
  |   thingie: 99
  |       val: 'a'
  )
  ( bObj: ChildClass, id: 0x7fe9f44dddf0
  |   thingie: 99
  |       val: ('b', 'c')
  )
  ( cObj: ChildClass, id: 0x7fe9f44dd460
  |   thingie: 99
  |       val: 'foo'
  )
)

We can enhance the geometry code we had before my making it use the Lister mixin.

In [123]:
from math import sqrt, pi

class Object2D(Lister): # inheriting "Lister" is the only change
    """base 2D object"""
    def describe(self):
        """prints a description of an object"""
        print(self.name, "of area", self.area())
    def area(self):
        return "unknown" # "virtual" method

class Circle(Object2D):
    name = "circle"
    def __init__(self, r):
        self.r = r
    def area(self):
        return pi * self.r**2

class Triangle(Object2D):
    name = "triangle" # sets a class attribute
    def __init__(self, a, b, c):
        self.a = a # sets an instance attribute 
        self.b = b
        self.c = c
    def area(self):
        s = 0.5 * (self.a + self.b + self.c)
        return sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))

class RightTriangle(Triangle):
    name = "right triangle"
    def __init__(self, b, h):
        self.b = b
        self.h = h
    def area(self):
        return 0.5 * self.b * self.h

class Rectangle(Object2D):
    name = "rectangle"
    def __init__(self, w, h):
        self.w = w
        self.h = h
    def area(self):
        return self.w * self.h    

class Square(Rectangle):
    name = "square"
    def __init__(self, w):
        # contrast the following with
        # self = Rectangle(w, w)
        Rectangle.__init__(self, w, w) # lazy

rect = Rectangle(3, 4)
sqr = Square(3)
circ = Circle(8)

for (i,obj) in enumerate((rect, sqr, circ)):
    print()
    print('item %d:' % i)
    print(obj)
item 0:
( Rectangle, id: 0x7fe9f4546c10
|         h: 4
|         w: 3
)

item 1:
( Square, id: 0x7fe9f4546160
|         h: 3
|         w: 3
)

item 2:
( Circle, id: 0x7fe9f452c130
|         r: 8
)

There's an inspect module that can do even more.

Enhancing Object Representations

The __repr__() Method

The __str__() Method

We talked about these before.

Enhancing Comparisons

Rich Comparison Methods

The bool Method

Enhancing Containers

Accessing Container Contents: The __getitem__(), __setitem__(), and__delitem__() Special Methods

Querying Containers: The __contains__() and __len__() Methods

Iterating Over Containers

Customizing Mappings: The __hash__() Method

See the text.

Customizing Built-in Classes

It's okay to extend existing classes:

In [ ]:
class EnhancedSet(set):

    def __mul__(self, other): # performs a Cartesian product
        return EnhancedSet((left, right)
                    for left in self
                        for right in other)

set0 = EnhancedSet((1,2,3))
print(set0)
In [ ]:
set1 = EnhancedSet(('alpha', 'beta', 'gamma', 3.7))
print(set1)
In [ ]:
set0.union(set1)
In [ ]:
set0 * set1

Metaclasses

Defer to book.

Object-Oriented Decorators

Defer to book.