Classes in Python are an essential part of the object-oriented approach to programming. A class describes a custom data type on the basis of which homogeneous objects are created in a program. Typically, they can include some kind of properties and methods to implement their current state as well as behavior. This article describes classes for beginners and dummies in Python 3, as well as to refresh the knowledge of experienced programmers.
The object-oriented approach to software development was intended to be a reliable replacement for the structural programming methodology. According to this now obsolete concept, each individual program is a hierarchical structure of functional blocks of code.
Due to this feature:
- The perception of the task at hand when working on a project is improved;
- Reduces the number of lines of code;
- The complexity of writing code is reduced.
The basic principles of OOP are the following mechanisms: abstraction, encapsulation, inheritance, and polymorphism. To create programs that process information in the form of objects, it is necessary to understand as well as comprehensively observe all four paradigms. The practical application of each can be found in the examples in this article. Let’s consider the basic principles of OOP:
- Abstraction performs the main task of OOP, allowing the programmer to form objects of a certain type with different characteristics and behavior through the use of classes.
- Encapsulation helps to hide implementation details of specific objects and protect their properties from tampering.
- Inheritance makes it possible to extend existing classes by automatically transferring their parameters and methods to new data structures.
- Polymorphism is used to create a single interface that has many different implementations, depending on the class of the applied object.
Creating a class and object
To define a new class in your program, you must type the keyword class and then add a name for the data structure to be created, ending with a colon. The following example shows the generation of an empty class named Example. As you can see, there is no information in it at all.
class Example: pass example = Example()
Despite the empty body of the Example class, you can already create a certain object with a unique identifier on its basis. The last line of code above is an example of generating an object with the name example and data type Example. The assignment operator is used here, as well as empty parentheses after the class name, just like in calling a method that has no arguments. By defining a new class, you can create as many objects based on it as you want. As mentioned above, this data structure can include some properties, i.e., variables that will be assigned to each instance of the class. Below is a simple example of a Python 3 class and object. The example describes a class called Data with the string word and the number number.
class Data: word = "Python" number = 3 data = Data() print(data.word + " + str(data.number)) Python 3
If you create an object based on the Data class, it will get both variables and their values that were originally defined. Thus, a data object has been generated. You can access its fields with the names word and number by using the dot operator, calling it through an instance of the class. The function print will print the values of the fields of the data object on the screen. Don’t forget that the number must be converted to a string form in order to process it in the print method along with the text value. In addition to fields, a custom class may include methods that will be given to all its instances. You can call the execution of a certain method through a created object in the same way you can access its fields, that is, with a point. This example demonstrates the Data class with the sayHello function that displays text on the screen.
class Data: def sayHello(self): print("Hello World!") data = Data() data.sayHello() Hello World!
In order to call the sayHello method, you need to create an object that belongs to the required Data class. You can then run the function through the generated instance with the identifier data, which will allow you to display a small text message.
The argument self
Let’s look at why self is needed and what self means in Python functions. As you may have noticed, the only attribute for a method from a class is the keyword self. You need to put it in every function to be able to call it on the current object. You can also use this keyword to access class fields in the described method. Self thus replaces the object identifier.
class Dog: name = "Charlie" noise = "Woof!" def makeNoise(self): print(self.name + " says: " + self.noise + " + self.noise) dog = Dog() dog.makeNoise() Charlie says: Woof! Woof!
Above is the Dog class that describes the dog. It has name fields with a starting value of “Charlie” and noise fields containing the sound the animal makes. The makeNoise method makes the dog bark by displaying an appropriate message on the screen. To do this, the print function uses access to the name and noise fields. Next you need to create an instance of the Dog class and call makeNoise on it.
In the previous code examples, all created objects got their field values directly from the class, as they were set by default. You can change the internal data of any object by using the object’s property access statement. But it is possible to pre-define the fields for an object by setting them at the time of its creation. For this purpose, the OOP uses a constructor that accepts the necessary parameters. The following example shows how the constructor works during the initialization of a Dog class object.
class Dog: def __init__(self, name, breed): self.name = name self.breed = breed dog = Dog("Max", "German Shepherd") print(dog.name + " is "+ dog.breed) Max is German Shepherd
The constructor looks like an ordinary method, but you cannot call it explicitly. Instead, it is called automatically whenever the program creates a new object for the class in which it is located. The name of each constructor is given as the __init__ identifier. The parameters it receives can be assigned to the fields of the future object by using the keyword self, as in the example above. So, the Dog class contains two fields: name and breed. The constructor takes parameters to change these properties during the initialization of a new object called dog. Every class contains at least one constructor if none of them has been explicitly specified. However, in the case where the programmer adds a constructor with some parameters to his class, a constructor that has no parameters will not work. In order to use it, it must be explicitly specified in the class. A key feature of OOP is abstraction, which makes it possible to create private objects based on a common class, that is, a certain abstract concept, such as a dog, because it can have its own name, breed, weight, height.
Working with a destructor is usually the prerogative of languages that provide more extensive memory management capabilities. Despite the clever work of the garbage collector, which ensures the timely removal of unnecessary objects, the destructor call is still available. You can override it in a class by specifying the name __del__.
class Data: def __del__(self): print "The object is destroyed" data = Data() del(data) The object is destroyed
Like the constructor, a destructor can contain some user-specified code that tells the class that the method completed successfully. In this example, an instance of the Data class is created and its destructor is called that takes the object itself as a parameter.
The ability for a class to inherit another class, thereby inheriting its properties and methods, is an important feature of OOP.
When inheriting classes in Python, one condition must be met: the descendant class must be a more special case of the parent class. The following example shows how the Person class is inherited by the Worker class. When describing a subclass in Python, the parent class name is written in parentheses.
class Person: name = "John" class Worker(Person): wage = 2000 human = Worker() print(human.name + " earns $" + str(human.wage)) John earns $2000
Person contains the name field, which is passed to the Worker class that has the wage property. All conditions of inheritance are satisfied, because the worker is human and also has a name. Now by creating an instance of the Worker class called human, you can freely access the fields from the parent data structure.
You can inherit not only one class but several classes at the same time, thereby obtaining their properties and methods. In this example, the Dog class acts as a subclass for Animal and Pet, since it can be both. From Animal Dog gets the ability to sleep (sleep method), while Pet gives the ability to play with the owner (play method). In turn, both parent classes inherited the name field from Creature. The Dog class also got this property and can use it.
class Creature: def __init__(self, name): self.name = name class Animal(Creature): def sleep(self): print(self.name + " is sleeping") class Pet(Creature): def play(self): print(self.name + " is playing") class Dog(Animal, Pet): def bark(self): print(self.name + " is barking") beast = Dog("Buddy") beast.sleep() beast.play() beast.bark() Buddy is sleeping Buddy is playing Buddy is barking
The above example creates an object of class Dog, named in the constructor. Then the sleep, play, and bark methods are executed in turn, two of which were inherited. The ability to bark is a unique feature of a dog, since not every animal or pet knows how to do this.
Since it is possible to inherit the behavior of a parent class in OOP, it is sometimes necessary to implement corresponding methods in a specific way. The following code is an example, where the Dog and Cat classes are descendants of the Animal class. As they should, they both inherit the makeNoise method, but there is no implementation for it in the parent class. This is because an animal is an abstract concept, and therefore not capable of making any particular sound. However, for a dog and a cat this command often has a common meaning. In that case, we can argue that the makeNoise method from Animal is abstract because it has no implementation body of its own.
class Animal: def __init__(self, name): self.name = name def makeNoise(self): pass class Dog(Animal): def makeNoise(self): print(self.name + " says: Woof!") class Cat(Animal): def makeNoise(self): print(self.name + " says: Meow!") dog = Dog("Baxter") cat = Cat("Oliver") dog.makeNoise() cat.makeNoise() Baxter says: Woof! Oliver says: Meow!
As you can see in the example, the Dog and Cat descendants get makeNoise, and then they redefine it in their own ways. This is the essence of polymorphism, which allows you to change the way a certain method works based on the needs of a certain class. Its name, however, remains the same for all its descendants, which helps to avoid confusion with the names.
In previous examples, all methods of classes were called using objects of the corresponding type. However, this approach is inconvenient when you don’t need to call any specific class properties in your program. For example, there is a certain structure Math that contains methods for arithmetic calculations. You can apply its functions without creating an object if they are marked as static. To mark a method as static in a class, Python uses the
class Math: @staticmethod def inc(x): return x + 1 @staticmethod def dec(x): return x - 1 print(Math.inc(10), Math.dec(10)) (11, 9)
In the above code, the Math class includes two static methods: inc, which takes a number as a parameter and returns the result incremented by one, and dec, which does the same thing, only in reverse. Both of these methods can be called using the dot operator and the name of the Math class where they were originally implemented.
By default, all class properties are externally accessible, so you can change them at any time, as you like, using the point operator. This is not always a good thing, since there is some risk of losing information or entering incorrect data, which can lead to malfunctioning of the program. This is especially dangerous when several programmers work on a project, and it is not always obvious what this or that field is needed for. Another OOP feature called encapsulation helps in this situation. It prescribes the use of private properties of the class, to which there is no access outside of it. To manage the contents of an object, you need to use special methods called getter (returns a value) and setter (sets a value).
class Cat: __name = "Kitty" def get_name(self): return self.__name def set_name(self, name): self.__name = name cat = Cat() print(cat.get_name()) cat.set_name("Misty") print(cat.get_name()) Kitty Misty
To limit the visibility of the fields, you have to give them a name that starts with a double underscore. In the example above, the Cat class has a private __name property and special get_name and set_name methods. A distinctive feature of this approach is the ability to set certain limits for input values. For example, you can prohibit entering a negative number or an empty string.
With the help of a special class property mechanism, you can make adjustments in the work with the point operator by assigning your own functions to it. The following example shows a class with a private field x, for which getter and setter are written. By assigning a special value to the x field with a property function that takes method names as arguments, you can adjust the operation of the point operator according to your needs.
class Data: def __init__(self, x): self.__set_x(x) def __get_x(self): print("Get X") return self.__x def __set_x(self, x): self.__x = x print("Set X") x = property(__get_x, __set_x) data = Data(10) print(data.x) data.x = 20 print(data.x) Set X Get X 10 20
Special operators are used in programming languages for processing primitive data types. For example, arithmetic operations are performed with the usual plus, minus, multiply and divide characters. However, these operators may well be needed to handle your own data types. Thanks to special functions, you can customize them for your own tasks. In this example, we create a Point class that has two fields: x and y. To compare two different objects of this type, you can write a special method or just overload the corresponding operator. To do that, you need to redefine the __eq__ function in its own class by implementing the new behavior in its body.
class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): return self.x == other.x and self.y == other.y print(Point(2, 5) == Point(2, 5)) print(Point(3, 8) == Point(4, 6)) True False
The redefined method returns the result of a comparison of two fields of different objects. Thus, it is now possible to compare two different points using a single normal operator. The result of its work is printed using the print method. Similarly to comparison, you can implement in Python overloading operators of addition, subtraction and other arithmetic and logical operations. You can also overload the standard functions str and len.
This article dealt with the main features of working with classes in Python language and clearly demonstrated the most important principles of object-oriented programming. Working with classes allows you to represent all the data in the program as interacting objects that have certain properties and behavior.