一个函数根据输入返回可迭代或不可迭代的结果算不算Pythonic?

8 投票
7 回答
1000 浏览
提问于 2025-04-15 14:31

(标题和内容在阅读了Alex的回答后进行了更新)

一般来说,如果一个函数根据参数的不同,有时返回一个可迭代对象(比如列表),有时返回一个单独的项目,这被认为是不太好的做法(不符合Python的风格)。

举个例子,struct.unpack这个函数总是返回一个元组,即使它里面只有一个元素。

我正在为一个模块最终确定API,有几个函数可以接受一个或多个参数(通过*args),像这样:

a = s.read(10)        # reads 10 bits and returns a single item
b, c = s.read(5, 5)   # reads 5 bits twice and returns a list of two items.

所以如果只有一个参数,它就返回一个单独的项目;如果有多个参数,它就返回一个列表。现在我觉得这样没问题,也不 confusing,但我怀疑其他人可能会有不同的看法。

这些函数最常见的用法是只想要返回一个单独的项目,所以总是返回一个列表(或元组)感觉不太对:

a, = s.read(10)       # Prone to bugs when people forget to unpack the object
a = s.read(10)[0]     # Ugly and it's not clear only one item is being returned

另一种选择是创建两个函数:

a = s.read(10)
b, c = s.read_list(5, 5)

这样也可以,但会让API变得复杂,用户需要记住更多的函数,而没有增加任何价值。

所以我的问题是:有时返回一个可迭代对象,有时返回一个单独的项目,这样做会让人困惑吗?如果是的话,最好的选择是什么?


更新:我认为大家的共识是,有时只返回一个可迭代对象是很不好的。我觉得在大多数情况下,最好的选择是总是返回可迭代对象,即使它里面只有一个项目。

不过,针对我特定的情况,我觉得我会选择分成两个函数(read(item) / readlist(*items)),原因是我认为单个项目的情况会比多个项目的情况发生得更频繁,这样使用起来更简单,API的变化对用户来说也不那么麻烦。

谢谢大家。

7 个回答

2

根据传入的参数返回一个对象或多个对象,确实挺麻烦的。不过,你标题里的问题其实更广泛,认为标准库的函数避免(或者“基本避免”)根据参数返回不同类型的说法是不对的。实际上,有很多反例。

比如,copy.copycopy.deepcopy 这两个函数返回的类型和你传入的参数是一样的,所以它们当然是“根据参数返回不同类型”。“返回和输入相同的类型”其实是非常常见的——你可以把“从容器中取回一个对象”也算在内,虽然通常是用方法而不是函数来实现;-)。另外,像 itertools.repeat(当你对它返回的迭代器进行迭代时),或者 filter 也是类似的例子:

>>> filter(lambda x: x>'f', 'zaplepidop')
'zplpiop'
>>> filter(lambda x: x>'f', list('zaplepidop'))
['z', 'p', 'l', 'p', 'i', 'o', 'p']

过滤一个字符串返回字符串,过滤一个列表返回列表。

等等,还有更多呢!-) 比如 pickle.loads 及其相关函数(比如在 marshal 模块中)返回的对象类型完全依赖于你传入的参数值。内置函数 eval 也是这样(在 Python 2.* 中,input 也是)。这是第二种常见的模式:根据参数的值构造或重构一个对象,可能的类型非常多样,甚至没有限制,然后返回它。

我不知道你观察到的具体反模式的好例子(我确实认为这是一种反模式,虽然不是什么高深的理由,只是因为处理起来麻烦和不方便;-)。不过,我举的这些例子都是很实用和方便的——这才是大多数标准库设计的真正区别所在!-)

3

一般来说,我得说返回两种不同类型的值是不太好的做法。

想象一下,下一个开发者来阅读和维护你的代码。起初,他/她看到一个使用你函数的方法时,会想:“哦,read() 只返回一个项目。”

但后来,他们会看到一些代码把 read() 的结果当成一个列表来处理。这样一来,最好的结果就是让他们感到困惑,不得不去仔细研究 read() 的用法。最糟糕的情况是,他们可能会认为使用 read() 的地方有bug,然后试图去修复它。

最后,一旦他们明白 read() 可能返回两种类型,他们就得问自己:“难道还有第三种返回类型我需要准备的吗?”

这让我想起一句话:“写代码的时候,想象下一个维护你代码的人是个知道你住哪的杀人狂。”

12

如果你有时候需要返回迭代器,有时候又只返回单个对象,我建议你始终返回一个迭代器,这样你就不用考虑太多了。

通常情况下,你会在需要迭代器的地方使用这个函数。如果你需要检查返回的是一个列表(可以迭代)还是一个对象(只用一次),那么直接返回一个迭代器会更简单,即使只用一次也没关系。

如果你需要在返回一个元素时做些不同的事情,可以用 if len(var): 来判断。

记住,一致性是非常重要的。

我倾向于返回一个一致的对象,不一定是同一种类型,但如果我返回的是可迭代的东西,我就始终返回可迭代的东西。

撰写回答