Python supports object-oriented programming (OOP) paradigm. In OOP, everything is an object, and objects have attributes (properties) and methods (functions) that can be accessed and manipulated.
Classes in Python are the building blocks of object-oriented programming. A class is a blueprint for creating objects that have similar attributes and behaviors. An object is an instance of a class, and it has its own set of attributes and behaviors.
In this blog post, we will explore Python classes and object-oriented programming in detail.
Defining a class in Python
To define a class in Python, we use the class
keyword followed by the name of the class. Here’s an example:
class Person:
pass
In this example, we have defined a Person
class that doesn’t do anything yet. We have used the pass
keyword to indicate that the class is empty.
Creating objects from a class (Instantiation)
To create an instance of a class in Python, you call the class as if it were a function, passing any required arguments to the
__init__
method.
Once we have defined a class, we can create objects from it. To create an object, we use the class name followed by parentheses. Here’s an example:
person1 = Person()
person2 = Person()
In this example, we have created two objects (person1
and person2
) from the Person
class. Both objects have the same attributes and behaviors as the Person
class.
Lets define an __init__
method:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
person1 = Person("Sachin", 30)
person2 = Person("Virat", 25)
In this example, we define a Person
class with an __init__
method that takes a name
and age
parameter, and initializes instance variables with the same names. We then create two instances (objects) of the Person
class, passing in different values for the name
and age
parameters.
Adding attributes to a class
In Python, we can add attributes to a class using the __init__
method. The __init__
method is a special method that is called when an object is created. Here’s an example:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
person1 = Person("Sachin", 25)
person2 = Person("Virat", 30)
print(person1.name) # Output: Sachin
print(person2.age) # Output: 30
In this example, we have added two attributes (name
and age
) to the Person
class using the __init__
method. When we create an object from the Person
class, we pass values for these attributes as arguments. The self
parameter is a reference to the object that is being created.
Adding methods to a class
In Python, we can add methods to a class just like we add functions to a module. Methods are functions that belong to a class, and they can access the attributes of the class. Here’s an example:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
print(f"My name is {self.name} and I am {self.age} years old.")
person1 = Person("Sachin", 25)
person1.introduce() # Output: My name is Sachin and I am 25 years old.
In this example, we have added a introduce
method to the Person
class. The method accesses the name
and age
attributes of the class using the self
parameter.
Class Variables
In addition to instance variables, you can also define class variables in Python classes. Class variables are shared among all instances of the class and can be accessed using either the class name or an instance of the class. Here’s an example:
class Person:
count = 0
def __init__(self, name, age):
self.name = name
self.age = age
Person.count += 1
person1 = Person("Sachin", 30)
person2 = Person("Virat", 25)
print(Person.count) # Output: 2
In this example, we define a count
class variable that is initialized to 0
. We increment this variable each time a new instance of the Person
class is created in the __init__
method. We can access the count
variable using either the class name Person
or an instance of the class.
Static Methods and Class Methods
In addition to regular instance methods, Python also allows you to define static methods and class methods in a class.
A static method is a method that belongs to the class rather than any particular instance of the class. It can be called using the class name or an instance of the class, but it doesn’t have access to any instance variables. Here’s an example:
class Calculator:
@staticmethod
def add(x, y):
return x + y
print(Calculator.add(2, 3)) # Output: 5
In this example, we define a Calculator
class with a static method add
that takes two parameters x
and y
and returns their sum. We can call this method using either the class name Calculator
or an instance of the class.
A class method is a method that belongs to the class and has access to class variables, but not instance variables. It’s defined using the @classmethod
decorator. Here’s an example:
class Person:
count = 0
def __init__(self, name, age):
self.name = name
self.age = age
Person.count += 1
@classmethod
def get_count(cls):
return cls.count
person1 = Person("Sachin", 30)
person2 = Person("Virat", 25)
print(Person.get_count()) # Output: 2
In this example, we define a get_count
class method that returns the value of the count
class variable. We use the @classmethod
decorator to indicate that this is a class method. We can call this method using either the class name Person
or an instance of the class.
Magic/Dunder Methods
Python classes allow you to define special methods, known as magic methods or dunder (short for “double underscore”) methods, that are called in response to certain operations. These methods are surrounded by double underscores on both sides of the method name, like __init__
.
For example, the __str__
method is called when an instance of a class is printed using the print
function. Here’s an example:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"Name: {self.name}, Age: {self.age}"
person1 = Person("Sachin", 30)
print(person1) # Output: Name: Sachin, Age: 30
In this example, we define a Person
class with an __init__
method that takes a name
and age
parameter, and initializes instance variables with the same names. We then define a __str__
method that returns a string representation of the instance, and includes the values of the name
and age
instance variables. We then create an instance of the Person
class and print it using the print
function.
Other commonly used magic methods include:
__repr__(self)
: returns a string that can be used to recreate the object.__len__(self)
: returns the length of the object.__add__(self, other)
: defines behavior for the+
operator.__eq__(self, other)
: defines behavior for the==
operator.
Property
In Python, a class can have attributes that are accessed using dot notation, like my_object.my_attribute
. However, sometimes you want to have more control over how an attribute is accessed or modified. This is where properties come in.
A property is a special kind of attribute that is defined using a decorator. When the attribute is accessed, the property’s getter
method is called, and when it is modified, the property’s setter
method is called. This allows you to define custom behavior for getting and setting the attribute.
Here’s an example:
class Person:
def __init__(self, name, age):
self._name = name
self._age = age
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not isinstance(value, str):
raise TypeError("Name must be a string")
self._name = value
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, int):
raise TypeError("Age must be an integer")
if value < 0 or value > 150:
raise ValueError("Age must be between 0 and 150")
self._age = value
In this example, we define a Person
class with name
and age
instance variables. We then define name
and age
properties using the @property
decorator. We also define setter methods for each property using the @name.setter
and @age.setter
decorators.
The getter methods simply return the value of the corresponding instance variable, while the setter methods perform some validation before setting the value of the instance variable. For example, the name
setter method checks that the new value is a string before setting it.
Here’s how you would use this class:
person = Person("Sachin", 30)
print(person.name) # Output: Sachin
print(person.age) # Output: 30
person.name = "Virat"
person.age = 25
print(person.name) # Output: Virat
print(person.age) # Output: 25
person.name = 42 # Raises TypeError
person.age = -1 # Raises ValueError
In this example, we create an instance of the Person
class with the name “Sachin” and age 30. We then access the name
and age
properties using dot notation. We also set the name
and age
properties to new values.
Note that when we set the name
property to an integer value, a TypeError
is raised because the name
setter method checks that the value is a string. Similarly, when we set the age
property to a negative value, a ValueError
is raised because the age
setter method checks that the value is between 0 and 150.
Properties are a powerful tool in Python that allow you to define custom behavior for accessing and modifying attributes. They can be used to enforce validation rules, perform calculations, or provide a more convenient interface for working with objects.
Inheritance
Inheritance is a key feature of object-oriented programming that allows us to create new classes based on existing ones. The new class (subclass) inherits the attributes and methods of the existing class (superclass). Here’s an example:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
pass
In this example, we have a superclass Animal
with an __init__
method that initializes the name
attribute, and a speak
method that doesn’t do anything. We also have two subclasses Dog
and Cat
that inherit from the Animal
class.
The Dog
class overrides the speak
method with a Woof!
string, while the Cat
class does not override the speak
method, so it inherits the speak
method from the Animal
class.
We can create objects from the Dog
and Cat
classes just like we would create objects from the Person
class:
dog = Dog("Fido")
cat = Cat("Whiskers")
print(dog.name) # Output: Fido
print(dog.speak()) # Output: Woof!
print(cat.name) # Output: Whiskers
print(cat.speak()) # Output: None
In this example, we have created a Dog
object dog
with the name
attribute "Fido"
, and a Cat
object cat
with the name
attribute "Whiskers"
. We can access the name
attribute of each object using the dot notation (dog.name
, cat.name
).
We can also call the speak
method of the Dog
object dog
using the dot notation (dog.speak()
), which returns the string "Woof!"
. However, when we call the speak
method of the Cat
object cat
using the dot notation (cat.speak()
), it returns None
, because the Cat
class doesn’t override the speak
method, which is not implemented in the base class Animal
.
Encapsulation
Encapsulation is another key feature of object-oriented programming that allows us to hide the internal details of a class from the outside world. We can do this by making some attributes and methods of a class private, which means they can only be accessed from within the class.
In Python, we can make attributes and methods private by prefixing their names with two underscores (__
) (pronounced dunder
/ magic
). Here’s an example:
class Car:
def __init__(self, make, model, year):
self.__make = make
self.__model = model
self.__year = year
def get_make(self):
return self.__make
def get_model(self):
return self.__model
def get_year(self):
return self.__year
car = Car("Toyota", "Camry", 2022)
print(car.get_make()) # Output: Toyota
print(car.get_model()) # Output: Camry
print(car.get_year()) # Output: 2022
print(car.__make) # Raises an AttributeError: 'Car' object has no attribute '__make'
In this example, we have defined a Car
class with three private attributes (__make
, __model
, __year
) and three public methods (get_make
, get_model
, get_year
) that return the values of these attributes. The get_make
, get_model
, and get_year
methods can be accessed from outside the class using the dot notation (car.get_make()
, car.get_model()
, car.get_year()
).
However, if we try to access the private __make
attribute of the Car
object car
using the dot notation (car.__make
), we get an AttributeError
because the __make
attribute is not accessible from outside the class.
Though private attribute / method can be access using the following syntax
# <instance>._<class name><attribute_name>
print(car._Car__make) # Output: Toyota
Polymorphism
Polymorphism is the ability of objects of different classes to be used interchangeably. In other words, if two objects have the same method or attribute name, they can be used in the same way, even if they belong to different classes.
For example, let’s say we have two classes Circle
and Square
, both of which have a get_area
method that returns the area of the shape. We can write a function that takes an object of either class as an argument and calls its get_area
method:
class Circle:
def __init__(self, radius):
self.radius = radius
def get_area(self):
return 3.14 * self.radius ** 2
class Square:
def __init__(self, side):
self.side = side
def get_area(self):
return self.side ** 2
def print_area(shape):
print(f"The area of the shape is {shape.get_area()}")
circle = Circle(5)
square = Square(4)
print_area(circle) # Output: The area of the shape is 78.5
print_area(square) # Output: The area of the shape is 16
In this example, we have defined two classes Circle
and Square
, both of which have a get_area
method. We have also defined a function print_area
that takes an argument shape
, which can be an object of either class Circle
or Square
.
When we call print_area(circle)
and print_area(square)
, the get_area
method of the corresponding object is called and the area of the shape is printed.
This is an example of polymorphism because the print_area
function can be used with objects of different classes (Circle
and Square
) that have the same method (get_area
).
Inheritance vs. Composition
Inheritance and composition are two ways to create new classes by combining existing classes.
Inheritance is a mechanism by which a new class is derived from an existing class, inheriting all the attributes and methods of the parent class. Inheritance allows us to create a hierarchy of classes that share common attributes and methods, while also allowing for specialization of those attributes and methods in the child classes.
Composition is a mechanism by which a class is composed of other objects, typically as instance variables. Composition allows us to create complex objects by combining simpler objects, and it can often be a more flexible alternative to inheritance.
Let’s look at an example of each:
Inheritance example:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
dog = Dog("Fido")
cat = Cat("Whiskers")
print(dog.name) # Output: Fido
print(dog.speak()) # Output: Woof!
print(cat.name) # Output: Whiskers
print(cat.speak()) # Output: Meow!
In this example, we have a superclass Animal
with an __init__
method that initializes the name
attribute, and a speak
method that doesn’t do anything. We also have two subclasses Dog
and Cat
that inherit from the Animal
class.
The Dog
class overrides the speak
method with a Woof!
string, while the Cat
class overrides the speak
method with a Meow!
string.
Composition example:
class Engine:
def start(self):
print("Engine started.")
def stop(self):
print("Engine stopped.")
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
self.engine.start()
def stop(self):
self.engine.stop()
car = Car()
car.start() # Output: Engine started.
car.stop() # Output: Engine stopped.
In this example, we have a Car
class that has an instance variable engine
that is an object of the Engine
class. The Car
class has its own start
and stop
methods that delegate to the corresponding methods of the Engine
object.
This is an example of composition because the Car
class is composed of an Engine
object, and it delegates the start
and stop
methods to the Engine
object.
Private
In Python, there is no true “private” variables or methods in a class. However, developers use a naming convention to indicate that a variable or method should not be accessed outside of the class.
The convention is to prefix the variable or method name with a double underscore __
(also known as “dunder”). For example, a private variable in a class called Person
could be named __name
.
By convention, variables and methods with a double underscore prefix are considered “private” and should not be accessed directly from outside the class. However, it is still possible to access these variables and methods by using name mangling, which involves prefixing the variable or method name with _classname
(where classname
is the name of the class).
Here’s an example:
class Person:
def __init__(self, name):
self.__name = name
def get_name(self):
return self.__name
person = Person("Sachin")
print(person.get_name()) # Output:Sachin
print(person.__name) # Raises an AttributeError
print(person._Person__name) # Output:Sachin
In this example, we have a private variable __name
in the Person
class, and a public method get_name
that returns the value of __name
.
When we try to access __name
directly using person.__name
, we get an AttributeError
, indicating that the variable is not accessible from outside the class.
However, we can still access the private variable using name mangling with _Person__name
. This is not recommended, as it goes against the principle of encapsulation and can lead to unexpected behavior. It’s best to only access private variables and methods through the public interface of the class.
Metaclass
In Python, a metaclass is a class that defines the behavior of other classes. When you define a class in Python, it is actually an instance of its metaclass. Metaclasses allow you to customize the behavior of classes in ways that are not possible using ordinary Python syntax.
The base metaclass in Python is the
type
metaclass
To define a metaclass in Python, you can create a new class that inherits from the built-in type
class. The type
class is itself a metaclass, so by subclassing it, you can create your own metaclass. Here’s an example:
class MyMeta(type):
def __new__(cls, name, bases, attrs):
print("Creating class", name)
return super().__new__(cls, name, bases, attrs)
class MyClass(metaclass=MyMeta):
pass
class MySubclass(MyClass):
pass
In this example, we define a new metaclass called MyMeta
that inherits from type
. The __new__
method is called when a new class is created using this metaclass. It takes four arguments: cls
, which is the metaclass itself, name
, which is the name of the new class, bases
, which is a tuple of the base classes of the new class, and attrs
, which is a dictionary of the attributes of the new class.
In this example, the __new__
method simply prints a message when a new class is created. We then define two classes, MyClass
and MySubclass
, and specify MyMeta
as the metaclass for both of them using the metaclass
argument.
When we run this code, we see that the __new__
method of MyMeta
is called twice, once for MyClass
and once for MySubclass
.
Metaclasses can be used to implement a wide range of custom behaviors for classes. For example, you could use a metaclass to automatically generate classes based on a set of configuration options, or to enforce certain constraints on the attributes of classes. However, metaclasses can be difficult to understand and use correctly, so they should be used with caution.
Metaclasses are a powerful and advanced feature of Python that allow you to create classes dynamically at runtime. They are especially useful for implementing frameworks, libraries, and other systems that need to generate classes programmatically.
In addition to the __new__
method shown in the previous example, metaclasses can also implement other special methods that control the behavior of the classes they create. For example:
__init__(cls, name, bases, attrs)
: This method is called after the__new__
method and is used to initialize the class object. It takes the same arguments as__new__
.__call__(cls, *args, **kwargs)
: This method is called when an instance of the class is created. It takes the class object as its first argument, followed by any additional arguments passed to the class constructor.__getattr__(cls, name)
: This method is called when an attribute of the class is accessed that does not exist. It allows you to define custom behavior for attribute access in the class.
Here’s an example of a metaclass that uses the __call__
method to customize the behavior of instances of the class:
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class MyClass(metaclass=Singleton):
pass
a = MyClass()
b = MyClass()
assert a is b
In this example, we define a metaclass called Singleton
that ensures that only one instance of the class is ever created. The _instances
attribute is a dictionary that stores the instances of the class that have been created. The __call__
method checks if an instance of the class has already been created and returns it if it has. Otherwise, it creates a new instance using the super().__call__
method.
We then define a class MyClass
that uses the Singleton
metaclass by specifying it as the metaclass
argument. When we create two instances of MyClass
, a
and b
, we see that they are both the same object, because the Singleton
metaclass ensures that only one instance of the class is ever created.
Abstract Interface
Metaclasses can also be used to define abstract interfaces in Python. An abstract interface is a set of methods that a class must implement in order to be considered a valid implementation of the interface. Abstract interfaces are commonly used in object-oriented programming to define a common set of functionality that can be implemented by multiple classes.
To define an abstract interface using metaclasses, you can create a new metaclass that defines the set of methods that must be implemented by classes that use the metaclass. Here’s an example:
class AbstractInterface(type):
def __new__(cls, name, bases, attrs):
if not any('required_method' in base.__dict__ for base in bases):
raise TypeError(f'{name} does not implement required methods')
return super().__new__(cls, name, bases, attrs)
class MyInterface(metaclass=AbstractInterface):
def required_method(self):
pass
class ValidImplementation(MyInterface):
def required_method(self):
print('ValidImplementation implements required_method')
class InvalidImplementation(MyInterface):
pass
In this example, we define a metaclass called AbstractInterface
that checks whether any of the base classes of a new class that uses the metaclass define a required_method
method. If none of the base classes define this method, the metaclass raises a TypeError
.
We then define a new interface called MyInterface
that defines a single required_method
method. We also define two classes, ValidImplementation
and InvalidImplementation
, that inherit from MyInterface
. ValidImplementation
implements the required_method
method correctly, while InvalidImplementation
does not.
When we try to create an instance of InvalidImplementation
, we see that a TypeError
is raised because the class does not implement the required_method
method defined by the MyInterface
interface.
Abstract interfaces can be a useful way to enforce a common set of functionality across multiple classes in an object-oriented program. Using a metaclass to define the interface allows you to define the set of required methods in a single location and ensure that any class that uses the interface implements these methods correctly.
Example
In Python, you can create an interface using an abstract base class (ABC) with the abc
module. An abstract base class is a class that cannot be instantiated directly, but can be subclassed to create concrete classes.
Here’s an example interface using a Python class:
import abc
class Shape(metaclass=abc.ABCMeta):
@abc.abstractmethod
def area(self):
pass
@abc.abstractmethod
def perimeter(self):
pass
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
def perimeter(self):
return 4 * self.side
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
def perimeter(self):
return 2 * 3.14 * self.radius
# This will raise a TypeError, because Shape is an abstract class
s = Shape()
# This will work, because Square is a concrete class that implements the area and perimeter methods
sq = Square(5)
print(sq.area()) # Output: 25
print(sq.perimeter()) # Output: 20
# This will work, because Circle is a concrete class that implements the area and perimeter methods
c = Circle(3)
print(c.area()) # Output: 28.26
print(c.perimeter()) # Output: 18.84
In this example, we define an abstract class called Shape
that has two abstract methods, area
and perimeter
. The @abc.abstractmethod
decorator is used to mark these methods as abstract, indicating that they must be implemented in any concrete subclass of Shape
.
We then define two concrete classes, Square
and Circle
, that subclass Shape
and implement the area
and perimeter
methods. Because Square
and Circle
implement all the abstract methods of Shape
, they are considered concrete classes that can be instantiated.
When we try to instantiate the abstract Shape
class directly, a TypeError
is raised, because we cannot create instances of an abstract class. However, we can create instances of Square
and Circle
, and call their area
and perimeter
methods to calculate the area and perimeter of the shapes.