Techno Blender
Digitally Yours.

Python Classes Made Easy: The Definitive Guide to Object-Oriented Programming | by Federico Trotta | Mar, 2023

0 35


Boost your Python skills with this comprehensive class reference

Image by Lukas Bieri on Pixabay

When it comes to classes, many Python developers struggle, for a lot of reasons. Firstly — in my opinion — because the concept of Object Oriented Programming is not always clear. Secondly, because the ideas behind classes and Object Oriented Programming (OOP) are a lot and the explanations we may find here and there (mainly, online) may be superficial.

In this article, I want to cover the most important concepts behind Python classes, and how to use them (with coding examples).

But, firstly, we’ll start this article by discussing Object Oriented Programming.

Table of Contents

Object Oriented Programming
Classes in Python
The "self" Parameter
The "__init__" Method
if __name__ == "__main__"
Type Hints
Docstrings (and how to invoke them)
Inheritance
Pro tip on how to use Python classes

Quoting and paraphrasing reference [1], we can say that, as Humans, we are perfectly aware of what objects are: they are everything tangible that can be felt with our senses and that can be manipulated. During our growth, we learn the power of abstraction and objects become models. In other words, we use our imagination, developed in years of experience, to simplify complex problems using simple concepts: this is the process of abstraction using models.

For example, we may model the Earth as a sphere (even if it is not a sphere!) to perform some calculations (for example, to determine its orbit).

This is not so much different from what we do in software development. In fact, for us developers, an object is not something that we can touch or feel, but it is a model of something that performs some software tasks.
More precisely, for us, we can say that an “object is a collection of data with associated behaviors”. [1]

Let’s make an example to clarify the concept (we paraphrase reference [1] for the following example).
We want to create an object called “Apple”. In this case, the data associated with it can be the color (for example, red). The behavior associated with it can be the fact that we want to put the apple in a basket.

An object in software development. Image by Author.

Now, what if we encounter a green apple? It is still an apple, but with a different color: can we use this concept? Here’s where classes come into help!

A class is a “template” that defines the structure of an object. This means that an object has its own particular set of attributes, regarding a certain class.

To use the example of the apple, we can say that the class Apple can have:

  • Data → the color
  • Behavior → where to put the apple

With this template, we can create how many objects we want. For example, we can create the object Apple_greenthat uses the class apple with color=green as data, and that puts the apples in the basket on the table.

We can also have an object called Apple_red that uses the class apple with color=red as data and that puts the apples back on the shelf.

The difference between an object and a class. Image by Author.

So, as we can understand with the example of the apples, we can say that data represent a specific characteristic of a certain object.
Behaviors, on the other hand, represent actions; these actions are expressed as methods of the class, in Python. And, as we’ll see later, a method of a class is defined the same way we define a function.

So: what is Object Oriented Programming?

OOP is “the practice of writing code towards modeling object” [1].
In other words, it means creating a series of interacting objects through their data and behaviors.

This does not mean that we’ll always have to create classes when we program in Python. Here we have to understand a simple concept: the power of Python relies on the fact that everything in this programming language is an object. In fact, even if we can’t see it, we are working with objects and classes under the hood in Python.

For example, let’s say we define a variablea = 3. If we want to see its type we can write the following code:

a = 3

type(a)

>>>

int

We have used the built-in function type() to determine the properties of the variable a and it told us that this is an int (that is to say, an integer). But int is a Python built-in class; this means that in Python we can define an integer variable, as we have seen above, without creating an actual class because Python does so for us (in other programming languages, we’d actually have to create a class for such a simple example).

So, let’s remember: whenever we program in Python, we always have to deal with objects and classes even if we don’t explicitly declare them because Python, under the hood, does the work to invoke built-in classes.

In Python, classes must be defined with a capital letter. Here’s how we can define a simple class:

class MyFirstClass:
a = 15 # this is the data of the class
pass

We have created a simple class that sets a=15.

Now, let’s make a class that actually does something more useful by adding a method (note: the class Point we’ll use throughout this article is taken from reference [1], and is modified as per my taste and needs):

class Point:

def origin(self): # this is a method
self.x = 0
self.y = 0

So, we’ve created a class called Pointwith a method, called origin, that does a simple thing: it gets a point in 2D to x=0and y=0 (the origin).

Now, as we can see, the method passes one argument called self. This can be called as we want but, as a standard, the community of Python developers calls it self: we’ll explain more details later on how it works.

Now, if we want to invoke the class, we can define a point p and do the following:

# Invoke the class
p = Point()

Now, we can access the method defined in the class by writing:

# Access the method
p.origin()

So, now our point has gone to the origin. To be sure, we can do the following:

# Print the coordinates
print(p.x, p.y)

>>>

0 0

The “self” Parameter

Now, let’s explain what the previously introduced self argument is, and how to use it.

The self argument serves to invoke the method. This is why we call it self as a standard: because we are invoking the method. It’s like we are telling Python: “Ehy! This method is self-invoked”.

When we need to pass multiple arguments to a method, self must be the first one: otherwise, another parameter will invoke the method (as we said, there is no need to call it self: this name is just a convention between developers).

Let’s see an example where we use self and compare it to another where we don’t use it.

class Test:

def printing(self):
print("this is a test")

So, we’ve created a simple class that, when we invoke the method printing, prints “this is a test”:

# Invoke the class
test = Test()

# Call the method
test.printing()

>>>

this is a test

Now, let’s see the example without self:

class Test2:

def printing2():
print("this is a test")

# Invoke the class
test2 = Test2()

# Call the method
test2.printing2()

>>>

Type Error: printing2() takes 0 positional arguments but 1 was given

So, in this case, Python returns an error that is telling us a simple thing: the method can’t be invoked because it has no arguments.
This is why we need self.

Now, let’s create a function with multiple arguments:

class Point:

def move(self, x, y):
self.x = x
self.y = y

def origin(self):
self.move(0,0)
return(self.x, self.y)

We have created a class that:

  1. moves two coordinates (xand y). As we can see, self invokes the method.
  2. gets the coordinates to the origin. When we invoke the origin method, in fact, it will move both the coordinates (its only argument is self) to the origin ( self.move(0, 0)), and returns the coordinates.

So, we can invoke the class and move the coordinates to the origin:

#Invoke the class
p = Point

# Call the method
p.origin()

>>>

0, 0

And the coordinates have gone to the origin.

Now, let’s read some lines back: we said that the movemethod moves the coordinates, but this is not true. The actual movement is done by the method origin.
In fact, the move method does just a simple thing: it invokes the needed values. This is why the first method of a class has a special nomenclature.

The “__init__” Method

So far, we have created a class that moves the coordinates of a point (x and y) to the origin. But what if we want to move the point to any particular position we may want?

To do so, we have to create another class. For our first try, let’s create it with the same logic used before. It can be something like that:

class Point:

def move(self, x, y):
self.x = x
self.y = y

def position(self):
print(self.x, self.y)

Now, let’s invoke the class and the method:

# Invoke the class
p = Point()

# Get to position
p.position(5,6)

>>>

TypeError: Point() takes no arguments

And…we’ve got an error! And it tells us that the class Pointtakes no argument. How is that possible? We have specified to pass self, x, and y; so, apart from self that invokes the method, the class should accept two values!

The error is in the first method. As we’ve said in the previous paragraph, the first method does only one thing: it invokes the needed values. It doesn’t move the point to a specific location.

This is the reason why the first method of a class must be a specific method called __init__. So, to properly invoke the needed values, we have to create the Pointclass as follows:

class Point2:

def __init__(self, x, y):
self.x = x
self.y = y

def position2(self):
print(self.x, self.y)

Now, let’s invoke it:

# Invoke the class
p = Point2(5,6)

# Get to position
p.position2()

>>>

5 6

And here we are!

NOTE:

there is even the possibility to create classes that do not uses the
__init__ method. In these cases, the variables can be invoked in
other ways.

Anyway: we won't talk about that case here because we believe this is
not a Pythonic way to develop Python software (also, the code becomes
a little bit longer and less redeable).

If __name__ == ”__main__”

Now, at the end of some classes, we may have found if __name__ == "__main__". If you don’t know what it is, here is a wide explanation.

Quoting and paraphrasing from [2]:

__name__ is a special built-in variable we have to use when we create interactable modules because it allows us to make a distinction between the modules we import and the modules we don’t.

By module, in Python, we intend packages and libraries, but even any piece of code that is separable from other code and that can work on its own. Given that classes (but even functions, or even any Python file) can work on their own, they can be considered modules.

__main__, on the other hand, is a special module that represents the name of the top-level code in a script or program.

So, basically, if we execute if __name__ = "__main__" we are checking if the code we are executing is top-level or not.

So, what is top-level code?

We take a very explanatory example from this thread in StackOverflow:

import sys         # top-level

3 + 4 # top-level

x = 0 # top-level

def f(): # top-level
import os # not top-level!
return 3 # not top-level

if x: # top-level
print 3 # not top-level
else:
print 4 # not top-level, but executes as part of an if statement
# that is top-level

class TopLevel(object): # top-level
x = 3 # not top-level, but executes as part of the class statement
def foo(self): # not top-level, but executes as part of the class statement
print 5 # not top-level

Well, as we can see, the problem is related to the Python interpreter.
Let me explain.

If a module is been executed on its own, then there is no problem. The problems arrive when a module is imported into another Python file. Suppose that the above script is imported in another file: when we import it, it immediately executes all the top-level code and all the code that is part of an ifor a class statement, as the comments in the above snippet of code tell us.

Let’s see it in more detail, with an example.
Let’s create a Python file called package_1.py like so:

# Define a function to print "Hello"
def print_hello():

print("hello")

#Invoke the function
print_hello()

If we execute it via terminal, we get:

The execution of the package_1.py file. Image by Author.

So, it perfectly works.
Now, let’s create a Python file called main.py and import into it the package_1.py file, then run main.py:

The execution of the main.py file. Image by Author.

As we can see, when main.pyis run, the module package_1.pyis immediately executed! This is something we don’t want for a simple reason: we want to use the code we import from a module when we actually call the code. In other words: if a whole module executes the code immediately after it is imported, it’s not useful at all.

To avoid that, we use if __name__ = "__main__" in our package_1.py and, after it, we invoke the print_hello() function:

The execution of the main.py file using ‘if __name__ == “__main__”’. Image by Author.

So, as [2] tells us:

if __name__ = "__main__" prevents the automatic execution of the top-level code when a module is executed.

Also, previously we invoked the print_hello() function in the package_1.py file because we wanted the function to actually work when we run the program on its own. Now, invoking print_hello() after if __name__ = "__main__" will invoke the function when package_1.py is run on its own.

So, to recap and clarify using this example, if we invoke print_hello()after if __name__ = "__main__" in package_1.py :

  • print_hello() will be executed when package_1.py is run on its own (i.e., via terminal).
  • print_hello() will not be executed when we run main.py.
NOTE

We understand that we've used simple examples, but a general
case is that we create modules based on classes:
this is why we have covered this topic in this article.

Starting from Python 3, “PEP 484 — Type Hints” has introduced type hints in Python.

Hitting a type means suggesting the type to pass to a function (or to a method, in the case of classes). We all know that comments and docstrings must do their work, but typing hints really help us understand what to expect in functions.

NOTE:

here we'll talk about functions and not classes, just for the sake
of simplicity.
As we have seen, in fact, a method of a class is defined exactly as
a function: so what we'll see in this paragraph is generalizable
to classes.

Let’s make a simple example. Let’s create a function that returns a value:

def any_call(variable: any) -> any:
return variable

So, we have created a function that takes one argument (called variable) and the type hints tell us that:

  • the type of variable can be any. A string, an integer, etc…
  • the function returns any type (and, in fact, it returns variable)

Don’t you find it useful? Well, I discovered it some weeks ago and find it amazing! Especially, this becomes interesting with more complicated examples.

For example, let’s say we want a function that gets a list as an argument and returns the count of how many elements are in the list. The function can be like the following:

def count_values(element: list[any]) -> int:
return sum(1 for elements in element if elements)

-----------------------

count_values([1,2,3,4])

>>>

4

----------------------

count_values(["hello", "stranger", "who", "are", "you", "?"])

>>>

6

So, here our function takes element as the only argument, and we know that it must be a list of any type, thanks to type hints. Then, the function returns an integer as type; in fact, it counts the number of elements in the list.

So, we believe the point is clear. This is a very good implementation for improving the readability of our code.

Documentation is the most important part of every software project for a simple reason: in the next two months we’ll barely remember what we have done, and why.

For this reason, writing notes and explanations of our code is very important. Unfortunately, comments are not enough because they have to be very short (but explanative).

So, what we can do is use docstrings. PEP 257 specifies that:

A docstring is a string literal that occurs as the first statement in a module, function, class, or method definition. Such a docstring becomes the __doc__ special attribute of that object.

All modules should normally have docstrings, and all functions and classes exported by a module should also have docstrings. Public methods (including the __init__ constructor) should also have docstrings. A package may be documented in the module docstring of the __init__.py file in the package directory.

In other words, in software development, a docstring is a string type in the code that documents the code itself. As reference [1] tells us, “unlike comments, docstrings are available at runtime, simplifying code inspection and providing help or metadata during execution”.

Let’s see how to use docstrings in a Python class:

class Point:
"""
this class moves a point in 2D, passing its coordinates
as argument of the method 'position'
"""
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y

def position(self) -> int:
print(self.x, self.y)

To access the documentation (aka, the docstrings) we type the following:

Point.__doc__

>>>

" this class moves a point in 2D, passing its coordinates as
argument of the function 'position' "

So, this is how we can access the docstrings related to a class. What if we want to access the docstring of a particular method of a class?
Let’s see it:

class Point:
""" this class moves a point in 2D, passing its coordinates
as argument of the function 'position'
"""
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y

def position(self) -> int:
""" this actually moves the point"""
print(self.x, self.y)

And to access the documentation of the position method:

Point.position.__doc__

>>>
'this actually moves the point'

And here we are.

But we actually can (and should!) do better. For example:

class Point:
""" this class moves a point in 2D, passing its coordinates
as argument of the function 'position'

Args:
param 1 (int): the coordinate x of the point in 2D
param 2 (int): the coordinate y of the point in 2D
"""
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y

def position(self) -> int:
""" This method actually moves the point.

Returns:
int: prints the integers that represent the coordinates x and y
of the point in 2D
"""
print(self.x, self.y)

This may seem redundant since we have used type hints, but it is not because:

  1. we may need, in a first attempt, to just invoke the documentation of our class (maybe because we have imported it as a module in another file).
  2. we may use tools (like Sphinx, for example) to create the documentation in HTML; these tools use directly the docstrings.

This is why using very explanatory docstrings is very important.

Here we want to talk about a Python magic: inheritance.

As we’ve said before, OOP is the methodology to develop code with objects interacting between them. This means that when we create classes in Python we often have to make them interact.

When interacting, classes inherit the properties and functionalities of other classes.
Yes: it’s like your old aunt decides to leave you her 21 billion $ (who hasn’t such an aunt?!).

Now, firstly we have to say that every class we create uses inheritance. This happens because, as we’ve said before, Python uses built-in classes. So, when we invoke built-in classes, our code is inheriting the properties of the invoked built-in class or object.

Now, let’s see inheritance in action, with a practical example (note: I have taken the following classes from reference [1], and modified them as per my taste and needs).
We want to create a class that stores the name and the surname of some contacts, populating an empty list. We can create something like that:

class Contact:
"""
This class saves the name and the surnames
of some contacts in a list
"""

# Create empty list
all_contacts = []

def __init__(self, name: str, surname: str) -> None:
""" This method initializes the arguments and appends
the arguments (name and surname)into the empty list.

Returns:
nothing.
"""
self.name = name
self.surname = surname
Contact.all_contacts.append(self) # Append to list

def __repr__(self) -> str:
"""
The built-in __repr__ method provides a string representation
of an object.

Returns:
the compiled list with name and surname of the contacts
"""
return (
f"{self.__class__.__name__}("
f"{self.name!r}, {self.surname!r}"
f")"
)

Now, let’s try it:

# Define a contact
contact_1 = Contact("Federico", "Trotta")

# Show the contacts in the list
Contact.all_contacts

>>>

[Contact('Federico', 'Trotta')]

Now, suppose we want to collect the email of the contacts but, for any reason, we want to create a separate class. We can create a class like the following:


class Email(Contact): # Email is inerithing from Contact

def get_mail(self, mail:"mail") -> None:
return mail

Now, let’s add an email like the following:

Email("federico trotta", "[email protected]")

Now, if we invoke Contact.all_contacts we get:

[Email('federico trotta', '[email protected]'),
Contact('Federico', 'Trotta')]

So, since our class Email has the class Contact as an argument, it inherits its properties. For example, the arguments passed to the Email class are appended to the list in the class Contact.

Also, this class inherits the fact that the __init()__ method requires two arguments. In fact, this is what we get with just one argument:

mail_2 = Email("[email protected]")

>>>

TypeError: __init__() missing 1 required positional argument: 'surname'

But, hey, wait!!! We haven’t used the __init()__ method in the class Email, so what happened?

It happened that the class Email inherited the __init()__ method, so there is no need to use it again!

If we want to use another __init()__ method in the child class (Email is a child class), we must make some adjustments like the following ones:

class General(Contact):

def __init__(self, name: str, email: str) -> None:
super().__init__(name, email)
self.email = email

And it works exactly like the previous one:

# Create a contact
general_contact = General("Federico Trotta", "[email protected]")

# Write the contact into the list
Contact.all_contacts

>>>

[General('Federico Trotta', '[email protected]')

So, the classes Email and General work exactly the same way and give us exactly the same results, but the power of inheritance shows us that without using the __init()__, method as we did with Email, the code is simplified.

Also, as we can see, in the case of the General class, we have used the super.__init()__ method: we must use it in the case of a child class, because it initializes the inherited attributes of the parent class (Contact).

If you came across this article because you struggle understanding classes, chances are that you don’t have a clear idea of why you should use them. If it is so, welcome to the club: I had the same struggle.

I mean: I understood that classes (and, of course, functions) help us automatize our code, but the fact that we have to invoke them created some difficulties for me.

This happened to me because I’ve started studying Python for Data Science (and here’re are my tips on how to properly do so) and, saying the truth, in many cases, there is no reason to use classes when we use Python for Data Science.

So, my advice to properly understand the need to use classes is to treat them as modules. This means that, when you want to start a new Python project, a piece of very good advice is to create a main.py file where you invoke all the classes you need; anyway, these classes shouldn’t be created in the main.py: they should be created in separate Python files (usually, we create one Python file for each class) and imported in the main.py where we use them.

In the next few days, I’ll create a detailed article to explain this in more detail: so don’t miss any notifications, and subscribe to my mailing list.

In this article, we’ve seen a comprehensive guide on classes, hoping it clarifies the main topics on it.

The only thing that you have to do now is to practice a lot with classes, hoping this guide will help you when you need it.

Need more content on Python to start or boost your career? Here are some of my articles that can help you:

Python:

Consider becoming a member: you could support me with no additional fee. Click here to become a member for less than 5$/month so you can unlock all the stories, and support my writing.

Bibliography and videography:

[1] Python Object-Oriented Programming — S.F. Lott, D. Phillips

[2] If __name__ == “__main__” for Python Developers (video)


Boost your Python skills with this comprehensive class reference

Image by Lukas Bieri on Pixabay

When it comes to classes, many Python developers struggle, for a lot of reasons. Firstly — in my opinion — because the concept of Object Oriented Programming is not always clear. Secondly, because the ideas behind classes and Object Oriented Programming (OOP) are a lot and the explanations we may find here and there (mainly, online) may be superficial.

In this article, I want to cover the most important concepts behind Python classes, and how to use them (with coding examples).

But, firstly, we’ll start this article by discussing Object Oriented Programming.

Table of Contents

Object Oriented Programming
Classes in Python
The "self" Parameter
The "__init__" Method
if __name__ == "__main__"
Type Hints
Docstrings (and how to invoke them)
Inheritance
Pro tip on how to use Python classes

Quoting and paraphrasing reference [1], we can say that, as Humans, we are perfectly aware of what objects are: they are everything tangible that can be felt with our senses and that can be manipulated. During our growth, we learn the power of abstraction and objects become models. In other words, we use our imagination, developed in years of experience, to simplify complex problems using simple concepts: this is the process of abstraction using models.

For example, we may model the Earth as a sphere (even if it is not a sphere!) to perform some calculations (for example, to determine its orbit).

This is not so much different from what we do in software development. In fact, for us developers, an object is not something that we can touch or feel, but it is a model of something that performs some software tasks.
More precisely, for us, we can say that an “object is a collection of data with associated behaviors”. [1]

Let’s make an example to clarify the concept (we paraphrase reference [1] for the following example).
We want to create an object called “Apple”. In this case, the data associated with it can be the color (for example, red). The behavior associated with it can be the fact that we want to put the apple in a basket.

An object in software development. Image by Author.

Now, what if we encounter a green apple? It is still an apple, but with a different color: can we use this concept? Here’s where classes come into help!

A class is a “template” that defines the structure of an object. This means that an object has its own particular set of attributes, regarding a certain class.

To use the example of the apple, we can say that the class Apple can have:

  • Data → the color
  • Behavior → where to put the apple

With this template, we can create how many objects we want. For example, we can create the object Apple_greenthat uses the class apple with color=green as data, and that puts the apples in the basket on the table.

We can also have an object called Apple_red that uses the class apple with color=red as data and that puts the apples back on the shelf.

The difference between an object and a class. Image by Author.

So, as we can understand with the example of the apples, we can say that data represent a specific characteristic of a certain object.
Behaviors, on the other hand, represent actions; these actions are expressed as methods of the class, in Python. And, as we’ll see later, a method of a class is defined the same way we define a function.

So: what is Object Oriented Programming?

OOP is “the practice of writing code towards modeling object” [1].
In other words, it means creating a series of interacting objects through their data and behaviors.

This does not mean that we’ll always have to create classes when we program in Python. Here we have to understand a simple concept: the power of Python relies on the fact that everything in this programming language is an object. In fact, even if we can’t see it, we are working with objects and classes under the hood in Python.

For example, let’s say we define a variablea = 3. If we want to see its type we can write the following code:

a = 3

type(a)

>>>

int

We have used the built-in function type() to determine the properties of the variable a and it told us that this is an int (that is to say, an integer). But int is a Python built-in class; this means that in Python we can define an integer variable, as we have seen above, without creating an actual class because Python does so for us (in other programming languages, we’d actually have to create a class for such a simple example).

So, let’s remember: whenever we program in Python, we always have to deal with objects and classes even if we don’t explicitly declare them because Python, under the hood, does the work to invoke built-in classes.

In Python, classes must be defined with a capital letter. Here’s how we can define a simple class:

class MyFirstClass:
a = 15 # this is the data of the class
pass

We have created a simple class that sets a=15.

Now, let’s make a class that actually does something more useful by adding a method (note: the class Point we’ll use throughout this article is taken from reference [1], and is modified as per my taste and needs):

class Point:

def origin(self): # this is a method
self.x = 0
self.y = 0

So, we’ve created a class called Pointwith a method, called origin, that does a simple thing: it gets a point in 2D to x=0and y=0 (the origin).

Now, as we can see, the method passes one argument called self. This can be called as we want but, as a standard, the community of Python developers calls it self: we’ll explain more details later on how it works.

Now, if we want to invoke the class, we can define a point p and do the following:

# Invoke the class
p = Point()

Now, we can access the method defined in the class by writing:

# Access the method
p.origin()

So, now our point has gone to the origin. To be sure, we can do the following:

# Print the coordinates
print(p.x, p.y)

>>>

0 0

The “self” Parameter

Now, let’s explain what the previously introduced self argument is, and how to use it.

The self argument serves to invoke the method. This is why we call it self as a standard: because we are invoking the method. It’s like we are telling Python: “Ehy! This method is self-invoked”.

When we need to pass multiple arguments to a method, self must be the first one: otherwise, another parameter will invoke the method (as we said, there is no need to call it self: this name is just a convention between developers).

Let’s see an example where we use self and compare it to another where we don’t use it.

class Test:

def printing(self):
print("this is a test")

So, we’ve created a simple class that, when we invoke the method printing, prints “this is a test”:

# Invoke the class
test = Test()

# Call the method
test.printing()

>>>

this is a test

Now, let’s see the example without self:

class Test2:

def printing2():
print("this is a test")

# Invoke the class
test2 = Test2()

# Call the method
test2.printing2()

>>>

Type Error: printing2() takes 0 positional arguments but 1 was given

So, in this case, Python returns an error that is telling us a simple thing: the method can’t be invoked because it has no arguments.
This is why we need self.

Now, let’s create a function with multiple arguments:

class Point:

def move(self, x, y):
self.x = x
self.y = y

def origin(self):
self.move(0,0)
return(self.x, self.y)

We have created a class that:

  1. moves two coordinates (xand y). As we can see, self invokes the method.
  2. gets the coordinates to the origin. When we invoke the origin method, in fact, it will move both the coordinates (its only argument is self) to the origin ( self.move(0, 0)), and returns the coordinates.

So, we can invoke the class and move the coordinates to the origin:

#Invoke the class
p = Point

# Call the method
p.origin()

>>>

0, 0

And the coordinates have gone to the origin.

Now, let’s read some lines back: we said that the movemethod moves the coordinates, but this is not true. The actual movement is done by the method origin.
In fact, the move method does just a simple thing: it invokes the needed values. This is why the first method of a class has a special nomenclature.

The “__init__” Method

So far, we have created a class that moves the coordinates of a point (x and y) to the origin. But what if we want to move the point to any particular position we may want?

To do so, we have to create another class. For our first try, let’s create it with the same logic used before. It can be something like that:

class Point:

def move(self, x, y):
self.x = x
self.y = y

def position(self):
print(self.x, self.y)

Now, let’s invoke the class and the method:

# Invoke the class
p = Point()

# Get to position
p.position(5,6)

>>>

TypeError: Point() takes no arguments

And…we’ve got an error! And it tells us that the class Pointtakes no argument. How is that possible? We have specified to pass self, x, and y; so, apart from self that invokes the method, the class should accept two values!

The error is in the first method. As we’ve said in the previous paragraph, the first method does only one thing: it invokes the needed values. It doesn’t move the point to a specific location.

This is the reason why the first method of a class must be a specific method called __init__. So, to properly invoke the needed values, we have to create the Pointclass as follows:

class Point2:

def __init__(self, x, y):
self.x = x
self.y = y

def position2(self):
print(self.x, self.y)

Now, let’s invoke it:

# Invoke the class
p = Point2(5,6)

# Get to position
p.position2()

>>>

5 6

And here we are!

NOTE:

there is even the possibility to create classes that do not uses the
__init__ method. In these cases, the variables can be invoked in
other ways.

Anyway: we won't talk about that case here because we believe this is
not a Pythonic way to develop Python software (also, the code becomes
a little bit longer and less redeable).

If __name__ == ”__main__”

Now, at the end of some classes, we may have found if __name__ == "__main__". If you don’t know what it is, here is a wide explanation.

Quoting and paraphrasing from [2]:

__name__ is a special built-in variable we have to use when we create interactable modules because it allows us to make a distinction between the modules we import and the modules we don’t.

By module, in Python, we intend packages and libraries, but even any piece of code that is separable from other code and that can work on its own. Given that classes (but even functions, or even any Python file) can work on their own, they can be considered modules.

__main__, on the other hand, is a special module that represents the name of the top-level code in a script or program.

So, basically, if we execute if __name__ = "__main__" we are checking if the code we are executing is top-level or not.

So, what is top-level code?

We take a very explanatory example from this thread in StackOverflow:

import sys         # top-level

3 + 4 # top-level

x = 0 # top-level

def f(): # top-level
import os # not top-level!
return 3 # not top-level

if x: # top-level
print 3 # not top-level
else:
print 4 # not top-level, but executes as part of an if statement
# that is top-level

class TopLevel(object): # top-level
x = 3 # not top-level, but executes as part of the class statement
def foo(self): # not top-level, but executes as part of the class statement
print 5 # not top-level

Well, as we can see, the problem is related to the Python interpreter.
Let me explain.

If a module is been executed on its own, then there is no problem. The problems arrive when a module is imported into another Python file. Suppose that the above script is imported in another file: when we import it, it immediately executes all the top-level code and all the code that is part of an ifor a class statement, as the comments in the above snippet of code tell us.

Let’s see it in more detail, with an example.
Let’s create a Python file called package_1.py like so:

# Define a function to print "Hello"
def print_hello():

print("hello")

#Invoke the function
print_hello()

If we execute it via terminal, we get:

The execution of the package_1.py file. Image by Author.

So, it perfectly works.
Now, let’s create a Python file called main.py and import into it the package_1.py file, then run main.py:

The execution of the main.py file. Image by Author.

As we can see, when main.pyis run, the module package_1.pyis immediately executed! This is something we don’t want for a simple reason: we want to use the code we import from a module when we actually call the code. In other words: if a whole module executes the code immediately after it is imported, it’s not useful at all.

To avoid that, we use if __name__ = "__main__" in our package_1.py and, after it, we invoke the print_hello() function:

The execution of the main.py file using ‘if __name__ == “__main__”’. Image by Author.

So, as [2] tells us:

if __name__ = "__main__" prevents the automatic execution of the top-level code when a module is executed.

Also, previously we invoked the print_hello() function in the package_1.py file because we wanted the function to actually work when we run the program on its own. Now, invoking print_hello() after if __name__ = "__main__" will invoke the function when package_1.py is run on its own.

So, to recap and clarify using this example, if we invoke print_hello()after if __name__ = "__main__" in package_1.py :

  • print_hello() will be executed when package_1.py is run on its own (i.e., via terminal).
  • print_hello() will not be executed when we run main.py.
NOTE

We understand that we've used simple examples, but a general
case is that we create modules based on classes:
this is why we have covered this topic in this article.

Starting from Python 3, “PEP 484 — Type Hints” has introduced type hints in Python.

Hitting a type means suggesting the type to pass to a function (or to a method, in the case of classes). We all know that comments and docstrings must do their work, but typing hints really help us understand what to expect in functions.

NOTE:

here we'll talk about functions and not classes, just for the sake
of simplicity.
As we have seen, in fact, a method of a class is defined exactly as
a function: so what we'll see in this paragraph is generalizable
to classes.

Let’s make a simple example. Let’s create a function that returns a value:

def any_call(variable: any) -> any:
return variable

So, we have created a function that takes one argument (called variable) and the type hints tell us that:

  • the type of variable can be any. A string, an integer, etc…
  • the function returns any type (and, in fact, it returns variable)

Don’t you find it useful? Well, I discovered it some weeks ago and find it amazing! Especially, this becomes interesting with more complicated examples.

For example, let’s say we want a function that gets a list as an argument and returns the count of how many elements are in the list. The function can be like the following:

def count_values(element: list[any]) -> int:
return sum(1 for elements in element if elements)

-----------------------

count_values([1,2,3,4])

>>>

4

----------------------

count_values(["hello", "stranger", "who", "are", "you", "?"])

>>>

6

So, here our function takes element as the only argument, and we know that it must be a list of any type, thanks to type hints. Then, the function returns an integer as type; in fact, it counts the number of elements in the list.

So, we believe the point is clear. This is a very good implementation for improving the readability of our code.

Documentation is the most important part of every software project for a simple reason: in the next two months we’ll barely remember what we have done, and why.

For this reason, writing notes and explanations of our code is very important. Unfortunately, comments are not enough because they have to be very short (but explanative).

So, what we can do is use docstrings. PEP 257 specifies that:

A docstring is a string literal that occurs as the first statement in a module, function, class, or method definition. Such a docstring becomes the __doc__ special attribute of that object.

All modules should normally have docstrings, and all functions and classes exported by a module should also have docstrings. Public methods (including the __init__ constructor) should also have docstrings. A package may be documented in the module docstring of the __init__.py file in the package directory.

In other words, in software development, a docstring is a string type in the code that documents the code itself. As reference [1] tells us, “unlike comments, docstrings are available at runtime, simplifying code inspection and providing help or metadata during execution”.

Let’s see how to use docstrings in a Python class:

class Point:
"""
this class moves a point in 2D, passing its coordinates
as argument of the method 'position'
"""
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y

def position(self) -> int:
print(self.x, self.y)

To access the documentation (aka, the docstrings) we type the following:

Point.__doc__

>>>

" this class moves a point in 2D, passing its coordinates as
argument of the function 'position' "

So, this is how we can access the docstrings related to a class. What if we want to access the docstring of a particular method of a class?
Let’s see it:

class Point:
""" this class moves a point in 2D, passing its coordinates
as argument of the function 'position'
"""
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y

def position(self) -> int:
""" this actually moves the point"""
print(self.x, self.y)

And to access the documentation of the position method:

Point.position.__doc__

>>>
'this actually moves the point'

And here we are.

But we actually can (and should!) do better. For example:

class Point:
""" this class moves a point in 2D, passing its coordinates
as argument of the function 'position'

Args:
param 1 (int): the coordinate x of the point in 2D
param 2 (int): the coordinate y of the point in 2D
"""
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y

def position(self) -> int:
""" This method actually moves the point.

Returns:
int: prints the integers that represent the coordinates x and y
of the point in 2D
"""
print(self.x, self.y)

This may seem redundant since we have used type hints, but it is not because:

  1. we may need, in a first attempt, to just invoke the documentation of our class (maybe because we have imported it as a module in another file).
  2. we may use tools (like Sphinx, for example) to create the documentation in HTML; these tools use directly the docstrings.

This is why using very explanatory docstrings is very important.

Here we want to talk about a Python magic: inheritance.

As we’ve said before, OOP is the methodology to develop code with objects interacting between them. This means that when we create classes in Python we often have to make them interact.

When interacting, classes inherit the properties and functionalities of other classes.
Yes: it’s like your old aunt decides to leave you her 21 billion $ (who hasn’t such an aunt?!).

Now, firstly we have to say that every class we create uses inheritance. This happens because, as we’ve said before, Python uses built-in classes. So, when we invoke built-in classes, our code is inheriting the properties of the invoked built-in class or object.

Now, let’s see inheritance in action, with a practical example (note: I have taken the following classes from reference [1], and modified them as per my taste and needs).
We want to create a class that stores the name and the surname of some contacts, populating an empty list. We can create something like that:

class Contact:
"""
This class saves the name and the surnames
of some contacts in a list
"""

# Create empty list
all_contacts = []

def __init__(self, name: str, surname: str) -> None:
""" This method initializes the arguments and appends
the arguments (name and surname)into the empty list.

Returns:
nothing.
"""
self.name = name
self.surname = surname
Contact.all_contacts.append(self) # Append to list

def __repr__(self) -> str:
"""
The built-in __repr__ method provides a string representation
of an object.

Returns:
the compiled list with name and surname of the contacts
"""
return (
f"{self.__class__.__name__}("
f"{self.name!r}, {self.surname!r}"
f")"
)

Now, let’s try it:

# Define a contact
contact_1 = Contact("Federico", "Trotta")

# Show the contacts in the list
Contact.all_contacts

>>>

[Contact('Federico', 'Trotta')]

Now, suppose we want to collect the email of the contacts but, for any reason, we want to create a separate class. We can create a class like the following:


class Email(Contact): # Email is inerithing from Contact

def get_mail(self, mail:"mail") -> None:
return mail

Now, let’s add an email like the following:

Email("federico trotta", "[email protected]")

Now, if we invoke Contact.all_contacts we get:

[Email('federico trotta', '[email protected]'),
Contact('Federico', 'Trotta')]

So, since our class Email has the class Contact as an argument, it inherits its properties. For example, the arguments passed to the Email class are appended to the list in the class Contact.

Also, this class inherits the fact that the __init()__ method requires two arguments. In fact, this is what we get with just one argument:

mail_2 = Email("[email protected]")

>>>

TypeError: __init__() missing 1 required positional argument: 'surname'

But, hey, wait!!! We haven’t used the __init()__ method in the class Email, so what happened?

It happened that the class Email inherited the __init()__ method, so there is no need to use it again!

If we want to use another __init()__ method in the child class (Email is a child class), we must make some adjustments like the following ones:

class General(Contact):

def __init__(self, name: str, email: str) -> None:
super().__init__(name, email)
self.email = email

And it works exactly like the previous one:

# Create a contact
general_contact = General("Federico Trotta", "[email protected]")

# Write the contact into the list
Contact.all_contacts

>>>

[General('Federico Trotta', '[email protected]')

So, the classes Email and General work exactly the same way and give us exactly the same results, but the power of inheritance shows us that without using the __init()__, method as we did with Email, the code is simplified.

Also, as we can see, in the case of the General class, we have used the super.__init()__ method: we must use it in the case of a child class, because it initializes the inherited attributes of the parent class (Contact).

If you came across this article because you struggle understanding classes, chances are that you don’t have a clear idea of why you should use them. If it is so, welcome to the club: I had the same struggle.

I mean: I understood that classes (and, of course, functions) help us automatize our code, but the fact that we have to invoke them created some difficulties for me.

This happened to me because I’ve started studying Python for Data Science (and here’re are my tips on how to properly do so) and, saying the truth, in many cases, there is no reason to use classes when we use Python for Data Science.

So, my advice to properly understand the need to use classes is to treat them as modules. This means that, when you want to start a new Python project, a piece of very good advice is to create a main.py file where you invoke all the classes you need; anyway, these classes shouldn’t be created in the main.py: they should be created in separate Python files (usually, we create one Python file for each class) and imported in the main.py where we use them.

In the next few days, I’ll create a detailed article to explain this in more detail: so don’t miss any notifications, and subscribe to my mailing list.

In this article, we’ve seen a comprehensive guide on classes, hoping it clarifies the main topics on it.

The only thing that you have to do now is to practice a lot with classes, hoping this guide will help you when you need it.

Need more content on Python to start or boost your career? Here are some of my articles that can help you:

Python:

Consider becoming a member: you could support me with no additional fee. Click here to become a member for less than 5$/month so you can unlock all the stories, and support my writing.

Bibliography and videography:

[1] Python Object-Oriented Programming — S.F. Lott, D. Phillips

[2] If __name__ == “__main__” for Python Developers (video)

FOLLOW US ON GOOGLE NEWS

Read original article here

Denial of responsibility! Techno Blender is an automatic aggregator of the all world’s media. In each content, the hyperlink to the primary source is specified. All trademarks belong to their rightful owners, all materials to their authors. If you are the owner of the content and do not want us to publish your materials, please contact us by email – [email protected]. The content will be deleted within 24 hours.
Leave a comment