How can I use a static method as a default parameter for the strategy design pattern?
Asked Answered
O

1

9

I want to make a class that uses a strategy design pattern similar to this:

class C:

    @staticmethod
    def default_concrete_strategy():
        print("default")

    @staticmethod
    def other_concrete_strategy():
        print("other")

    def __init__(self, strategy=C.default_concrete_strategy):
        self.strategy = strategy

    def execute(self):
        self.strategy()

This gives the error:

NameError: name 'C' is not defined

Replacing strategy=C.default_concrete_strategy with strategy=default_concrete_strategy will work but, left as default, the strategy instance variable will be a static method object rather than a callable method.

TypeError: 'staticmethod' object is not callable

It will work if I remove the @staticmethod decorator, but is there some other way? I want the default parameter to be self documented so that others will immediately see an example of how to include a strategy.

Also, is there a better way to expose strategies rather than as static methods? I don't think that implementing full classes makes sense here.

Overlie answered 10/2, 2014 at 8:19 Comment(5)
The strategy pattern is mostly useless in python. Since you have functions as first class objects you can just pass functions around.Bechtel
@Bechtel Well as you can see the strategy is a first class object function. I think this is still called a strategy pattern no?Overlie
Yes, but the strategy pattern was mostly invented in languages that do not allow functions to be passed around. What I mean is that in 99% of the use cases of your class you can simply pass the function directly and obtain the same results with less complexity.Bechtel
@Bechtel can you please give an example of how this can be done?Overlie
Also related to question Calling class staticmethod within the class body?Coquelicot
M
16

No, you cannot, because the class definition has not yet completed running so the class name doesn't exist yet in the current namespace.

You can use the function object directly:

class C:    
    @staticmethod
    def default_concrete_strategy():
        print("default")

    @staticmethod
    def other_concrete_strategy():
        print("other")

    def __init__(self, strategy=default_concrete_strategy.__func__):
        self.strategy = strategy

C doesn't exist yet when the methods are being defined, so you refer to default_concrete_strategy by the local name. .__func__ unwraps the staticmethod descriptor to access the underlying original function (a staticmethod descriptor is not itself callable).

Another approach would be to use a sentinel default; None would work fine here since all normal values for strategy are static functions:

class C:    
    @staticmethod
    def default_concrete_strategy():
        print("default")

    @staticmethod
    def other_concrete_strategy():
        print("other")

    def __init__(self, strategy=None):
        if strategy is None:
            strategy = self.default_concrete_strategy
        self.strategy = strategy

Since this retrieves default_concrete_strategy from self the descriptor protocol is invoked and the (unbound) function is returned by the staticmethod descriptor itself, well after the class definition has completed.

Martinmas answered 10/2, 2014 at 8:27 Comment(10)
Or he could first define default_concrete_strategy without decorating it, and then decorate it after the definition of __init__ (as decorators were used before 2.4), although this would be quite cumbersome.Bechtel
@Bakuriu: exactly; much easier to just unwrap.Martinmas
It's a shame if this is the only way. It would have been nice to see an example directly in the code of how to pass a parameter. @Bechtel would decorating a method in the constructor be helpful?Overlie
@Overlie No. you have to put the @staticmethod in the class. I meant something like def method():pass; def __init__(self, func=method):pass; method = staticmethod(method). However I must say that I'd prefer simply use None as default value and check for it in the __init__.Bechtel
@mtanti: no, because descriptors need to be set on objects on the class not on the instance.Martinmas
@mtanti: it is not the only way, but it is the most readable way. The alternative is to apply the decorator 'by hand'; define default_concrete_strategy without the @staticmethod line, then after you defined the __init__ method, use default_concrete_strategy = staticmethod(default_concrete_strategy).Martinmas
@Bechtel I didn't know you could decorate a method like that. Interesting.Overlie
@mtanti: @expression followed by a class or function definition is nothing more than syntactic sugar for name_of_obj = expression(name_of_obj) after the class or function definition has completed.Martinmas
I don't think it's made clear in the answer but you have to place the default_concrete_strategy staticmethod ABOVE the function you would like to execute the method in as parameter, in this case the __init__ function. Great tip otherwise thanks! Used the .__func__ solution for a non-init method for a class I was creating.Subito
@decoder247 that’s because function defaults are executed when the function is created, not when it is being called. So the name default_concrete_strategy must exist in the class scope when def __init__(...): is being executed to create the function object.Martinmas

© 2022 - 2024 — McMap. All rights reserved.