方法重构:从多个关键字参数到一个参数对象
有时候,一个方法的可选参数(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 个回答
我看到几种解决方法:
自动化,比如线程本地存储或者其他可以获取这些值的上下文。很多网页框架都遵循这种方式,比如这里 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 ⇾ 保持一个命名的关键字参数,因为
显式比隐式好
我不太确定你具体想要什么,所以也许你可以编辑一下,添加一些额外的信息,这样会更有帮助(比如,你说的干净代码是什么意思,为什么 *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__)
更好的方法是使用反射来调用子函数。
你只需要一种获取函数信息的方法。你可以这样做:
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
不过,这似乎会有一些额外的开销。
在编程中,有时候我们需要在代码里使用一些特殊的符号或字符,这些符号可能会影响代码的运行。比如说,如果你在代码里直接写了一个引号("),程序可能会搞不清楚你是想结束一个字符串,还是在字符串里使用引号。这就需要我们用一些方法来告诉程序,这个引号是字符串的一部分,而不是结束符号。
为了避免这种混淆,我们可以使用转义字符。转义字符通常是一个反斜杠(\),它的作用就是告诉程序:接下来的字符要特别处理。例如,如果你想在字符串里放一个引号,你可以这样写:\"。这样程序就知道这个引号是字符串的一部分,而不是结束符号。
同样的道理,如果你想在字符串里放一个反斜杠本身,你也需要用转义字符来处理,写成 \\。这样程序就能正确理解你想表达的意思。
总之,转义字符就是用来解决代码中可能出现的符号混淆问题,让程序能够正确理解你的意图。
def foo(*args, **kwargs):
sub_foo(*args, **kwargs)
我觉得你描述的情况是“上下文”设计模式的一个例子。
我通常把你提到的“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作为上下文对象,因为你已经在那里面有了参数值。