from IPython.display import HTML
HTML(open("notes.css", "r").read())
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
class ClassName[(*BaseClasses)]:
[ classDocString ]
suite
class Thingie:
pass
thingie = Thingie()
thingie.name = "I Dunno"
thingie.otherStuff = 124
print(thingie.name)
# syntax similar to C structs
#print(dir(thingie))
print(thingie.__dict__)
thingie.__class__
if 1:
thingie.otherStuff = 456
else:
thingie.__dict__['otherStuff'] = 456
print(thingie.otherStuff)
thing2 = thingie.__class__()
thing2
dir(thingie.__class__)
thingie.name
thing1 = Thingie()
thing1.name = "I dunno, either"
thing1.name
(Using the __dict__
attribute to assign values is not Pythonic.)
__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
class Person:
def __init__(self, name):
self.name = name
self.parkingSpot = None
bob = Person("Bob")
print(bob.name)
print(bob.parkingSpot)
class Person:
def __init__(self, name=None, parkingSpot=None):
self.name = name
self.parkingSpot = parkingSpot
bob = Person()
print(bob.name)
print(bob.parkingSpot)
general rule: __init__()
should just set attributes
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).
object.attribute
assigning to an attribute adds to the object's namespace
you can do this anywhere
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
from pprint import pprint
print('for a Box b...')
b = Box(10,23)
print('box instance:', b)
print(' hex(id(b)):', hex(id(b)))
Box.__doc__
print('b\'s attributes and methods:')
print(' b.area():', b.area())
help(Box)
help(b)
print(' dir(Box):')
pprint(dir(Box))
print(' dir(b):')
pprint(dir(b))
Box.__class__
b.__class__
access '__'
entities normally. For example:
Box.__doc__
b.__class__.__doc__
geom2d
¶This package implements a simple 2D object demo, mainly computing areas.
not intended to be accessed outside the class
begin with __
(double underscores) but do not end with them (why?)
Python "mangles" the name
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())
print(' Box.all:')
pprint(Box.every())
b2 = Box.every()[1]
b2.area()
print(' dir(Box):', dir(Box))
for box in Box.every():
print(box.area())
b2.w
#b2.width
b2.width = 19
Unit
Class Example¶Here is a basic Unit
s class. All it does is keep the name of the
units and print in out via the __str__()
special method.
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)
str(feet)
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.
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'))
print(Units("five", "furlongs"))
print(Units(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).
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)
# so this works...
print(twoFt*5)
# 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.
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)
print(ft*5)
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).
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'))
This version adds __sub__()
and __neg__()
special methods, so we can
perform more complicated arithmetic with Units
es.
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'))
ft = Units(1, 'ft')
print(5*ft - 3*ft)
Next, we add the __repr__()
special method. Notice how this is "lossless".
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'))
ft12 = Units(12, 'ft')
print(repr(ft12))
ft12
Sometimes, str()
and repr()
may return different strings:
aString = "this is a string"
print(aString)
repr(aString)
print(repr(aString))
Sometimes, they may be the same:
x = 2**0.5
print(str(x))
print(repr(x))
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.
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)
x = 4*ft
print(x)
print('(' + str(x) + ')**2 =', x**2)
print('(' + str(x) + ')**3 =', x**3)
print(x, '*', x, '=', x*x)
print(x, '*', x, '*', x, '=', x*x*x)
The next version allows us to convert values from one unit to another
with the in_()
method.
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'))
y = x.in_('cm')
z = y.in_('ft')
print(y, 'is', z)
print(y, '+', z, 'is', y + z)
print(z, '-', y, 'is', z - y)
To allow Python to sort Unit
s, all we need is the __lt__()
(i.e.
"$<$") magic method. (We still need __eq__()
to compare Unit
s for
equality.)
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
print(f'{ft3} < {ft4}? {ft3 < ft4}')
print(f'{ft4} < {m}? {ft4 < m}')
print(f'{ft3} < {cm4}? {cm4}')
print(f'{ft3} == {cm4}? {ft3 == cm4}')
print(f'{ft3} > {cm4}? {ft3 > cm4}')
print(f'{ft1} == {in12}? {ft1 == in12}')
lst = [m, cm, ft, in_, ft3, ft4, cm4, in12, ft1]
print('lst:')
pprint(lst)
print('sorted(lst):')
pprint(sorted(lst))
Left to the reader: Allow Units
to have a denominator (e.g. "m/s
") and allow division.
class
definitionsuper()
Function: The geom2d
Module¶super()
lets an inheriting class refer to its parent without giving the name explicitly.
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)
rect = Rectangle(3, 4)
rect.describe()
sqr = Square(3)
circ = Circle(8)
for (i,obj) in enumerate((rect, sqr, circ)):
print()
print('item {}:'.format(i))
obj.describe()
interval
Module¶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))
Let's create a class Interval
to do interval arithmetic in Python.
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))
Let's modify this so that we can mix Interval
s with scalars to some degree.
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))
What happens if we try to combine interval
with geom2d
?
circ = Circle(Interval(10,20))
circ.describe()
sqr = Square(Interval(7,9))
sqr.describe()
And if we try to combine interval
with geom2d
and units
?
a = Units(10, 'ft')
b = Units(20, 'ft')
circ = Circle(Interval(a, b))
circ.describe()
sqr = Square(Interval(2*a, 3.5*b))
sqr.describe()
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.
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)
print('Units instance attributes:')
dumpObject(Units(1, 'ft'))
Mind you, it doesn't work all the time. With builtins, for instance:
if 0:
print('abs function:')
print(abs.__doc__)
dumpObject(abs)
Aside: The help()
function works because it depends on __doc__
, not __dict__
.
help(abs)
Left to the reader: Fix dumpObject()
so that it doesn't complain quite so much.
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
cls()
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.)
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)
inst.aStaticMethod()
inst.aClassMethod()
It's more Pythonic now to use decorators. The reason for this is another interesting consideration in language design. Hence...
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__)
dumpObject(inst)
dir(inst)
Here's a "normal" instance invocation.
inst.anInstanceMethod()
The class method can be invoked through an instance ...
inst.aClassMethod()
... or through the class itself.
MyClass.aClassMethod()
Likewise for static methods:
inst.aStaticMethod()
MyClass.aStaticMethod()
__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.
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)
if 0:
second = OnlyOne()
# We can use OnlyOne as a "mixin" class.
class Foo(OnlyOne):
pass
foo0 = Foo()
foo1 = Foo()
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.
These are for built-in functions like abs()
, floor()
, len()
, and the like. See Table 10-5. The math
module also invokes them.
Rule:
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.
__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.
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))
print('f is', f)
This gives us an ability we didn't have before: A Fit
that describes itself.
f.slope
__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:
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)
Even though we never set row0
's attributes explicitly, we can still access them:
print(row0.name)
row0.name = "Doug"
dumpObject(table)
Here's another example: The Const
class allows us to create constants in Python: values that, once set, cannot be reset.
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)
const.pi = 3.15
mypi = const.pi
mypi = 3.0
__delattr__()
Method¶Deletes an attribute.
__getattribute__()
Method¶__getattribute__()
access takes precedence over __getattr__
and can access attributes that aren't in the object's __dict__
. We use it above.
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.
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'
print("let's check:")
print("galahad.favoriteColor:", galahad.favoriteColor)
print("Now trying 'yellow'.")
galahad.favoriteColor = 'yellow'
__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.
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.
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")
tryObject(KnightWithoutSlots(), 'knight without slots')
print()
tryObject(KnightWithSlots(), 'knight with slots')
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
class ChildClass(Lister):
def __init__(self, val):
self.val = val
self.thingie = 99
#self.func = self.method
def method(self):
return True
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
We can enhance the geometry code we had before my making it use the Lister
mixin.
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)
There's an inspect
module that can do even more.
__getitem__()
, __setitem__()
, and__delitem__()
Special Methods¶See the text.
It's okay to extend existing classes:
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)
set1 = EnhancedSet(('alpha', 'beta', 'gamma', 3.7))
print(set1)
set0.union(set1)
set0 * set1
Defer to book.
Defer to book.