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 as artist - 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.