13. More examples

This chapter contains several examples of different topics which we learned in previous chapters,

13.1. Generalized attribute validation

In this chapter ‘Functions’, @property’, ‘Decorators’ and ‘Descriptors’ are described. Also, these techniques are used together in final example for attribute validation. Attribute validation is defined in Section Section 13.1.1.1.

13.1.1. Function

Various features are provided in Python3 for better usage of the function. In this section, ‘help feature’ and ‘argument feature’ are added to functions.

13.1.1.1. Help feature

In Listing 13.1, anything which is written between 3 quotation marks after the function declaration (i.e. line 6-9), will be displayed as the output of ‘help’ command as shown in line 13.

Note

Our aim is to write the function which adds the integers only, but currently it is generating the output for the ‘strings’ as well as shown in line 21. Therefore, we need ‘attribute validation’ so that inputs will be verified before performing the operations on them.

Listing 13.1 Help feature
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# addIntEx.py

# line 6-9 will be displayed,
# when help command is used for addIntEx as shown in line 13.
def addIntEx(x,y):
    '''add two variables (x, y):
        x: integer
        y: integer
        returnType: integer
    '''
    return (x+y)

help(addIntEx)  # line 6-9 will be displayed as output

#adding numbers: desired result
intAdd = addIntEx(2,3)
print("intAdd =", intAdd) # 5

# adding strings: undesired result
# attribute validation is used for avoiding such errors
strAdd = addIntEx("Meher ", "Krishna")
print("strAdd =", strAdd) # Meher Krishna

13.1.1.2. Keyword argument

In Listing 13.2, ‘addKeywordArg(x, *, y)’ is a Python feature; in which all the arguments after ‘*’ are considered as positional argument. Hence, ‘x’ and ‘y’ are the ‘positional’ and ‘keyword’ argument respectively. Keyword arguments must be defined using the variable name e.g ‘y=3’ as shown in Lines 9 and 12. If name of the variable is not explicitly used, then Python will generate error as shown in Line 16. Further, keyword argument must be defined after all the positional arguments, otherwise error will be generated as shown in Line 19.

Lastly, in Line 2, the definition ‘addKeywordArg(x: int, *, y: int) -> int’ is presenting that inputs (x and y) and return values are of integer types. These help features can be viewed using metaclass command, i.e. ‘.__annotations__’, as shown in Lines 23 and 24. Note that, this listing is not validating input types. In next section, input validation is applied for the functions.

Listing 13.2 Keyword argument
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# addKeywordArg.py
def addKeywordArg(x:int, *,  y:int) -> int:
    '''add two numbers:
         x: integer, postional argument
         y: integer, keyword argument
         returnType: integer    '''
    return (x+y)

Add1 = addKeywordArg(2, y=3) # x: positional arg and y: keyword arg
print(Add1) # 5

Add2 = addKeywordArg(y=3, x=2) # x and y as keyword argument
print(Add2) # 5

## it's wrong, because y is not defined as keyword argument
#Add3 = addPositionalArg(2, 3) # y should be keyword argument i.e. y=3

## keyword arg should come after positional arg
#Add4 = addPositionalArg(y=3, 2) # correct (2, y=3)

help(addKeywordArg) # Lines 3-6 will be displayed as output

print(addKeywordArg.__annotations__)
## {'return': <class 'int'>, 'x': <class 'int'>, 'y': <class 'int'>}

## line 2 is only help (not validation), i.e. string addition will still unchecked
strAdd = addKeywordArg("Meher ", y = "Krishna")
print("strAdd =", strAdd)

13.1.1.3. Input validation

In previous section, help features are added to functions, so that the information about the functions can be viewed by the users. In this section, validation is applied to input arguments, so that any invalid input will not be process by the function and corresponding error be displayed to the user.

In Listing 13.3, Lines 8-9 are used to verify the type of input variable ‘x’. Line 8 checks whether the input is integer or not; if it is not integer that error will be raised by line 9, as shown in lines 19-22. Similarly, Lines 11-12 are used to verify the type of variable ‘y’.

Listing 13.3 Input Validation
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# addIntValidation.py
def addIntValidation(x:int, *,  y:int)->int:
    '''add two variables (x, y):
        x: integer, postional argument
        y: integer, keyword argument
        returnType: integer
        '''
    if type(x) is not int: # validate input 'x' as integer type
        raise TypeError("Please enter integer value for x")

    if type(y) is not int: # validate input 'y' as integer type
        raise TypeError("Please enter integer value for y")

    return (x+y)

intAdd=addIntValidation(y=3, x=2)
print("intAdd =", intAdd)

#strAdd=addIntValidation("Meher ", y = "Krishna")
## Following error will be generated for above command,
## raise TypeError("Please enter integer value for x")
## TypeError: Please enter integer value for x

help(addIntValidation) # Lines 3-6 will be displayed as output
print(addIntValidation.__annotations__)
## {'return': <class 'int'>, 'x': <class 'int'>, 'y': <class 'int'>}

13.1.2. Decorators

Decorators are used to add additional functionalities to functions. In Section Section 13.1.1.3, ‘x’ and ‘y’ are validated individually; hecce, if there are large number of inputs, then the method will not be efficient. Decorator will be used in Section Section 13.1.5 to write the generalized validation which can validate any kind of input.

13.1.2.1. Add decorators and problems

Listing 13.4 is the decorator, which prints the name of the function i.e. whenever the function is called, the decorator will be executed first and print the name of the function and then actual function will be executed. The decorator defined above the function declaration as shown in line 4 of Listing 13.5.

Listing 13.4 Decorator which prints the name of function
1
2
3
4
5
6
# funcNameDecorator.py
def funcNameDecorator(func): # function as input
    def printFuncName(*args, **kwargs): #take all arguments of function as input
        print("Function Name:", func.__name__) # print function name
        return func(*args, **kwargs) # return function with all arguments
    return printFuncName

In Listing 13.5, first decorator ‘funcNameDecorator’ is imported to the listing in Line 2. Then, decorator is applied to function ‘addIntDecorator’ in Line 4. When Line 15 calls the function ‘addIntDecorator’, the decorator is executed first and name of function is printed, after that print command at Line 16 is executed.

Warning

In the Listing, we can see that Help function is not working properly now as shown in Listing 19. Also, Decorator removes the metaclass features i.e. ‘annotation’ will not work, as shown in Line 24.

Listing 13.5 Decorator applied to function
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#addIntDecorator.py
from funcNameDecorator import funcNameDecorator

@funcNameDecorator
def addIntDecorator(x:int, *,  y:int) -> int:
    '''add two variables (x, y):
        x: integer, postional argument
        y: integer, keyword argument
        returnType: integer
    '''
    return (x+y)

## decorator will be executed when function is called,
## and function name will be displayed as output as shown below,
intAdd=addIntDecorator(2, y=3) # Function Name: addIntDecorator
print("intAdd =", intAdd) # 5

##problem with decorator: help features are not displayed as shown below
help(addIntDecorator) # following are the outputs of help command
## Help on function wrapper in module funcNameDecorator:
## wrapper(*args, **kwargs)

## problem with decorator: no output is displayed
print(addIntDecorator.__annotations__) # {}

Note

It is recommonded to define the decorators in the separate files e.g. ‘funcNameDecorator.py’ file is used here. It’s not good practice to define decorator in the same file, it may give some undesired results.

13.1.2.2. Remove problems using functools

The above problem can be removed by using two additional lines in Listing 13.4. The listing is saved as Listing 13.6 and Lines 2 and 6 are added, which solves the problems completely. In Line 2, ‘wraps’ is imported from ‘functools’ library and then it is applied inside the decorator at line 6. Listing 13.7 is same as Listing 13.5 except new decorator which is defined in Listing 13.6 is called at line 4.

Listing 13.6 Decorator with ‘wrap’ decorator
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# funcNameDecoratorFunctool.py
from functools import wraps

def funcNameDecoratorFunctool(func): # function as input
    #func is the function to be wrapped
    @wraps(func)
    def printFuncName(*args, **kwargs): #take all arguments of function as input
        print("Function Name:", func.__name__) # print function name
        return func(*args, **kwargs) # return function with all arguments
    return printFuncName
Listing 13.7 Help features are visible again
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# addIntDecoratorFunctool.py
from funcNameDecoratorFunctool import funcNameDecoratorFunctool

@funcNameDecoratorFunctool
def addIntDecorator(x:int, *,  y:int) -> int:
    '''add two variables (x, y):
        x: integer, postional argument
        y: integer, keyword argument
        returnType: integer
    '''
    return (x+y)

intAdd=addIntDecorator(2, y=3) # Function Name: addIntDecorator
print("intAdd =", intAdd) # 5

help(addIntDecorator) # lines 6-9 will be displaed

print(addIntDecorator.__annotations__)
##{'return': <class 'int'>, 'y': <class 'int'>, 'x': <class 'int'>}

13.1.3. @property

In this section, area and perimeter of the rectangle is calculated and ‘@property’ is used to validate the inputs before calculation. Further, this example is extended in the next sections for adding more functionality for ‘attribute validation’.

Explanation Listing 13.8

In line 28 of the listing, @property is used for ‘length’ attribute of the class Rectangle. Since, @property is used, therefore ‘getter’ and ‘setter’ can be used to validate the type of length. Note that, in setter part, i.e. Lines 34-40, self._length (see ‘_’ before length) is used for setting the valid value in ‘length’ attribute. In the setter part validation is performed at Line 38 using ‘isinstance’. n Line 54, the value of length is passed as float, therefore error will be raised as shown in Line 56.

Now, whenever ‘length’ is accessed by the code, it’s value will be return by getter method as shown in Lines 28-32. In the other words, this block will be executed every time we use ‘length’ value. To demonstrate this, print statement is used in Line 31. For example, Line 44 print the length value, therefore line 31 printed first and then length is printed as shown in Lines 45-46.

Also, @property is used for the method ‘area’ as well. Therefore, output of this method can be directly obtained as shown in Lines 48-52. Further, for calculating area, the ‘length’ variable is required therefore line 51 will be printed as output, which is explained in previous paragraph.

Note

In this listing, the type-check applied to ‘length’ using @property. But, the problem with this method is that we need to write it for each attribute e.g. length and width in this case which is not the efficient way to do the validation. We will remove this problem using Descriptor in next section.

Listing 13.8 Attribute validation using @property
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# rectProperty.py
class Rectangle:
    '''
    -Calculate Area and Perimeter of Rectangle
    -getter and setter are used to displaying and setting the length value.
    -width is set by init function
    '''

    # self.length is used in below lines,
    # but length is not initialized by __init__,
    # initialization is done by .setter at lines 34 due to @property at line 27,
    # also value is displayed by getter (@propety) at line 28-32
    # whereas `width' is get and set as simple python code and without validation
    def __init__(self, length, width):
        #if self._length is used, then it will not validate through setter.
        self.length = length
        self.width = width

    @property
    def area(self):
        '''Calculates Area: length*width'''
        return self.length * self.width

    def perimeter(self):
        '''Calculates Perimeter: 2*(length+width)'''
        return 2 * (self.length + self.width)

    @property
    def length(self):
        '''displaying length'''
        print("getting length value through getter")
        return self._length

    @length.setter
    def length(self, value):
        '''setting length'''
        print("saving value through setter", value)
        if not isinstance(value, int): # validating length as integer
            raise TypeError("Only integers are allowed")
        self._length = value

r = Rectangle(3,2) # following output will be displayed
## saving value through setter 3
print(r.length) # following output will be displayed
## getting length value through getter
## 3

## @property is used for area,
## therefore it can be accessed directly to display the area
print(r.area) # following output will be displayed
## getting length value through getter
## 6

#r=Rectangle(4.3, 4) # following error will be generated
## [...]
## TypeError: Only integers are allowed

# print perimeter of rectangle
print(Rectangle.perimeter(r))
## getting length value through getter
## 10

13.1.4. Descriptors

Descriptor are the classes which implement three core attributes access operation i.e. get, set and del using ‘__get__’, ‘__set__’ and ‘__del__’ as shown in Listing 13.9. In this section, validation is applied using Descriptor to remove the problem with @property.

Explanation Listing 13.9

Here, class integer is used to verify the type of the attributes using ‘__get__’ and ‘__set__’ at Lines 6 and 12 respectively. The class ‘Rect’ is calling the class ‘Integer’ at Lines 19 and 20. The name of the attribute is passed in these lines, whose values are set by the Integer class in the form of dictionaries at Line 16. Also, value is get from the dictionary from Line 10. Note that, in this case, only one line is added for each attribute, which removes the problem of ‘@property’ method.

Listing 13.9 Attribute validation using Descriptor
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# rectDescriptor.py
class Integer:
    def __init__(self, parameter):
        self.parameter = parameter

    def __get__(self, instance, cls):
        if instance is None: # required if descriptor is
            return self # used as class variable
        else: # in this code, only following line is required
            return instance.__dict__[self.parameter]

    def __set__(self, instance, value):
        print("setting %s to %s" % (self.parameter, value))
        if not isinstance(value, int):
            raise TypeError("Interger value is expected")
        instance.__dict__[self.parameter] = value

class Rect:
    length = Integer('length')
    width = Integer('width')
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        '''Calculates Area: length*width'''
        return self.length * self.width

r = Rect(3,2)
## setting length to 3
## setting width to 3

print(r.length) # 3

print("Area:", Rect.area(r)) # Area:  6

#r = Rect(3, 1.5)
## TypeError: Interger value is expected

13.1.5. Generalized validation

In this section, decorators and descriptors are combined to create a validation, where attribute-types are defined by the individual class authors.

Note

Note that, various types i.e. ‘@typeAssert(author=str, length=int, width=float)’ will be defined by class Author for validation.

Explanation Listing 13.10

In this code, first a decorator ‘typeAssert’ is applied to class ‘Rect’ at line 27. The typeAssert contains the name of the attribute along with it’s valid type. Then the decorator (Lines 19-24), extracts the ‘key-value’ pairs i.e. ‘parameter-expected]_type’ (see Line 21) and pass these to descriptor ‘TypeCheck’ through Line 22. If type is not valid, descriptor will raise error, otherwise it will set the values to the variables. Finally, these set values will be used by the class ‘Rect’ for further operations.

Listing 13.10 Generalized attribute validation
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#rectGeneralized.py
class TypeCheck:
    def __init__(self, parameter, expected_type):
        self.parameter = parameter
        self.expected_type = expected_type

    def __get__(self, instance, cls):
        if instance is None: # required if descriptor is
            return self # used as class variable
        else: # in this code, only following line is required
            return instance.__dict__[self.parameter]

    def __set__(self, instance, value):
        print("setting %s to %s" % (self.parameter, value))
        if not isinstance(value, self.expected_type):
            raise TypeError("%s value is expected" % self.expected_type)
        instance.__dict__[self.parameter] = value

def typeAssert(**kwargs):
    def decorate(cls):
        for parameter, expected_type in kwargs.items():
            setattr(cls, parameter, TypeCheck(parameter, expected_type))
        return cls
    return decorate

 # define attribute types here in the decorator
@typeAssert(author=str, length = int, width = float)
class Rect:
    def __init__(self, *,  length, width, author = ""): #require kwargs
        self.length = length
        self.width = width * 1.0 # to accept integer as well
        self.author = author

r = Rect (length=3, width=3.1, author = "Meher")
## setting length to 3
## setting width to 3.1
## setting author to Meher

#r = Rect (length="len", width=3.1, author = "Meher") # error shown below
## File "rectProperty.py", line 42,
## [ ... ]
## TypeError: <class 'int'> value is expected

13.1.6. Summary

In this chapter, we learn about functions, @property, decorators and descriptors. We see that @property is useful for customizing the single attribute whereas descriptor is suitable for multiple attributes. Further, we saw that how decorator and descriptor can be used to enhance the functionality of the code with DRY (don’t repeat yourself) technique.

13.2. Inheritance with Super

In this section, inheritance is discussed using super command. In most languages, the super method calls the parent class, whereas in python it is slightly different i.e. it consider the child before parent, as shown in this section.

13.2.1. Super : child before parent

Lets understand super with the help of an example. First, create a class Pizza, which inherits the DoughFactory for getting the dough as below,

# pizza.py

class DoughFactory(object):

    def get_dough(self):
        return 'white floor dough'


class Pizza(DoughFactory):

    def order_pizza(self, *toppings):
        print("getting dough")

        # dough = DoughFactory.get_dough()
        ## above line is commented to work with DRY principle
        ## use super as below,
        dough = super().get_dough()
        print("Making pie using '%s'" % dough)

        for topping in toppings:
            print("Adding %s" % topping)

if __name__ == '__main__':
    Pizza().order_pizza("Pepperoni", "Bell Pepper")

Run the above code and we will get below output,

$ python -i pizza.py

getting dough
Making pie using 'white floor dough'
Adding Pepperoni
Adding Bell Pepper
>>> help(Pizza)
    Help on class Pizza in module __main__:

    class Pizza(DoughFactory)
     |  Method resolution order:
     |      Pizza
     |      DoughFactory
     |      builtins.object

Note

The resolution order shows that the way in which python interpretor tries to find the methods i.e. it tries to find get_dough in Pizza class first; if not found there, it will go to DoughFactory.

Now, create the another class in separate python file as below,

# wheatDough.py

from pizza import Pizza, DoughFactory


class WheatDoughFactory(DoughFactory):

    def get_dough(self):
        return("wheat floor dough")

class WheatPizza(Pizza, WheatDoughFactory):
    pass

if __name__ == '__main__':
    WheatPizza().order_pizza('Sausage', 'Mushroom')

Note

In python, Inheritance chain is not determine by the Parent class, but by the child class.

If we run the wheatDough.py, it will call the super command in class Pizza in pizza.py will not call his parent class i.e. DoughFactory, but the parent class of the child class i.e. WheatDoughFactory. This is called Dependency injection.

Important

  • super consider the children before parents i.e. it looks methods in child class first, then it goes for parent class.
  • Next, it calls the parents in the order of inheritance.
  • use keyword arguments for cooperative inheritance.

For above reasons, super is super command, as it allows to change the order of Inheritance just by modifying the child class, as shown in above example.

Note

For better understanding of the super command, some more short examples are added here,

13.2.2. Inherit __init__

In the following code, class RectArea is inheriting the __init__ function of class RectLen. In the other word, length is set by class RectLen and width is set by class RectArea and finally area is calculated by class RectArea.

# rectangle.py

class RectLen(object):
    def __init__(self, length):
        self.length = length


class RectArea(RectLen):
    def __init__(self, length, width):
        self.width = width
        super().__init__(length)
        print("Area : ", self.length * self.width)


RectArea(4, 3)  # Area :  12

In the same way, the other functions of parent class can be called. In following code, printClass method of parent class is used by child class.

# printClass.py

class A(object):
    def printClassName(self):
        print(self.__class__.__name__)

class B(A):
    def printName(self):
        super().printClassName()

a = A()
a.printClassName()  # A

b = B()
b.printClassName()  # B

Note

In above code, print(self.__class__.__name__) is used for printing the class name, instead of print(“A”). Hence, when child class will inherit this function, then __class__ will use the name of the child class to print the name of the class, therefore Line 15 prints “B” instead of A.

13.2.3. Inherit __init__ of multiple classes

In this section, various problems are discussed along with the solutions, which usually occurs during multiple inheritance.

13.2.3.1. Problem : super() calls __init__ of one class only

In following example, class C is inheriting the class A and B. But, the super function in class C will inherit only one class init function i.e. init function of the class which occurs first in the inheritance order.

#multipleInheritance.py

class A(object):
    def __init__(self):
        print("A")

class B(object):
    def __init__(self):
        print("B")

class C(A, B):
    def __init__(self):
        super().__init__()

# init of class B is not inherited
c = C() # A

13.2.3.2. Solution 1

Following is the first solution, where __init__ function of classes are invoked explicitly.

#multipleInheritance.py

class A(object):
    def __init__(self):
        print("A")

class B(object):
    def __init__(self):
        print("B")

class C(A, B):
    def __init__(self):
        A.__init__(self)  # self is required
        B.__init__(self)

c = C()
# A
# B

13.2.3.3. Correct solution

Following is the another solution of the problem; where super() function is added in both the classes. Note that, the super() is added in class B as well, so that class(B, A) will also work fine.

#multipleInheritance.py

class A(object):
    def __init__(self):
        print("reached A")
        super().__init__()
        print("A")

class B(object):
    def __init__(self):
        print("reached B")
        super().__init__()
        print("B")

class C(A, B):
    def __init__(self):
        super().__init__()

c = C()
# reached A
# reached B
# B
# A

The solution works fine here because in Python super consider the child before parent, which is discussed in Section Super : child before parent. Please see the order of output as well.

13.2.4. Math Problem

This section summarizes the above section using math problem. Here, we want to calculate (x * 2 + 5), where x = 3.

13.2.4.1. Solution 1

This is the first solution, __init__ function of two classes are invoked explicitly. The only problem here is that the solution does not depend on the order of inheritance, but on the order of invocation, i.e. if we exchange the lines 15 and 16, the solution will change.

# mathProblem.py

class Plus5(object):
    def __init__(self, value):
        self.value = value + 5

class Multiply2(object):
    def __init__(self, value):
        self.value = value * 2

class Solution(Multiply2, Plus5):
    def __init__(self, value):
        self.value = value

        Multiply2.__init__(self, self.value)
        Plus5.__init__(self, self.value)


s = Solution(3)
print(s.value)  # 11

13.2.4.2. problem with super

One of the problem with super is that, the top level super() function does not work if it has some input arguments. If we look the output of following code carefully, then we will find that error is generated after reaching to class Plus5. When class Plus5 uses the super(), then it calls the metaclass’s (i.e. object) __init__ function, which does not take any argument. Hence it generates the error ‘object.__init__() takes no parameters’.

To solve this problem, we need to create another class as shown in next section.

# mathProblem.py

class Plus5(object):
    def __init__(self, value):

        print("Plus 5 reached")
        self.value = value + 5

        super().__init__(self.value)
        print("Bye from Plus 5")

class Multiply2(object):
    def __init__(self, value):

        print("Multiply2 reached")
        self.value = value * 2

        super().__init__(self.value)
        print("Bye from Multiply2")

class Solution(Multiply2, Plus5):
    def __init__(self, value):
        self.value = value
        super().__init__(self.value)


s = Solution(3)
print(s.value)

# Multiply2 reached
# Plus 5 reached
# [...]
# TypeError: object.__init__() takes no parameters

13.2.4.3. Solution 2

To solve the above, we need to create another class, and inherit it in classes Plus5 and Multiply2 as below,

In below code, MathClass is created, whose init function takes one argument. Since, MathClass does not use super function, therefore above error will not generate here.

Next, we need to inherit this class in Plus5 and Multiply2 for proper working of the code, as shown below. Further, below code depends on order of inheritance now.

# mathProblem.py

class MathClass(object):
    def __init__(self, value):
        print("MathClass reached")
        self.value = value
        print("Bye from MathClass")

class Plus5(MathClass):
    def __init__(self, value):
        print("Plus 5 reached")
        self.value = value + 5
        super().__init__(self.value)
        print("Bye from Plus 5")

class Multiply2(MathClass):
    def __init__(self, value):
        print("Multiply2 reached")
        self.value = value * 2
        super().__init__(self.value)
        print("Bye from Multiply2")

class Solution(Multiply2, Plus5):
    def __init__(self, value):
        self.value = value
        super().__init__(self.value)


s = Solution(3)
print(s.value)  # 11

# Multiply2 reached
# Plus 5 reached
# MathClass reached
# Bye from MathClass
# Bye from Plus 5
# Bye from Multiply2
# 11

## uncomment below to see the Method resolution order
print(help(Solution))
# class Solution(Multiply2, Plus5)
#  |  Method resolution order:
#  |      Solution
#  |      Multiply2
#  |      Plus5
#  |      MathClass
#  |      builtins.object

13.2.5. Conclusion

In this section, we saw the functionality of the super() function. It is shown that super() consider the child class first and then parent classes in the order of inheritance. Also, help command is used for observing the ‘method resolution order’ i.e. hierarchy of the inheritance.

13.3. Generators

Any function that uses the ‘yield’ statement is the generator. Each yield temporarily suspends processing, remembering the location execution state (including local variables and pending try-statements). When the generator iterator resumes, it picks-up where it left-off (in contrast to functions which start fresh on every invocation).

13.3.1. Feed iterations

Typically, it is used to feed iterations as below,

   # generatorEx.py

   def countdown(n):
       while (n>0):
           yield n
           n -= 1

   for x in countdown(5):
       print(x)  # 5 4 3 2 1

   print()
   c = countdown(3)
   print(next(c))  # 3
   print(next(c))  # 2
   print(next(c))  # 1
   print(next(c))
   # Traceback (most recent call last):
   #   File "rectangle.py", line 16, in <module>
   #     print(next(c))
   # StopIteration

If the generator exits without yielding another value, a StopIteration exception is raised.

13.3.2. Receive values

‘yield’ can receive value too. Calling the function creates the generator instance, which needs to be advance to next yield using ‘next’ command. Then generator is ready to get the inputs, as shown below,

# generatorEx.py

def rxMsg():
    while True:
        item = yield
        print("Message : ", item)

msg = rxMsg()
print(msg)  # <generator object rxMsg at 0xb7049f8c>

next(msg)
# send : Resumes the execution and “sends” a value into the generator function
msg.send("Hello")
msg.send("World")

13.3.3. Send and receive values

Both send and receive message can be combined together in generator. Also, generator can be closed, and next() operation will generate error if it is used after closing the generator, as shown below,

# generatorEx.py

def rxMsg():
    while True:
        item = yield
        yield("Message Ack: " + item)


msg = rxMsg()

next(msg)
m1 = msg.send("Hello")
print(m1)  # Message Ack: Hello

next(msg)
m2 = msg.send("World")
print(m2)  # Message Ack: World

msg.close()  # close the generator

next(msg)
# Traceback (most recent call last):
#   File "rectangle.py", line 21, in <module>
#     next(msg)
# StopIteration

13.3.4. Return values in generator

Generator can return values which is displayed with exception,

# generatorEx.py

def rxMsg():
    while True:
        item = yield
        yield("Message Ack: " + item)
        return "Thanks"


msg = rxMsg()

next(msg)
m1 = msg.send("Hello")
print(m1)  # Message Ack: Hello

next(msg)
# Traceback (most recent call last):
#   File "rectangle.py", line 16, in <module>
#     next(msg)
# StopIteration: Thanks

m2 = msg.send("World")
print(m2)

13.3.5. ‘yield from’ command

When yield from <expr> is used, it treats the supplied expression as a subiterator. All values produced by that subiterator are passed directly to the caller of the current generator’s methods,

# generatorEx.py

def chain(x, y):
    yield from x
    yield from y

a = [1, 2, 3]
b = [20, 30]

for i in chain(a, b):
    print(i, end=' ')  # 1, 2, 3, 20, 30

print()
for i in chain(chain(a, a), chain(b, a)):
    print(i, end=' ')  # 1 2 3 1 2 3 20 30 1 2 3