How to use Python for object-oriented programming (OOP)
Object-oriented programming (OOP) is a programming method that reduces development times and makes it easier to read and maintain your code. Python can use structured and functional paradigms as well, but its OOP functionality is incredibly strong and intuitive. The language includes many built-in data types, like lists and strings.
In OOP, complex problems can be solved by building a hierarchy of objects that interact with one another. In Python, data and corresponding behaviours, or methods, are tightly coupled. Data within an object is protected and can be accessed by the self parameter from inside the object. Methods belong to an object, rather than being defined as global functions seen in other methodologies.
One advantage of using OOP in Python is that it’s incredibly flexible. Let’s delve into that.
Why use object-oriented programming in Python?
OOP is an imperative programming language, in which the control flow is fully described within the script itself. Declarative programming, by contrast, describes the desired results of computation in terms of the language’s core functions.
Python’s flavour of OOP creates and defines objects that combine data and methods. So, an object has attributes that define its state, such as ‘height’, ‘age’ and ‘location’. That same object, representing a person, has methods that define its behaviour, like ‘singing’ or ‘dancing’. Because these are not global attributes, you can talk about the height, age, and location of a building object and use actions like ‘construct’ or ‘demolish’ with the expectation that the object will perform its defined behaviour without any confusion.
Objects are building blocks. They’re used to define and solve larger problems by breaking them down into smaller pieces. For each kind of object, you write a class to define its attributes. Object-oriented programs can be built out of standard modules that you’ve used before, or ones designed by someone else. You never have to duplicate your work. That makes programming faster and more portable. It’s also more teachable, for the same reasons.
Find out more about OOPs, programming paradigms and learn the basics of Python with our Python tutorial.
How to instantiate an object in Python
If you’ve never heard the term, ‘instantiate’ means that you create a new object from a class. The new object, also called class instance, will contain all the data attributes and method defined by the class. So, classes serve as blueprints for the creation, or instantiation, of objects.
Let’s assume we’re writing code for a kitchen environment. We can model our objects so that they represent objects in the real world, like glasses, plates, bottles, and cups.
We define the behaviour and state of every object. For example, containers could be empty or full. They could be open or closed. In order for the objects to interact with one another as intended, we need to give them scope and definition. We can talk about the maximum volume of every container. We can say whether it will accept wet or dry contents. We can define the temperature ranges that it can store safely.
By doing this, the objects will be able to interact. A cup can be poured into a glass, for example. If the contents are too hot, the glass might shatter even though the cup was fine with those contents.
So how does modifying the state of an object work in OOP in Python? Let’s look at an example:
cup = Container(400)
assert cup.volume() == 400
assert not cup.is_full()
# add some water to the cup
cup.add('Water', 250)
assert cup.volume_filled() == 250
# add more water, filling the cup
cup.add('Water', 150)
assert cup.is_full()
PythonCreating abstraction with OOP in Python
A coding abstraction hides unnecessary information. These are important mental shortcuts that allow a programmer to focus more on solving the task at hand. For example, instead of asking ‘is the volume of the bottle’s contents equal to the bottle’s total capacity?’, an abstraction would simply ask ‘is the bottle full?’ The more abstract version is more concise, so it is preferable.
# instantiate an empty glass
glass = Container(250)
# add water to the glass
glass.add('Water', 250)
# is the glass full?
assert glass.is_full()
# a longer way to ask the same question
assert glass.volume_filled() == glass.volume()
PythonAbstract concepts can become new ideas in Python. Using the Python additions operator, the plus sign can add numbers, merge the contents of multiple lists, combine a literal string with the contents of a variable, and more:
assert 42 + 9 == 51
assert ['Jack', 'John'] + ['Jim'] == ['Jack', 'John', 'Jim']
PythonBy defining abstractions like the additions operator for the container, we can write code that reads like human language:
# pitcher with 1000 ml capacity
pitcher = Container(1000)
# glass with 250 ml capacity
glass = Container(250)
# fill glass with water
glass.fill('Water')
# transfer the content from the glass to the pitcher
pitcher += glass
# pitcher now contains water from glass
assert pitcher.volume_filled() == 250
# glass is empty
assert glass.is_empty()
PythonOOP terminology can be confusing, with different sources using different terms for the same concept. We’ve collected the most-easily misunderstood terms for your convenience:
OOP term | Explanation |
---|---|
Object | A ‘smart’ or ‘alive’ data structure that combines internal state and functionality. |
Class | A blueprint for the creation of objects. Classes roughly correspond to types in non-OOP languages. |
Class instance | Another term for ‘object’. Note that, although the term is sometimes incorrectly used, there is no such thing as an ‘object instance’. |
Attribute | A component of an object, also called ‘member’. Data attributes are also called ‘fields’ and correspond to variables that store state. Function attributes are called ‘methods’. |
Instance attribute | An attribute that belongs to an object. Instances attributes are unique to each object. |
Class attribute | An attribute that belongs to a class. Class attributes are shared between all instances of the class. |
Class object | Classes are objects themselves. When referring to a class in code, we may use the term ‘class object’. |
How does OOP in Python work?
Attributes are the object’s components, such as data and functions. Instead of restricting access to attributes via keywords, Python considers attributes starting with an underscore to be private. For example, _internal_attr
or _internal_method()
.
Methods can be used to set or retrieve object details. They use self as their first argument, which points to a specific instance of a class. Inside methods, self acts as a placeholder for an instantiated object.
Since every objects encapsulates its own state, accessing internal data via the reference self._internal
is fine. External access violates encapsulation and should be avoided:
class ExampleObject:
def public_method(self):
self._internal = 'changed from inside method'
# instantiate object
obj = ExampleObject()
# this is fine
obj.public_method()
assert obj._internal == 'changed from inside method'
# works, but not a good idea
obj._internal = 'changed from outside'
PythonDefining a class in Python
Simple values like strings and numbers are used in Python to represent single pieces of information. To define something more complex, like a data structure that is made up of multiple attributes such as height, weight, and age, you create a class.
Once the class is defined, it allows you to instantiate objects. In Python, a class is called as a function. When called as a function, the class acts as a constructor that delivers a class instance. Internally, the constructor calls the initialisation function __init__()
that sets up the object’s state.
Let’s say we’re modeling the concept of a container as a class, with the name ‘Container’. The following methods might be used to define important interactions:
Method | Explanation |
---|---|
__init__
|
Initialize new container with initial values. |
__repr__
|
Outputs condition of container. |
volume
|
Outputs volume of container. |
volume_filled
|
Outputs filling state of the container. |
volume_available
|
Outputs remaining volume in container. |
is_empty
|
Tests if container is empty. |
is_full
|
Tests if container is full. |
empty
|
Empties container and returns contents. |
_add
|
Internal method to add a substance without performing checks. |
add
|
Public method to add specified amount of substance if space is available. |
fill
|
Fills the remaining volume of the container with a substance. |
pour_into
|
Transfers contents of the container to another container. |
__add__
|
Implements addition operator for container; falls back to pour_into method.
|
With that in mind, here’s the code that we can use to define the container class:
class Container:
def __init__(self, volume):
# volume in ml
self._volume = volume
# start out with empty container
self._contents = {}
def __repr__(self):
"""
Textual representation of container
"""
repr = f"{self._volume} ml Container with contents {self._contents}"
return repr
def volume(self):
"""
Volume getter
"""
return self._volume
def is_empty(self):
"""
Container is empty if it has no contents
"""
return self._contents == {}
def is_full(self):
"""
Container is full if volume of contents equals capacity
"""
return self.volume_filled() == self.volume()
def volume_filled(self):
"""
Calculate sum of volumes of contents
"""
return sum(self._contents.values())
def volume_available(self):
"""
Calculate available volume
"""
return self.volume() - self.volume_filled()
def empty(self):
"""
Empty the container, returning its contents
"""
contents = self._contents.copy()
self._contents.clear()
return contents
def _add(self, substance, volume):
"""
Internal method to add a new substance / add more of an existing substance
"""
# update volume of existing substance
if substance in self._contents:
self._contents[substance] += volume
# or add new substance
else:
self._contents[substance] = volume
def add(self, substance, volume):
"""
Public method to add a substance, possibly returning left over
"""
if self.is_full():
raise Exception("Cannot add to full container")
# we can fit all of the substance
if self.volume_filled() + volume <= self.volume():
self._add(substance, volume)
return self
# we can fit part of the substance, returning the left over
else:
leftover = volume - self.volume_available()
self._add(substance, volume - leftover)
return {substance: leftover}
def fill(self, substance):
"""
Fill the container with a substance
"""
if self.is_full():
raise Exception("Cannot fill full container")
self._add(substance, self.volume_available())
return self
def pour_into(self, other_container):
"""
Transfer contents of container to another container
"""
if other_container.volume_available() < self.volume_filled():
raise Exception("Not enough space")
# get the contents by emptying container
contents = self.empty()
# add contents to other container
for substance, volume in contents.items():
other_container.add(substance, volume)
return other_container
def __add__(self, other_container):
"""
Implement addition for containers:
`container_a + container_b` <=> `container_b.pour_into(container_a)`
"""
other_container.pour_into(self)
return self
PythonIf we instantiate a glass and fill it with water, the result will be a glass at full capacity:
glass = Container(300)
glass.fill('Water')
assert glass.is_full()
PythonAnd when we take the water back, the glass is empty again:
contents = glass.empty()
assert contents == {'Water': 300}
assert glass.is_empty()
PythonFor a slightly more complex example, we’ll mix orange juice and wine in a pitcher, which is our version of a mimosa! First, we create the containers, then we fill them with wine and orange juice respectively:
pitcher = Container(1500)
bottle = Container(700)
carton = Container(500)
# fill ingredients
bottle.fill('Red wine')
carton.fill('Orange juice')
PythonWe can use the addition-assignment operator += to pour the contents of both containers into the pitcher:
# pour ingredients into pitcher
pitcher += bottle
pitcher += carton
# check that everything worked
assert pitcher.volume_filled() == 1200
assert bottle.is_empty() and carton.is_empty()
PythonThis works because our container class uses the __add__()
method. The assignment of pitcher += bottle is transformed into pitcher = pitcher + bottle. Then pitcher + bottle is translated by Python into pitcher.__add__(bottle)
.
Static attributes
A static method is bound to a class, not an object instantiated from the class. Static methods can’t access or modify an object’s state but can be called without referencing an object.
Static methods are mostly utility methods that only operate on their arguments. In contrast, a class method receives a class as its first parameter. They’re mostly used to perform an operation on a complete class, without the need or desire to access any individual object. For example, conversions (metric to imperial,etc.) can be implemented as static methods.
Attributes are called ‘static’ because they exist before an object is instantiated. Static attributes can be either data or methods.
Python doesn’t have a static keyword to explicitly distinguish between object attributes and class attributes. Instead, the @staticmethod decorator is used.
Let’s look at an example of a static method for our Container classes. This one will be a conversion of milliliters to fluid ounces:
# inside of class `Container`
...
@staticmethod
def floz_from_ml(ml):
return ml * 0.0338140227
PythonAccess to static attributes is performed with an attribute reference in dot notation using the pattern Classname.attr.
floz = Container.floz_from_ml(1000)
assert floz == 33.8140227
PythonInterfaces
An ‘interface’ is the collection of all public methods of an object. It defines and records the behaviour of an object, serving as a kind of API.
Unlike C++, Python doesn’t have separate layers for interfaces (AKA header files) and implementations. There is no explicit ‘interface’ keyword.
Python determines which methods are bound to an object and the class that it was instantiated from at runtime. Therefore, the language doesn’t require explicit interfaces. Instead, it uses ‘duck typing’:
“If it walks like a duck and it quacks like a duck, then it must be a duck” – Source: https://docs.python.org/3/glossary.html#term-duck-typing
What’s duck typing? It means your Python program can flexibly use objects of different classes in the same context. You can rely on the language checking for the presence of a given method and using it as long as the context and syntax are correct.
Inheritance
Inheritance is a useful feature in object-oriented programming that allows you to build a hierarchy of classes. So, a child class already defines all of the attributes of the parent. Multiple inheritance can be used flexibly in Python as well.
Let’s extend our container class to allow for sealed containers. We’ll define a new class called SealableContainer, which will inherit attributes from Container. We can also define a new Sealable class. It will allow us to apply and remove the seal. Since the Sealable class just provides another class with new methods, it is called a ‘mixin’:
class Sealable:
"""
Implementation needs to:
- initialize `self._seal`
"""
def is_sealed(self):
return self._seal is not None
def is_open(self):
return not self.is_sealed()
def is_closed(self):
return not self.is_open()
def open(self):
"""
Opening removes and returns the seal
"""
seal = self._seal
self._seal = None
return seal
def seal_with(self, seal):
"""
Closing attaches the seal and returns the Sealable
"""
self._seal = seal
return self
PythonOur SealableContainer inherits from the Container class and the Sealable mixin. We override the __init__()
method and define two new parameters. This will let us set the contents and seal state of our SealableContainer at instantiation.
class SealableContainer(Container, Sealable):
"""
Start out with empty, open container
"""
def __init__(self, volume, contents = {}, seal = None):
# initialize `Container`
super().__init__(volume)
# initialize contents
self._contents = contents
# initialize `self._seal`
self._seal = seal
def __repr__(self):
"""
Append 'open' / 'closed' to textual container representation
"""
state = "Open" if self.is_open() else "Closed"
repr = f"{state} {super().__repr__()}"
return repr
def empty(self):
"""
Only open container can be emptied
"""
if self.is_open():
return super().empty()
else:
raise Exception("Cannot empty sealed container")
def _add(self, substance, volume):
"""
Only open container can have its contents modified
"""
if self.is_open():
super()._add(substance, volume)
else:
raise Exception("Cannot add to sealed container")
PythonSimilar to using the __init__()
method, we override other methods to differentiate our SealableContainer from the unsealed Container. We override __repr__()
so that the open or closed state is also being output. We also override the empty()
and _add()
methods, with the effect that closed containers must be opened before emptying or filling them. We use super()
to access the existing functionality of the parent class.
Congratulations, you’ve just learned how to use Python for object-oriented programming. Let’s celebrate by mixing a Cuba Libre! We’ll need a glass, a small bottle of cola, and a shot glass filled with 20cl of rum:
glass = Container(330)
cola_bottle = SealableContainer(250, contents = {'Cola': 250}, seal = 'Bottlecap')
shot_glass = Container(40)
shot_glass.add('Rum', 20)
PythonNow we can add ice to the glass before pouring the rum in. Because the cola bottle starts off in the sealed state, we need to open it first. Then we can pour the contents into the glass:
glass.add('Ice', 50)
# add rum
glass += shot_glass
# open cola bottle
if cola_bottle.is_closed():
cola_bottle.open()
# pour cola into glass
glass += cola_bottle
Python