为什么是string.join(list)而不是list.join(string)?

2077

这个问题一直让我困扰。似乎这样会更好:

["Hello", "world"].join("-")

比这个:

"-".join(["Hello", "world"])

这样做的特定原因是什么?


5
为了更容易记忆和理解, - 表示将列表连接并转换为字符串。它关注的是结果。 - AbstProcDo
8
我认为最初的想法是因为join()返回一个字符串,所以必须从字符串上下文中调用。在列表上使用join()并没有太多意义,因为列表是对象容器,不应该具有仅针对字符串的单独功能。 - Joshua Burns
6
“因为Python的类型系统不够强大”是完全错误的。正如Yoshiki Shibukawa的回答(早在您发表评论的8年前!)所说,iterable.join()被认为是可能性,但被拒绝是因为这是一个较差的API设计 - 不是因为无法实现。” - Arthur Tacca
7
可能因为我习惯于使用JavaScript,所以我的看法有些偏颇,但是在我看来,如果你想要将某个元素加入列表中,应该是列表的一个方法。现在的感觉反过来了。 - Zimri Leijen
8
好的,str.split() 返回一个非字符串类型,这很有道理。在这里似乎使用相同的逻辑也应该没问题,对吗?(仅探讨非字符串输出的概念性问题) - ntjess
显示剩余4条评论
11个回答

1428

这是因为任何可迭代对象都可以连接起来(例如列表、元组、字典、集合),但其内容和“连接器”必须是字符串。

例如:

'_'.join(['welcome', 'to', 'stack', 'overflow'])
'_'.join(('welcome', 'to', 'stack', 'overflow'))
'welcome_to_stack_overflow'

使用非字符串类型将引发以下错误:

TypeError: sequence item 0: expected str instance, int found


111
即使从代码层面看起来可行,我在概念上仍然不同意。list.join(string) 看起来更像一种面向对象的方法,而 string.join(list) 对我来说听起来更加过程化。 - Eduardo Pignatelli
45
为什么这个方法没有在可迭代对象上实现? - Steen Schütt
19
尽管整数列表可迭代,但它没有有意义的连接方式。 - recursive
29
我已经尝试使用print(str.join('-', my_list)),它能正常工作,感觉更好。 - pimgeek
39
因为可迭代对象并不是具体的类型,它是一个接口,任何定义了 __iter__ 方法的类型都可以是可迭代对象。要求所有可迭代对象都实现 join 方法将会使得通用接口变得更加复杂(该接口还覆盖了非字符串类型的可迭代对象),只是为了解决一个非常特定的问题。在字符串上定义 join 方法避免了这个问题,但代价是顺序“不直观”。可能更好的选择是保持 join 作为一个函数,第一个参数是可迭代对象,第二个参数(可选)是连接符字符串——但这已经成为了既定事实。 - user4815162342
显示剩余6条评论

444
这在Python-Dev的存档中的String methods... finally主题中进行了讨论,并得到了Guido的接受。这个主题始于1999年6月,str.join被包含在于2000年9月发布的Python 1.6中(并支持Unicode)。Python 2.0(支持str方法,包括join)于2000年10月发布。
这个帖子中提出了四个选项:
  • separator.join(items)
  • items.join(separator)
  • items.reduce(separator)
  • join作为内置函数
Guido希望不仅支持listtuple,还支持所有的序列/可迭代对象。
对于新手来说,items.reduce(separator)很难理解。 items.join(separator)将序列与字符串/Unicode之间引入了意外的依赖关系。 join()作为一个独立的内置函数只能支持特定的数据类型。因此,使用内置命名空间并不好。如果join()要支持多种数据类型,创建一个优化的实现将会很困难:如果使用__add__方法来实现,时间复杂度将是O(n²)。
分隔符字符串(separator)不应该被省略。明确比隐式更好。
以下是一些额外的想法(我自己和我的朋友的):
  • Unicode支持正在到来,但尚未最终确定。当时UTF-8最有可能取代UCS-2/-4。为了计算UTF-8字符串的总缓冲区长度,该方法需要知道字符编码。
  • 当时,Python已经决定了一个常见的序列接口规则,用户可以创建类似序列(可迭代)的类。但是直到2.2版本,Python才支持扩展内置类型。当时很难提供基本的可迭代类(在另一个评论中提到)。

Guido的决定记录在一封历史邮件中,决定使用separator.join(items)

有趣,但似乎是正确的!Barry,去做吧...
--Guido van Rossum


5
很好,这份文件记录了推理过程。了解更多关于“从序列到str/unicode的意外依赖”是否仍然存在将是很好的。 - zdim
6
这是最佳答案,因为它提供了权威的背景和原因,说明了为什么选择了它。 - tschwab
@zdim 如果在可迭代对象上实现join方法,就必须涉及将项目转换为字符串,这将从可迭代对象的实现引入str依赖。这种情况将始终如此。 - Jason C
@zdim 如果在可迭代对象上实现join方法,就必须涉及将项目转换为字符串,这将从可迭代对象的实现引入str依赖。这种情况将始终如此。 - undefined
我想知道为什么他们没有考虑使用string.join(sep, seq)或类似的方法。 ‍♂️ - Jason C
1
@JasonC 如果你想的话,你实际上可以这样调用它:str.join(",", ["a", "b", "c"]) 返回 "a,b,c" - undefined

258
因为join()方法存在于字符串类中,而不是列表类中。 参见http://www.faqs.org/docs/diveintopython/odbchelper_join.html历史注解。当我刚开始学习Python时,我期望join是列表的一个方法,它可以接受分隔符作为参数。许多人有同样的感觉,关于join方法还有一个故事。在Python 1.6之前,字符串没有所有这些有用的方法。有一个单独的字符串模块,其中包含了所有的字符串函数;每个函数都将字符串作为第一个参数。这些函数被认为非常重要,可以将它们放到字符串本身上,对于像lower、upper和split这样的函数来说是有意义的。但是许多Python核心程序员反对新的join方法,认为它应该是列表的一个方法,或者它根本不应该移动,而只是留在旧的字符串模块的一部分中(它仍然有很多有用的东西)。我专门使用新的join方法,但你会看到写法不同的代码,如果它真的让你很烦恼,你可以使用旧的string.join函数。--- Mark Pilgrim,《Dive into Python》

16
Python 3的string库已经移除了所有冗余的str方法,所以您无法再使用string.join()。就个人而言,我从未觉得它有多“有趣”,它非常合理,因为您可以连接不止列表,但连接器始终是一个字符串! - Martijn Pieters
可以使用 str.join(str_instance, list[str]) 代替 string.join(str_instance, list[str])。这样做的好处是,您不需要为这种基本功能导入任何模块。 - Tigran

81

我同意这一点起初可能不太直观,但有一个很好的原因。Join不能是列表的方法,因为:

  • 它必须适用于不同的可迭代对象(元组、生成器等)
  • 它必须在不同类型的字符串之间具有不同的行为。

实际上有两种join方法(Python 3.0):

>>> b"".join
<built-in method join of bytes object at 0x00A46800>
>>> "".join
<built-in method join of str object at 0x00A28D40>
如果join是列表的一个方法,那么它必须检查它的参数以决定调用哪个参数。而且你不能将字节和字符串连接在一起,所以现在他们这样做是有意义的。

49
为什么要用 string.join(list) 而不是 list.join(string)
这是因为 join 是一个“字符串”方法!它可以从任何可迭代对象中创建一个字符串。如果我们把该方法放在列表上,那么对于不是列表的可迭代对象怎么办呢?
比如说,如果你有一个由字符串组成的元组,如果这是一个 list 方法,你就必须将每个这样的字符串迭代器强制转换为一个 list,然后才能将这些元素连接成单个字符串!举个例子:
some_strings = ('foo', 'bar', 'baz')

让我们自己编写列表连接方法:

class OurList(list): 
    def join(self, s):
        return s.join(self)

要使用它,请注意我们必须先从每个可迭代对象创建一个列表,以连接该可迭代对象中的字符串,这会浪费内存和处理能力:

>>> l = OurList(some_strings) # step 1, create our list
>>> l.join(', ') # step 2, use our list join method!
'foo, bar, baz'

因此,我们看到在使用我们的列表方法时必须添加一个额外的步骤,而不能仅仅使用内置的字符串方法:

>>> ' | '.join(some_strings) # a single step!
'foo | bar | baz'

生成器的性能注意事项

Python用于使用str.join创建最终字符串的算法实际上必须对可迭代对象进行两次遍历,因此,如果您提供一个生成器表达式,则必须先将其材料化为列表,然后才能创建最终的字符串。

因此,虽然传递生成器通常比列表推导更好,但str.join是一个例外:

>>> import timeit
>>> min(timeit.repeat(lambda: ''.join(str(i) for i in range(10) if i)))
3.839168446022086
>>> min(timeit.repeat(lambda: ''.join([str(i) for i in range(10) if i])))
3.339879313018173

然而,str.join 操作在语义上仍然是一个“字符串”操作,因此在 str 对象上拥有它比在其他迭代对象上拥有它更有意义。


25

把它看作是“split”操作的自然正交操作。

我理解为什么它适用于任何可迭代对象,因此不能仅仅在列表上实现。

为了可读性,我希望在语言中看到它,但我不认为这是实际可行的-如果可迭代性是一个接口,那么它可以添加到接口中,但它只是一种约定,因此没有集中的方法将其添加到可迭代的事物集合中。


15

-"-".join(my_list) 中表示将列表中的元素连接起来并转换为字符串。这是结果导向的。(只为了更容易记住和理解)

我制作了一份详尽的字符串方法速查表供您参考。

string_methods_44 = {
    'convert': ['join','split', 'rsplit','splitlines', 'partition', 'rpartition'],
    'edit': ['replace', 'lstrip', 'rstrip', 'strip'],
    'search': ['endswith', 'startswith', 'count', 'index', 'find','rindex', 'rfind',],
    'condition': ['isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isnumeric','isidentifier',
                  'islower','istitle', 'isupper','isprintable', 'isspace', ],
    'text': ['lower', 'upper', 'capitalize', 'title', 'swapcase',
             'center', 'ljust', 'rjust', 'zfill', 'expandtabs','casefold'],
    'encode': ['translate', 'maketrans', 'encode'],
    'format': ['format', 'format_map']}

14

主要是因为 someString.join() 的结果是一个字符串。

序列(列表、元组或其他类型)不会出现在结果中,只有一个字符串。由于结果是一个字符串,因此作为字符串的方法是有意义的。


2

您不仅可以连接列表和元组。您几乎可以连接任何可迭代对象。可迭代对象包括生成器、映射、过滤器等。

>>> '-'.join(chr(x) for x in range(48, 55))
'0-1-2-3-4-5-6'

>>> '-'.join(map(str, (1, 10, 100)))
'1-10-100'

使用生成器、映射、过滤等功能的美妙之处在于它们占用的内存很少,几乎可以即刻创建。

这正是为什么它们在概念上如此重要的另一个原因。

str.join(<iterator>)

只有授予str这种能力才是高效的。而不是授予所有迭代器(列表、元组、集合、字典、生成器、映射、过滤器)都具有对象作为共同父级的join。

当然,range()和zip()也是迭代器,但它们永远不会返回str,因此不能与str.join()一起使用。

>>> '-'.join(range(48, 55))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sequence item 0: expected str instance, int found

1
“不是授予所有迭代器加入(join)的权限,它们之间只有对象作为共同父级,这似乎是不明智的原因(不使用iter.join())。” - zdim

2
变量my_list"-"都是对象。具体来说,它们分别是类liststr的实例。函数join属于类str。因此,使用语法"-".join(my_list)是因为对象"-"my_list作为输入。

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