Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strategy pattern in python

I am coming from C# background and in order to implement a strategy pattern, we would always use an interface, for example: ILoggger. Now as I understand, in duck-typed languages such as Python, we can avoid this base class/contract.

My question is, is this the best way to implement a strategy pattern by taking advantage of duck typing? And, does this duck typing way of doing this make it clear to the next user of my code that this is an "point of extension"? Also, I think it is better to use type hints to help the next person looking at your code to see what the type of the strategy should be, but with duck-typing without base class/contract, which type do you use? One of the already concrete classes?

Here is some code:

class FileSystemLogger():
    def log(self, msg):
        pass

class ElasticSearchLogger():
    def log(self, msg):
        pass

# if i wanted to use type hints, which type should logger be here?
class ComponentThatNeedsLogger():
    def __init__(self, logger):
        self._logger = logger

# should it be this?
class ComponentThatNeedsLogger():
    def __init__(self, logger : FileSystemLogger):
        self._logger = logger

Could someone please advise what is the most standard/Pythonic/readable way to handle this?

I am not looking for the "here is answer in 2 lines of code" answer.

like image 908
bronzefalcon Avatar asked Nov 21 '25 10:11

bronzefalcon


2 Answers

If you really wanted to go classes all the way and enforce your base class usage create an ABC: abstract base class / method and some implementations of it:

Attributation: used Alex Vasses answer here for lookup purposes

from abc import ABC, abstractmethod

class BaseLogger(ABC):
    """ Base class specifying one abstractmethod log - tbd by subclasses."""
    @abstractmethod
    def log(self, message):
        pass

class ConsoleLogger(BaseLogger):
    """ Console logger implementation."""
    def log(self, message):
        print(message)

class FileLogger(BaseLogger):
    """ Appending FileLogger (date based file names) implementation."""
    def __init__(self):
        import datetime 
        self.fn = datetime.datetime.now().strftime("%Y_%m_%d.log")

    def log(self,message):
        with open(self.fn,"a") as f:
            f.write(f"file: {message}\n")

class NotALogger():
    """ Not a logger implementation."""
    pass

Then use them:

# import typing # for other type things

class DoIt:
    def __init__(self, logger: BaseLogger):
        # enforce usage of BaseLogger implementation
        if isinstance(logger, BaseLogger):
            self.logger = logger
        else:
            raise ValueError("logger needs to inherit from " + BaseLogger.__name__)

    def log(self, message):
        # use the assigned logger
        self.logger.log(message)

# provide different logger classes
d1 = DoIt(ConsoleLogger())
d2 = DoIt(FileLogger())

for k in range(5):
    d1.log(str(k))
    d2.log(str(k))

with open(d2.logger.fn) as f:
    print(f.read())

try:
    d3 = DoIt( NotALogger())
except Exception as e:
    print(e)

Output:

0
1
2
3
4 
file: 0
file: 1
file: 2
file: 3
file: 4

logger needs to inherit from BaseLogger

As a sidenote: python already has quite sophisticated abilities to log. Look into Logging if that is the sole purpose of your inquiry.

like image 170
Patrick Artner Avatar answered Nov 23 '25 23:11

Patrick Artner


As far as I know the most common way to implement the strategy pattern in Python is passing a function (or callable). Functions are first class objects in Python so if all the consumer needs is a function then you don't need to give it more than that. Of course you can annotate it if you want. Assuming you only want to log strings:

class ComponentThatNeedsLogger:
    def __init__(self, log_func: Callable[[str], None]):
        self._log_func = log_func

This Allows you to create a simple logger on the fly:

ComponentThatNeedsLogger(
    log_func=print
)

But you can also leverage all the power of classes to create a complex logger and pass just the relevant method.

class ComplexLogger:
    def __init__(self, lots_of_dependencies):
        # initialize the logger here

    def log(self, msg: str): None
        # call private methods and the dependencies as you need.

    def _private_method(self, whatever):
        # as many as you need.

ComponentThatNeedsLogger(
    log_func= ComplexLogger(lots_of_dependencies).log
)
like image 30
Stop harming Monica Avatar answered Nov 24 '25 01:11

Stop harming Monica