print(type(1))
print(type("hello"))
print(type([]))
print(type(()))
print(type({}))
def f(x):
print(x)
print(type(f))
<class 'int'>
<class 'str'>
<class 'list'>
<class 'tuple'>
<class 'dict'>
<class 'function'>
Python is a so-called “multi-paradigm” language, meaning it allows for multiple ways to code and design programs. One of these ways is object-oriented programming (OOP). OOP is a powerful paradigm but involves fairly complex concepts (polymorphism, inheritance, etc.). Fortunately for us, Python does not enforce coding in OOP. However, the internal workings of Python are heavily influenced by OOP, and most widely used packages rely on objects to varying degrees. In this tutorial, we will study the basics of OOP to be autonomous when its use is necessary.
You may have heard that Python is an “object-oriented programming” language. OOP is a programming paradigm that structures programs around an abstraction, the object, which contains attributes (characteristics of the object) and methods (functions specific to the object) that act on itself. To illustrate this somewhat abstract definition, we can take the example (source) of a “lemon” object that contains the attributes “flavor” and “color”, and a method “squeeze” that allows extracting its juice.
In Python, everything is an object (in the OOP sense). Let’s see what this means by retrieving the type of various objects we’ve seen in previous tutorials.
print(type(1))
print(type("hello"))
print(type([]))
print(type(()))
print(type({}))
def f(x):
print(x)
print(type(f))
<class 'int'>
<class 'str'>
<class 'list'>
<class 'tuple'>
<class 'dict'>
<class 'function'>
These elements are all of different types, but they have one thing in common: the term class
. Just as the def
statement defines a function, the class
statement defines a class of Python objects. Thus, each object usable in Python has a class that defines the object, its attributes, and its methods.
Let’s see how we can use the class
statement to define our “lemon” object.
class Lemon:
def __init__(self, color, juice_qty):
self.flavor = "sour"
self.color = color
self.juice = juice_qty
def get_juice_qty(self):
print("There is " + str(self.juice) + " mL of juice left in the lemon.")
def squeeze_juice(self, amount):
if amount > self.juice:
print("There is not enough juice in the lemon for the requested amount.")
else:
self.juice = max(0, self.juice - amount) # avoids any negative value of `juice`
Let’s analyze the syntax for constructing a class of objects:
The class
statement defines the class of objects. Different objects can be created according to the model defined by this class. By convention, the class name should start with an uppercase letter.
The class specifies several functions. In this particular context, these functions are called “methods”: they are specific to the defined class of objects.
A first very specific method, named __init__
, is called the constructor. It allows defining the attributes attached to this class of objects. It is possible to pass parameters to the function (such as color
and juice_qty
) to define attributes specific to an instance of the object (more details on this concept in the next section).
The constructor has a mandatory parameter: self
. It is a reference to the instances that will be created from this class. Note the syntax that defines an attribute: self.attribute = value
.
The other methods are defined by the user. They also take self
as a parameter, allowing them to perform operations on/from the attributes. As they are functions, they can also accept other parameters. Thus, the squeeze_juice
function takes an amount
parameter that defines how much juice is extracted from the lemon when squeezed.
The class can be seen as the recipe that allows creating an object: it defines the attributes and methods that all objects defined from this class will have. Defining a class as above simply makes this recipe available in the Python environment. To create an object according to this class, it must be instantiated.
= Lemon(color="yellow", juice_qty=45)
lemon1 = Lemon(color="green", juice_qty=32)
lemon2
print(type(lemon1))
print(type(lemon2))
<class '__main__.Lemon'>
<class '__main__.Lemon'>
Here, we have created two instances of the Lemon
class. These two instances are independent: Python sees them as two distinct objects. However, they were created from the same class and therefore have the same type.
This distinction between the class and its instances helps to better understand the meaning of the self
parameter. It is a reference to the instances that will be created according to the class, allowing the specification of their attributes and methods. When a given instance is created, it essentially becomes the self
.
An attribute is a variable associated with an object. An attribute can contain any Python object.
Once the object is instantiated, it is possible to access its attributes. The syntax is simple: instance.attribute
.
print(lemon1.color)
print(lemon2.color)
print(lemon1.juice)
print(lemon2.juice)
yellow
green
45
32
We can see that the two instances are independent: although they are of the same type, their attributes differ.
Modifying an attribute of an instance is very simple, the syntax is: instance.attribute = new_value
.
= "red"
lemon2.color print(lemon2.color)
red
It is also possible to add an attribute according to the same logic: instance.new_attribute = value
. However, this is not good programming practice, as the class precisely serves to define the attributes that objects of a given class can have. Therefore, it is generally preferable to define attributes within the class rather than outside.
The two instances we created illustrate different types of attributes:
Class attributes. These are attributes that have the same value for every instance created according to this class. Here, it is the flavor
attribute: all lemons are sour, so there is no reason to allow modifying this parameter during instantiation. Strictly speaking, we could even define this attribute outside the constructor.
Instance attributes. These are attributes whose values can vary between different instances created according to the same class. Here, these are the color
and juice
attributes: there are lemons of different colors and larger or smaller lemons that will therefore have different amounts of juice. It is up to the user to define these attributes during instantiation.
A method is a function associated with an object. It can use its attributes, modify them, and involve other methods of the object.
The syntax for calling a method of an instantiated object is as follows: instance.method(parameters)
.
lemon1.get_juice_qty()
There is 45 mL of juice left in the lemon.
Two remarks can be made about this syntax. The first is that a method is a function attached to an instance of an object. Unlike functions defined via the def
statement, methods do not have an independent existence outside the object’s instance. In our case, calling the get_juice_qty()
function independently of the object returns an error.
get_juice_qty()
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[19], line 1 ----> 1 get_juice_qty() NameError: name 'get_juice_qty' is not defined
The second remark is that we no longer specify the self
parameter when manipulating an instance. The instance itself has become the self
(or rather a self). The link between the method and its instance is already made since the method cannot be used without first calling the instance.
The main interest of methods is that they can access attributes and perform operations based on them, as well as modify them. Let’s take our example again to illustrate this possibility.
= Lemon(color="yellow", juice_qty=45)
lemon1
lemon1.get_juice_qty()12)
lemon1.squeeze_juice( lemon1.get_juice_qty()
There is 45 mL of juice left in the lemon.
There is 33 mL of juice left in the lemon.
The get_juice_qty
method simply displays the value of an attribute in a formatted way. The squeeze_juice
method, on the other hand, permanently modifies the value of the juice
attribute, as shown by the second call to get_juice_qty
.
The previous example is interesting because it illustrates both an advantage and a disadvantage of OOP.
The fact that objects have attributes allows maintaining the state of a resource—in our example, the amount of juice contained in a given Lemon
class object. To take more realistic examples, this property is interesting and used in several cases:
Training a machine learning model. It is common to train a model once and then want to continue training it longer or with different data. Saving the state in an instance of the Model
class allows for this. This is why most machine learning packages in Python are based on OOP.
The continuous operation of a web application. Such an application must keep things in memory to provide a smooth user experience: the fact that the user has logged in, their history, etc. Again, most web frameworks (Django
, Flask
, etc.) rely on OOP.
At the same time, using objects that keep a state in memory can limit the reproducibility of analyses. To illustrate this, let’s return to the example in the tutorial: execute the following cell several times in a row.
{python}
lemon1.get_juice_qty()
lemon1.squeeze_juice(12)
lemon1.get_juice_qty()
The three executions give different results, even though the executed code is strictly the same. This illustrates the reproducibility issue: when using OOP, you must be mindful of the state of objects kept in memory, or you risk not getting the same results when replicating an analysis.
1/ “In Python, everything is an object”: what does this phrase mean?
2/ What is the purpose of the class
statement?
3/ What is the purpose of the __init__
constructor?
4/ What is the purpose of self
?
5/ What is the difference between a class and an instance?
6/ What is an attribute?
7/ What is the difference between a method and a function?
8/ How can you tell the difference between an attribute and a method when calling them?
9/ Can an attribute be modified by a method? Can an attribute be modified outside a method?
10/ When is OOP generally used?
1/ It means that all Python objects (numbers, strings, lists, etc.) are objects in the OOP sense: they have attributes and methods defined by a class.
2/ The class
statement is used to define a class of objects.
3/ The __init__
constructor is a special method that allows the user to define an object’s attributes.
4/ self
serves as a reference to the instance within the class. It indicates who will carry the attributes and methods once the object is instantiated.
5/ The class is the “recipe” that defines all the characteristics of the object. But the object is not truly created until the class is instantiated, i.e., when an instance is created from the class.
6/ An attribute is a variable associated with an object.
7/ A method is a specific function: it is associated with an object and does not exist independently of it.
8/ The presence of parentheses distinguishes between calling an attribute and calling a method. Attribute call: instance.attribute Method call: instance.method() with any parameters.
9/ Yes, this is one of the main uses of methods. But an attribute can also be manually modified.
10/ When manipulating objects whose state of a resource should be maintained within a program.
Assume that the juice contained in a lemon is a proportional function of its mass, defined as follows: \(juice = \frac {mass} {4}\) where mass is in grams and juice in mL.
Modify the Lemon
class, reproduced in the following cell, so that:
During instantiation, the user no longer defines the quantity of juice, but the mass of the lemon.
The juice
attribute is calculated according to the above formula.
Add a method that displays “The mass of the lemon is x grams.”
Instantiate a new lemon and verify that everything works as expected.
class Lemon:
def __init__(self, color, juice_qty):
self.flavor = "sour"
self.color = color
self.juice = juice_qty
def get_juice_qty(self):
print("There is " + str(self.juice) + " mL of juice left in the lemon.")
def squeeze_juice(self, amount):
if amount > self.juice:
print("There is not enough juice in the lemon for the requested amount.")
else:
self.juice = max(0, self.juice - amount) # avoids any negative value of `juice`
# Test your answer in this cell
class Lemon:
def __init__(self, color, mass):
self.flavor = "sour"
self.color = color
self.mass = mass
self.juice = mass / 4
def get_mass(self):
print("The mass of the lemon is " + str(self.mass) + " grams.")
def get_juice_qty(self):
print("There is " + str(self.juice) + " mL of juice left in the lemon.")
def squeeze_juice(self, amount):
if amount > self.juice:
print("There is not enough juice in the lemon for the requested amount.")
else:
self.juice = max(0, self.juice - amount) # avoids any negative value of `juice`
= Lemon("yellow", 500)
lemon
lemon.get_mass() lemon.get_juice_qty()
The mass of the lemon is 500 grams.
There is 125.0 mL of juice left in the lemon.
Exercise freely inspired by: https://github.com/Pierian-Data/Complete-Python-3-Bootcamp
We have seen that OOP is particularly interesting when we want to manipulate objects that keep the state of a resource. This is, for example, the case of a bank account, which keeps a balance and allows or disallows certain operations based on this balance.
Implement a BankAccount
class with:
Two attributes: holder
(account holder’s name) and balance
(account balance in euros).
A show_balance
method that displays: “The balance of the account of holder_name is x euros.”
A deposit
method that accepts an amount
parameter. When a deposit is made, the account balance is incremented by the deposit amount.
A withdraw
method that accepts an amount
parameter. When a withdrawal is made:
If the amount is less than the balance: the balance is decremented by the amount, and “Withdrawal accepted.” is displayed.
If the amount is greater than the balance: “Withdrawal denied: insufficient funds.” is displayed and the balance remains unchanged.
A transfer
method that accepts an amount
parameter and a recipient
parameter that takes another instance of the BankAccount
class (i.e., another client). For example, client1.transfer(recipient=client2, amount=1000)
:
If the amount is less than client1’s balance: client1’s balance is decremented by the amount, client2’s balance is incremented by the amount.
If the amount is greater than client1’s balance: “Transfer denied: insufficient funds.” is displayed and the balances of both clients remain unchanged.
Create two clients and test that the various functionalities to be implemented work as expected.
# Test your answer in this cell
class BankAccount:
def __init__(self, holder, balance):
self.holder = holder
self.balance = balance
def show_balance(self):
print("The balance of the account of " + self.holder + " is " + str(self.balance) + " euros.")
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if self.balance >= amount:
self.balance -= amount
print("Withdrawal accepted.")
else:
print("Withdrawal denied: insufficient funds.")
def transfer(self, recipient, amount):
if self.balance >= amount:
+= amount
recipient.balance self.balance -= amount
else:
print("Transfer denied: insufficient funds.")
= BankAccount("Bernard", 2000)
client1 = BankAccount("Bianca", 5000)
client2
client1.show_balance()
client2.show_balance()
print() # newline
1000)
client1.deposit(# +1000
client1.show_balance()
print()
6000)
client2.withdraw(# no change
client2.show_balance()
print()
1000)
client2.withdraw(# -1000
client2.show_balance()
print()
5000)
client2.transfer(client1, # no change
client2.show_balance()
print()
2000)
client2.transfer(client1, # - 2000
client2.show_balance() # + 2000 client1.show_balance()
The balance of the account of Bernard is 2000 euros.
The balance of the account of Bianca is 5000 euros.
The balance of the account of Bernard is 3000 euros.
Withdrawal denied: insufficient funds.
The balance of the account of Bianca is 5000 euros.
Withdrawal accepted.
The balance of the account of Bianca is 4000 euros.
Transfer denied: insufficient funds.
The balance of the account of Bianca is 4000 euros.
The balance of the account of Bianca is 2000 euros.
The balance of the account of Bernard is 5000 euros.