如何实现运行时选择方法的策略模式?

1 投票
3 回答
2011 浏览
提问于 2025-04-18 12:55

背景

我正在尝试在Python 2.7中实现某种变体的策略模式。我希望能够实例化一个名为'my_strategy'的基类,但在运行时可以切换不同的'score'方法实现。我会在'my_strategy'中有很多公共方法,但会有很多不同的'score'实现。主要部分展示了我想要如何使用它。在这里,评分的实现当然是个简单的示例。

我尝试过的(也就是我到目前为止的代码)

strategy.py:

from algo_one import *
#from algo_two import *


class my_strategy ( object ):

    def __init__(self, candidate = ""):
        self.candidate = candidate
        self.method = 'default'
        self.no = 10
        self._algo = algo_one

    def set_strategy(self, strategy='default'):
        self.strategy = strategy
        if self.strategy == 'algo_one':
            self._algo = algo_one
        elif self.strategy == 'algo_two':
            # self._algo = algo_two
            pass 
        else:
            self._algo = None

    def score(self, *args):
        if len(args) > 0:
            self.candidate = args[0]
        self._algo.score(self.candidate)

if  __name__ == "__main__":
    s = my_strategy()  
    s.strategy = 'algo_one'
    s.candidate = "hello world"
    print s.score()  
    print s.score("hi")
    # s.set_method('algo_two')
    # print s.score("hi")

我想把选定的策略保存在某种私有指针中,以指向子类的方法。

algo_one.py:

from strategy import my_strategy
class algo_one ( my_strategy ):    
    def score(self, candidate):
        return len(candidate)*self.no 

我可以有一个没有类的方法,但之后我需要访问基类的公共变量。

algo_two.py:

from strategy import my_strategy
class algo_two ( my_strategy ):
    def score(self, candidate):
        return len(candidate)*3 

我还有一个空的init.py。

错误信息

1. 在score中 self._algo.score(self.candidate)

类型错误:未绑定的方法score()必须用algo_one实例作为第一个参数(而得到了str实例)

2. 如果我取消注释第二个策略的导入:

from algo_two import *

我会得到以下错误。

导入错误:无法导入名称my_strategy

我猜这是因为我遇到了某种循环依赖。

3.

from algo_one import *

这显然不太好(无法检测未定义的名称),但如果我

from algo_one import algo_one

我会得到

导入错误:无法导入名称algo_one

问题

我觉得这些错误是相互关联的,而且我的整体方法可能有问题。如果不只是解决错误,我希望能得到改进设计的建议。或者任何评论也可以。我也欢迎关于这个问题标题的建议。谢谢!

3 个回答

1
  1. 你没有在 __init__ 方法里创建你的算法对象。记住,要创建一个类的对象,你需要调用它:

    self._algo = algo_one()
    
  2. 是的,这就是循环依赖。不过,我不明白为什么 algo_one 和 algo_two 需要从 my_strategy 继承。你可以把它们做成普通对象,或者从其他地方的一个基类继承。或者,把它们都放在同一个文件里——在 Python 中,没必要把类放在不同的文件里。

  3. 这是和第二个问题一样的情况。

1

你现在面临的一个主要问题是,你的算法试图从基础类进行子类化,这其实是个很大的设计缺陷(你已经意识到了这一点)。不如使用简单的方法绑定,这样可以处理所有必要的事情:

def algo_one(candidate):
    # do stuff
    return "A fluffy unicorn"

def algo_two(candidate):
    # do some other stuff
    return "Awesome rabbits"

# not really necessary, just to make it easier to add new algorithms
STRATEGIES = { "one": algo_one, "two": algo_two }

class Strategy(object):
     def __init__(self):
         ...

     def set_strategy(self, which):
         if which not in STRATEGIES:
             raise ValueError("'%s' is an unknown strategy" % which)
         # compatibility checks  about the entries in STRATEGIES omitted here
         self._algo = STRATEGIES[which]

     def score(self, *args):
         # ...
         return self._algo(...)

如果你需要一种更复杂的方法(这取决于你的具体需求),让所有的算法彼此了解,可以把算法和选择策略的部分分成不同的类,相互引用(下面是简化版):

class ScoreAlgo(object):
    def __init__(self, parent):
        self._strategy = parent # if you need a back-reference, just be aware of circular dependencies in the garbage collection
    def __del__(self):
        self._strategy = None # resolve circular dependency for the GC

    def score(self, candidate):
        return None

class Strategy(object):
    def __init__(self):
        ...
    def set_strategy(self, ...):
        ...
        self._algo = ScoreAlgo(self)

    def score(self, ...):
        return self._algo.score(...)

如果你需要很多不同的算法,建议把ScoreAlgo做成一个抽象基类,这样子类就必须实现score()这个方法。

你还可以使用混入模式(这比方法绑定稍微正式一些)或者其他几种方式。这些都取决于你的整体需求。

编辑:我刚刚在两个def score():的代码块中添加了return,这样可以避免大家对为什么这些方法可能不返回任何东西产生困惑。

5

你把事情想得太复杂了。Python中的函数是第一类对象,所以在Python中实现策略模式的最简单方法就是把一个“策略”函数传递给你的“上下文”对象(也就是使用这个策略的对象)。有趣的是,任何可调用的对象(也就是说,任何实现了__call__方法的对象)都可以使用。

def default_score_strategy(scorer):
    return len(scorer.candidate) * 3

def universal_answer_score_strategy(scorer):
    return 42 # definitly the universal answer <g>

class ComplicatedStrategy(object):
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, scorer):
         return len(scorer.candidate) * self.factor


class Scorer(object):
    def __init__(self, candidate="", strategy=default_score_strategy):
        self.candidate = candidate
        self.strategy = strategy

    def score(self):
        return self.strategy(self)

s1 = Scorer("foo")
s2 = Scorer("bar", strategy=universal_answer_score_strategy)
s3 = Scorer("baaz", strategy=ComplicatedStrategy(365))

注意,你的策略不一定要和Scorer类在同一个模块里(当然,默认的策略除外),而且包含Scorer类的模块也不需要导入策略模块——也不需要知道这些策略是在哪里定义的:

# main.py

from mylib.scores import Scorer
from myapp.strategies import my_custom_strategy

s = Scorer("yadda", my_custom_strategy)

撰写回答