2

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.

  • Probably a dupe of [How to write strategy pattern ...](https://stackoverflow.com/questions/963965/how-to-write-strategy-pattern-in-python-differently-than-example-in-wikipedia) – Patrick Artner Jun 08 '19 at 13:48
  • 1
    In what way would you need metaclasses here? `ComponentThatNeedsLogger.__init__` just needs an argument that has a `log` method. – chepner Jun 08 '19 at 13:49
  • 2
    The question is: are you defining `*Logger` classes because `ComponentThatNeedsLogger` uses an object that invokes a `log` method, or does `ComponentThatNeedsLogger` use an object with a `log` method because you defined `*Logger` classes? You can probably get away with simply passing a *function* (with, e.g., type `Callable[str,None]`) to `ComponentThatNeedsLogger`. Not everything has to be implemented in terms of classes. – chepner Jun 08 '19 at 13:52
  • 2
    Maybe you could use a `BaseLogger` with an `@abstractmethod` logger method `def log(...)` that your concrete loggers extend? See [abstract classes in python](https://stackoverflow.com/questions/13646245/is-it-possible-to-make-abstract-classes-in-python) if you want to go the very explicit class based route ... – Patrick Artner Jun 08 '19 at 13:52
  • 1
    @PatrickArtner: This is exactly how I would do it if I were to use same form as in C#, but question was does duck-typing provide a more elegant answer. – bronzefalcon Jun 09 '19 at 17:11
  • I presented my suggestion as answer, you got two other answers as well - anybody finding your question in the future has a good choice of answers to build upon. I think your questions is a good fit for SO even if you do not quite get what you were looking for. Maybe others will present other solutions as well. – Patrick Artner Jun 09 '19 at 17:18
  • +1 for "does this ... make it clear to the next user...?" I'm always writing for the next poor guy. (He's usually me!) ;) – AJ. Feb 26 '21 at 08:24

3 Answers3

4

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.

Patrick Artner
  • 50,409
  • 9
  • 43
  • 69
  • 1
    This is how I would write it if I were to use the C# form. I was trying to ask if there was a more "Pythonic" way of doing this since it has duck typing. Also I am aware of the python logging module, I just used that for example's sake. – bronzefalcon Jun 08 '19 at 22:19
  • Out of the answers provided here at current moment, I am going to say I favor this one the most because if you are going to be writing a medium to large app, you will need stable/readable/scalable code and it seems this approach matches those requirements the most. Although I think before I am going to start writing this app, I will read a book about OOP in python, so I get better grasp on it. – bronzefalcon Jun 09 '19 at 18:33
2

In Python, there's generally no need for a full-scale Strategy pattern with compile-time type enforcement thanks to runtime safety and graceful termination.

If you wish to customize some parts of existing code, the common practice is:

  • replace the appropriate method -- whether by subclassing, incl. with a mix-in, or just assignment (a method is an attribute of a live object like any other and can be reassigned just the same; the replacement can even be not a function but an object with __call__);
    • note that in Python, you can create objects containing code (which includes functions and classes) on the fly by placing its definition inside a function. The definition is then evaluated as the enclosing function executes and you can use accessible variables (aka closure) as ad-hoc parameters; or
  • accept a callback at some point (or an object whose methods you will be calling at appropriate moments, effectively acting as a set of callbacks); or
  • accept a string parameter that is a constant from a certain set which the code then tests in if/else or looks up in some registry (whether global to a module or local to a class or object);
    • there is enum since 3.4 but for simple cases, it is considered too much drawbacks for the benefits (is unreadable when debugging and requires boilerplate) since Python is more to the flexibility side compared to C# on the flexibility-vs-scalability scale.
ivan_pozdeev
  • 33,874
  • 19
  • 107
  • 152
  • For #1, I am not sure how this applies to a situation where I can have a set of strategies. Replacing methods on the fly to me seems like it is just asking for trouble. For #2, a callback might work, but what if later you need multiple methods, with one callback you will try to stuff all kinds of optional parameters for the callback to account for the multiple methods. The object I agree is a good choice, but yet I still dont see how this applies to situations where I need something like a strategy pattern. For #3, what happens if I need this in 15 places? – bronzefalcon Jun 08 '19 at 22:32
  • @InfinityHorizon You didn't specify your use case so I don't know whether you need to select "strategy" per-class, per-object or per-call -- so I listed all the options. #1 is best for selecting per-class; replacing on the fly is useful for e.g. monkey-patching where you don't have control over the code or creating tailor-made objects (effectively create a new class but with less overhead). #2 is best for selecting per-object and acts at sub-method level (effectively, you replace a part of a method's implementation) but you can accept a callback each call, too. #3 is for selecting per-call. – ivan_pozdeev Jun 09 '19 at 00:42
  • If you pass a bound or unbound method as a callback, you can parameterize it via "its" object, independently from the "host" object. – ivan_pozdeev Jun 09 '19 at 00:46
  • I think our definition of strategy pattern are different. I am talking about something closer to GOF strategy pattern, while you are talking about something more generic. I still do not agree that monkey-patching is good approach. If someone looks at your class A, they would want to know what it does (they will also check its sub/super classes) With monkey-patching, they have to cheek the whole repo to find out exactly how it is used. It would be extremely dangerous to change that class without checking whole repo... – bronzefalcon Jun 09 '19 at 01:11
  • What http://www.blackwasp.co.uk/Strategy.aspx describes is the #2 "set of callbacks" option, per-object. But the general principle is the same -- ["Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use."](https://en.wikipedia.org/wiki/Strategy_pattern) -- so why limit the explanation and options to that? It's irrelevant at which point before the call those "instructions" are received, the net effect is the same. – ivan_pozdeev Jun 09 '19 at 01:32
  • Yes, monkey-patching is not scalable, so don't use it if you need scalability more than the extra flexibility. Its main strength is the ability to deal with code outside your control (which rarely changes), and the patching person rather than the original class' author is the one responsible for reacting to changes. It's still an option so I had to mention it. – ivan_pozdeev Jun 09 '19 at 03:00
2

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
)
Stop harming Monica
  • 12,141
  • 1
  • 36
  • 56
  • I get this, but what if I need later to have logDebug/logError/logInfo ... this is the disadvantage to just passing a function. Of could you could use a second parameter of LOGTYPE, but then you will might get into where you will have a set of optional parameters the user will have to know which combination of them to use in which situation. – bronzefalcon Jun 08 '19 at 22:25
  • If you want several logging strategies for different kinds of messages you can pass several functions. – Stop harming Monica Jun 09 '19 at 12:20