python22.sci2u.dk Active sci2u
Loading...
Login
Prefer latest booklet Prefer latest booklet Login
Link to booklet is copied to clipboard!
Saved to booklet!
Removed from booklet!
Started capturing to booklet.
Stopped capturing to booklet.
Preferring latest booklet.
No longer prefers latest booklet.
Entered edit mode.
Exited edit mode.
Edit mode

Click on the booklet title or a chapter title to edit (max. 30 characters)


Python book

Basic Python Numerical Python Plotting with Matplotlib Advanced Python
4.1 Object oriented Python
4.2 Classes and objects
4.3 Methods I
4.4 Built-in methods
4.5 Object attributes
4.6 Object initialization
4.7 Object comparison
4.8 Methods II
4.9 Decorator: @property
4.10 Getter-setter
4.11 Full example
4.12 External references
Add Chapter..

Advanced Python

4.1 Object oriented Python

Much programming in a computer language like Python is about handling data. Often, when writing such programs, a programmer wants to be able to:
  1. reuse parts of the code when data or operations on it take a new form, rewriting only the necessary parts of the code
  2. shield parts of the code (e.g. variables and functions) from other parts of the code to help collaborators assess the code in self-contained chunks
  3. develop a higher level of abstraction when handling the data, not having to care everywhere in your code about how exactly data is represented
These objectives may be achieved in Object Oriented Programming in which the data is referred to by object variables that are constructed in such a way that "they know by themselves how operations are made on the data".
In the following sections, it is shown how this is done in Python.


4.2 Classes and objects

A Python class is a means to store data in some organized manner. The simplest possible declaration of a class, one that contains no data looks like this:
class EmptyClass():
    pass
The class has the name 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>
By default, the 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
we get the answer that they are not, which is a little funny since they are both empty. We shall return to that below.


4.3 Methods I

The class declaration normally involves some definitions of functions, so-called Python methods, that act on the objects. As a first example, consider that a method id_as_string() is added to the ClassWithMethod class:
class ClassWithMethod():
  
    def id_as_string(self):
        return str(hex(id(self)))
The method takes one argument, 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'
Note how the 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)
You will get this error message:
TypeError: id_as_string() takes 1 positional argument but 2 were given
that tells you that the method got two arguments, which are in fact the object1 that Python provided from object1. and the object1 that you gave in .id_as_string(object1).
So far, our use of a class and some objects being instances of that class looks ugly, but don't despair, it a minute it will appear highly logical and useful.


4.4 Built-in methods

It turns out that when the 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
Notice how the former output <__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.


4.5 Object attributes

Okay, time is up for adding some data to objects. Variables that live inside objects are called attributes. They can be referred to as object_name + . + attribute_name. Consider an empty class:
class EmptyClass():
      pass
Some instances of the class, i.e. some objects, may be created, and some attributes set:
object1 = EmptyClass()
object2 = EmptyClass()
object1.data = [1, 2]
object1.more_data = 'hello'
object2.data = [0, 0]
Later, the values of the attributes may be retrieved:
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]


4.6 Object initialization

Now, the whole idea of objects was to wrap data in clever data structures. With the above we are not yet obtaining much of that. For instance, the programmer interacting with our class still has to know the name of the object attribute to initiate the data. To get one step further, we shall turn to the built-in method, __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"
Now, the assignment of values to the object attribute happens in the __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.


4.7 Object comparison

Returning to the question of whether or not two objects are the same we may test for the objects just created:
print('object1 == object2:', object1 == object2)
print('object1 == object3:', object1 == object3)
object1 == object2: False
object1 == object3: False
and find that even the first two, 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
Now the comparison gives the expected:
print('object1 == object2:', object1 == object2)
print('object1 == object3:', object1 == object3)
object1 == object2: True
object1 == object3: False


4.8 Methods II

We now know enough about classes to make a non-trivial one. We shall define class 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')
We start by making an instance of the class, an object. If we forget to provide the data we get an error message:
rotor1 = Rotor()
TypeError: __init__() missing 1 required positional argument: 'some_data'
Similarly, if the data has the wrong format, either like this:
rotor1 = Rotor('no list')
or this:
rotor1 = Rotor([25])
we get our own error message:
AssertionError: ERROR: invalid data, must be list with len > 1
Lets now do it right:
rotor1 = Rotor([3, 5, 1])
print('rotor1: ',rotor1)
rotor1:  3-5-1
With a proper instance 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
We may repeat the process on another instance of the class:
rotor2 = Rotor([2, 4, 1])
print('rotor2: ',rotor2)
rotor2.rotate_data()
print('rotor2: ',rotor2)
rotor2:  2-4-1
rotor2:  4-1-2


4.9 Decorator: @property

Here is an example of a class that recalculates a dependent piece of data, an average of some other data, upon initialization:
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
Here is an example where the dependent piece of data, an average of some other data, is available via a method on the object:
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
Notice how () must be given on .average since it is now a method (= a function) rather than just an attribute.
We can get rid of the annoying parentheses, (), 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


4.10 Getter-setter

In the above example for the 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
If one now changes one of the attributes, .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
Obviously, this problem will not be there with the two other classes, ClassWithMethod and ClassWithPropertyDecoratedMethod, but for these classes, the average value is recalculated every time it is accessed, which might be a waste.
The way to resolve these issues is to "hide" the data in the object and introduce an interface for the external world to access it. Typically, one would prepend an underscore to the attribute name of a hidden variable, i.e. one would turn .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
Now for the outside world to be able to change the value of the date in the object, one then introduces a so-called setter-function:
class ClassName():
    def __init__(self,g):
        self._g = g

    @g.setter
    def g(self,new_g):
        self._g = new_g
where the decorator, @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
and then the class may react to this in its other methods. Below is given an example of that.


4.11 Full example

Here is a full example, where the data of a class are shielded off from the outside world with getter- and setter-functions, and where the calculation of some property is delayed until the point, where it is requested.
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
Note how the average was not calculated between changing .g and .h, but only when the .average attribute was accessed at the very end.


4.12 External references

Read more about classes and object oriented programming here


Sci2u Assignment: 820
Delete "Python book"?
Once deleted this booklet is gone forever!
Block is found in multiple booklets!

Choose which booklet to go to:

© 2019-2022 Uniblender ApS