抑制字符串被视为可迭代对象的处理

16

更新:

Python官网的讨论中,有人提出了使内置字符串不可迭代的想法。我的问题不同之处在于,我只是偶尔想要关闭这个功能;但整个讨论仍然非常相关。

以下是由Guido发表的关键评论,他试行了非可迭代str

[...] 我实现了这个(真的很简单),但后来发现我必须修复大量遍历字符串的地方。例如:

  • sre解析器和编译器使用像set("0123456789")这样的东西,并且还要遍历输入正则表达式的字符来解析它。

  • difflib为两个字符串列表(文件的典型逐行差异)或两个字符串(典型的行内差异)定义了API,甚至可以为任意两个列表(用于一般序列差异)进行比较。

  • optparse.py、textwrap.py和string.py的小更改。

我还没有到使regrtest.py框架工作的地步(由于difflib问题)。

我放弃了这个项目;补丁是SF补丁1471291。我不再支持这个想法;它不切实际,而且有关于循环遍历字符串的好理由被反驳了,这些理由可以在sre和difflib中找到。

原始问题:

虽然字符串是一个可迭代对象,这是语言的一个很好的特性,但当与鸭子类型结合时,它可能会导致灾难:

# 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()。然后,每当我需要字符串字面值时,我将创建此类的对象。

以下是一些有关的问题:

如何判断 Python 变量是字符串还是列表?

如何确定变量是否可迭代但不是字符串


我认为你已经基本上回答了自己的问题。如果你必须这样做,那么你的两种方法是最好的方式,但最好的答案是确保它不会发生。 - Gareth Latty
2
我建议您仅使用 isinstance(fields, str) 检查 - 您不太可能需要创建自己的类,使其像字符串一样。或者,将 fields 设为最后一个可变参数。 (如果您感到疲倦并忘记 应该在它周围加括号,则无法帮助您)。 - millimoose
任何将字符串定义为通用字符列表的库/语言都会遇到这个问题。这似乎不是Python的问题。 - Apalala
5个回答

8
很不幸,没有自动实现这一点的方法。你提出的解决方案 (一个不能迭代的 str 子类) 与 isinstance() 有相同的问题...即你必须记得在使用字符串时到处使用它,因为没有办法让 Python 在原生类的位置使用它。当然,你也不能对内置对象进行monkey-patch。
如果你发现自己要编写一个函数,需要接受可迭代容器或字符串,那么也许你的设计存在问题。但有时可能无法避免。
在我看来,最不会影响代码内部结构的方法是将检查放入一个函数中,并在进入循环时调用该函数。这至少可以将行为更改放在你最可能看到它的地方:在 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...

我给出的函数作为例子怎么样?你会说这是“错误设计”还是“无法避免的”情况? - max
我有点犹豫不决。有时候我想说“在接受内容时要开明一些”,有时候我又想说“尽可能满足用户的需求”。但在你的情况下,也许可以先将值作为 *args 设置,然后再设置你想要的名称?这样你就总是会得到一个可迭代对象,调用者只需要指定他们拥有的名称数量即可。如果他们已经有了一个元组,那么在调用你时他们只需要解包它即可。 - kindall
...并且为了对自己进行反驳,最好将名称放在前面(以匹配getattr()setattr()之类的内容)。就像我说的那样,我犹豫不决。那么**kwargs怎么样?这样你就可以只指定Name='Dagger',ShortName='Dagger'而不会太笨重。 - kindall
是的,如果有很多属性,那会变得很混乱,但如果只有几个属性,这可能是最好的选择。或者您可以使用一些符号来从其他参数中获取值(例如 ShortName='@Name')。 - kindall
@kindall 我认为当你这样做的时候,更好的选择是我的类方式,或者如果在Python 3中,我给出的扩展元组拆包示例。 - Gareth Latty
显示剩余2条评论

6
为了扩展并回答这个问题:
不,你不应该这样做。
它改变了人们对字符串期望的功能。
这意味着在程序中增加了额外的开销。
这基本上是没有必要的。
检查类型非常不符合Pythonic的风格。
你可以这样做,并且你提供的方法可能是最好的方式(如果你必须这样做,请参见@kindall的方法),但这根本不值得去做,而且也不太符合Pythonic的风格。从一开始就避免错误。在你的例子中,你可能需要问问自己,这是否更多地与参数的清晰度有关,以及使用命名参数或splat是否是更好的解决方案。
例如:改变顺序。
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")

唯一的问题在于引入了一个类,并且需要使用关键字来指定value参数,尽管这样可以保持清晰明了。

或者,如果您使用的是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")

然而,这种方式在阅读函数调用时会留下一些奇怪的痕迹,因为人们通常认为参数不会以这种方式处理。

3
我知道这不符合Python的规范,所以我做这件事感到很糟糕...但是如何避免这些错误呢?我们说的是一个括号丢失.. 偶尔避免几乎是不可能的,不是吗? - max
1
@max 正如我所说,我认为这不是字符串迭代的问题,而是你在方法中构造参数的方式有问题。 - Gareth Latty

4
在这种情况下,类型检查并不是不符合Python风格或不好的做法。只需执行以下操作:

if isinstance(var, (str, bytes)):
    var = [var]

在通话开始时。或者,如果您想教育呼叫者:
if isinstance(var, (str, bytes)):
    raise TypeError("Var should be an iterable, not str or bytes")

2
你认为创建一个不可迭代的字符串怎么样?
class non_iter_str(str):
    def __iter__(self):
        yield self

>>> my_str = non_iter_str('stackoverflow')
>>> my_str
'stackoverflow'
>>> my_str[5:]
'overflow'
>>> for s in my_str:
...   print s
... 
stackoverflow

这正是我最初的想法;但@kindall提到了其中一个缺点,即“你必须记住在使用字符串的每个地方都使用它”,包括我的代码的其他用户。 - max

0

不要试图使您的字符串不可迭代,改变您看待问题的方式:您的参数之一是可迭代的或者...

  • 字符串
  • 整数
  • 自定义类
  • 等等。

当您编写函数时,首先要做的是验证参数,对吗?

def set_fields(record, fields, value):
    if isinstance(fields, str):
        fields = (fields, )  # tuple-ize it!
    for f in fields:
        record[f] = value

这将对您处理其他函数和参数(可以是单数或复数)非常有帮助。


1
这很不符合Python的风格。考虑使用列表或其他迭代器而不是元组?Python是一种鸭子类型的语言,进行类型检查并不是一个好主意,这违背了该语言的理念。 - Gareth Latty
不要检查它是否为元组。请检查它不是字符串或字节。 - Lennart Regebro
@LennartRegebro:谢谢——以不同的方式听到它让我恍然大悟。回答已更新。 - Ethan Furman
1
@Lattyware:正如Lennart所说,我的错误在于检查了一个“tuple”,而不是检查它不是一个“str”。isinstance有其作用,这就是其中之一。回答已更新。 - Ethan Furman

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