This Python OOP explains to you the Python object-oriented programming clearly so that you can apply it to develop software more effectively.
By the end of this Python OOP module, you’ll have good knowledge of object-oriented principles. And you’ll know how to use Python syntax to create reliable and robust software applications.
- Section 1. Classes and objects
- An object is a container that contains data and functionality.
- The data represents the object at a particular moment in time. Therefore, the data of an object is called the state. Python uses attributes to model the state of an object.
- The functionality represents the behaviors of an object. Python uses functions to model the behaviors. When a function is associated with an object, it becomes a method of the object.
- In other words, an object is a container that contains the state and methods.
- Before creating objects, you define a class first. And from the class, you can create one or more objects. The objects of a class are also called instances of a class.
- Everything in Python is an object, including classes.
- To define a class in Python, you use the class keyword followed by the class name and a colon.
- By convention, you use capitalized names for classes in Python. If the class name contains multiple words, you use the CamelCase format, for example SalesEmployee.
When printing out the person object, you’ll see its name and memory address:
<__main__.Person object at 0x000001C46D1C47F0>
- Class variables are bound to the class. They’re shared by all instances of that class.
The following example adds the extension and version class variables to the HtmlDocument class:
class HtmlDocument:
extension = 'html'
version = 5
Both extension and version are the class variables of the HtmlDocument class. They’re bound to the HtmlDocument class.
To get the values of class variables, you use the dot notation (.). For example:
HtmlDocument.extension # html
HtmlDocument.version # 5
If you access a class variable that doesn’t exist, you’ll get an AttributeError exception. For example:
HtmlDocument.media_type # AttributeError: type object 'HtmlDocument' has no attribute 'media_type'
Another way to get the value of a class variable is to use the getattr() function. The getattr() function accepts an object and a variable name. It returns the value of the class variable. For example:
extension = getattr(HtmlDocument, 'extension') # html
If the class variable doesn’t exist, you’ll also get an AttributeError exception. To avoid the exception, you can specify a default value if the class variable doesn’t exist like this:
media = getattr(HtmlDocument, 'media', 'text/html') # text/html
To set a value for a class variable, you use the dot notation:
HtmlDocument.version = 10
or you can use the setattr() built-in function:
setattr(HtmlDocument, 'version', 10)
Since Python is a dynamic language, you can add a class variable to a class at runtime after you have created it. For example, the following adds the media_type class variable to the HtmlDocument class:
HtmlDocument.media_type = 'text/html'
print(HtmlDocument.media_type)
Similarly, you can use the setattr() function:
setattr(HtmlDocument, media_type, 'text/html')
print(HtmlDocument.media_type)
To delete a class variable at runtime, you use the delattr() function:
delattr(HtmlDocument, 'version')
Or you can use the del keyword:
del HtmlDocument.version
Python stores class variables in the __dict__
attribute. The dict is a mapping proxy, which is a dictionary. For example:
from pprint import pprint
class HtmlDocument:
extension = 'html'
version = '5'
HtmlDocument.media_type = 'text/html'
pprint(HtmlDocument.__dict__)
mappingproxy({'__dict__': <attribute '__dict__' of 'HtmlDocument' objects>,
'__doc__': None,
'__module__': '__main__',
'__weakref__': <attribute '__weakref__' of 'HtmlDocument' objects>,
'extension': 'html',
'media_type': 'text/html',
'version': '5'})
As clearly shown in the output, the __dict__
has three class variables: extension, media_type, and version besides other predefined class variables.
Python does not allow you to change the dict directly. For example, the following will result in an error:
HtmlDocument.__dict__['released'] = 2008
# TypeError: 'mappingproxy' object does not support item assignment
However, you can use the setattr() function or dot notation to indirectly change the dict attribute.
Also, the key of the dict are strings that will help Python speeds up the variable lookup.
Although Python allows you to access class variables through the dict dictionary, it’s not a good practice. Also, it won’t work in some situations. For example:
print(HtmlDocument.__dict__['type']) # BAD CODE
When you add a function to a class, the function becomes a class attribute. Since a function is callable, the class attribute is called a callable class attribute.
- By definition, a method is a function that is bound to an instance of a class.
- When you define a function inside a class, it’s purely a function. However, when you call the function via an instance of a class, the function becomes a method. Therefore, a method is a function that is bound to an instance of a class.
- A method has the first argument (self) as the object to which it is bound.
- Python automatically passes the bound object to the method as the first argument. By convention, its name is self.
- In Python, class variables are bound to a class while instance variables are bound to a specific instance of a class. The instance variables are also called instance attributes.
- Python stores instance variables in the
__dict__
attribute of the instance. Each instance has its own__dict__
attribute and the keys in this dict may be different. - When you access a variable via the instance, Python finds the variable in the dict attribute of the instance. If it cannot find the variable, it goes up and look it up in the dict attribute of the class.
The following defines a HtmlDocument class with two class variables:
from pprint import pprint
class HtmlDocument:
version = 5
extension = 'html'
pprint(HtmlDocument.__dict__)
print(HtmlDocument.extension) # html
print(HtmlDocument.version) # 5
mappingproxy({'__dict__': <attribute '__dict__' of 'HtmlDocument' objects>,
'__doc__': None,
'__module__': '__main__',
'__weakref__': <attribute '__weakref__' of 'HtmlDocument' objects>,
'extension': 'html',
'version': 5})
The HtmlDocument class has two class variables: extension and version. Python stores these two variables in the __dict__
attribute.
When you access the class variables via the class, Python looks them up in the dict of the class. The following creates a new instance of the HtmlDocument class:
home = HtmlDocument()
The home is an instance of the HtmlDocument class. It has its own __dict__
attribute:
pprint(home.__dict__) # {}
The home.__dict__
is now empty. The home.dict stores the instance variables of the home object like the HtmlDocument.__dict__
stores the class variables of the HtmlDocument class.
Since a dictionary is mutable, you can mutate it e.g., adding a new element to the dictionary. Python allows you to access the class variables from an instance of a class. For example:
print(home.extension)
print(home.version)
In this case, Python looks up the variables extension and version in home.__dict__
first. If it doesn’t find them there, it’ll go up to the class and look up in the HtmlDocument.__dict__
However, if Python can find the variables in the dict of the instance, it won’t look further in the dict of the class.
The following defines the version variable in the home object:
home.version = 6
print(home.__dict__)
Python adds the version variable to the __dict__
attribute of the home object:
The dict now contains one instance variable:
{'version': 6}
If you access the version attribute of the home object, Python will return the value of the version in the home.__dict__
dictionary:
print(home.version) # 6
If you change the class variables, these changes also reflect in the instances of the class:
HtmlDocument.media_type = 'text/html'
print(home.media_type) # text/html
- Instance methods are bound to a specific instance of a class.
- Instance methods can access instance attributes within the same class. To invoke instance methods, you need to create the instance of a class first.
- To create the class method, you place @classmethod decorator above the method definition. Rename the self parameter to cls as a first parameter.
- Class methods can't access instance attributes. It can only access class attributes.
- To call the class methods you use classname followed by dot and then the method name
ClassName.MethodName()
. - When to use class methods?: You can use class methods for any methods that are not bound to a specific instance but the class. In practice, you often use class methods for methods that create an instance of the class. When a method creates an instance of the class and returns it, the method is called a factory method.
- Following is the difference between class methods and instance methods:
S.No Features class methods Instance methods 1 Binding Class An instance of the class 2 Calling Class.method() object.method() 3 Accessing Class attributes Instance & class attributes - Reference: https://www.pythontutorial.net/python-oop/python-class-methods/
- Encapsulation is one of the four fundamental concepts in object-oriented programming including abstraction, encapsulation, inheritance, and polymorphism.
- Encapsulation is the packing of data and functions that work on that data within a single object. By doing so, you can hide the internal state of the object from the outside. This is known as information hiding.
- Encapsulation is the packing of data and methods into a class so that you can hide the information and restrict access from outside.
- The idea of information hiding is that if you have an attribute that isn’t visible to the outside, you can control the access to its value to make sure your object is always has a valid state.
- Private attributes can be only accessible from the methods of the class. In other words, they cannot be accessible from outside of the class. Python doesn’t have a concept of private attributes. In other words, all attributes are accessible from the outside of a class. By convention, you can define a private attribute by prefixing a single underscore (_)
_attribute
. - If you prefix an attribute name with double underscores (__) like this:
__attribute
Python will automatically change the name of the __attribute to:_class__attribute
. This is called the name mangling in Python. By doing this, you cannot access the__attribute
directly from the outside of a class like:instance.__attribute
. However, you still can access it using the _class__attribute name:instance._class__attribute
- Reference: https://www.pythontutorial.net/python-oop/python-private-attributes/
- A class attribute is shared by all instances of the class. To define a class attribute, you place it outside of the
__init__()
method. - Use
class_name.class_attribute
orobject_name.class_attribute
to access the value of the class_attribute. - When you access an attribute via an instance of the class
circle.pi
Python searches for the attribute in the instance attribute list. If the instance attribute list doesn’t have that attribute, Python continues looking up the attribute in the class attribute list. Python returns the value of the attribute as long as it finds the attribute in the instance attribute list or class attribute list. However, if you access an attribute directlyCircle.pi
, Python directly searches for the attribute in the class attribute list. - Use class attributes for storing class contants, track data across all instances, and setting default values for all instances of the class.
- Since a constant doesn’t change from instance to instance of a class, it’s handy to store it as a class attribute.
- Tracking data across of all instances: When a new instance gets created, the constructor adds the instance to the list.
- Sometimes, you want to set a default value for all instances of a class. In this case, you can use a class attribute.
- Reference: https://www.pythontutorial.net/python-oop/python-class-attributes/
- Static methods aren’t bound to an object. In other words, static methods cannot access and modify an object state.
- In addition, Python doesn’t implicitly pass the cls parameter (or the self parameter) to static methods. Therefore, static methods cannot access and modify the class’s state.
- Use static methods to define utility methods or group a logically related functions into a class.
- Use the @staticmethod decorator to define a static method.
- Following is the difference between class methods and static methods:
S.No Class Methods Static Methods 1 Python implicitly pass the cls argument to class methods. Python doesn’t implicitly pass the cls argument to static methods 2 Class methods can access and modify the class state. Static methods cannot access or modify the class state. 3 Use @classmethod decorators to define class methods. Use @staticmethod decorators to define static methods.
- Use the Python property() class to define a property for a class.
- Lets have a class with two attributes name and age. Since age is an instance of a class, you can assign it a new value using
person.age = 12
. The following assignment is also technically validperson.age = -2
but not logically. So, every time you need to check using if/else conditionperson.age > 0
. To avoid the repetitive code you will use getter and setter methods in thePerson
class. But this strategy will not work with backward compatibility. - By convention the getter and setter have the following name:
get_<attribute>()
andset_<attribute>()
. - User property on the class variables for backward compatibility also.
property(fget=None, fset=None, fdel=None, doc=None)
- Reference:
- You can user property on the class variables. To get the age of a Person object, you can use either the age property or the get_age() method. This creates an unnecessary redundancy.
- To avoid redundancy you use @property on getter (props) and @props.setter on the setter.
- Use the @property decorator to create a property for a class.
- You can create read-only property by creating only the getter property on the attribute.
- One of the most common use cases of property() is building managed attributes that validate the input data before storing or even accepting it as a secure input.
- Using @props and @props.setter for multiple instance attributes, makes your code repetition and breaks the DRY (Don’t Repeat Yourself) principle, so you would want to refactor this code to avoid it. To do so, you can abstract out the repetitive logic using a descriptor. To avoid duplicating the logic, you may have a method that validates data and reuse this method in other properties. This approach will enable reusability. However, Python has a better way to solve this by using descriptors.
- Reference:
- General syntax to raise an exception is
raise [expression [from another_expression]]
- Reference:
- Inheritance allows a class to reuse existing attributes and methods of another class.
- The class that inherits from another class is called a child class, a subclass, or a derived class.
- The class from which other classes inherit is call a parent class, a super class, or a base class.
- Use
isinstance()
to check if an object is an instance of a class. - Use
issubclass()
to check if a class is a subclass of another class. - Use
super()
to call the methods of a parent class from a child class. - Reference:
- The overriding method allows a child class to provide a specific implementation of a method that is already provided by one of its parent classes.
- Reference:
- Python uses dictionaries to store instance attributes of instances of a class. This allows you to dynamically add more attributes to instances at runtime but also create a memory overhead.
- Define
__slots__
in the class if a class only contains fixed (or predetermined) instance attributes, you can use the slots to instruct Python to use a more compact data structure instead of dictionaries. The__slots__
optimizes the memory if the class has many objects. - Reference:
- In object-oriented programming, an abstract class is a class that cannot be instantiated. However, you can create classes that inherit from an abstract class.
- Abstract classes are classes that you cannot create instances from.
- Typically, you use an abstract class to create a blueprint for other classes.
- Similarly, an abstract method is an method without an implementation. An abstract class may or may not include abstract methods.
- Python doesn’t directly support abstract classes. But it does offer a module that allows you to define abstract classes. To define an abstract class, you use the abc (abstract base class) module. The abc module provides you with the infrastructure for defining abstract base classes.
- It is also very usefull in type checking.
- Reference:
- Use Python
Protocol
to define implicit interfaces. - In
duck typing
, the behaviors and properties of an object determine the object type, not the explicit type of the object. - The duck typing is inspired by the duck test:
If it walks like a duck and its quacks like a duck, then it must be a duck
. - When we use Protocols then there is no more import dependencies.
- Protocols were introduced in Python 3.8 as an alternative to Python ABC and they are differently in typing point of view. ABC classes relay on the nominal typing that means if you want typing relationship be there
A is type(B)
then you need to explicitly write down in your code for example using inheritance. So, you have abstract base class, you create subclasses from that abstract base class, you inherit from it and establish a relationship. Python interpreter uses that relationship to determine whether or not the types matched. Protocols are different, they relay on the structure typing and that means instead of having explicitly defining typing (A is of Protocol), Python looks at how the structure is of these objects. Do they have the same method? Do they have the same properties? If so, it will assume that the types matched. So that means that the usage of Protocols is actually also quite different. You don't establish generally inheritance relationships with them you don't inherit from a protocol class but the protocol defines the interface that is expected in the part of program that refers to it. So, if you have a function or method in a class that gets an argument of a particular protocol type then anything that implements those methods that hase those properties can be passed as an argument to that function or method and structure typing that will actually do the comparsion of the structure of the objects is going to make sure that program works as expected to. This fits very will with the Python runtime type checking system that treats two objects the same if they have the same methods and properties that also called duct typing. - Reference(s):
- An enumeration is a set of members that have associated unique constant values.
- Create a new enumeration by defining a class that inherits from the Enum type of the
enum
module. - The members have the same types as the enumeration to which they belong.
- Use the
enumeration[member_name]
to access a member by its name andenumeration(member_value)
to access a member by its value. - Enumerations are iterable.
- Enumeration members are hashable.
- Enumerable are immuable.
- Cannot inherits from an enumeration unless it has no members.
- When an enumeration has different members with the same values, the first member is the main member while others are aliases of the main member.
- Use the
@enum.unique
decorator from the enum module to enforce the uniqueness of the values of the members. - Use enum
auto()
class to generate unique values for enumeration members. - Python enumerations are classes. It means that you can add methods to them, or implement the dunder methods to customize their behaviors.
__str__
,__eq__
,__lt__
,__bool__
. Define an emum class with no members and methods and extends this base class. - Reference(s):
- When a class inherits from a single class, you have single inheritance. Python allows a class to inherit from multiple classes. If a class inherits from two or more classes, you’ll have multiple inheritance.
class ChildClass(ParentClass1, ParentClass2, ParentClass3):
- Python multiple inheritance allows one class to inherit from multiple classes.
- The Method Order Resolution(MRO) defines the class search path to find the method to call. To get the MRO of the inherited class you can use
class_name.__mro__
. When the parent classes have methods with the same name and the child class calls the method, Python uses the method resolution order (MRO) to search for the right method to call. - Reference(s):
- A mixin class provides method implementions for resuse by multiple related subclasses.
- A mixin is a class that provides method implementations for reuse by multiple related child classes. However, the inheritance is not implying an is-a relationship.
- A mixin doesn’t define a new type. Therefore, it is not intended for direction instantiation(
mx_instance = MixinClass()
) - A mixin bundles a set of methods for reuse. Each mixin should have a single specific behavior, implementing closely related methods.
- Typically, a child class uses multiple inheritance to combine the mixin classes with a parent class.
- Since Python doesn’t define a formal way to define mixin classes, it’s a good practice to name mixin classes with the suffix Mixin.
- Reference(s):
- Suppose you have a
class Person
with two instance attributesfirst_name
andlast_name
And you want the first_name and last_name attributes to be non-empty strings. These plain attributes cannot guarantee this. To enforce thedata validity
you can use@property
with a getter and setter methods. This code works perfectly fine. However, it isredundant
because the validation logic validates thefirst
andlast
names is the same. Also, if the class has more attributes that require a non-empty string, you need to duplicate this logic in other properties. In other words, this validation logic is not reusable. To avoid duplicating the logic, you may have a method that validates data and reuse this method in other properties. This approach will enable reusability. However, Python has a better way to solve this by usingdescriptors
. - In Python, the descriptor protocol consists of three methods:
__get__
gets an attribute value.__set__
sets an attribute value.__delete__
deletes an attribute.- Optionally, a descriptor can have the
__set_name__
method that sets an attribute on an instance of a class to a new value.
- A descriptor is an object of a class that implements one of the methods specified in the descriptor protocol.
- Descriptors have two types:
data descriptor
andnon-data descriptor
.- A
data descriptor
is an object of a class that implements the__set__
and/or__delete__
method. - A
non-data descriptor
is an object that implements the__get__
method only.
- A
- Reference(s):
- Let say you have
class Person
has the__init__
method that initializes thename
andage
attributes. If you want to have a string representation of the Person object, you need to implement the__str__
or__repr__
method. Also, if you want to compare two instances of the Person class by an attribute, you need to implement the__eq__
method. However, if you use thedataclass
, you’ll have all of these features (and even more) without implementing these dunder methods. - The dataclasses module has the
astuple()
andasdict()
functions that convert an instance of the dataclass to a tuple and a dictionary. - To create readonly objects from a dataclass, you can set the frozen argument of the dataclass decorator to True i.e.,
@dataclass(frozen=True)
. - If don’t want to initialize an attribute in the init method, you can use the field() function from the dataclasses module. Use post_init method to initalize attributes that depends on other attributes.
- By default, a dataclass implements the eq method. To allow different types of comparisons like lt, lte, gt, gte, you can set the order argument of the @dataclass decorator to True i.e.,
@dataclass(order=True)
. - Reference(s):