方法重构:从多个关键字参数到一个参数对象

5 投票
7 回答
766 浏览
提问于 2025-04-17 22:38

有时候,一个方法的可选参数(kwargs)多到我觉得应该重新整理一下。

举个例子:

def foo(important=False, debug=False, dry_run=False, ...):
    ....
    sub_foo(important=imporant, debug=debug, dry_run=dry_run, ...)

我现在比较喜欢的解决方案是:

class Args(object):
    ...

def foo(args):
    sub_foo(args)

第一个问题:怎么称呼Args?有没有什么大家都知道的描述或者设计模式?

第二个问题:Python有没有什么可以用作Args的基类?

更新

我已经用Python工作了13年,每天都在用。我写过很多带有可选参数的方法,也用过很多这样的函数。最近几周我读了一本书《代码整洁之道》,我觉得这本书很不错。现在感觉就像换了一副眼镜。我以前的代码能用,但看起来不太好。把长方法拆分成几个小方法很简单,但我不太确定该怎么处理那些参数过多的方法。

7 个回答

0

我看到几种解决方法:

自动化,比如线程本地存储或者其他可以获取这些值的上下文。很多网页框架都遵循这种方式,比如这里 https://stackoverflow.com/a/19484699/705086。我觉得这在Python中是最符合风格的,因为它更容易阅读。可以把它称作简易的上下文导向编程。这就像直接访问 sys.argv,但更精确。

这种方法最适合处理一些跨越多个功能的事情,比如授权、日志记录、使用限制、重试等等……

collections.namedtuple 特别有用,如果同一组参数经常重复出现,或者这种类型的多个实例很常见,比如:

job = collections.namedtuple("job", "id started foo bar")
todo = [job(record) for record in db.select(…)]

**kwargs,匿名的,容易出错,当传入意外的关键字参数时。

self,如果你一直在不同的函数之间传递参数,也许这些参数应该是类或对象的成员。

你也可以将这些方法混合使用,在你的例子中:

  • debug ⇾ 自动化上下文
  • dry_run ⇾ 自动化上下文
  • important ⇾ 保持一个命名的关键字参数,因为 显式比隐式好
0

我不太确定你具体想要什么,所以也许你可以编辑一下,添加一些额外的信息,这样会更有帮助(比如,你说的干净代码是什么意思,为什么 *args 和 **kwargs 不能满足这个要求,你想要达到的最终目标是什么等等)。

我再提一个还没提到的想法。你可以创建一个字典,然后通过使用 ** 来作为关键字参数传递它。

def foo(important=False, debug=False, dry_run=False):
    print important, debug, dry_run

args = dict()
args['important'] = True
args['debug'] = True
args['dry_run'] = False
foo(**args)

或者,因为你想涉及面向对象编程(OOP),你也可以考虑使用一个对象。

class Args(object):
    pass

def foo(important=False, debug=False, dry_run=False):
    print important, debug, dry_run

args = Args()
args.important = True
args.debug = True
args.dry_run = False
foo(**args.__dict__)
0

更好的方法是使用反射来调用子函数。

你只需要一种获取函数信息的方法。你可以这样做:

def passthru(func):
    l = inspect.stack()[1][0].f_locals
    args = inspect.getargspec(func)[0]
    kwargs = dict((x, l[x]) for x in args if x in l)
    return func(**kwargs)

def f(x=1, y=2):
    print x,y

def g(x=4):
    passthru(f)

f()
1 2

g()
4 2

g(6)
6 2

不过,这似乎会有一些额外的开销。

1

在编程中,有时候我们需要在代码里使用一些特殊的符号或字符,这些符号可能会影响代码的运行。比如说,如果你在代码里直接写了一个引号("),程序可能会搞不清楚你是想结束一个字符串,还是在字符串里使用引号。这就需要我们用一些方法来告诉程序,这个引号是字符串的一部分,而不是结束符号。

为了避免这种混淆,我们可以使用转义字符。转义字符通常是一个反斜杠(\),它的作用就是告诉程序:接下来的字符要特别处理。例如,如果你想在字符串里放一个引号,你可以这样写:\"。这样程序就知道这个引号是字符串的一部分,而不是结束符号。

同样的道理,如果你想在字符串里放一个反斜杠本身,你也需要用转义字符来处理,写成 \\。这样程序就能正确理解你想表达的意思。

总之,转义字符就是用来解决代码中可能出现的符号混淆问题,让程序能够正确理解你的意图。

def foo(*args, **kwargs):
    sub_foo(*args, **kwargs)
3

我觉得你描述的情况是“上下文”设计模式的一个例子。

我通常把你提到的“Args”称为“上下文”(如果它特别针对某个功能,我会叫它“FooContext”)。

我看到的最好的解释在这里:http://accu.org/index.php/journals/246(这是Allen Kelly在《Overload Journal》第63期中写的《封装上下文模式》,我是在另一个StackOverflow的回答中看到的:https://stackoverflow.com/a/9458244/3427357)。

如果你想深入了解,还有一些不错的论文可以参考:http://www.two-sdg.demon.co.uk/curbralan/papers/europlop/ContextEncapsulation.pdf https://www.dre.vanderbilt.edu/~schmidt/PDF/Context-Object-Pattern.pdf

正如另一个StackOverflow的回答指出的那样(https://stackoverflow.com/a/1135454/3427357),一些人认为上下文模式是危险的(参见:http://misko.hevery.com/2008/07/18/breaking-the-law-of-demeter-is-like-looking-for-a-needle-in-the-haystack/)。

但我认为“德梅特法则”的警告主要是关于不要让你的设计变得过于复杂,而不是清理在解决其他问题时意外产生的杂乱。如果你在多个函数调用层之间传递一个“重要”的布尔值,你已经进入了测试的地狱,而在这种情况下,你描述的重构在我看来通常是个好主意。

我觉得在Python中没有标准的基类来实现这个,除非你懒得直接用一个argparse.Namespace作为上下文对象,因为你已经在那里面有了参数值。

撰写回答