class EmptyClass(): pass
EmptyClass
and does nothing because of the pass
statement.
Instances of a class are called Python objects. They are created by calling the class as if it were
a function. Here two objects, two instances of class EmptyClass
, are created and inspected with the
print
function:
object1 = EmptyClass() print(object1) object2 = EmptyClass() print(object2)
<__main__.EmptyClass object at 0x11260dad0> <__main__.EmptyClass object at 0x1127f1fd0>
print
function reveals the memory location of the objects, and it is seen that
the two empty objects are located at different positions in the memory. Asking if the two objects are the same:
print('object1 == object2:', object1 == object2)
object1 == object2: False
id_as_string()
is added to
the ClassWithMethod
class:
class ClassWithMethod(): def id_as_string(self): return str(hex(id(self)))
self
, which a reference to the instance of the class. Now, making
one such instance, we may call the method, by adding a .
and the id_as_string()
after the variable name, here object1
, referring to the instance:
object1 = ClassWithMethod() print('object1: ',object1) object1.id_as_string()
object1: <__main__.ClassWithMethod object at 0x11260da50> '0x11260da50'
id_as_string()
method is defined in the class declaration as if it takes an argument,
self
while we do not provide that argument when the method is invoked as in object1.id_as_string()
.
This comes about because Python is providing that argument for us behind the scene. In fact,
object1.id_as_string()
carries enough information to provide the proper value for self
,
namely the object object1
preceding the .id_as_string()
. Thus Python does not
need us to declare that first argument. It is a little funny, but you will get use to it. If you try to give it anyhow:
object1.id_as_string(object1)
TypeError: id_as_string() takes 1 positional argument but 2 were given
object1
that Python
provided from object1.
and the object1
that you gave in .id_as_string(object1)
.
print
function refers to a Python object, it calls
a method called __str__()
on the object.
I.e. the <__main__.ClassWithMethod object at 0x11260da50>
output is the result of the print
function performing a object1.__str__()
evaluation. We may alter that very method in the class definition e.g. like this:
class SelfPrintingClass(): def __str__(self): return 'an instance of the SelfPrintingClass class at ' + str(hex(id(self))) object1 = SelfPrintingClass() print(object1) object2 = SelfPrintingClass() print(object2)
an instance of the SelfPrintingClass class at 0x11281d890 an instance of the SelfPrintingClass class at 0x11260dad0
<__main__.ClassWithMethod object at 0x11260da50>
has gone and our new layout has now been adopted. Albeit we have no data yet, we get a glimpse of
the idea behind object oriented programming. The two variables, object1
and object2
,
soon to contain some data, do know how to present themselves when printed. I.e. we have stowed away
the problem of how some data is printed and our main program can be ignorant about that. In a minute we will
have some real data to print, and this statement may become more clear.
object_name
+ .
+ attribute_name
. Consider
an empty class:
class EmptyClass(): pass
object1 = EmptyClass() object2 = EmptyClass() object1.data = [1, 2] object1.more_data = 'hello' object2.data = [0, 0]
print(object1.data, object1.more_data, object2.data) for object in [object1, object2]: print(' printed in for-loop:',object.data)
[1, 2] hello [0, 0] printed in for-loop: [1, 2] printed in for-loop: [0, 0]
__init__()
, which is called whenever a new object is created, i.e.
whenever a new instance of a class is made. The method takes self
as input followed by any
sequence of arguments, possibly with defaults. It may look like this:
class ClassWithInit(): def __init__(self,tag='no tag'): self.tag = tag def __str__(self): return 'an instance of the ' + self.__class__.__name__ + \ ' class with tag="' + self.tag + '"' object1 = ClassWithInit() # init with no argument, expect 'no tag' print(object1) object2 = ClassWithInit() # init with no argument print(object2) object3 = ClassWithInit('special tag') # init with an argument print(object3)
an instance of the ClassWithInit class with tag="no tag" an instance of the ClassWithInit class with tag="no tag" an instance of the ClassWithInit class with tag="special tag"
__init__()
method. Here, it receives
no argument for the first two objects and one for the last object.
So, the first two objects get created with the default value for the tag, while the
last object gets a tag passed in the creation.
print('object1 == object2:', object1 == object2) print('object1 == object3:', object1 == object3)
object1 == object2: False object1 == object3: False
object1
and object2
, that have
similar attribute values (the default 'no tag'
) are not considered the same.
The situation can be changed by modifying the built-in method, __eq__()
, on the
class like this:
class ClassWithEqual(): def __init__(self,tag='no tag'): self.tag = tag def __str__(self): return 'tag="' + self.tag + '" at: ' + str(hex(id(self))) def __eq__(self, other): if isinstance(other, self.__class__): # they are of the same class, so result depends on the tag return self.tag == other.tag else: # they are not even of the same class, so cannot be equal return False object1 = ClassWithEqual() print(object1) object2 = ClassWithEqual() print(object2) object3 = ClassWithEqual('special tag') print(object3)
tag="no tag" at: 0x112827450 tag="no tag" at: 0x11260d810 tag="special tag" at: 0x11281d890
print('object1 == object2:', object1 == object2) print('object1 == object3:', object1 == object3)
object1 == object2: True object1 == object3: False
Rotor
as one containing and handling lists that contain more than one element. The class has a method, rotate_data()
which makes a cyclic shift of the elements in the list:
class Rotor(): def __init__(self,some_data): assert isinstance(some_data,list) and len(some_data) > 1, \ 'ERROR: invalid data, must be list with len > 1' self.data = some_data def __str__(self): if hasattr(self,'data'): return '-'.join([str(d) for d in self.data]) else: return 'No data yet' def rotate_data(self): if hasattr(self,'data'): self.data = self.data[1:] + [self.data[0]] else: print('WARNING: no data to rotate')
rotor1 = Rotor()
TypeError: __init__() missing 1 required positional argument: 'some_data'
rotor1 = Rotor('no list')
rotor1 = Rotor([25])
AssertionError: ERROR: invalid data, must be list with len > 1
rotor1 = Rotor([3, 5, 1]) print('rotor1: ',rotor1)
rotor1: 3-5-1
rotor1
of the Rotor
class, we may now
call the rotate_data()
method:
print('rotor1: ',rotor1) rotor1.rotate_data() print('rotor1: ',rotor1)
rotor1: 3-5-1 rotor1: 5-1-3
rotor2 = Rotor([2, 4, 1]) print('rotor2: ',rotor2) rotor2.rotate_data() print('rotor2: ',rotor2)
rotor2: 2-4-1 rotor2: 4-1-2
class ClassWithAttribute(): def __init__(self,a,b): self.a = a self.b = b self.average = (a + b)/ 2 def __str__(self): return 'a={}, b={}, average={}'.format(self.a,self.b,self.average)
object1 = ClassWithAttribute(1,3) object2 = ClassWithAttribute(5,7) print(object1) print(object2) print('') object1.average # note: no () since it is a value
a=1, b=3, average=2.0 a=5, b=7, average=6.0 2.0
class ClassWithMethod(): def __init__(self,c,d=7): self.c = c self.d = d def __str__(self): return 'c={}, d={}, average={}'.format(self.c,self.d,self.average()) def average(self): return (self.c + self.d) / 2
object1 = ClassWithMethod(1,3) object2 = ClassWithMethod(5) # note: 7 will be assumed for d print(object1) print(object2) print('') object1.average() # note: () since it is a method
c=1, d=3, average=2.0 c=5, d=7, average=6.0 2.0
()
must be given on .average
since it is now a method (= a function) rather than just an attribute.
()
, by
introducing the decorator @property
:
class ClassWithPropertyDecoratedMethod(): def __init__(self,e,f=None): self.e = e if f is not None: self.f = f else: # assume same value twice self.f = e def __str__(self): return 'e={}, f={}, average={}'.format(self.e,self.f,self.average) @property def average(self): return (self.e + self.f) / 2
object1 = ClassWithPropertyDecoratedMethod(1,3) object2 = ClassWithPropertyDecoratedMethod(5) # note: same value will be assumed for f print(object1) print(object2) print('') object1.average # note: no () since it is a property-decorated method
e=1, f=3, average=2.0 e=5, f=5, average=5.0 2.0
ClassWithAttribute
class
there is one problem. Consider we have an instance of the class:
object = ClassWithAttribute(1,3) print(object)
a=1, b=3, average=2.0
.a
or .b
on the object, the
.average
attribute is not recalculated and will be wrong:
object.a = 100 print(object)
a=100, b=3, average=2.0
ClassWithMethod
and
ClassWithPropertyDecoratedMethod
,
but for these classes, the average value is recalculated every time it
is accessed, which might be a waste.
.g
into
._g
. Then one would define a
property-decorated function .g
that returns the value
of the hidden attribute. This functions is called a
getter-function. We have something like this:
class ClassName(): def __init__(self,g): self._g = g @property def g(self): return self._g
class ClassName(): def __init__(self,g): self._g = g @g.setter def g(self,new_g): self._g = new_g
@g.setter
, is an important part of
the syntax. Here the only thing that happens when an attribute is
changed is that the hidden attribute is updated. However, now the
object may detect that some of its data has changed and make a note
of it. I.e. the def g(self, new_g
could be expanded as:
@g.setter def g(self,new_g): self._g = new_g self.somehing_happened = True
class LazyCalculation(): def __init__(self,g,h): self._g = g self._h = h self.average_calculated = False def __str__(self): return 'g={}, h={}, average={}'.format(self._g,self._h,self.average) @property def g(self): return self._g @property def h(self): return self._h @g.setter def g(self,new_g): print('... changing g') self._g = new_g self.average_calculated = False @h.setter def h(self,new_h): print('... changing h') self._h = new_h self.average_calculated = False @property def average(self): if not self.average_calculated: print('... calculating') self._average = (self._g + self._h) / 2 self.average_calculated = True return self._average
object = LazyCalculation(1,3) print(object) print('') object.average object.g = 100 object.h = 300 object.average
... calculating g=1, h=3, average=2.0 ... changing g ... changing h ... calculating 200.0
.g
and .h
, but only when the
.average
attribute was accessed at the very end.
Choose which booklet to go to: