Python seems a very interesting language, where everything is on your hand. You can write code that works or write beautiful code with the popular and beloved concepts like SOLID, clean code and design patterns. I won’t make this post long and I will try to write brief concepts about Python from now on. In this post, I will be talking about the Factory pattern, how we can implement that in Python and how to create Abstraction to make things more simple.
Let’s say we have an audio player, and we can play wav and mp3 formats. So based on the parameter wav
or mp3
we load files and play them. Let’s make an interface first.
from abc import ABC, abstractmethod
class AudioPlayer(ABC):
@abstractmethod
def load(self, file: str) -> (str):
pass
@abstractmethod
def play(self) -> (str):
pass
I have used the abc
package to implement the formal interface concept. The @abstractmethod
decorator implies that these methods should be overridden by concrete classes. So let’s make the players now.
class Mp3Player(AudioPlayer):
def __init__(self):
self.format = "mp3"
self.file = None
def load(self, file: str) -> (str):
self.file = file
return f"Loading {self.format} file named {file}"
def play(self) -> (str):
return f"playing {self.file}"
class WavPlayer(AudioPlayer):
def __init__(self):
self.format = "wav"
self.file = None
def load(self, file: str) -> (str):
self.file = file
return f"Loading {self.format} file named {file}"
def play(self) -> (str):
return f"playing {self.file}"
So we have the Mp3Player
and Wavplayer
. They implement both methods load
and play
. These two classes are identical here, but in real life implementation, the load should be different, maybe the play too. Now it’s time to create the factory. Here’s the magic of Python comes in play!
player_factory = {
'mp3': Mp3Player,
'wav': WavPlayer
}
This is amazing! You can map classes in dictionaries, so simply! In other languages, you might have to write several switch cases or if-else. Now you can directly use this factory to call our load and play. This is called a dispatcher in Python.
mp3_player = player_factory['mp3']()
print(mp3_player.load("creep.mp3"))
print(mp3_player.play())
wav_player = player_factory['wav']()
print(wav_player.load("that's_life.wav"))
print(wav_player.play())
See how we can initialize a class based on a parameter! mp3_player = player_factory[‘mp3’]()
— this is really cool. So the whole code looks like this —
from abc import ABC, abstractmethod
class AudioPlayer(ABC):
@abstractmethod
def load(self, file: str) -> (str):
raise NotImplementedError
@abstractmethod
def play(self) -> (str):
raise NotImplementedError
class Mp3Player(AudioPlayer):
def __init__(self):
self.format = "mp3"
self.file = None
def load(self, file: str) -> (str):
self.file = file
return f"Loading {self.format} file named {file}"
def play(self) -> (str):
return f"playing {self.file}"
class WavPlayer(AudioPlayer):
def __init__(self):
self.format = "wav"
self.file = None
def load(self, file: str) -> (str):
self.file = file
return f"Loading {self.format} file named {file}"
def play(self) -> (str):
return f"playing {self.file}"
player_factory = {
'mp3': Mp3Player,
'wav': WavPlayer
}
mp3_player = player_factory['mp3']()
print(mp3_player.load("creep.mp3"))
print(mp3_player.play())
wav_player = player_factory['wav']()
print(wav_player.load("that's_life.wav"))
print(wav_player.play())
Now you can ask what if a user gives mp4
in player_factory
initialization, what will happen. Ok, the code will crash. Here we can make an abstraction and hide all the complexity of class creation and also validating upon the parameters.
class AudioPlayerFactory:
player_factory = {
'mp3': Mp3Player,
'wav': WavPlayer
}
@staticmethod
def make_player(format: str):
if format not in AudioPlayerFactory.player_factory:
raise Exception(f"{format} is not supported")
return AudioPlayerFactory.player_factory[format]()
Now we can just use the AudioPlayerFactory
to load and play.
mp3_player = AudioPlayerFactory.make_player('mp3')
print(mp3_player.load("creep.mp3"))
print(mp3_player.play())
wav_player = AudioPlayerFactory.make_player('wav')
print(wav_player.load("that's_life.wav"))
print(wav_player.play())
mp4_player = AudioPlayerFactory.make_player('mp4')
print(mp4_player.load("what_a_wonderful_life.mp4"))
print(mp4_player.play())
You will see the Exception for the mp4 file. You can handle that in your own way. So the new code is —
from abc import ABC, abstractmethod
class AudioPlayer(ABC):
@abstractmethod
def load(self, file: str) -> (str):
raise NotImplementedError
@abstractmethod
def play(self) -> (str):
raise NotImplementedError
class Mp3Player(AudioPlayer):
def __init__(self):
self.format = "mp3"
self.file = None
def load(self, file: str) -> (str):
self.file = file
return f"Loading {self.format} file named {file}"
def play(self) -> (str):
return f"playing {self.file}"
class WavPlayer(AudioPlayer):
def __init__(self):
self.format = "wav"
self.file = None
def load(self, file: str) -> (str):
self.file = file
return f"Loading {self.format} file named {file}"
def play(self) -> (str):
return f"playing {self.file}"
class AudioPlayerFactory:
player_factory = {
'mp3': Mp3Player,
'wav': WavPlayer
}
@staticmethod
def make_player(format: str):
if format not in AudioPlayerFactory.player_factory:
raise Exception(f"{format} is not supported")
return AudioPlayerFactory.player_factory[format]()
mp3_player = AudioPlayerFactory.make_player('mp3')
print(mp3_player.load("creep.mp3"))
print(mp3_player.play())
wav_player = AudioPlayerFactory.make_player('wav')
print(wav_player.load("that's_life.wav"))
print(wav_player.play())
mp4_player = AudioPlayerFactory.make_player('mp4')
print(mp4_player.load("what_a_wonderful_life.mp4"))
print(mp4_player.play())
Hope this helps you to design factories 😀
Top comments (3)
Neat! Very clean-code! This seems another approach to the pattern. You tried to separate the class creation based on the file extensions. But you had to use
if-else
. That's what I tried to show, that using python dispatcher, we can map functions or classes to certain enums, strings, int or whatever your approach will be. We can implement in many other ways. That's the beauty of programming and Python 😃Thanks for sharing.
Yes, I completely agree with you. I haven't looked into the Protocols yet. But there are new libraries written with nominal typing. Can you please give a hint of why would you like to avoid patterns in FP, what about extensions and SOLID principles?
WOW! This is truly amazing! I'm living in a cave I think, need to learn a lot about FP. That's why I started to write, I need comments of experts on my thinkings. Thanks a lot. And obviously I will look into F#, though I started to learn Rust this year. Again thank you for your valuable opinion and awesome examples.