Object-Oriented Programming
This tutorial revisits OOP concepts, by presenting them in Python. In a second part, an exercise is proposed to create a toolbox for MP3 collection management.
All the proposed pieces of code should be tested in a script.py
.
1. Object-Oriented Programming (OOP) in Python
Playing with class, and the main concepts of OOP in Python.
1.a. Structuring element together
First a class can be reduced to a collection of class attributes and functions.
# Definition:
class MyObjectType :
world= "world"
def function1() :
return "hello"
def function2() :
return MyObjectType.world
# Use:
hello= MyObjectType.function1()
print( hello + ' ' + MyObjectType.function2() )
To notice the indentations that state what is inside a class with a new level of indentation for instructions of the class's function. In this example, MyObjectType is a class with one attribute (world) and two functions (function1(), function2()).
1.b. Class Instance
The main feature of class is the capacity to instantiate objects (class instances). An instance of a class is an object defined as a specific type. It is possible to associate attributes with an instance. In this case, instance-attribute values can be different for different instances of the same class (by opposition to class-attributes).
# Definition:
class MyObjectType :
class_word= "hello" # A class attribute
# Use:
anInstance= MyObjectType() # Instanciate a MyObjectType object
anInstance.instance_word= "world" # an instance attribute
aSecondInstance= MyObjectType()
aSecondInstance.instance_word= "nobody"
assert( type(anInstance) is MyObjectType )
print( anInstance.class_word + ' ' + anInstance.instance_word )
At this point, anInstance.class_word
is MyObjectType.class_word
,
and for no confusion, you should use MyObjectType.class_word
to manipulate this attribut.
Another good way to manipulate a class attribute: type(anInstance).classAttribut
.
Typically, nasty manipulation of class-attributes can transform them into instance attributes for specific instances that generate failure, difficult to debug...
As in this example :
# Definition:
class MyObjectType :
classAttribut= "Void"
# Use:
anInstance= MyObjectType()
MyObjectType.classAttribut= "Hello"
print( anInstance.classAttribut )
anInstance.classAttribut= "World" # I should not do that...
print( MyObjectType.classAttribut )
MyObjectType.classAttribut= "Hmmm"
print( anInstance.classAttribut + " - " + MyObjectType.classAttribut )
1.c. Methods
The strength of OOP relies on the instances that define a context for the executions of functions associated to the class. A function associated to an instance is named a METHOD. In Python, a method is a class function where the instance is explicit as the first parameter. A convention supposes that this instance is named self.
# Definition:
class MyObjectType :
def method(self) :
return 42
# Use:
anInstance= MyObjectType() # Instanciate a MyObjectType
v1= anInstance.method()
v2= MyObjectType.method(anInstance)
if v1 == v2 :
print( "Hello World" )
print( f"{MyObjectType.method}\nvs {anInstance.method}")
As in this example, in Python a method can be called directly on an instance (v1) or as a class function (v2). In the first case, the method is bound to an instance :
# Definition:
class MyObjectType :
def method(self) :
return 42
anInstance= MyObjectType()
print( f"{MyObjectType.method}\nvs\n{anInstance.method} ({anInstance})")
As a result, in Python, a method with \(3\) arguments is defined by a function of \(4\) arguments.
def aMethod(self, argument1, argument2, argument3) :
pass
2. Built-in Python
Python defines numerous tools natively. Many of those tools rely on functions/methods associated with types. There are named built-in functions. Those functions can be redefined (overridden) to attach a specific behavior to developer types (classes).
2.a. Built-in Class
First of them are the built-in functions defined in Python objects:
__init__(self)
: Instance initialization, called at instance construction.__del__(self)
: Instance destruction, called when a instance is deleted.__str__(self)
: Transform an instance in a string
# Definition:
class MyObjectType :
def __init__(self):
print('Initialization')
def __del__(self):
print('Destruction')
def __str__(self):
return "> MyObjectType::instance <"
# Use:
anInstance= MyObjectType()
print( anInstance )
To notice that it is possible to change the number of parameters in __init__
to generate a constructor over parameters.
In general, all the instance attributes are created in the __init__
method. But it is not mandatory (in fact, in Python, the number and definition of instance attributes are dynamic).
It is also possible to call the parent method when overriding with super
.
# Definition:
class MyObjectType :
def __init__(self):
print('Initialization')
def __del__(self):
print('Destruction')
def __str__(self):
return "> MyObjectType::instance <"
# Definition:
class ObjectSpecific(MyObjectType) :
def __init__(self, aNumber):
super(MyObjectType, self).__init__()
self._aNumber= aNumber
def __str__(self):
return f"> MyObjectType::instance-{self._aNumber} <"
# Use:
anInstance= ObjectSpecific(42)
print( anInstance )
2.b. Good practices.
As we already see, several rules are more good practices than language constraints.
- The current instance, context for a method execution, is always named self.
__init__
method (if defined) is your first method.- Initialize your instance attributes into the
__init__
method. - Attribute names start with
_
. - Class names start with an Uppercase letter.
- etc.
Most of those conventions are presented in the style guide for Python code
2.c. Operators.
Finally, most of the operator can be redefined based on built-in function. For a complete list with other type built-in functions, see docs.python.org
An example with the addition:
class Vector :
def __init__(self, x, y):
self._x= x
self._y= y
def __add__(self, another):
return Vector( self._x+another._x, self._y+another._y )
def __str__(self):
return f"({self._x}, {self._y})"
# Use:
a= Vector( 10.7, 8.0 )
b= Vector( -2.1, 34.0 )
print( f"{a} + {b} = {a+b}" )
3. Let's Play
We now have an idea of what OOP is capable of in Python. The exercise here is to put those notions in music.
3.a Read a Mp3 sound
As a first exercie we want a class representing a song. First, an exploration on internet showed us the librairie: playsound3 allowing us for play mp3 music.
This example loads a music song and play it :
import time
from playsound3 import playsound
# You can play sounds in the background
sound = playsound("./song.mp3", block=False)
# and check if they are still playing
if sound.is_alive() :
print("Sound is playing!")
time.sleep( 5.0 )
# and stop them whenever you like.
sound.stop()
So as a first result, we aim to have a class (Song for instance) with methods to load a music file and to play it.
Something like this:
class song :
# To implement ...
asong= Song()
asong.load("./song.mp3")
aSound.play()
- The song file : here
3.b Accessor
Our Song class defines a few attributes for an instance: a title, an artist name, an album name, and the number of tracks in the album. First, we want a constructor that defines all of these attributes. Then we ask for an accessor method for each of these attributes.
class song :
# To implement ...
asong= Song("Rodriguez", "Can't Get Away", "Searching for Sugar Man", 7 )
asong.load("./song.mp3")
print( f"{asong.artist()} - {asong.album()} {asong.track()} - {asong.title()}" )
asound.play()
In fact, we will prefer to get metadata from the file directly when loading it. To do that we can count on eyeD3 Python library (documentation).
Here's a piece of code to help in this mission:
import eyed3
audiofile = eyed3.load("song.mp3")
print( audiofile.tag.artist )
print( audiofile.tag.album )
print( audiofile.tag.album_artist )
print( audiofile.tag.title )
print( audiofile.tag.track_num.count)
It is also possible to define default parameter values in the __init__
method to be capable of instantiating a new song without metadata.
To learn how to do that, let's go on internet.
For instance, the [w3schools]( is an excellent entrance point regarding web technologies and presents, among others, the notion of function default parameter, with a sandbox...
3.c Some commands
You should now have a first skeleton of the application we want to create. However, in fact, the goal is to create several commands. So the best way to do that in a first move, is to create several Python files. A first Python file will implement our Song class with all the required functionality as methods of the class. Then we add a Python file for each command we want to implement.
In your directory you should have :
songpkg.py # With the Song class
command1.py
command2.py
command3.py
...
The command script should be as small as possible.
All the important code is in songpkg.py
, and imported in your command file.
For instance, the play.py
command will look like
import songpkg
asong= songpkg.Song()
asong.load("./song.mp3")
print( f"{asong.artist()} - {asong.album()} {asong.track()} - {asong.title()}" )
asound.play()
The expected command:
play.py
: Takes a file name of a song as an argument and print the metadata of that song before playing the song.set.py
: Takes a file name of a song and all metadata as command arguments and save the file with those metadata.playlist.py
: Takes a playlist as an argument (a text file, with a list of MP3 files to play) and plays it.search.py
: Takes an artist name, a search all the local MP3 files matching that artist.rename.py
: search all MP3 recursively in a directory, and rename them asartist - album track - title.mp3
.
To do that, you will certainly requires os Python modul (again on w3schools) and more specifically, the os.listdir()
returning a list of the names of the entries in a directory.