11. Methods and @property

11.1. Introduction

In this chapter, we will understand the usage of ‘methods’, ‘@property’ and ‘descriptor’.

11.2. Methods, staticmethod and classmethod

In previous chapters, we saw the examples of methods, e.g. ‘area’ and ‘cost’ in Listing 10.1, inside the class without any decorator with them. The ‘decorators’ adds the additional functionality to the methods, and will be discussed in details in Chapter 12.

In this section, we will quickly review the different types of methods with an example. Then these methods will be used with our previous examples.

In the below code, two decorators are used with the methods inside the class i.e. ‘staticmethod’ and ‘classmethod’. Please see the comments and notice: how do the different methods use different values of x for adding two numbers,

Note

We can observe the following differences in these three methods from the below code,

  • method : it uses the instance variable (self.x) for addition, which is set by __init__ function.
  • classmethod : it uses class variable for addition.
  • staticmethod : it uses the value of x which is defined in main program (i.e. outside the class ). If x = 20 is not defined, then NameError will be generated.
# mehodsEx.py

# below x will be used by static method
# if we do not define it, the staticmethod will generate error.
x = 20

class Add(object):
    x = 9  # class variable

    def __init__(self, x):
        self.x = x  # instance variable

    def addMethod(self, y):
        print("method:", self.x + y)

    @classmethod
    # as convention, cls must be used for classmethod, instead of self
    def addClass(self, y):
        print("classmethod:", self.x + y)

    @staticmethod
    def addStatic(y):
        print("staticmethod:", x + y)



def main():
    # method
    m  = Add(x=4) # or m = Add(4)
    # for method, above x = 4, will be used for addition
    m.addMethod(10)  # method :  14

    # classmethod
    c = Add(4)
    # for class method, class variable x = 9, will be used for addition
    c.addClass(10)  # clasmethod : 19

    # for static method, x=20 (at the top of file), will be used for addition
    s = Add(4)
    s.addStatic(10)  # staticmethod : 30

if __name__ == '__main__':
    main()

Below is the output for above code,

$ python methodEx.py

method: 14
classmethod: 19
staticmethod: 30

11.3. Research organization

In previous chapter, we had two users who are modifying the class attributes. First was the mathematician (mathematician.py), who is modifying the ‘radius’ in Listing 10.4. And other is the box company, who were modifying the method ‘perimeter’ in Listing 10.5. Lastly, we had a contributor who is creating a ‘table formatter’ in Listing 10.9 for displaying the output nicely.

Now, we have another research organization, who were doing their research analysis based on the diameters (not using radius), therefore they want to initialize the class-objects based on the diameter directly.

Note

If we change the the ‘radius’ to ‘diameter’ in the __init__ function of class Ring, then we need to modify the ‘area’ and ‘cost’ methods as well. Also, the codes of other users (mathematician and box company) will break. But we can solve this problem using ‘classmethod’ as shown next.

11.3.1. Multiple constructor using ‘classmethod’

The ‘classmethod’ is a very useful tools to create the multiple constructor as shown in the below listing.

 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
62
63
64
65
66
67
# pythonic.py

import math
import time

class Ring(object):
    """ Here we will see the actual logic behind various pieces of Python
    language e.g. instances, variables, method and @property etc.
    Also, we will see the combine usage of these pieces to complete a
    design with Agile methodology.
    """

    # class variables
    date = time.strftime("%Y-%m-%d", time.gmtime()) # today's date "YYYY-mm-dd"
    center = 0.0 # center of the ring

    def __init__(self, date=date, metal="Copper", radius=5.0,
                price=5.0, quantity=5):
        """ init is not the constructor, but the initializer which
        initialize the instance variable

        self : is the instance

        __init__ takes the instance 'self' and populates it with the radius,
        metal, date etc. and store in a dictionary.

        self.radius, self.metal etc. are the instance variable which
        must be unique.
        """

        self.date = date
        self.metal = metal
        self.radius = radius
        self.price = price
        self.quantity = quantity


    # Multiple constructor
    # below constructor is added for the 'research organization' who are
    # doing their work based on diameters,
    @classmethod
    def diameter_init(cls, diameter):
        radius = diameter/2;  # change diameter to radius
        return cls(radius) # return radius

    def cost(self):
        return self.price * self.quantity

    def area(self):
        return math.pi * self.radius**2

    def perimeter(self):
        return 2 * math.pi * self.radius

def main():
    print("Center of the Ring is at:", Ring.center) # modify class variable
    r = Ring(price=8) # modify only price
    print("Radius:{0}, Cost:{1}".format(r.radius, r.cost()))
    print("Radius:{0}, Perimeter:{1:0.2f}".format(r.radius, r.perimeter()))

    print() # check the new constructor 'diameter_init'
    d = Ring.diameter_init(diameter=10)
    print("Radius:{0}, Perimeter:{1:0.2f}".format(d.radius, d.perimeter()))


if __name__ == '__main__':
    main()

Following are the outputs for above code,

$ python pythonic.py

Center of the Ring is at: 0.0
Radius:5.0, Cost:40
Radius:5.0, Perimeter:31.42

Radius:5.0, Perimeter:31.42

11.3.2. Additional methods using ‘staticmethod’

Now, the research organization wants one more features in the class, i.e. Meter to Centimeter conversion. Note that this feature has no relation with the other methods in the class. In the other words, the output of Meter to Centimeter conversion will not be used anywhere in the class, but it’s handy for the organization to have this feature in the package. In such cases, we can use static methods.

Note

There is no point to create a ‘meter_cm’ method as ‘meter_cm(self, meter)’ and store it in the dictionary, as we are not going to use it anywhere in the class. We added this to meet the client’s requirement only, hence it should be added as simple definition (not as methods), which can be done using @staticmethod. In the other words, static methods are used to attached functions to classes.

 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
62
63
64
65
66
67
68
69
70
71
72
73
# pythonic.py

import math
import time

class Ring(object):
    """ Here we will see the actual logic behind various pieces of Python
    language e.g. instances, variables, method and @property etc.
    Also, we will see the combine usage of these pieces to complete a
    design with Agile methodology.
    """

    # class variables
    date = time.strftime("%Y-%m-%d", time.gmtime()) # today's date "YYYY-mm-dd"
    center = 0.0 # center of the ring

    def __init__(self, date=date, metal="Copper", radius=5.0,
                price=5.0, quantity=5):
        """ init is not the constructor, but the initializer which
        initialize the instance variable

        self : is the instance

        __init__ takes the instance 'self' and populates it with the radius,
        metal, date etc. and store in a dictionary.

        self.radius, self.metal etc. are the instance variable which
        must be unique.
        """

        self.date = date
        self.metal = metal
        self.radius = radius
        self.price = price
        self.quantity = quantity


    # Multiple constructor
    # below constructor is added for the 'research organization' who are
    # doing their work based on diameters,
    @classmethod
    def diameter_init(cls, diameter):
        radius = diameter/2;  # change diameter to radius
        return cls(radius) # return radius

    @staticmethod  # meter to centimeter conversion
    def meter_cm(meter):
        return(100*meter)


    def cost(self):
        return self.price * self.quantity

    def area(self):
        return math.pi * self.radius**2

    def perimeter(self):
        return 2 * math.pi * self.radius

def main():
    print("Center of the Ring is at:", Ring.center) # modify class variable
    r = Ring(price=8) # modify only price
    print("Radius:{0}, Cost:{1}".format(r.radius, r.cost()))
    print("Radius:{0}, Perimeter:{1:0.2f}".format(r.radius, r.perimeter()))

    print() # check the new constructor 'diameter_init'
    d = Ring.diameter_init(diameter=10)
    print("Radius:{0}, Perimeter:{1:0.2f}".format(d.radius, d.perimeter()))
    m = 10 # 10 meter
    print("{0} meter = {1} centimeter".format(m, d.meter_cm(m)))

if __name__ == '__main__':
    main()

Following is the output of above code,

$ python pythonic.py
Center of the Ring is at: 0.0
Radius:5.0, Cost:40
Radius:5.0, Perimeter:31.42

Radius:5.0, Perimeter:31.42
10 meter = 1000 centimeter

11.4. Micro-managing

Micro-managing is the method, where client tells us the ways in which the logic should be implemented. Lets understand it by an example.

Note

Now the research organization wants that the ‘area’ should be calculated based on the ‘perimeter’. More specifically, they will provide the ‘diameter’, then we need to calculate the ‘perimeter’; then based on the ‘perimeter’, calculate the ‘radius’. And finally calculate the ‘area’ based on this new radius. This may look silly, but if we look closely, the radius will change slightly during this conversion process; as there will be some difference between (diameter/2) and (math.pi*diameter)/( 2*math.pi). The reason for difference is: on computer we do not cancel the math.pi at numerator with math.pi at denominator, but solve the numerator first and then divide by the denominator, and hence get some rounding errors.

11.4.1. Wrong Solution

The solution to this problem may look pretty simple, i.e. modify the ‘area’ method in the following way,

 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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# pythonic.py

import math
import time

class Ring(object):
    """ Here we will see the actual logic behind various pieces of Python
    language e.g. instances, variables, method and @property etc.
    Also, we will see the combine usage of these pieces to complete a
    design with Agile methodology.
    """

    # class variables
    date = time.strftime("%Y-%m-%d", time.gmtime()) # today's date "YYYY-mm-dd"
    center = 0.0 # center of the ring

    def __init__(self, date=date, metal="Copper", radius=5.0,
                price=5.0, quantity=5):
        """ init is not the constructor, but the initializer which
        initialize the instance variable

        self : is the instance

        __init__ takes the instance 'self' and populates it with the radius,
        metal, date etc. and store in a dictionary.

        self.radius, self.metal etc. are the instance variable which
        must be unique.
        """

        self.date = date
        self.metal = metal
        self.radius = radius
        self.price = price
        self.quantity = quantity


    # Multiple constructor
    # below constructor is added for the 'research organization' who are
    # doing their work based on diameters,
    @classmethod
    def diameter_init(cls, diameter):
        radius = diameter/2;  # change diameter to radius
        return cls(radius) # return radius

    @staticmethod  # meter to centimeter conversion
    def meter_cm(meter):
        return(100*meter)

    def cost(self):
        return self.price * self.quantity

    def area(self):
        # return math.pi * self.radius**2
        p = self.perimeter() # calculate perimeter
        r = p / ( 2 * math.pi)
        return math.pi * r**2

    def perimeter(self):
        return 2 * math.pi * self.radius

def main():
    # print("Center of the Ring is at:", Ring.center) # modify class variable
    # r = Ring(price=8) # modify only price
    # print("Radius:{0}, Cost:{1}".format(r.radius, r.cost()))
    # print("Radius:{0}, Perimeter:{1:0.2f}".format(r.radius, r.perimeter()))

    print() # check the new constructor 'diameter_init'
    d = Ring.diameter_init(diameter=10)
    print("Radius:{0}, Perimeter:{1:0.2f}, Area:{2:0.2f}".format(
        d.radius, d.perimeter(), d.area()))
    # print("Radius:{0}, Perimeter:{1:0.2f}".format(d.radius, d.perimeter()))
    # m = 10 # 10 meter
    # print("{0} meter = {1} centimeter".format(m, d.meter_cm(m)))

if __name__ == '__main__':
    main()
  • If we run the above code, we will have the following results, which is exactly right.
$ python pythonic.py

Radius:5.0, Perimeter:31.42, Area:78.54

Error

Above solution looks pretty simple, but it will break the code in ‘box.py in Listing 10.5’, as it is modifying the perimeter by a factor of 2.0 by overriding the perimeter method. Therefore, for calculating the area in box.py file, python interpretor will use the child class method ‘perimeter’ for getting the value of perimeter. Hence, the area will be calculated based on the ‘modified parameter’, not based on actual parameter.

  • In box.py, the area will be wrongly calculated, because it is modifying the perimeter for their usage. Now, radius will be calculated by new perimeter (as they override the perimeter method) and finally area will be modified due to change in radius values, as shown below,
>>> from box import *
>>> b = Box.diameter_init(10)
>>> b.radius
5.0
>>> b.perimeter()  # doubled value of perimeter (desired)
62.83185307179586
>>> b.area()  # wrong value of area, see next for correct result
314.1592653589793
>>>
>>> import math
>>> math.pi * b.radius**2  # desired radius
78.53981633974483
>>> math.pi * (b.perimeter()/(2*math.pi))**2  # calculated radius
314.1592653589793

11.4.2. Correct solution

Note

The above problem can be resolved by creating a local copy of perimeter in the class. In this way, the child class method can not override the local-copy of parent class method. For this, we need to modify the code as below,

 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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# pythonic.py

import math
import time

class Ring(object):
    """ Here we will see the actual logic behind various pieces of Python
    language e.g. instances, variables, method and @property etc.
    Also, we will see the combine usage of these pieces to complete a
    design with Agile methodology.
    """

    # class variables
    date = time.strftime("%Y-%m-%d", time.gmtime()) # today's date "YYYY-mm-dd"
    center = 0.0 # center of the ring

    def __init__(self, date=date, metal="Copper", radius=5.0,
                price=5.0, quantity=5):
        """ init is not the constructor, but the initializer which
        initialize the instance variable

        self : is the instance

        __init__ takes the instance 'self' and populates it with the radius,
        metal, date etc. and store in a dictionary.

        self.radius, self.metal etc. are the instance variable which
        must be unique.
        """

        self.date = date
        self.metal = metal
        self.radius = radius
        self.price = price
        self.quantity = quantity


    # Multiple constructor
    # below constructor is added for the 'research organization' who are
    # doing their work based on diameters,
    @classmethod
    def diameter_init(cls, diameter):
        radius = diameter/2;  # change diameter to radius
        return cls(radius) # return radius

    @staticmethod  # meter to centimeter conversion
    def meter_cm(meter):
        return(100*meter)

    def cost(self):
        return self.price * self.quantity

    def area(self):
        # return math.pi * self.radius**2
        # p = self.perimeter() # wrong way to calculate perimeter
        p = self._perimeter()  # use local copy of perimeter()
        r = p / ( 2 * math.pi)
        return math.pi * r**2

    def perimeter(self):
        return 2 * math.pi * self.radius

    # local copy can be created in the lines after the actual method
    _perimeter = perimeter # make a local copy of perimeter

def main():
    # print("Center of the Ring is at:", Ring.center) # modify class variable
    # r = Ring(price=8) # modify only price
    # print("Radius:{0}, Cost:{1}".format(r.radius, r.cost()))
    # print("Radius:{0}, Perimeter:{1:0.2f}".format(r.radius, r.perimeter()))

    print() # check the new constructor 'diameter_init'
    d = Ring.diameter_init(diameter=10)
    print("Radius:{0}, Perimeter:{1:0.2f}, Area:{2:0.2f}".format(
        d.radius, d.perimeter(), d.area()))
    # print("Radius:{0}, Perimeter:{1:0.2f}".format(d.radius, d.perimeter()))
    # m = 10 # 10 meter
    # print("{0} meter = {1} centimeter".format(m, d.meter_cm(m)))

if __name__ == '__main__':
    main()

Below is the result for above listing,

$ python pythonic.py

Radius:5.0, Perimeter:31.42, Area:78.54

Now, we will get the correct results for the ‘box.py’ as well, as shown below,

>>> from box import *
>>> b = Box.diameter_init(10)
>>> b.radius
5.0
>>> b.perimeter()  # doubled value of perimeter (desired)
62.83185307179586
>>> b.area()  # desired area
78.53981633974483

11.5. Private attributes are not for privatizing the attributes

In Section 10.7, we saw that there is no concept of data-hiding in Python; and the attributes can be directly accessed by the the clients.

Note

There is a misconception that the double underscore (‘__’) are used for data hiding in Python. In this section, we will see the correct usage of ‘__’ in Python.

11.5.1. Same local copy in child and parent class

In previous section, we made a local copy of method ‘perimeter’ as ‘_perimeter’. In this way, we resolve the problem of overriding the parent class method.

But, if the child class (i.e. box.py) makes a local copy the perimeter as well with the same name i.e. _perimeter, then we will again have the same problem, as shown below,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# box.py
# user-3 : creating boxes for the ring

from pythonic import Ring

class Box(Ring):
    """ Modified perimeter for creating the box """

    def perimeter(self): # override the method 'perimeter'
        # perimeter is increased 2.0 times
        return Ring.perimeter(self) * 2.0

    _perimeter = perimeter

def main():
    b = Box(radius=8) # pass radius = 8
    print("Radius:", b.radius)
    print("Modified perimeter: %0.2f" % b.perimeter()) # (2*pi*radius) * 2

if __name__ == '__main__':
    main()

Now, we will have same problem as before,

>>> from box import *
>>> b = Box.diameter_init(10)
>>> b.area()  # wrong value of area again,
314.1592653589793

11.5.2. Use ‘__perimeter’ instead of ‘_perimeter’ to solve the problem

The above problem can be solved using double underscore before the perimeter instead of one underscore, as shown below,

  • First replace the _perimeter with __ perimeter in the ‘pythonic.py’ as below,
 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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# pythonic.py

import math
import time

class Ring(object):
    """ Here we will see the actual logic behind various pieces of Python
    language e.g. instances, variables, method and @property etc.
    Also, we will see the combine usage of these pieces to complete a
    design with Agile methodology.
    """

    # class variables
    date = time.strftime("%Y-%m-%d", time.gmtime()) # today's date "YYYY-mm-dd"
    center = 0.0 # center of the ring

    def __init__(self, date=date, metal="Copper", radius=5.0,
                price=5.0, quantity=5):
        """ init is not the constructor, but the initializer which
        initialize the instance variable

        self : is the instance

        __init__ takes the instance 'self' and populates it with the radius,
        metal, date etc. and store in a dictionary.

        self.radius, self.metal etc. are the instance variable which
        must be unique.
        """

        self.date = date
        self.metal = metal
        self.radius = radius
        self.price = price
        self.quantity = quantity


    # Multiple constructor
    # below constructor is added for the 'research organization' who are
    # doing their work based on diameters,
    @classmethod
    def diameter_init(cls, diameter):
        radius = diameter/2;  # change diameter to radius
        return cls(radius) # return radius

    @staticmethod  # meter to centimeter conversion
    def meter_cm(meter):
        return(100*meter)

    def cost(self):
        return self.price * self.quantity

    def area(self):
        # return math.pi * self.radius**2
        # p = self.perimeter() # wrong way to calculate perimeter
        p = self.__perimeter()  # use local copy of perimeter()
        r = p / ( 2 * math.pi)
        return math.pi * r**2

    def perimeter(self):
        return 2 * math.pi * self.radius

    # local copy can be created in the lines after the actual method
    __perimeter = perimeter # make a local copy of perimeter

def main():
    # print("Center of the Ring is at:", Ring.center) # modify class variable
    # r = Ring(price=8) # modify only price
    # print("Radius:{0}, Cost:{1}".format(r.radius, r.cost()))
    # print("Radius:{0}, Perimeter:{1:0.2f}".format(r.radius, r.perimeter()))

    print() # check the new constructor 'diameter_init'
    d = Ring.diameter_init(diameter=10)
    print("Radius:{0}, Perimeter:{1:0.2f}, Area:{2:0.2f}".format(
        d.radius, d.perimeter(), d.area()))
    # print("Radius:{0}, Perimeter:{1:0.2f}".format(d.radius, d.perimeter()))
    # m = 10 # 10 meter
    # print("{0} meter = {1} centimeter".format(m, d.meter_cm(m)))

if __name__ == '__main__':
    main()
  • Next replace the _perimeter with __ perimeter in the ‘box.py’ as below,
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# box.py
# user-3 : creating boxes for the ring

from pythonic import Ring

class Box(Ring):
    """ Modified perimeter for creating the box """

    def perimeter(self): # override the method 'perimeter'
        # perimeter is increased 2.0 times
        return Ring.perimeter(self) * 2.0

    __perimeter = perimeter

def main():
    b = Box(radius=8) # pass radius = 8
    print("Radius:", b.radius)
    print("Modified perimeter: %0.2f" % b.perimeter()) # (2*pi*radius) * 2

if __name__ == '__main__':
    main()

Important

_ _ is not designed for making the attribute private, but for renaming the attribute with class name to avoid conflicts due to same name as we see in above example, where using same name i.e. _perimeter in both parent and child class resulted in the wrong answer.

If we use _ _perimeter instead of _perimeter, then _ _perimeter will be renamed as _ClassName__perimeter. Therefore, even parent and child class uses the same name with two underscore, the actual name will be differed because python interpretor will add the ClassName before those names, which will make it different from each other.

  • Now, we have same name in both the files i.e. __perimeter, but problem will no longer occur as the ‘class-names’ will be added by the Python before __perimeter. Below is the result for both the python files,
$ python pythonic.py

Radius:5.0, Perimeter:31.42, Area:78.54
>>> from box import *
>>> b = Box.diameter_init(10)
>>> b.area()
78.53981633974483

Note

There is no notion of private attributes in Python. All the objects are exposed to users. * But, single underscore before the method or variable indicates that the attribute should not be access directly. * _ _ is designed for freedom not for privacy as we saw in this section.

11.6. @property

Now, we will see the usage of @property in Python.

11.6.1. Managing attributes

Currently, we are not checking the correct types of inputs in our design. Let’s see our ‘pythonic.py’ file again.

>>> from pythonic import *
>>> r = Ring()
>>> r.radius
5.0
>>> r.radius = "Meher"
>>> 2 * r.radius
'MeherMeher'

Note that in the above code, the string is saved in the radius; and the multiplication is performed on the string. We do not want this behavior, therefore we need to perform some checks before saving data in the dictionary, which can be done using @property, as shown below,

Important

  • @property decorator allows . operator to call the function. Here, self.radius will call the method radius, which returns the self._radius. Hence, _radius is stored in the dictionary instead of dict.
  • If we use ‘return self.radius’ instead of ‘return self._radius’, then the @property will result in infinite loop as self.property will call the property method, which will return self.property, which results in calling the @property again.
  • Further, we can use @property for type checking, validation or performing various operations by writing codes in the setter method e.g. change the date to todays date etc.
 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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# pythonic.py

import math
import time

class Ring(object):
    """ Here we will see the actual logic behind various pieces of Python
    language e.g. instances, variables, method and @property etc.
    Also, we will see the combine usage of these pieces to complete a
    design with Agile methodology.
    """

    # class variables
    date = time.strftime("%Y-%m-%d", time.gmtime()) # today's date "YYYY-mm-dd"
    center = 0.0 # center of the ring

    def __init__(self, date=date, metal="Copper", radius=5.0,
                price=5.0, quantity=5):
        """ init is not the constructor, but the initializer which
        initialize the instance variable

        self : is the instance

        __init__ takes the instance 'self' and populates it with the radius,
        metal, date etc. and store in a dictionary.

        self.radius, self.metal etc. are the instance variable which
        must be unique.
        """

        self.date = date
        self.metal = metal
        self.radius = radius
        self.price = price
        self.quantity = quantity

    @property
    # method-name should be same as attribute i.e. 'radius' here
    def radius(self):
        return self._radius # _radius can be changed with other name

    @radius.setter
    def radius(self, val):
        # 'val' should be float or int
        if not isinstance(val, (float, int)):
            raise TypeError("Expected: float or int")
        self._radius = float(val)

    # Multiple constructor
    # below constructor is added for the 'research organization' who are
    # doing their work based on diameters,
    @classmethod
    def diameter_init(cls, diameter):
        radius = diameter/2;  # change diameter to radius
        return cls(radius) # return radius

    @staticmethod  # meter to centimeter conversion
    def meter_cm(meter):
        return(100*meter)

    def cost(self):
        return self.price * self.quantity

    def area(self):
        # return math.pi * self.radius**2
        # p = self.perimeter() # wrong way to calculate perimeter
        p = self.__perimeter()  # use local copy of perimeter()
        r = p / ( 2 * math.pi)
        return math.pi * r**2

    def perimeter(self):
        return 2 * math.pi * self.radius

    # local copy can be created in the lines after the actual method
    __perimeter = perimeter # make a local copy of perimeter

def main():
    # print("Center of the Ring is at:", Ring.center) # modify class variable
    # r = Ring(price=8) # modify only price
    # print("Radius:{0}, Cost:{1}".format(r.radius, r.cost()))
    # print("Radius:{0}, Perimeter:{1:0.2f}".format(r.radius, r.perimeter()))

    print() # check the new constructor 'diameter_init'
    d = Ring.diameter_init(diameter=10)
    print("Radius:{0}, Perimeter:{1:0.2f}, Area:{2:0.2f}".format(
        d.radius, d.perimeter(), d.area()))
    # print("Radius:{0}, Perimeter:{1:0.2f}".format(d.radius, d.perimeter()))
    # m = 10 # 10 meter
    # print("{0} meter = {1} centimeter".format(m, d.meter_cm(m)))

if __name__ == '__main__':
    main()
  • Below is the results for above code,
>>> from pythonic import *
>>> r = Ring()
>>> r.radius
5.0
>>> r.radius = 1
>>> r.radius
1.0
>>> r.radius = 3.0
>>> r.radius
3.0
>>> r.radius = "Meher"
Traceback (most recent call last): [...]
TypeError: Expected: float or int
  • Below is the dictionary of the object ‘r’ of code. Note that, the ‘radius’ does not exist there anymore, but this will not break the codes of other users, due to following reason.

Note

@property is used to convert the attribute access to method access.

In the other words, the radius will be removed from instance variable list after defining the ‘property’ as in above code. Now, it can not be used anymore. But, @property decorator will convert the attribute access to method access, i.e. the dot operator will check for methods with @property as well. In this way, the code of other user will not break.

>>> r.__dict__
{'date': '2017-11-01', 'metal': 'Copper', '_radius': 3.0, 'price': 5.0, 'quantity': 5}

11.6.2. Calling method as attribute

If a method is decorated with @property then it can be called as ‘attribute’ using operator, but then it can not be called as attribute, as shown in below example,

>>> class PropertEx(object):
...     def mes_1(self):
...             print("hello msg_1")
...
...     @property
...     def mes_2(self):  # can be called as attribute only
...             print("hello msg_2")
...
>>> p = PropertEx()
>>> p.mes_1()
hello msg_1
>>> p.mes_2
hello msg_2
>>> p.mes_2()  # can not be called as method
hello msg_2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable

11.6.3. Requirement from research organization

Now, we get another rule from the research organization as below,

  • we do not want to store radius as instance variable,
  • instead convert radius into diameter and save it as instance variable in the dictionary.

Note

This condition will raise following two problems,

  • Main problem here is that, other users already started using this class and have access to attribute ‘radius’. Now, if we replace the key ‘radius’ with ‘diameter’, then their code will break immediately.
  • Also, we need to update all our code as everything is calculated based on radius; and we are going to remove these attribute from diameter.

This is the main reason for hiding attributes in java and c++, as these languages have no easy solution for this. Hence, these languages use getter and setter method.

But in python this problem can be solved using @property. @property is used to convert the attribute access to method access.

In the other words, as radius will be removed from instance variable list after meeting the need of the organization, therefore it can not be used anymore. But, @property decorator will convert the attribute access to method access, i.e. the dot operator will check for methods with @property as well. In this way, the codes of other users will not break, as shown next.

 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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# pythonic.py

import math
import time

class Ring(object):
    """ Here we will see the actual logic behind various pieces of Python
    language e.g. instances, variables, method and @property etc.
    Also, we will see the combine usage of these pieces to complete a
    design with Agile methodology.
    """

    # class variables
    date = time.strftime("%Y-%m-%d", time.gmtime()) # today's date "YYYY-mm-dd"
    center = 0.0 # center of the ring

    def __init__(self, date=date, metal="Copper", radius=5.0,
                price=5.0, quantity=5):
        """ init is not the constructor, but the initializer which
        initialize the instance variable

        self : is the instance

        __init__ takes the instance 'self' and populates it with the radius,
        metal, date etc. and store in a dictionary.

        self.radius, self.metal etc. are the instance variable which
        must be unique.
        """

        self.date = date
        self.metal = metal
        self.radius = radius
        self.price = price
        self.quantity = quantity

    @property
    # method-name should be same as attribute i.e. 'radius' here
    def radius(self):
        # return self._radius # _radius can be changed with other name
        return self.diameter/2 # _radius can be changed with other name

    @radius.setter
    def radius(self, val):
        # 'val' should be float or int
        if not isinstance(val, (float, int)):
            raise TypeError("Expected: float or int")
        # self._radius = float(val)
        self.diameter = 2 * float(val)

    # Multiple constructor
    # below constructor is added for the 'research organization' who are
    # doing their work based on diameters,
    @classmethod
    def diameter_init(cls, diameter):
        radius = diameter/2;  # change diameter to radius
        return cls(radius) # return radius

    @staticmethod  # meter to centimeter conversion
    def meter_cm(meter):
        return(100*meter)

    def cost(self):
        return self.price * self.quantity

    def area(self):
        # return math.pi * self.radius**2
        # p = self.perimeter() # wrong way to calculate perimeter
        p = self.__perimeter()  # use local copy of perimeter()
        r = p / ( 2 * math.pi)
        return math.pi * r**2

    def perimeter(self):
        return 2 * math.pi * self.radius

    # local copy can be created in the lines after the actual method
    __perimeter = perimeter # make a local copy of perimeter

def main():
    # print("Center of the Ring is at:", Ring.center) # modify class variable
    # r = Ring(price=8) # modify only price
    # print("Radius:{0}, Cost:{1}".format(r.radius, r.cost()))
    # print("Radius:{0}, Perimeter:{1:0.2f}".format(r.radius, r.perimeter()))

    print() # check the new constructor 'diameter_init'
    d = Ring.diameter_init(diameter=10)
    print("Radius:{0}, Perimeter:{1:0.2f}, Area:{2:0.2f}".format(
        d.radius, d.perimeter(), d.area()))
    # print("Radius:{0}, Perimeter:{1:0.2f}".format(d.radius, d.perimeter()))
    # m = 10 # 10 meter
    # print("{0} meter = {1} centimeter".format(m, d.meter_cm(m)))

if __name__ == '__main__':
    main()
  • Lets check the output of this file first, which is working fine as below. Note that the dictionary, now contains the ‘diameter’ instead of ‘radius’
>>> from pythonic import *
>>> r = Ring()
>>> r.__dict__  # dictionary contains 'diameter' not 'radius'
{'date': '2017-11-01', 'metal': 'Copper',
'diameter': 10.0, 'price': 5.0, 'quantity': 5}
>>>
>>> r.radius  # radius is still accessbile
5.0
>>> r.area()  # area is working fine
78.53981633974483
>>> r.diameter # diameter is accessbile
10.0
  • Next verify the output for the ‘box.py’ file again.
>>> from box import *
>>> b = Box.diameter_init(10)
>>> b.area()  # area is working fine
78.53981633974483
>>> b.__dict__
{'date': 5.0, 'metal': 'Copper', 'diameter': 10.0, 'price': 5.0, 'quantity': 5}
>>>