Python中是否有内置的方法来获取可迭代对象的长度?

86
例如,在Python中,文件是可迭代的,它们会遍历文件中的每一行。我想计算行数。
一种快速方法是这样做:
lines = len(list(open(fname)))

然而,这会将整个文件一次性加载到内存中,这实际上违背了迭代器的初衷(它只需要将当前行保留在内存中)。
这种方法行不通:
lines = len(line for line in open(fname))

由于生成器没有长度属性,所以有什么方法可以在不定义计数函数的情况下实现这一点吗?

def count(i):
    c = 0
    for el in i: c += 1
    return c

我理解整个文件需要被读取!但我不希望一次性全部读入内存。


要计算行数,你无论如何都需要将文件加载到内存中! - hasen
列表(所有序列类型)也是可迭代的。你所说的是“迭代器”。 - user3850
4
@hasen:是的,但不是一次性全部完成。 - Claudiu
10个回答

95

除了迭代可迭代对象并计算迭代次数外,没有其他方法。这正是它成为可迭代对象而不是列表的原因。这甚至不是一个特定于Python的问题。看一下经典的链表数据结构。查找长度是一个O(n)操作,需要迭代整个列表来查找元素数量。

正如mcrute上面提到的,你可能可以将你的函数简化为:

def count_iterable(i):
    return sum(1 for e in i)

当然,如果您正在定义自己的可迭代对象,您可以始终自己实现__len__并保留某个元素计数。


这段代码可以使用itertools.tee()函数进行优化。 - user3850
1
@Matt Joiner:调用count_iterable会消耗迭代器,因此您将无法进一步使用它。预先使用i,i2 = itertools.tee(i)复制迭代器可以解决这个问题,但是在函数内部不起作用,因为count_iterable不能作为副作用更改其参数(但是为简单的sum()定义一个函数似乎是不必要的...)。我想那大概是我两年前的推理。进一步思考后,我可能会改用.seek(0)(并重新命名该函数,因为它不再适用于任意迭代器)。 - user3850
4
敲打 itertools.tee。我总是忘记它必须将原始迭代器的数据存储在某个地方,这与操作者想要的相反。 - user3850
2
没错。如果你必须消耗整个可迭代对象才能得到计数,那么你实际上会将所有数据加载到tee的临时存储中,直到被另一个迭代器消耗。 - Kamil Kisiel
优秀而简洁的解决方案,通过使用通配符略微改进,如 sum(1 for _ in i)。我之所以建议这样做,是因为PyCharm指出了未使用的循环变量。感谢PyCharm! - Huw Walters
显示剩余2条评论

25
如果你需要计算行数,你可以这样做,我不知道有更好的方法来做这件事:
line_count = sum(1 for line in open("yourfile.txt"))

17

cardinality 包提供了一个高效的 count() 函数和一些相关函数,用于计算和检查任何可迭代对象的大小:http://cardinality.readthedocs.org/

import cardinality

it = some_iterable(...)
print(cardinality.count(it))

它在内部使用 enumerate()collections.deque() 将所有实际的循环和计数逻辑移动到 C 级别,从而比 Python 中的 for 循环大大提高了速度。


12

我一直在使用这个重新定义的方法:

def len(thingy):
    try:
        return thingy.__len__()
    except AttributeError:
        return sum(1 for item in iter(thingy))

1
它永远不会返回...请参考Triptych的示例。 - bortzmeyer
5
“用时需谨慎” (又称“我们都是成年人”)是 Python 的原则之一。至少曾经是如此。 - Jürgen A. Erhard
2
这里没有必要显式地调用 __len__iter;普通的 len(thingy) 以标准方式调用 __len__,而对任何东西进行迭代都会隐式地将其转换为迭代器,因此 for item in iter(thingy) 只是一种更慢、更长的拼写方式,与 for item in thingy 相同。 - ShadowRanger
1
@ShadowRanger:如果你重新定义了 len,尝试调用 len 会让你很不爽。 - Nick Matteo
1
@Kundor:哈!没错。我错过了它实际上是重新定义len,而不仅仅是提供一个更广泛的定义。个人而言,我会备份一份len,以便在函数中使用,例如在重新定义之前添加“_len = len”,然后在替换函数内部使用“_len”。如果可能的话,我会尽量避免直接手动调用特殊方法(它更丑陋,并且至少在3.7之前,它比调用内置方法要慢,因为它必须构造一个绑定方法,而len()可以避免这种情况)。 - ShadowRanger

11

原来这个常见问题已经有实现的解决方案了。可以考虑使用more_itertools库中的ilen()函数。

more_itertools.ilen(iterable)

打印文件中多行的示例(使用with语句安全处理文件关闭):

# Example
import more_itertools

with open("foo.py", "r+") as f:
    print(more_itertools.ilen(f))

# Output: 433

这个例子返回的结果与之前介绍的文件行总数的解决方案相同:

# Equivalent code
with open("foo.py", "r+") as f:
    print(sum(1 for line in f))

# Output: 433

8
绝对不行,因为可迭代对象没有保证是有限的。考虑下面这个完全合法的生成器函数:
def forever():
    while True:
        yield "I will run forever"

尝试使用 len([x for x in forever()]) 计算此函数的长度显然行不通。
正如您所指出的,迭代器/生成器的许多目的是能够处理大型数据集而无需将其全部加载到内存中。无法立即获取长度应被视为一种权衡。

28
sum()、max() 和 min() 的情况也是如此,但这些聚合函数需要迭代器作为参数。 - ttepasse
12
我降低了这个内容的评分,主要是因为其中的“绝对”,这并不正确。任何实现了__len__()方法的东西都有长度,无论是无限大还是非无限大。 - user3850
1
@hop,这个问题涉及到一般情况下的可迭代对象。实现__len__的可迭代对象是一个特殊情况。 - Kenan Banks
4
是的,但是正如hop所说,以“绝对”开始意味着通用性,包括所有特殊情况。 - James Brady
2
是的,如果给定一个无限生成器,它将永远不会终止。但这并不意味着在所有情况下这个想法都没有意义。在文档字符串中简单地警告说明这个限制就足以正确使用。 - Aaron Robson
显示剩余2条评论

3

由于显然在当时没有注意到重复,我将在此处发布我的答案中的一部分

当可迭代对象可能很长时(并且在可迭代对象很短时不会明显变慢),有一种方法可以比sum(1 for i in it)更快地执行,同时保持固定的内存开销行为(不像len(list(it))那样)以避免交换抖动和更大输入的重新分配开销。

# On Python 2 only, get zip that lazily generates results instead of returning list
from future_builtins import zip

from collections import deque
from itertools import count

def ilen(it):
    # Make a stateful counting iterator
    cnt = count()
    # zip it with the input iterator, then drain until input exhausted at C level
    deque(zip(it, cnt), 0) # cnt must be second zip arg to avoid advancing too far
    # Since count 0 based, the next value is the count
    return next(cnt)

len(list(it)) 相似,ilen(it) 在 CPython 上执行循环的 C 代码(dequecountzip 都是用 C 实现的);避免每次循环执行字节码是提高 CPython 性能的关键。不再重复所有性能数字,只需指向我的完整性能细节答案

在我的测试中(使用Python 3.7.3标准cpython解释器),这是所有不将整个可迭代对象放入内存的方法中最快的。 - Nick Matteo

1
"用于过滤的代码变体如下:"
sum(is_good(item) for item in iterable)

这可以自然地读作“计算好的项目”,比起以下方式更为简短易懂(尽管可能不那么惯用):

sum(1 for item in iterable if is_good(item)))

注意:在数字上下文中,True评估为1的事实在文档中已经指定(https://docs.python.org/3.6/library/stdtypes.html#boolean-values),因此此强制转换不是一种黑客行为(与其他语言如C / C ++相反)。

1
请注意,作为 CPython 的实现细节,后者更快;genexpr 中的过滤器减少了生成器中进出(适度昂贵)的转换次数,并且 sum 专门针对 int 输入进行了优化(确切的 intbool 是子类不算),因此产生 True 强制它采用缓慢的(Python 对象)路径,而产生 1 则让它使用快速的(C long)路径(直到总和超过 C long 的容量为止)。 - ShadowRanger

0
如果你考虑一下,如何在不读取整个文件的情况下找到文件中的行数呢?当然,你可以找到文件的大小,如果你能保证每行的长度是x,那么你就可以得到文件中的行数。但是除非你有某种限制,否则我无法看出这样做有什么作用。此外,由于可迭代对象可能是无限长的...

4
我想阅读整个文件,但不希望一次性将其全部加载到内存中。 - Claudiu

-1

我在我的一些代码中的两个常见过程之间进行了测试,这些代码找出了n个顶点上有多少个图形,以查看计数生成列表元素的方法哪种更快。Sage有一个生成器graphs(n),它生成所有n个顶点上的图形。我创建了两个函数,以两种不同的方式获取迭代器获得的列表的长度,并使用time.time()函数计时每个函数(平均100次测试运行)。这两个函数如下:

def test_code_list(n):
    l = graphs(n)
    return len(list(l))

并且

def test_code_sum(n):
    S = sum(1 for _ in graphs(n))
    return S

现在我对每个方法计时
import time

t0 = time.time()
for i in range(100):
    test_code_list(5)
t1 = time.time()

avg_time = (t1-t0)/10

print 'average list method time = %s' % avg_time


t0 = time.time()
for i in range(100):
    test_code_sum(5)
t1 = time.time()

avg_time = (t1-t0)/100

print "average sum method time = %s" % avg_time

平均列表方法时间=0.0391882109642

平均求和方法时间=0.0418473792076

因此,通过这种方式计算n=5个顶点的图形数量时,列表方法略快(虽然100次测试运行不是很大的样本量)。但是,当我尝试在n=7个顶点上尝试图形(即将graphs(5)更改为graphs(7))时,结果如下:

平均列表方法时间=4.14753051996

平均求和方法时间=3.96504004002

在这种情况下,求和方法略快。总的来说,这两种方法的速度大致相同,但差异可能取决于您的列表长度(也可能只是因为我仅对100次测试运行进行了平均,这并不是很高--否则需要很长时间)。


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