Python:为什么要使用str.join(iterable)而不是str.join(*strings)?

3

我经常将str.join()的参数放在一个列表中,例如:

'.'.join([str_one, str_two])

对我而言,额外的列表包装总是显得多余。我想做...

'.'.join(str_one, str_two, str_three, ...)

... or if I have a list ...

'.'.join(*list_of_strings)

是的,我是一个极简主义者,是的,我很挑剔,但更多的是我对这里的历史好奇,或者我是否错过了什么。也许在 splat 函数(注:指 *args)出现之前曾经有过一段时间?

编辑:

我想指出的是 max() 函数可以处理这两个版本:

max(iterable[, key]) max(arg1, arg2, *args[, key])


你更喜欢额外的“splat”还是额外的列表?除了“splat”可能并不是一直存在的这个事实之外。**是相当新的。 - pydsigner
这是一个重复的内容,我现在无法搜索原始内容。答案是该函数以此形式存在,因为它支持连接任何类型的集合(而不仅仅是列表)。你想要的版本基本上是没有意义的,因为参数仍然被函数作为一个集合接收。 - vikki
3
可迭代的使用情况更为普遍 - 你真的有多少个定义好的想要对称连接的唯一变量?它们要么相关,应该放在列表中,要么不相关,连接它们是奇怪的。 - Eric
2
@user1062565:对于两个字符串,str_one + '.' + str_two 有什么问题吗?这样更少的字符更清晰! - Eric
1
@pydsigner:我不知道11年算不算“相当新” ;) - lqc
显示剩余6条评论
3个回答

4

对于短列表,这并不重要,而且只需键入2个字符即可。但我认为str.join()最常见的用例是以下情况:

''.join(process(x) for x in some_input) 
# or
result = []
for x in some_input:
    result.append(process(x))
''.join(result)

如果使用可变参数而不是可迭代对象,那么这个语句将会被写成:

当输入数据条目数量很多时,您希望高效地生成输出字符串。

''.join(*(process(x) for x in some_input))
# or
''.join(*result)

这会创建一个(可能很长的)元组,只是为了将其作为*args传递。

所以在短的情况下,这是2个字符,而在大数据情况下浪费更多。

历史记录

函数定义中的*args是很久以前就添加到Python中的:

==> 发布版本0.9.8(1993年1月9日) <==

需要(a)来容纳变量长度参数列表;现在有一个显式的“varargs”功能(在最后一个参数之前加上“*”)。需要(b)与旧类定义兼容:在发布版本0.9.4之前,具有多个参数的方法必须声明为“def meth(self,(arg1,arg2,...)):...”。

一种适当的将列表传递给此类函数的方法是使用内置函数apply(callable, sequence)。(请注意,这并未提及可以在版本1.4文档中首次看到的**kwargs。)
使用*语法调用函数的能力首次出现在1.6版本的发布说明中:

现在有一种特殊的语法,您可以使用它来代替apply()函数。f(*args, **kwds)等同于apply(f, args, kwds)。您还可以使用变体f(a1, a2, *args, **kwds),并且可以省略其中一个:f(args),f(*kwds)

但在语法文档中缺失,直到2.2版本才加入。
在2.0版本之前,str.join()甚至不存在,你需要执行from string import join

我没有想到列表推导式的情况。谢谢! - Alex Buchanan
似乎在0.9.8版本中添加了*args,也称为可变参数,请参见:http://svn.python.org/projects/python/trunk/Misc/HISTORY并搜索vararg。 - Don Question
1
你所写的是,“如果join接受可变参数而不是一个可迭代对象,这会创建一个(可能很长的)元组。”但是,如果你将一个可迭代对象传递给join,Python仍然会创建一个(可能很长的)列表,因此没有区别。请参见PyUnicode_Join,它调用PySequence_Fast,将一个可迭代对象转换为一个列表。 - Gareth Rees

2

你需要编写自己的函数来完成这个任务。

>>> def my_join(separator, *args):
        return separator.join(args)

>>> my_join('.', '1', '2', '3')
'1.2.3'

请注意,这并不能避免创建额外的对象,它只是“隐藏”了一个额外的对象正在被创建。如果您检查 args 的类型,您会发现它是一个“元组”。

如果您不想创建一个函数并且您有一个固定的字符串列表,则可以使用格式化字符串(format)而不是 join:

'{}.{}.{}.{}'.format(str_one, str_two, str_three, str_four)

最好只使用 '.'.join((a, b, c)) 这种写法。

我本来想解释的,但按下回车键切换到下一行并不是评论中最明智的做法;-) 我肯定没有投反对票——我很少这样做——而且从来不会没有解释就投票。 - Don Question
谢谢Mark!我的问题不在于如何,而在于为什么。引入自定义连接是过度设计,使用format()连接两个字符串似乎也总是过度设计。再次强调,我故意对样式过于挑剔,因为Python在样式方面设定了如此高的标准。 - Alex Buchanan
我正在查找有关闪运算符介绍的参考资料,因为我有印象它不是从Python一开始就有的,我认为方法签名S.join(iterable) -> string不应该包含闪运算符,因为出于兼容性原因,像您不希望原本使用CPython构建的内容在其他实现中因此基本问题而崩溃。 - Don Question
@user1062565:为什么只有编写str.join的人才能回答这个问题。可能是Guido... - Mark Byers
我找不到关于引入*操作符的任何参考资料。我发现的最早的参考资料是Python 1.5,所以我无法确定如果“晚”引入(如果这不是我的想象)可能与此有关-抱歉。 - Don Question

2
哎呀,这个问题有点难!试着争论哪种风格更加极简......很难给出一个好的答案而不过于主观,因为这完全是关于惯例的。
问题是:我们有一个接受有序集合的函数;它应该将其作为单个参数还是可变长度的参数列表进行接受? Python通常的答案是:作为单个参数;如果你真的有理由,可以使用VLAL。让我们看看Python库如何反映这一点:
标准库中有一些VLAL的示例,最值得注意的是:
- 当函数可以使用任意数量的单独序列进行调用时,例如zip、map或itertools.chain, - 当有一个序列要传递,但你并不真正期望调用者将其全部作为单个变量。这似乎符合str.format。
使用单个参数的常见情况有:
- 当您想对单个序列进行一些通用数据处理时。这适用于功能三元组(map*、reduce、filter)以及它们的专业衍生品,例如sum或str.join。还有像enumerate这样的有状态转换。 - 模式是“消耗一个可迭代对象,提供另一个可迭代对象”或“消耗一个可迭代对象,提供一个结果”。
希望这回答了你的问题。
注意:map在技术上是var-arg的,但常见用例只是map(func, sequence) -> sequence,它与reduce和filter属于同一类别。
*模糊的情况map(func, *sequences)在概念上类似于map(func, izip_longest(sequences)) - zip遵循var-arg约定的原因已经解释过了。
我希望你能理解我的思路;毕竟这完全是编程风格问题,我只是指出Python库函数中的一些模式。

我认为map(func,*sequences)更像是starmap(func,izip_longest(sequences)) - 即元组在发送到func时被解包,而不是作为元组传递,这只是一个小细节。 - mgilson
没错,这就是为什么我说“从概念上讲”(~ =“关于传递哪些数据,而不是如何传递数据”)。虽然我没有想到starmap,但它在这里也是一个很好的例子! - Kos

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