Python Classes and Objects in Depth – A Complete Guide

Python Classes and Objects in Depth – A Complete Guide

3. März 2022 Aus Von admin

We can think of Classes as blueprints for Objects. Various Object-instances can be created based on one Class blueprint and attributes as well as functionalities defined in the Class blueprint will be passed as Object-properties after the Object instance has been created. Classes are callable and whenever we call a Class we create an Object instance of that particular Class. We say that Objects instances will be created based on a certain Class or Objects are of type of a certain Class.

In Classes functions are called methods. The init-method __init__() in a Class is called whenever a Class is called to create an Object instance using Class(). The init-method define attributes, variables and set the Object instance to a defined initial state. Therefore the init-Method is called constructor.

The Parameter self

Let’s imagine a Class Car that has attributes like brand, color and horsepower. We define the init-method using def __init__(self) and define all attributes using self.<attribut> = <value> in the init-method. A peculiarity of Python is that the init-method has at least one parameter self which is always passed by default.

The parameter self ensure that each attribute and its values will be linked to the correct Object instance stored in a certain memory address space.

 class Car:
	def __init__(self):
		self.brand = None
        self.color = None
        self.horsepower = None
	
car1 = Car()
print(car1)

#Print-Output
#<__main__.Car object at 0x10e0ffc40>

When an Object instance car1 of the Class Car will be created using car1 = Car() the init-method within the Class-Definition will be called and the self parameter will be passed to the new Object instance car1. If we print the Object instance car1 on the console using print(car1) we don’t see a list of attributes or something similar, but we see the specific location in memory where the Object instance car1 has been stored. This memory address is stored in the self parameter.

To show the peculiarity of Python with the self parameter more clearly, please take a look at the following notation.

class Car:
    def __init__(self):
        self.brand = None
        self.color = None
        self.horsepower = None
        self.self = self #reference is stored in self


car1 = Car()
print(car1)
print(car1.self)
print(car1.brand)

#Print-Output
#<__main__.Car object at 0x10e0ffc40>
#<__main__.Car object at 0x10e0ffc40>
#None

Within the init-method the value of the self parameter is assigned to the attribute self.self using self.self = self. The output for print(car1.brand) is None as expected. print(car1) and print(car1.self) show exactly the same print output. The memory address ist stored in the parameter self and the Object instance car1 is a reference to this memory address.

The dot notation

We can assign values to each attribute, access each value and print the value to the console using the dot notation print(<object-instance>.<attribute-name>).

class Car:
	def __init__(self):
		self.brand = None
        self.color = None
        self.horsepower = None
        
car1 = Car()
car1.brand = "BMW"
car1.color = "red"
car1.horsepower = 120

car2 = Car()
car2.brand = "Audi"
car2.color = "blue"
car2.horsepower = 150

print(car1.brand)
print(car2.brand)

#Print-Output
#BMW
#Audi

We create the Object car1 as an instance of the Class Car. car1 is a BMW, is red, has 120 PS. And we create another Object car2. car2 is an Audi, is blue, has 150 PS. car1 and car2 are Object Instances of the Class Car. It is also often said that car1 and car2 are Objects of type Car. We access the individual attributes of the objects with the dot notation and get a print output of the brand using print(car1.brand) and print(car2.brand).

Working with Object instances

To make the code clearer the attribute values ​​can also be passed as parameters directly during object instantiation.

class Car:
	def __init__(self, brand, color, horsepower):
		self.brand = brand
        self.color = color
        self.horsepower = horsepower
        
car1 = Car("BMW", "red", 120)
car2 = Car("Audi", "blue", 150)

print(car1.brand)
print(car2.brand)

#Print-Output
#BMW
#Audi

Therefore the init-method must have beside the parameter self also the parameters brand, color and horsepower. When we create the Object instances car1 and car2 of the Class Car we pass the parameters in the brackets during the call of the Class Car .

As mentioned above, functions in Classes are called methods. We have already got to know one method, the init method __init__() that will be executed when an Object instance will be created.

Classes can have also methods designed by the developer himself as well. In our example we add the drive-Method to the Class Car as follows.

class Car:
    def __init__(self, brand, color, horsepower):
        self.brand = brand
        self.color = color
        self.horsepower = horsepower
        self.x = 0
        self.y = 0

    def drive(self):
        self.x += 10
        self.y += 15
		print("X and Y have been increased by 10 and 15")

car1 = Car("BMW", "red", 120)

print(car1.x)
print(car1.y)

car1.drive()

print(car1.x)
print(car1.y)

#Print-Output
#0
#0
#X and Y have been increased by 10 and 15
#10
#15	

Here we add 2 additional attributes to the constructor self.x and self.y and set both attributes initially to the value 0. The drive-method drive() is defined to increase the values self.x by 10 and self.y by 15 and to print a string to the console. In the above example you can see that the values of self.x and self.y are 0 when we print them to the console directly after the Object car1 has been created. Then we call the drive-method on the Object instance car1 using car1.drive() and self.x and self.y have been increase by 10 or 15 plus the string has been printed to the console.

Inheritance

There can be several classes in programs, including classes that are very similar and only differ in details. This is where the concept of inheritance comes into play. We want to write a program in which there are 3 classes of vehicles: there are cars, trucks and motorcycles.

We assume all vehicles have the attributes brand, color, and horsepower, but cars should have two drive wheels, trucks four, and motorcycles should have only one drive wheel. In addition, the vehicles have different drive-methods because they can move at different speeds.

class Car:
    def __init__(self, brand, color, horsepower):
        self.brand = brand
        self.color = color
        self.horsepower = horsepower
        self.drivewheels = 2
        self.x = 0
        self.y = 0

    def drive(self):
        self.x += 10
        self.y += 15
        print("Car accelerates by 10 and 15")

class Truck:
    def __init__(self, brand, color, horsepower):
        self.brand = brand
        self.color = color
        self.horsepower = horsepower
        self.drivewheels = 4
        self.x = 0
        self.y = 0

    def drive(self):
        self.x += 5
        self.y += 10
        print("Truck accelerates by 5 and 10")

class Motorcycle:
    def __init__(self, brand, color, horsepower):
        self.brand = brand
        self.color = color
        self.horsepower = horsepower
        self.drivewheels = 1
        self.x = 0
        self.y = 0

    def drive(self):
        self.x += 30
        self.y += 40
        print("Motorcycle accelerates by 30 and 40")

car1 = Car("BMW", "blue", 120)
truck1 = Truck("Volvo", "black", 270)
moto1 = Motorcycle("Yamaha", "green", 70)

print(car1.brand, car1.x, car1.y)
car1.drive()
print(car1.brand, car1.x, car1.y)

print(truck1.brand, truck1.y, truck1.y)
truck1.drive()
print(truck1.brand, truck1.y, truck1.y)

print(moto1.brand, moto1.x, moto1.y)
moto1.drive()
print(moto1.brand, moto1.x, moto1.y)

#Print-Output
#BMW 0 0
#Car accelerates by 10 and 15
#BMW 10 15
#Volvo 0 0
#Truck accelerates by 5 and 10
#Volvo 10 10
#Yamaha 0 0
#Motorcycle accelerates by 30 and 40
#Yamaha 30 40

We create 3 Object instances from Car, Truck and Motorcycle and have access to their attributes and to their individual drive-methods. Everything is working fine but most of the code is redundant.

Instead of duplicating the code in each Class, we can define a parent Class from which all Child classes inherit their properties. The parent Class in our example will be the Class Vehicle.

class Vehicle:
    def __init__(self, brand, color, horsepower):
        self.brand = brand
        self.color = color
        self.horsepower = horsepower
        self.x = 0
        self.y = 0

	def inheritedmethod(self):
		print("this method was inherited")

class Car(Vehicle):
    drivewheels = 2

    def drive(self):
        self.x += 10
        self.y += 15
        print("Car accelerates by 10 and 15")

class Truck(Vehicle):
    drivewheels = 4

    def drive(self):
        self.x += 5
        self.y += 10
        print("Truck accelerates by 5 and 10")

class Motorcycle(Vehicle):
    drivewheels = 1

    def drive(self):
        self.x += 30
        self.y += 40
        print("Motorcycle accelerates by 30 and 40")


car1 = Car("BMW", "blue", 120)
truck1 = Truck("Volvo", "black", 270)
moto1 = Motorcycle("Yamaha", "green", 70)

print(car1.brand, car1.x, car1.y, car1.drivewheels)
car1.drive()
print(car1.brand, car1.x, car1.y, car1.drivewheels)

print(truck1.brand, truck1.y, truck1.y, truck1.drivewheels)
truck1.drive()
print(truck1.brand, truck1.y, truck1.y, truck1.drivewheels)

print(moto1.brand, moto1.x, moto1.y, moto1.drivewheels)
moto1.drive()
print(moto1.brand, moto1.x, moto1.y, moto1.drivewheels)
moto1.inheritedmethod()

#Print-Output
#BMW 0 0 2
#Car accelerates by 10 and 15
#BMW 10 15 2
#Volvo 0 0 4
#Truck accelerates by 5 and 10
#Volvo 10 10 4
#Yamaha 0 0 1
#Motorcycle accelerates by 30 and 40
#Yamaha 30 40 1
#this method was inherited

The constructor __init__() of the parent-Class Vehicle contain the initial setup that is inherited to the child-Classes. The parent-Class Vehicle also inherit the method inheritedmethod() to each child-Class. Each Object instance created from a child-Class will have this initial setup. The child-Classes itself have no __init__() constructor in these examples. Each Object instance such as car1, truck1 and moto1 created from their child-Classes Car, Truck and Motorcycle can access all inherited properties from their common parent-Class Vehicle and their individual Class attributes drivewheels and the individual methods drive().

note: I will explain Class-attributes later in the text

Inherited methods from the parent-Class can be overwritten in the child-Class.

class Vehicle:
    def __init__(self, brand, color, horsepower):
        self.brand = brand
        self.color = color
        self.horsepower = horsepower
        self.x = 0
        self.y = 0

    def inheritedmethod(self):
        print("this method was inherited")

class Car(Vehicle):
    drivewheels = 2

    def drive(self):
        self.x += 10
        self.y += 15
        print("Car accelerates by 10 and 15")

    def inheritedmethod(self):
        print("this method was inherited and! overwritten")

car1 = Car("BMW", "blue", 120)

print(car1.brand, car1.x, car1.y, car1.drivewheels)
car1.drive()
print(car1.brand, car1.x, car1.y, car1.drivewheels)
car1.inheritedmethod()

#Print-Output
#BMW 0 0 2
#Car accelerates by 10 and 15
#BMW 10 15 2
#this method was inherited and! overwritten

In order to overwrite an inherited method, the same method head is simply written down again in the child-Class and the code of the method body is then adapted accordingly.

The super() method

A reference to the parent-Class can be established using the super() method. This allows us to take methods from the parent-Class and add additional properties in the code of the method in the child-Class.

class Vehicle:
    def __init__(self, brand, color, horsepower):
        self.brand = brand
        self.color = color
        self.horsepower = horsepower
        self.x = 0
        self.y = 0

    def inheritedmethod(self):
        print("this method was inherited")

class Car(Vehicle):
    def __init__(self, brand, color, horsepower, drivewheels):
        super().__init__(brand, color, horsepower)
        self.drivewheels = drivewheels

    def drive(self):
        self.x += 10
        self.y += 15
        print("Car accelerates by 10 and 15")

    def inheritedmethod(self):
        super().inheritedmethod()
        print("this method was inherited and! overwritten")

class Truck(Vehicle):
    def __init__(self, brand, color, horsepower, drivewheels):
        super().__init__(brand, color, horsepower)
        self.drivewheels = drivewheels

    def drive(self):
        self.x += 5
        self.y += 10
        print("Truck accelerates by 5 and 10")
        
car1 = Car("BMW", "blue", 120, 2)
truck1 = Truck("Magirus Deuz", "grey", 177, 4)

print(car1.brand, car1.x, car1.y, car1.drivewheels)
car1.drive()
print(car1.brand, car1.x, car1.y, car1.drivewheels)
car1.inheritedmethod()

print(truck1.brand, truck1.y, truck1.y, truck1.drivewheels)
truck1.drive()
print(truck1.brand, truck1.y, truck1.y, truck1.drivewheels)
truck1.inheritedmethod()

#Print-Output
#BMW 0 0 2
#Car accelerates by 10 and 15
#BMW 10 15 2
#this method was inherited
#this method was inherited and! overwritten
#Magirus Deuz 0 0 4
#Truck accelerates by 5 and 10
#Magirus Deuz 10 10 4
#this method was inherited

We had no __init__() method so far in our child-Classes but we defined for each child-Class the specific Class attribute drivewheels. This approach works fine. To understand how the super() method works we code for each child-Class a specific __init__() method and provide all parameters plus the parameter drivewheels. This means the value for drivewheels will be passed when the Object-instance from the child-Class is created and we have more flexibility there. Finally, it is possible that cars for example can have 4 drive wheels instead of 2. Then we use the super() method to refer to the __init__() method in the parent-Class and pass the properties required there and create the additional property self.drivewheels = drivewheels . In the child-Class Car we also use the super() method in the method inheritedmethod and add additional functionality (simple print command). We don’t do this in the child-Class Truck. Then we create the Object instances car1 and truck1 and access the properties and methods.

Special attributes

The attributes used so far are all 100 percent usable outside of the respective class definition. In the code below I have simplified the classes considerably to focus on the essentials.

class Vehicle:
    def __init__(self, brand, color, horsepower):
        self.brand = brand
        self.color = color
        self.horsepower = horsepower

class Car(Vehicle):
    def __init__(self, brand, color, horsepower, drivewheels):
        super().__init__(brand, color, horsepower)
        self.drivewheels = drivewheels

car1 = Car("BMW", "blue", 120, 2)

print(car1.brand)
car1.brand = "Audi"
print(car1.brand)

#Print-Output
#BMW
#Audi

We access the attribute brand and print the value BMW to the console. Then we change the value from BMW to Audi using the dot notation and overwrite the existing value.

Sometimes developers of a Class want to signal developers that attributes should only be used inside the Class and not outside. Developers can mark attributes with one underscore _ or with two underscores __ to show that attributes should not or can not be easily overridden.

class Vehicle:
    def __init__(self, brand, color, vmax):
        self._brand = brand
        self.__color = color
        self.vmax = vmax

class Car(Vehicle):
    def __init__(self, brand, color, vmax, drivewheels):
        super().__init__(brand, color, vmax)
        self.drivewheels = drivewheels

car1 = Car("BMW", "blue", 100, 2)

print(car1._brand)
car1._brand = "Audi"
print(car1._brand)

print(car1._Vehicle__color)
car1._Vehicle__color = "black"
print(car1._Vehicle__color)

#Print-Output
#BMW
#Audi
#blue
#black

In the example above we have the attribute _brand with one underscore. This shows the developer that _brand is a special attribute which is defined in the parent-Class Vehicle and inherited to the child-Class Car and should not be overwritten. However, overwriting is possible without any problems using the dot notation. The attribute __color is also defined in parent-Class Vehicle and inherited to child-Class Car but can not be overwritten using the standard dot notation. The value of the attribute __color can only be accessed using a different attribute name _Vehicle__color. Obviously the name of the attribute __color can only be accessed if we extend the attribute name at the beginning with <_parent-Classname><__attribute-name>. This is called Name Mangling.

Multiple inheritance

So far we had a parent-Class and single inheritance of properties to a child-Class. But it is also possible to define more parent-Classes and inherit their properties to a child-Class. This is called multiple inheritance. With multiple inheritance we can create complex data structures. In the following code I define 3 parent-Classes Person, Organisation, Building and 2 child-Classes Employee and Location.

class Person:
    def __init__(self, userid, name, lastname, age):
        self.userid = userid
        self.name = name
        self.lastname = lastname
        self.age = age

class Organisation:
    def __init__(self, company, orgunit):
        self.company = company
        self.orgunit = orgunit

class Building:
    def __init__(self, country, postalcode, street, city):
        self.country = country
        self.postalcode = postalcode
        self.street = street
        self.city = city

class Employee(Person, Organisation):
    def __init__(self, userid, name, lastname, age, company, orgunit):
        Person.__init__(self, userid, name, lastname, age)
        Organisation.__init__(self,  company, orgunit)

class Location(Building, Organisation):
    def __init__(self, country, postalcode, street, city, company, orgunit):
        Building.__init__(self, country, postalcode, street, city)
        Organisation.__init__(self, company, orgunit)


employee1 = Employee("12345", "Parick", "Rottländer", 54, "Company 1", "OE123456")
employee2 = Employee("67890", "Carol", "Meier", 32, "Company 2", "OE678901")
location1 = Location("Germany", "81234", "First Street 20", "Munich", "Company 1", "OE123456")
location2 = Location("Italy", "I23345", "Second Street 100", "Verona", "Company 2", "OE678901")

print(employee1.name, employee1.orgunit)
print(employee2.name, employee2.orgunit)
print(location1.country, location1.city)
print(location2.country, location2.city)

#Print-Output
#Parick OE123456
#Carol OE678901
#Germany Munich
#Italy Verona

In the code example above we define the child-Class Employee with properties from the parent-Classes Person and Organisation. Child-Class Building is defined with properties from parent-Classes Location and Organisation. Therefore the child-Class Employee inherits properties from parent-Classes Person and Organization and the child-Class Location inherit properties from parent-Classes Building and Organisation. In each child-Class the first __init__() constructor define the complete setup of an Object instance. Then each inherited parent-Class __init()__ constructor is overwritten using the dot notation <parent-Class>.__init()__. Then we create the object instances employee1, employee2 and location1 and location2 passing all required attributes and have access to each of those attributes using the dot notation.

Instance- and Class-attributes

If attributes of a Class are defined within the __init()__ method, these are Instance-attributes and are available as initial setup in each Object instance created from that Class. If we define attributes in a Class before the __init()__ method then these are Class-attributes.

class Person:
    counter = 0
    def __init__(self, name, lastname, age):
        self.userid = "P" +"-" +str(Person.counter)
        self.name = name
        self.lastname = lastname
        self.age = age
        Person.counter += 1

class Organisation:
    def __init__(self, company, orgunit):
        self.company = company
        self.orgunit = orgunit

class Employee(Person, Organisation):
    def __init__(self, name, lastname, age, company, orgunit):
        Person.__init__(self, name, lastname, age)
        Organisation.__init__(self,  company, orgunit)

print(Person.counter)
employee1 = Employee("Parick", "Rottländer", 54, "Company 1", "OE123456")
print(employee1.userid)
print(Person.counter)
employee2 = Employee("Carol", "Meier", 32, "Company 2", "OE678901")
print(employee2.userid)
print(Person.counter)

#Print-Output
#0
#P-0
#1
#P-1
#2

In the Class Person I define the Class-attribute counter and set its value to 0. Within the __init__() method of the Class Person I access the Class-attribute counter using str(Person.counter) to create a unique userid. Whenever an Object instance of Person is created Person.counter increase by 1 (see the respective print-output).

Modules

In the examples above we write the code in one Python file with the ending <filename>.py. Each Python file is a Module itself and can be executed independently. But a developer must not code any method on his own he or she can use existing code from third parties by loading other modules into his Python file. After modules have been loaded methods and properties defined in those modules can be user in the code.

Python has numerous built-in modules. All of them can be used for free by developers in the code. In the following examples I will use the build-in module math because math contain the parameter pi that we need for the calculation of the area of a circle for a given radius.

#objectMain.py

import math

class Circle:
    def __init__(self, radius):
        self.radius = radius

    #calculate area from given radius
    def area(self):
        return math.pi * self.radius ** 2

c1 = Circle(5)
print(c1.area())

#Print-Output
#78.53981633974483

In the file objectMain.py we first import math and define the Class Circle. Then we pass the parameter radius in the __init()__ constructor. The instance attribute self.radius receive the given value radius when an Object instance is created with a given radius value. The area()method use the parameter pi from the math Module and return the calculated area for the given radius.

But we can also develop methods on our own separately in separate Python files and make them available as modules. We can then either import the entire module or just individual methods from the module. I define a method radius() in a separate module objectModuleCalc.py to calculate the radius from a given area.

#objectModuleCalc.py

import math

#calculate radius from given area
def radius(area):
    return math.sqrt(area/math.pi)

To use the method radius() in my objectMain.py file I must import the module from the objectModuleCalc.py file using import <filename>.

#objectMain.py

import math
import objectModuleCalc

class Circle:
    def __init__(self, radius):
        self.radius = radius

    #calculate area from given radius
    def area(self):
        return math.pi * self.radius ** 2

c1 = Circle(5)
print(c1.area())

print(objectModuleCalc.radius(78.54))

#Print-Output
#78.53981633974483
#5.000005846084075

After the import I can access the method radius() in the imported module using the dot notation<filename>.<method>.

Another way to import the Module from the objectModuleCalc.py file is using an alias import <filename> as <alias>.

#objectMain.py

import math
import objectModuleCalc as calc

class Circle:
    def __init__(self, radius):
        self.radius = radius

    #calculate area from given radius
    def area(self):
        return math.pi * self.radius ** 2

c1 = Circle(5)
print(c1.area())

print(calc.radius(78.54))

#Print-Output
#78.53981633974483
#5.000005846084075

After the import I can access the code in the imported module using the dot notation<alias>.<method>.

In the example above you see that the method area() can only be used when an object instance has been created. The method is used on the Object instance using c1.area().

Static Methods

With static methods we can use a method defined in a class without having created an Object instance. We have no Class reference and no Object reference. We use the Function Decorator @staticmathod before the method definition.

#objectMain.py

import math

class Circle:
    def __init__(self, radius):
        self.radius = radius

    #calculate area from given radius
    @staticmethod
    def area(radius):
        return math.pi * radius ** 2

print(Circle.area(5))

#Print-Output
#78.53981633974483

Static methods have no reference to the Class and no reference to the Object instance once it has been created. Therefore we can not access any attributes defined in the Class like Class-attributes or the Instance-attribute self.radius. This is the reason why we must pass a separate parameter radius into the method area(). The value of the parameter radius can then be used by the variable radius within the method area() when we call the method using <Classname>.<method>(<parameter>).

Class methods

With Class methods we can use a method defined in a Class also without having created an Object instance. We use the Function Decorator @classmethod before the method definition.

#objectMain.py

import math

class Circle:
    counter = 0
    def __init__(self, radius):
        self.radius = radius
        Circle.counter += 1

    #calculate area from given radius
    @staticmethod
    def area(radius):
        return math.pi * radius ** 2

    #print the current counter value
    @classmethod
    def printcounter(cls):
        print("Actual value of counter is: " +str(cls.counter))

Circle.printcounter()
c1 = Circle(5)
Circle.printcounter()

#Print-Output
#Actual value of counter is: 0
#Actual value of counter is: 1

I define the Class method printcounter(cls). The parameter cls is the Class reference to ensure that also Class-attributes like counter can be used within the Class-method. The Class-method is called using <Classname>.<class-method-name>. In the Class Circle we define the Class-attribute counter ant set it to the value of 0. The __init__() constructor create an Object instance of type Circle when we call Circle() . Each time we create an Object instance, the counter value of the Class Circle is incremented by 1. We define the Class-method printcounter(cls) and pass the parameter cls to establish the Class reference. Then we can access the Class attribute counter in the printcounter(cls) method using cls.counter. We call the Class method using <Classname>.<classmethod> and see in the print output that the counter value increased by 1 after we created the Object instance c1.

note: In the __init__() constructor above we still use the parameter radius and we pass a value for radius when we create an Object instance from Class Circle. This does not really make sense in this example because we do not use it as we the method area() is defined as static method and there we pass a separate radius value with the method call. Because we will need passing the radius in the text below I decided to keep the __init__() constructor as it is to avoid confusion.

The Class method printcounter(cls) above return a print() function. So when we call the method printcounter(cls) we get a print output at the console.

A Class-method can also return an Object instance. Therefore I define another Class-method radius(cls, area) in the code below. This Class-method has 2 parameters: cls is the Class parameter and stand for the Class reference to ensure that also Class-attributes can be used within the Class method and area to pass the given area value to calculate the radius.

#objectMain.py

import math

class Circle:
    counter = 0
    def __init__(self, radius):
        self.radius = radius
        Circle.counter += 1

    #calculate area from given radius
    @staticmethod
    def area(radius):
        return math.pi * radius ** 2

    #print the current counter value
    @classmethod
    def printcounter(cls):
        print("Actual value of counter is: " +str(cls.counter))

    #calculate radius from given area
    @classmethod
    def radius(cls, area):
        r = math.sqrt(area / math.pi)
        return Circle(r)

Circle.printcounter()
c1 = Circle(5)
print(type(c1))
print(c1.radius)
Circle.printcounter()
c2 = Circle.radius(78)
print(type(c2))
print(c2.radius)
Circle.printcounter()

#Print-Output
#Actual value of counter is: 0
#<class '__main__.Circle'>
#5
#Actual value of counter is: 1
#<class '__main__.Circle'>
#4.982787485166879
#Actual value of counter is: 2

The Class method radius()here is the same method we defined above in a separate module. Here we use radius() to calculate the radius r for a given area value that we pass with the method call. The Class method radius() return an Object instance of type Circle passing the calculated radius using return Circle(r). After we create the Object instance c1 using c1 = Circle(5) we see in the print output above that c1 is of type Circle class '__main__.Circle . We can access the passed radius value using the dot notation c1.radius. Then we call the Class-method Circle.radius(cls, area) with the area parameter 78 and assign the return value to the variable c2 using c2 = Circle.radius(78). As we expected c2 is also of type class '__main__.Circle . We can access the calculated radius value using c2.radius. In the print output you also see that the counter increased from 0 to 2 meaning that 2 Object instances c1 and c2 both of type Circle have been created.

A Class-method can return an Object instance of type of the calling class. Therefore I define a new child-Class Rim. Rim inherits all properties of the parent-Class Circle using Rim(Circle) and Rim has no __init__() constructor. Instead we use pass so that we can create an Object instance of Rim and all properties come from the parent-Class Circle.

#objectMain.py

import math

class Circle:
    counter = 0
    def __init__(self, radius):
        self.radius = radius
        Circle.counter += 1

    #calculate area from given radius
    @staticmethod
    def area(radius):
        return math.pi * radius ** 2

    #print the current counter value
    @classmethod
    def printcounter(cls):
        print("Actual value of counter is: " +str(cls.counter))

    #calculate radius from given area
    @classmethod
    def radius(cls, area):
        r = math.sqrt(area / math.pi)
        return cls(r)

class Rim(Circle):
    pass

Circle.printcounter()
c2 = Circle.radius(78)
print(type(c2))
print(c2.radius)

Circle.printcounter()
r1 = Rim.radius(120)
print(type(r1))
print(r1.radius)

Circle.printcounter()

#Print-Output
#Actual value of counter is: 0
#<class '__main__.Circle'>
#4.982787485166879
#Actual value of counter is: 1
#<class '__main__.Rim'>
#6.180387232371033
#Actual value of counter is: 2

The Class method radius() in the parent-Class Circle has changed a bit: The method return not necessarily an Object instance of type Circle but an Object instance of the calling Class return cls(r). Then we call the Class method Circle.radius(cls, area) with the area parameter 78 and assign the return value to the variable c2 using c2 = Circle.radius(78). As expected c2 is of type class '__main__.Circle . When we call the Class method using Rim.radius(cls, area) with the area parameter 120 and assign the return value to the variable r1 using r1 = Rim.radius(120) we see that r1 is of type class '__main__.Rim .

Property attributes and setters

In the text above we have already dealt with attributes that begin with one underscore _ or double underscores __ and thus indicate that these attributes should be touched carefully from the outside or even cannot be changed at all in the case when we use double underscores.

Here we want to deal again with accessing attributes and changing their values. To show the problem I use the Class Circle and the method area() that is defined within the Class Circle to calculate the area value for the given radius that we pass when we create an Object instance c1using c1 = Circle(<radius>).

import math

class Circle:
    def __init__(self, radius):
        self.radius = radius

    # calculate area from given radius
    def area(self):
        return math.pi * self.radius ** 2

c1 = Circle(5)
print(c1.radius)
print(c1.area())
c1.radius = -10
print(c1.radius)

#Print-Output
#5
#78.53981633974483
#-10
#print(c1.area())
#314.1592653589793

In the code above you see that we are able to change the value of radius from outside to -10 using c1.radius = -10. This doesn’t make much sense, even if the value of the area is calculated correctly because self.radius ** 2 in the area() method.

Then we define the instance attribute self._radius in the __init__() constructor. The radius value passed when we create the Object instance will be assigned to self_radius using self_radius = radius . The underscore shows that the attribute should be touched carefully from the outside.

import math

class Circle:
    def __init__(self, radius):
        self._radius = radius

    # calculate area from given radius
    def area(self):
        return math.pi * self._radius ** 2

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, radius):
        if radius >= 0:
            self._radius = radius

c1 = Circle(5)

print(c1.radius)
print(c1.area())

c1.radius = -10
print(c1.radius)
print(c1.area())

c1.radius = 10
print(c1.radius)
print(c1.area())

c1._radius = -10
print(c1.radius)
print(c1.area())

#Print-Output
#5
#78.53981633974483
#5
#78.53981633974483
#10
#314.1592653589793
#-10
#314.1592653589793

First we define a Property attribute using the Property Decorator @Property. The Property attribute definition is a method that return self_radius when we call radius. This mean when we can call <Object-Instance>.radius instead of <Object-Instance>._radius when we want to access the Property attribute. Then we define a Property setter using @<Property-attribute>.setter, in this case we use @radius.setter. The Property setter definition is a method that set self._radius = radius only in case radius is greater or equal to 0. In the following we create the Object instance c1 with given radius of 5 and access the given radius using c1.radius. As you see in the print output we are able to change the value to 10. Even if we are not able to change the value to -10 when we use c1.radius = -10 we are still able to change the value to -10 using c1._radius = -10.

We define the instance attribute self.__radius in the __init__() constructor. The radius value passed when we create the Object instance will be assigned to self__radius using self__radius = radius. The double underscore __ shows that the attribute can not be touched from the outside.

class Circle:
    def __init__(self, radius):
        self.__radius = radius

    # calculate area from given radius
    def area(self):
        return math.pi * self.__radius ** 2

    @property
    def radius(self):
        return self.__radius

    @radius.setter
    def radius(self, radius):
        if radius >= 0:
            self.__radius = radius

c1 = Circle(5)
print(c1.radius)
print(c1.area())

c1.radius = -10
print(c1.radius)
print(c1.area())

c1.radius = 10
print(c1.radius)
print(c1.area())

c1.__radius = -10
print(c1.radius)
print(c1.area())

#Print-Output
#5
#78.53981633974483
#5
#78.53981633974483
#10
#314.1592653589793
#10
#314.1592653589793

The Property attribute definition is a method that now return self__radius when we call radius. The Property setter definition is now a method that set self.__radius = radius only in case radius is greater or equal to 0. We create the Object instance c1 with given radius of 5 and access the given radius using c1.radius. As you see in the print output we are able to change the value to 10 but we are not able to change the value to -10 when we use c1.radius = -10 and c1.__radius = -10.