Python:使用“..%(var)s ..”%locals()是一个好的实践吗?

72

我发现了这种模式(或反模式),我非常喜欢它。

我觉得它非常灵活:

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个回答

90

对于小型应用程序和所谓的“一次性”脚本,使用@kaizer.se提到的vars增强功能和@RedGlyph提到的.format版本是可以的。

然而,对于具有长期维护寿命和许多维护者的大型应用程序,这种做法可能会导致维护头痛,我认为这就是@S.Lott的回答的出发点。 让我解释一些涉及的问题,因为对于没有开发和维护大型应用程序(或可重用组件)的人来说,这些问题可能并不明显。

在“严肃”的应用程序中,您不会将格式字符串硬编码 - 或者,如果您已经这样做了,它将以_('Hello {name}。'之类的形式存在,其中_来自gettext或类似的国际化/本地化框架。 问题在于这样的应用程序(或可重用模块,可能会在此类应用程序中使用),必须支持国际化(也称为i18n)和本地化(也称为L10n):您希望您的应用程序能够在某些国家和文化中发出“Hello Paul”,在其他一些国家和文化中发出“Hola Paul”,在其他一些国家和文化中发出“Ciao Paul”等等。 因此,格式字符串会在运行时自动用另一个字符串替换,具体取决于当前的本地化设置; 它不是硬编码的,而是存在某种形式的数据库中。 出于所有意图和目的,请将该格式字符串始终视为变量,而不是字符串文字。

因此,您拥有的基本上是

formatstring.format(**locals())

你无法轻易地检查格式化将使用何种本地名称。必须打开并浏览L10N数据库,识别将在不同设置中使用的格式字符串,并验证它们。

因此,在实践中,您不知道将使用哪些本地名称--这极大地限制了函数的维护。您不敢重命名或删除任何本地变量,因为它可能会严重破坏具有某些(对您而言)模糊组合的语言、区域设置和偏好的用户体验。

如果您具有出色的集成/回归测试,则在 beta 版本发布之前将捕获错误--但 QA 将向您尖叫,发布将延迟... 而且,老实说,虽然以 单元测试实现 100% 覆盖率是合理的,但在考虑到 L10N 和其他众多原因的所有依赖项的支持版本的组合爆炸时,使用 集成测试实现 100% 覆盖率确实是不合理的。因此,您不会冒险破坏程序,因为“它们将在 QA 中被捕获”(如果您这样做,那么您可能不会在开发大型应用程序或可重用组件的环境中长久工作;-)。

因此,在实践中,您将永远不会删除“name”本地变量,即使用户体验人员已经将该问候语更改为更合适的“欢迎,可怕的霸主!”(和相应的 L10n 版本)。所有这些都是因为您选择了 locals()...

因此,由于您限制了编辑和维护代码的能力,所以您正在积累杂物--也许那个“name”本地变量只存在于从数据库或类似地方获取它,因此保留它(或其他本地变量)不仅是杂物,而且还会降低性能。表面上 locals() 的便利真的值得吗?-)

但是,请等一下,更糟糕的是!像 lint 一样的程序(例如 pylint)可以为您提供许多有用的服务之一,即警告您未使用的本地变量(希望它也能为未使用的全局变量做到这一点,但对于可重用组件来说,这太难了;-)。这样,您就可以快速、廉价地捕获大多数偶发的拼写错误,例如 if ...: nmae = ...,而不是通过查看单元测试断言并进行侦探工作来找出它为什么会出现错误(您确实拥有强迫症、无处不在的单元测试,最终捕获到这个问题,对吧?-)-- lint 将告诉您有一个未使用的本地变量 nmae,并且您将立即修复它。

但是,如果你的代码中有一个 blah.format(**locals()) 或等效的 blah % locals()... 你就很遗憾了!-) 究竟是不是未使用变量 nmae,还是它确实被传递给你正在使用的外部函数或方法呢?可怜的 lint 不知道-- 要么它仍然会发出警告(导致“狼来了”的效果,最终让你忽略或禁用这样的警告),要么就永远不会发出警告(具有相同的最终效果:没有警告;-)。

将此与“显式优于隐式”的替代方案进行比较...:

blah.format(name=name)

现在不再需要担心维护、性能和破坏性问题了;太棒了!你可以立即清楚地告诉每个相关人员(包括lint),确切地使用了哪些本地变量,以及用于什么目的。

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

所以,总结一下:“ γνῶθι σεαυτόν!”嗯,我的意思是“了解你的代码的目的和范围”。如果它是一个一次性或者几乎不需要未来维护、不会在更广泛的上下文中重新使用等等的东西,那么请继续使用locals()进行小而美的便利;如果你知道其他情况,甚至如果你并不完全确定,就要谨慎行事,让事情更加明确——遭受一点小不便,明确你正在做什么,并享受所有的好处。

顺便说一句,这只是Python努力支持“小型、一次性、探索性、可能交互性”的编程的例子之一(通过允许和支持风险便利的方式,扩展远远超出了locals()——想想import *evalexec以及几种其他方法,可以混合命名空间并冒险影响维护),以及“大型、可重用的企业级”应用和组件。它可以在两者方面做得相当好,但只有当你“了解自己”的时候,才能避免使用“方便性”部分,除非你绝对确定你实际上能够负担得起它们。往往关键考虑因素是“这对我的命名空间、编译器、lint &c、人类读者和维护者的形成和使用意味着什么?”。

请记住,“命名空间是一个伟大的想法——让我们做更多的这样的事情!”这就是Python之禅的结论...但是作为一种“成年人语言”,Python让定义这意味着什么的边界,作为你的开发环境、目标和实践的结果。要负责任地使用这种力量!-)


1
一个很好的答案。我认为大多数程序没有国际化,因此在很多情况下这不是一个问题。但在那些情况下,字符串插值确实是不好的。 - Paul Biggar
5
@Paul,我不得不表示不同意:字符串插值非常好,尤其是对于i18n/L10n支持 - 只需要在明确命名的变量上进行操作!问题不在于插值,而在于将locals()传递给外部函数或方法。顺便说一句,Python对Unicode的不断支持(现在是3.*中默认的文本字符串)正是为了帮助改变“大多数程序没有进行i18n”的事实 - 应该有更多的程序进行i18n,而不只是现在这样;随着互联网在中国等地迅速发展(通过智能手机、微型电脑等),以英语为中心的假设变得越来越怪异。 - Alex Martelli
2
我认为有可能通过调整locales/gettext来插入{self}{password}或其他意外显示的对象到格式字符串中。这可能存在安全风险。最好在真实代码中明确指定。 - John La Rooy
2
@Paul,我称之为“字符串格式化”——名称完全是任意的(无论您使用特制字典还是现有字典,这显然是相同的语句),并且没有涉及插值——请参见插值的定义http://en.wikipedia.org/wiki/Interpolation。将'a'和'c'合并以获得'b',那才是字符串的插值。 - Alex Martelli
4
在PHP或Perl中,"hello {$name}s you are $age years old" 是字符串插值。这也是Python中相同的模式。我认为你是在开玩笑关于插值定义的问题 - “字符串插值”一词早已被用来描述这种类型的字符串格式。它甚至是PEP215的标题。 - Paul Biggar
显示剩余6条评论

10

一百万年也不会这样做。目前不清楚格式化的上下文是什么:locals 可能包含几乎所有变量。self.__dict__ 没有那么模糊。让未来的开发人员费解哪些是本地的变量,哪些不是本地的变量,非常糟糕。

这是故意制造的神秘感。为什么要给你的组织留下未来维护方面的头疼呢?


上下文是locals()出现的函数。如果它是一个很好的短函数,你可以直接查看vars。如果它是一个非常长的函数,那么应该进行重构。self.__dict__有什么更清晰的作用呢? - foosion
6
我不明白为什么在格式化字符串中引用局部变量名会比在代码中引用局部变量名更加不清晰。 - Robert Rossney
self.__dict__ 基于类定义 -- 这通常与 __init __() 方法紧密绑定,并在文档字符串中仔细记录。locals() 经常是一个相当随机的名称集合。 - S.Lott
@PaulMcMillan:你会遇到什么样的问题? - endolith
@endolith: 你会遇到一些问题,比如后来改变了作用域或者重命名了一个变量以匹配新的语义意义,或者其他人在您的作用域中注入了一些内容......这些情况都不够明确,无法在真实项目中进行维护。 - Paul McMillan
显示剩余2条评论

9

关于"表兄弟",与其使用obj.__dict__,使用新的字符串格式化在视觉上更加美观:

def example2(obj):
    print "The file at {o.path} has {o.length} bytes".format(o=obj)

我经常在 repr 方法中使用它,例如:

def __repr__(self):
    return "{s.time}/{s.place}/{s.warning}".format(s=self)

9

我认为这是一个很好的模式,因为您可以利用内置功能来减少需要编写的代码量。我个人认为这很符合Pythonic的风格。

我从不编写不必要的代码 - 较少的代码比更多的代码更好,例如使用locals()就可以让我写更少的代码,并且也非常易于阅读和理解。


我喜欢在函数顶部使用它,当我需要从输入参数构建一个字典时。它非常有效,我觉得它也符合Python的风格。有时候可能会被滥用,我能理解这一点。 - radtek

8

"%(name)s" % <dictionary> 或者更好的方式是使用 "{name}".format(<parameters>),这样做有以下优点:

  • 比 "%0s" 更易读
  • 不依赖于参数顺序
  • 不强制使用字符串中的所有参数

我倾向于使用 str.format(),因为它应该是 Python 3 中执行此操作的方式(根据 PEP 3101),并且已经从 2.6 开始提供。但是,使用 locals(),您需要这样做:

print("hello {name} you are {age} years old".format(**locals()))

6

使用内置的vars([object])文档)可能会让第二个看起来更好:

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

当然,效果是一样的。

2

现在有一种官方的方法可以做到这一点,Python 3.6.0起支持:格式化字符串字面量

它的用法如下:

f'normal string text {local_variable_name}'

例如,代替这些:

"hello %(name)s you are %(age)s years old" % locals()
"hello {name} you are {age} years old".format(**locals())
"hello {} you are {} years old".format(name, age)

只需要这样做:

f"hello {name} you are {age} years old"

Here's the official example:

>>> name = "Fred"
>>> f"He said his name is {name}."
'He said his name is Fred.'
>>> width = 10
>>> precision = 4
>>> value = decimal.Decimal("12.34567")
>>> f"result: {value:{width}.{precision}}"  # nested fields
'result:      12.35'

参考资料:


1
这真是让我开心的一天。我将在所有不需要支持2.x或<3.6的Python代码中使用它! - svenevs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接