抑制字符串作为可迭代对象的处理
更新:
在2006年,有人在python.org上提议让内置字符串变得不可迭代。我的问题和这个提议不同,我只是想偶尔禁止这个特性;不过整个讨论还是很相关的。
以下是Guido的关键评论,他试验性地实现了不可迭代的str
:
[...] 我实现了这个(其实很简单),但后来发现我得修复很多遍历字符串的地方。例如:
sre解析器和编译器使用像set("0123456789")这样的东西,还会遍历输入正则表达式的字符来解析它。
difflib有一个API,定义了两种字符串列表(文件的逐行差异),或者两个字符串(典型的行内差异),甚至是两个任何东西的列表(用于通用序列差异)。
在optparse.py、textwrap.py、string.py中有小的改动。
而且我甚至还没到regrtest.py框架能正常工作的地步(因为difflib的问题)。
我放弃了这个项目;这个补丁是SF补丁1471291。我不再支持这个想法;这根本不实际,而且我在sre和difflib中发现的用例反驳了“很少有好的理由去遍历字符串”这个前提。
原始问题:
虽然字符串作为可迭代对象是语言的一个不错特性,但结合鸭子类型(duck typing)可能会导致灾难:
# record has to support [] operation to set/retrieve values
# fields has to be an iterable that contains the fields to be set
def set_fields(record, fields, value):
for f in fields:
record[f] = value
set_fields(weapon1, ('Name', 'ShortName'), 'Dagger')
set_fields(weapon2, ('Name',), 'Katana')
set_fields(weapon3, 'Name', 'Wand') # I was tired and forgot to put parentheses
不会抛出异常,除了在很多地方测试isinstance(fields, str)
,没有简单的方法来捕捉这个问题。在某些情况下,这个bug会花很长时间才能找到。
我想在我的项目中完全禁用字符串作为可迭代对象的处理。这是个好主意吗?能否轻松安全地做到?
也许我可以继承内置的str
,这样如果我想让它的对象被当作可迭代对象,我就需要明确调用get_iter()
。然后每当我需要一个字符串字面量时,我就创建这个类的对象。
以下是一些相关的问题:
5 个回答
在这种情况下,类型检查并不是不符合Python风格或者说不好。你只需要在调用的开始部分加上:
if isinstance(var, (str, bytes)):
var = [var]
或者,如果你想让调用者了解更多信息,可以这样做:
if isinstance(var, (str, bytes)):
raise TypeError("Var should be an iterable, not str or bytes")
为了更详细地解释一下,下面是我的看法:
不,这样做是不好的。
- 这样会改变人们对字符串的预期功能。
- 这会在你的程序中增加额外的负担。
- 大部分情况下,这其实是没必要的。
- 检查类型在Python中并不是一种优雅的做法。
你可以这么做,你提到的方法可能是最好的选择(顺便说一下,我觉得子类化是更好的选择 如果你真的需要这样做,可以参考@kindall的方法),但其实这样做不值得,而且也不符合Python的风格。最好还是避免一开始就出现bug。在你的例子中,你可以问问自己,问题是不是出在参数的清晰度上,是否使用命名参数或者拆包会是更好的解决方案。
比如:改变参数的顺序。
def set_fields(record, value, *fields):
for f in fields:
record[f] = value
set_fields(weapon1, 'Dagger', *('Name', 'ShortName')) #If you had a tuple you wanted to use.
set_fields(weapon2, 'Katana', 'Name')
set_fields(weapon3, 'Wand', 'Name')
比如:使用命名参数。
def set_fields(record, fields, value):
for f in fields:
record[f] = value
set_fields(record=weapon1, fields=('Name', 'ShortName'), value='Dagger')
set_fields(record=weapon2, fields=('Name'), value='Katana')
set_fields(record=weapon3, fields='Name', value='Wand') #I find this easier to spot.
如果你真的想保持参数的顺序,但又觉得命名参数不够清晰,那可以考虑把每个记录做成类似字典的项,而不是字典(如果还不是的话),这样就可以有:
class Record:
...
def set_fields(self, *fields, value):
for f in fileds:
self[f] = value
weapon1.set_fields("Name", "ShortName", value="Dagger")
这里唯一的问题是引入了一个类,而且值参数必须用关键字来传递,虽然这样可以保持清晰。
另外,如果你使用的是Python 3,你总是可以选择使用扩展的元组拆包:
def set_fields(*args):
record, *fields, value = args
for f in fields:
record[f] = value
set_fields(weapon1, 'Name', 'ShortName', 'Dagger')
set_fields(weapon2, 'Name', 'Katana')
set_fields(weapon3, 'Name', 'Wand')
或者,作为我的最后一个例子:
class Record:
...
def set_fields(self, *args):
*fields, value = args
for f in fileds:
self[f] = value
weapon1.set_fields("Name", "ShortName", "Dagger")
不过,这样做在阅读函数调用时会有些奇怪,因为通常人们会认为参数不会以这种方式处理。
很遗憾,目前没有自动化的方法来实现这个功能。你提到的解决方案(一个不可迭代的 str
子类)和 isinstance()
遇到的问题是一样的……也就是说,你必须在每次使用字符串的地方都记得用它,因为没有办法让 Python 自动用这个子类替代原生的字符串类。当然,你也不能随便修改内置对象。
我建议,如果你发现自己在写一个函数,它既能接受可迭代的容器又能接受字符串,也许你的设计有点问题。不过,有时候你确实无法避免这种情况。
在我看来,最简单的方法是把检查放到一个函数里,然后在进入循环时调用这个函数。这样至少可以让你在最容易看到的地方(也就是 for
语句中)看到行为的变化,而不是藏在某个类的深处。
def iterate_no_strings(item):
if issubclass(item, str): # issubclass(item, basestring) for Py 2.x
return iter([item])
else:
return iter(item)
for thing in iterate_no_strings(things):
# do something...