Python: 使用 "..%(var)s.." % locals() 是个好习惯吗?

72 投票
7 回答
18972 浏览
提问于 2025-04-15 14:59

我发现了这个模式(或者说反模式),我对此非常满意。

我觉得它非常灵活:

def example():
    age = ...
    name = ...
    print "hello %(name)s you are %(age)s years old" % locals()

有时候我会用它的一个类似的东西:

def example2(obj):
    print "The file at %(path)s has %(length)s bytes" % obj.__dict__

我不需要创建一个虚假的元组,也不需要计算参数的数量,还要保持%s在元组中的匹配位置。

你喜欢这个吗?你会用它吗?是/否,请解释一下。

7 个回答

9

我觉得这个方法很好,因为它利用了内置的功能,减少了你需要写的代码量。我个人觉得这很符合Python的风格。

我从来不写不必要的代码——代码越少越好。比如使用 locals() 这个方法,可以让我写更少的代码,而且也很容易阅读和理解。

10

绝对不可能。这里的格式化到底是指什么并不清楚:locals 可能包含几乎任何变量。而 self.__dict__ 就没那么模糊了。这样留给未来的开发者去猜测哪些是局部变量,哪些不是,实在是太糟糕了。

这故意让人摸不着头脑。为什么要给你的团队增加未来维护的麻烦呢?

90

对于小型应用和所谓的“临时”脚本来说,使用locals()是可以的,尤其是@kaizer.se提到的vars增强和@RedGlyph提到的.format版本。

但是,对于那些需要长期维护的大型应用和有很多维护者的项目来说,这种做法可能会带来维护上的麻烦。我想这就是@S.Lott的回答所要表达的意思。让我来解释一下其中的一些问题,因为对于那些没有经历过开发和维护大型应用(或可重用组件)的人来说,这些问题可能并不明显。

在一个“严肃”的应用中,你不会把格式字符串硬编码进去——或者说,如果你这样做了,它会以某种形式存在,比如_('Hello {name}.'),这里的_来自于gettext或类似的国际化(i18n)/本地化(L10n)框架。关键是这样的应用(或可能在这些应用中使用的可重用模块)必须支持国际化和本地化:你希望你的应用能够在某些国家和文化中显示“Hello Paul”,在其他地方显示“Hola Paul”,再在其他地方显示“Ciao Paul”,等等。因此,格式字符串在运行时会根据当前的本地化设置自动替换,而不是硬编码,它存储在某种数据库中。可以想象,这个格式字符串总是一个变量,而不是一个固定的字符串。

所以,实际上你得到的是

formatstring.format(**locals())

而且你无法简单地检查到底使用了什么本地名称来进行格式化。你必须打开并查看本地化数据库,识别在不同设置中将要使用的格式字符串,并验证它们。

因此,实际上你并不知道将会使用哪些本地名称——这会严重影响函数的维护。你不敢重命名或删除任何本地变量,因为这可能会严重破坏某些用户(对你来说可能是陌生的语言、地区和偏好的组合)的用户体验。

如果你有很好的集成/回归测试,问题会在测试版发布之前被发现——但质量保证(QA)会对你大喊大叫,发布会被推迟……说实话,虽然追求100%的单元测试覆盖率是合理的,但在考虑到设置的组合爆炸(对于本地化和许多其他原因)以及所有依赖项的支持版本时,追求100%的集成测试覆盖率就不太现实了。所以,你不能轻易冒险,因为“它们会在QA中被发现”(如果你这样做,你可能在开发大型应用或可重用组件的环境中坚持不久;-)。

所以,实际上你永远不会删除“name”这个本地变量,即使用户体验团队早就把问候语改成了更合适的“欢迎,恐怖的霸主!”(以及相应的本地化版本)。这一切都是因为你使用了locals()……

因此,你因为限制了维护和编辑代码的能力而积累了冗余代码——而且也许这个“name”本地变量只是因为它是从数据库或类似地方获取的,所以保留它(或其他本地变量)不仅仅是冗余,它还会降低你的性能。使用locals()的表面便利性真的值得这样吗?-)

但等等,还有更糟糕的!像lint这样的程序(例如pylint)可以为你提供许多有用的服务,其中之一就是警告你关于未使用的本地变量(希望它也能处理未使用的全局变量,但对于可重用组件来说,这有点困难;-)。这样你就能快速且廉价地捕捉到大多数偶尔的拼写错误,比如if ...: nmae = ...,而不是看到单元测试失败后再去调查为什么它失败了(你全面的单元测试,最终会捕捉到这个问题,对吧?-)——lint会告诉你有一个未使用的本地变量nmae,你会立即修复它。

但是如果你的代码中有blah.format(**locals()),或者等价的blah % locals()……你就麻烦了,朋友!-) 可怜的lint怎么知道nmae实际上是一个未使用的变量,还是它被你传递给locals()的某个外部函数或方法所使用?它无法判断——要么它会无论如何发出警告(导致“狼来了”的效果,最终让你忽视或禁用这样的警告),要么它根本不会发出警告(同样的最终效果:没有警告;-)。

把这个和“显式优于隐式”的替代方案进行比较……:

blah.format(name=name)

这样——没有维护、性能和是否妨碍lint的担忧了,真是太好了!你让所有相关人员(包括lint;-)立刻清楚地知道使用了哪些本地变量,以及它们的具体用途。

我可以继续说下去,但我觉得这篇文章已经够长了;-)。

所以,总结一下:“γνῶθι σεαυτόν!”嗯,我的意思是“认识你自己!”而这里的“你自己”实际上是指“你代码的目的和范围”。如果这是一个一次性的东西,永远不会进行国际化和本地化,几乎不需要未来的维护,也不会在更广泛的上下文中重用等等,那么就可以使用locals()来享受它的小而精致的便利;如果你知道情况并非如此,或者即使你不完全确定,也要谨慎行事,让事情更明确——忍受一下明确说明你在做什么的小不便,享受所有随之而来的好处。

顺便说一句,这只是Python努力支持“简单、一次性、探索性、可能是交互式”的编程(通过允许和支持超出locals()的风险便利——想想import *evalexec以及其他几种可能混淆命名空间并因便利而带来维护影响的方式),以及“大型、可重用、企业级”的应用和组件的一个例子。它在这两者之间做得相当不错,但前提是你要“认识你自己”,并避免在绝对确定可以承受的情况下使用“便利”部分。通常,关键考虑是:“这对我的命名空间有什么影响,以及编译器、lint、其他人类读者和维护者对它们的形成和使用的意识如何?”

记住,“命名空间是一个伟大的想法——让我们多用一些!”这是Python之禅的结尾……但Python作为一个“成年人语言”,让定义这意味着什么的边界,这取决于你的开发环境、目标和实践。请负责任地使用这个能力!-)

撰写回答