Designing Python Classes
Transcription
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