Designing Python Classes

Programming with Python
Lecture 6: Designing Python Classes
IPEC Winter School 2015
B-IT
Dr. Tiansi Dong & Dr. Joachim Köhler
Constructor and destructor
●
constructor : __init__
●
destructor : __del__
–
__del__ is called automatically when an instance's memory
space is reclaimed
>>> class Life:
def __init__(self, name='nobody'):
print('hello ', name)
self.name = name
def __del__(self):
print('Goopbye ', self.name)
>>> x = Life('Brian')
Hello Brian
>>> x = Life('Loretta')
hello Loretta
goodbye Brian
Operator Overloading 1
●
Subtraction operation: __sub__
–
__sub__ specifies the subtraction operation
>>> class Number:
def __init__(self, start):
self.data = start
def __sub__(self, other):
return Number(self.data - other)
>>> x = Number(4)
>>> y = x -4
>>> y
<__main__.Number object at 0x0292EA70>
>>> y.data
0
>>> y = 3-x
Traceback (most recent call last):
File "<pyshell#42>", line 1, in <module>
y = 3-x
TypeError: unsupported operand type(s) for -: 'int' and 'Number'
Operator Overloading 2
●
No polymorphism for operator overloading in Python
>>> class Number:
def __init__(self, start):
self.data = start
def __sub__(self, other):
return Number(self.data - other)
def __sub__(other, self):
return Number(other - self.data)
>>> x = Number(4)
>>> y = x-4
Traceback (most recent call last):
File "<pyshell#47>", line 1, in <module>
y = x-4
File "<pyshell#45>", line 7, in __sub__
return Number(other - self.data)
AttributeError: 'int' object has no attribute 'data'
>>> y = 4-x
Traceback (most recent call last):
File "<pyshell#48>", line 1, in <module>
y = 4-x
TypeError: unsupported operand type(s) for -: 'int' and 'Number'
Operator Overloading 3
●
__sub__, __rsub__ and __isub__
–
__sub__ does not support the use of instance appearing on the
right side of the operator
–
__isub__ supports in-place subtraction
class Number:
def __init__(self, start):
self.data = start
def __sub__(self, other):
return Number(self.data - other)
def __rsub__(self, other):
return Number(other - self.data)
def __isub__(self, other):
self.data -=other
return self
>>> x = Number(4)
>>> x
<__main__.Number object at
0x029C7B90>
>>> y = x-4
>>> y.data
0
>>> y = 5-x
>>> y.data
1
>>> x -= 3
>>> x.data
1
>>>
Operator Overloading 4
●
Boolean tests: __bool__and __len__
–
Python first tries __bool__ to obtain a direct Boolean value, if
missing, tries __len__ to determine a truth value from the
object length
–
In Python 2.6 __bool__ is not recognized as a special method
>>> class Truth:
def __bool__(self):
return True
>>> X = Truth()
>>> if X: print('yes')
yes
>>> class Truth0:
def __len__(self):
return 0
>>> X = Truth0()
>>> if not X: print('no')
no
>>> class Truth:
def __bool__(self):
return True
def __len__(self):
return 0
>>> X = Truth()
>>> if X: print('yes')
yes
#Python 3.0 tries __bool__first
#Python 2.6 tries __len__ first
Python 2.6 users should use
__nonzero__ instead of __bool__
Operator Overloading 5
●
Comparisons: __lt__, __gt__ , __le__, __ge__,
__eq__, __ne__
–
For comparisons of <, >, <= ,>=, ==, !=
–
No relations are assumed among these comparison relations
–
__cmp__ used in Python 2.6, removed in Python 3.0
>>> class Comp:
data = “hallo”
def __gt__(self, str):
return self.data > str
def __lt__(self, str):
return self.data < str
>>> X = Comp()
>>> print(X > 'hi')
False
>>> print(X < 'hi')
True
Operator Overloading 6
●
String representation: __repr__ and __str__
–
__str__ is tried first for the print function and the str built-in
function
–
__repr__ is used in all other contexts, as well as for the print
function and the str function when __str__ is not available.
Operator Overloading 7
>>> class adder:
def __init__(self, value=0):
self.data = value
def __add__(self, other):
self.data += other
>>> class addrepr(adder):
def __repr__(self):
return 'addrepr(%s)' % self.data
>>> class addstr(adder):
def __str__(self):
return '[Value: %s]' % self.data
>>> class addboth(adder):
def __str__(self):
return '[Value: %s]' % self.data
def __repr__(self):
return 'addboth(%s)' % self.data
>>> x0 = adder()
>>> print(x0)
<__main__.adder object at 0x0299FE90>
>>> x1 = addrepr(2)
>>> x1
addrepr(2)
>>> str(x1)
'addrepr(2)'
>>> x2 = addstr(4)
>>> x2
<__main__.addstr object at 0x0299FE50>
>>> print(x2)
[Value: 4]
>>> repr(x2)
'<__main__.addstr object at 0x0299FE50>'
>>> x3 = addboth(4)
>>> str(x3)
>>> x3+1
'[Value: 5]'
>>> x3
>>> repr(x3)
addboth(5)
'addboth(5)'
>>> print(x3)
[Value: 5]
Operator Overloading 8
●
Attribute reference: __getattr__ and __setattr__
–
__getattr__ is called automatically for undefined attribute
qualifications
–
__setattr__ intercepts all attribute assignments. If this method is
defined, self.attr = value becomes
self.__setattr__('attr', value)
class Empty:
def __getattr__(self, attrname):
if attrname == "age":
return 40
else:
raise AttributeError(attrname)
def __setattr__(self, attr, value):
if attr=='age':
self.__dict__[attr] = value
##can we write self.attr=value?
else:
raise AttributeError( attr + ' not allowed')
>>> X = Empty()
>>> X.age
40
>>> X.age=4
Operator Overloading 9
●
Call expressions: __call__
–
__call__ is called automatically when an instance is called
–
__call__ can pass any positional or keyword arguments
–
All of the argument-passing modes are supported by __call__
>>> class Prod:
def __init__(self, value):
self.value = value
def __call__(self, other):
return self.value*other
>>> x=Prod(3)
>>> x
<__main__.Prod object at 0x9811f4c>
>>> x(5)
15
Operator Overloading 10
●
Function interfaces and Callback-based code
–
Functions can be registered as event handlers (callbacks)
–
When events occur, these handlers will be called.
class Callback:
def __init__(self, color):
self.color = color
def __call__(self):
print('pressed ', self.color)
>>> cb1 = Callback('blue')
>>> cb2 = Callback('green')
>>> class Button:
def __init__(self):
self.cmd = None
def press(self, cmd):
self.cmd = cmd
self.cmd()
>>> b1 = Button()
>>> b1.press(cb1)
pressed blue
>>> b2 = Button()
>>> b2.press(cb2)
pressed green
For dialog system:
'press' → voice to your eardrum,
'callback' → voice from your mouth
Operator Overloading 11
●
Indexing and Slicing: __getitem__ and __setitem__
–
__getitem__ is called automatically for instance-indexing
X[i] calls X.__getitem__(i)
–
__setitem__ is called for index assignment
>>> class Indexer:
data = [1,2,3,4]
def __getitem__(self, index):
print('get item:', index)
return self.data[index]
def __setitem__(self, index, value):
self.data[index] = value ##can we write self[index]=value?
>>> X = Indexer()
>>> X[0]
get item: 0
1
>>> X[-1]
get item: -1
4
>>>x[3] = -1
Operator Overloading 12
●
Index iteration __getitem__ in the for loop
–
for statement works by repeatedly indexing a sequence from 0 to
higher indexes.
–
If this method is defined, for loop calls the class's __getitem__
>> class stepper:
def __getitem__(self, i):
return self.data[i]
data = "abcdef"
>>> x = stepper()
>>> for item in x:
print(item, end=",")
a,b,c,d,e,f,
Operator Overloading 13
●
Iterator objects: __iter__ and __next__
–
In all iteration contexts, __iter__ will be firsty tried, before
trying __getitem__
–
__iter__ shall return an iterator object, whose __next__
built-in will be repeated called to iterate items until a
StopIteration exception is raised.
Operator Overloading 13 continued
class Squares:
def __init__(self, start, stop):
self.value = start -1
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.value == self.stop:
raise StopIteration
self.value += 1
return self.value **2
>>> for i in Squares(1,6):
print(i, end=" ")
1 4 9 16 25 36
>>>
>>>
>>>
1
>>>
0
>>>
1
>>>
4
>>>
9
>>>
16
>>>
25
>>>
36
>>>
49
X = Squares(-1, -3)
I = iter(X)
next(I)
next(I)
next(I)
next(I)
next(I)
next(I)
next(I)
next(I)
next(I)
Operator Overloading 14
●
Membership: __contains__ , __iter__,
__getitem__
–
In all iteration contexts, __contains__ is perferred over
__iter__, which is perferred over __getitem__
class Iters:
>>> X = Iters([1,2])
– def __init__(self, value):
>>> 3 in X
self.data = value
contains: False
def __getitem__(self, i):
>>> for i in X:
print('get[%s]:' % i, end=' ')
print(i, end='|')
return self.data[i]
def __iter__(self):
iter=> next: 1|next: 2|next:
print('iter=> ', end= ' ')
self.ix = 0
>>> [i*2 for i in X]
return self
iter=> next: next: next: [2, 4]
def __next__(self):
print('next:', end=' ')
if self.ix == len(self.data): raise StopIteration
item = self.data[self.ix]
self.ix += 1
return item
def __contains__(self, x):
print('contains: ', end=' ')
return x in self.data
__contains__ is automatically called by the in built-in
function
Overloading not by Signatures
●
In some programming language, polymorphism also means
overloading with signatures. But not for Python
>>> class C:
def func(self):
print(1)
def func(self, x):
print(2)
>>> x = C()
>>> x.func()
class C:
def func(self, *args):
if len(args)==1:
print(1)
elif len(args) ==2:
print(2)
●
In Python, polymorphism means: X.func() depends on X
Class Inheritance and Class Composition
●
●
Inheritance: Is-a relation
–
A researcher is a kinds of person
–
A graduate-student is a kind of researcher
–
A professor is a kind of researcher
Composition: Has-a relation
class Person:
def __init__(self, name, age)
…
class Researcher(Person):
def __init__(self, research_area)
…
class GraduateStudent(Researcher):
def __init__(self, level)
class Professor(Researcher):
def __init__(self, ranks)
–
A university has several departments
–
A department has several working-groups
–
A working-group has one professor and several graduate-students
class WorkingGroup:
def __init__(self, prof, *students)
…
class Department():
def __init__(self, *groups)
…
class University():
def __init__(self, *departments)
Multiple Inheritance
●
●
●
A class can have more than one super-classes.
When searching for an attribute, Python traverses all superclasses in the class header, from left to right until a match is
found
Depth-first searching or breadth-first searching?
–
In classic classes (until Python 3.0), depth-first searching
is performed
–
In new-style classes (all classes in Python 3.0 and
above), breadth-first searching is performed
Pseudo-private Attribute: “name mangling”
>>> class C1:
def func1(self): self.X = 11
def func2(self): print(self.X)
>>> class C2:
def funca(self): self.X = 22
def funcb(self): print(self.X)
>>> class C3(C1,C2): …
>>> x = C3()
●
What is “mangling”?
–
●
to injure with deep disfiguring wounds by cutting, tearing, or
crushing <people … mangled by sharks — V. G. Heiser>
How does Python do “name mangling”?
–
Class attributes, starting with two underscores and not ending
with two underscores, are automatically prefixed with
underscore and the class name
Pseudoprivate Attribute: “name mangling”
>>> class C1:
def func1(self): self.__X = 11
def func2(self): print(self.__X)
>>> class C2:
def funca(self): self.__X = 22
def funcb(self): print(self.__X)
>>> class C3(C1,C2): pass
>>> x = C3()
>>> print(x.__dict__)
{'_C1__X': 11, '_C2__X': 22}
>>> x.func1()
11
>>> x.funca()
22
Methods are Objects: Bound and Unbound
●
Bound class method: self + function
>>>class C:
def dosomething(self, message):
print(message)
>>>x = C()
>>>obj = x.dosomething
>> obj(“hello world”)
hello world
●
Unbound class method: no self
–
Python 3.0 has dropped the notion of unbound methods,
adopted the notion of simple methods
>>>class D:
def dosimple(message):
print(message)
>>>D.dosimple(“hello world”)
hello world
Class Delegation: “Wrapper” Object
●
An object wraps another object, in order to control, protect, or trace
>>>class Wrapper:
def __init__(self, obj):
self.wrapped = obj
def __getattr__(self, attrname):
print("Trace:", attrname)
return getattr(self.wrapped, attrname)
def __repr__(self):
return "Wrapped in Wrapper: %s" % self.wrapped
>>>x = Wrapper([1,2,3])
>>>x.append(4)
Trace: append
>>>x
Wrapped in Wrapper: [1, 2, 3, 4]
# x.append(4)
# def append(self, x):
pass
Generic Object Factories
●
Classes can be passed as parameters to functions
>>>def factory(aClass, *args):
return aClass(*args)
>>>class C:
def dosomething(self, message):
print(message)
>>>class Person:
def __init__(self, name, job):
self.name = name
self.job = job
>>>obj1 = factory(C)
>>>obj2 = factory(Person, “Bob”, “dev”)
Instance Slots: __slots__
●
The special attribute __slots__, located at the top-level of a class
statement, defines the sequence of string names, only those can be
assigned as instance attributes
>>> class C:
__slots__ = ['name', 'age']
def __init__(self, name):
self.name=name
>>> bob = C('Bob')
>>> bob
<__main__.C object at 0x028734D0>
>>> bob.job = 'dev'
Traceback (most recent call last):
File "<pyshell#13>", line 1, in <module>
bob.job = 'dev'
AttributeError: 'C' object has no attribute 'job'
>>> bob.__dict__
Traceback (most recent call last):
File "<pyshell#14>", line 1, in <module>
bob.__dict__
AttributeError: 'C' object has no attribute '__dict__'
Instance Slots: __slots__
●
●
Without an attribute namespace dictionary __dict__, it is not
possible to assign new names to instances.
To let an instance with slots accept new attributes, we can include
'__dict__' into the __slots__
>>> class D:
__slots__ = ['name', 'age', '__dict__']
>>> d = D()
>>> d.name = 'Bob'
>>> d.age=30
>>> d.job='dev'
>>> d.__slots__
['name', 'age', '__dict__']
>>> d.__dict__
{'job': 'dev'}
>>> for attr in list(d.__dict__) + d.__slots__:
print(attr, '==>', getattr(d,attr))
job ==> dev
name ==> Bob
age ==> 30
__dict__ ==> {'job': 'dev'}
Class Properties
●
<attr>=property(get,set,del,docs) states the access,
assignment, delete, and doc-string functions for a class attribute
<attr>
>>> class A:
def __init__(self):
self._age = 0
def getage(self):
return self._age
def setage(self, value):
print('set age:', value)
self._age = value
age = property(getage, setage, None, None)
>>>
>>>
0
>>>
set
>>>
44
>>>
44
bob = A()
bob.age
bob.age=44
age: 44
bob.age
bob._age
Here we have _age and age two
attributes. Can we just use one?
Like this:
class A:
def getage(self):
return 40
def setage(self, value):
print('set age:',
value)
self.age = value
age = property(getage,
setage,
None,
None)
Static Methods and Class Methods
●
●
Static methods are like simple functions without instance inside a
class, declared with staticmethod
Class methods pass a class instead of an instance, declared with
classmethod
>>> class M:
def ifunc(self,x):
print(self,x)
def sfunc(x):
print(x)
def cfunc(cls,x):
print(cls,x)
>>> obj = M()
>>> obj.ifunc(3)
<__main__.M object at 0x028AD9D0> 3
>>> M.ifunc(obj,2)
<__main__.M object at 0x028AD9D0> 2
>>> M.sfunc(12)
12
>>> obj.sfunc(1239)
>>> M.cfunc(4)
>>> class M:
def ifunc(self,x):
print(self,x)
def sfunc(x):
print(x)
def cfunc(cls,x):
print(cls,x)
sfunc = staticmethod(sfunc)
cfunc = classmethod(cfunc)
>>> obj = M()
>>> obj.ifunc(3)
<__main__.M object at 0x028AD9D0> 3
>>> M.ifunc(obj,2)
<__main__.M object at 0x028AD9D0> 2
>>> M.sfunc(12)
12
>>> obj.sfunc(1239)
1239
>>> M.cfunc(4)
<class '__main__.M'> 4
>>> obj.cfunc(89)
<class '__main__.M'> 89