小孙那天写了一个消息队列的处理逻辑,需要把最近收到的消息按时间倒序排列。他拿到一个列表,里面是按时间正序排列的消息对象。他想都没想,写下了这行代码:
messages = get_messages()
messages = messages.reverse()
然后他拿着messages去遍历,准备处理最新的消息。结果程序直接报错:
TypeError: 'NoneType' object is not iterable
他盯着屏幕愣了五秒:messages明明是个列表,怎么变成None了?他明明记得reverse()就是用来反转列表的,为什么反转之后列表没了?
问题出在那个赋值上。reverse()是原地反转,它不返回反转后的列表,它返回None。小孙把None赋给了messages,原列表确实反转了,但他把变量指向了None,反转后的列表就这样被他亲手弄丢了。
“我就想倒个序,怎么还把数据搞没了?”
这个坑的根源,是他没搞清楚reverse和reversed的区别。这两个东西名字很像,功能也像,但行为完全不同。
先看一个最直观的对比
nums = [1, 2, 3, 4, 5]
# 方式一:reverse()
result1 = nums.reverse()
print("reverse() 之后 nums:", nums)
print("reverse() 的返回值:", result1)
# 方式二:reversed()
nums2 = [1, 2, 3, 4, 5]
result2 = reversed(nums2)
print("reversed() 之后 nums2:", nums2)
print("reversed() 的返回值:", result2)
print("reversed() 转成列表:", list(result2))
输出:
reverse() 之后 nums: [5, 4, 3, 2, 1]
reverse() 的返回值: None
reversed() 之后 nums2: [1, 2, 3, 4, 5]
reversed() 的返回值: <list_reverseiterator object at 0x...>
reversed() 转成列表: [5, 4, 3, 2, 1]
区别非常明显:
reverse()是列表的方法,它把原列表原地反转,原列表被修改,返回值是None。
reversed()是内置函数,它不碰原列表,返回一个迭代器,你需要遍历它或者转成列表才能看到反转后的结果。
用一个生活场景来理解。你有一排书,按顺序摆在书架上。
reverse()的做法是:直接把书架上的书顺序倒过来。第一本和最后一本交换,第二本和倒数第二本交换。书架上的书变了,但你手里没有新东西。
reversed()的做法是:不碰书架,而是给你一个“反向阅读指南”。你拿着这个指南,从最后一本开始读,读到第一本。书架本身纹丝不动,你只是换了一种方式去读它。
区别一目了然:一个改原数据,一个不改,返回的是一个“反向查看器”。
reverse():列表的原地反转方法
reverse()是列表对象的方法,没有参数,没有返回值(返回None)。
lst = [1, 2, 3]
lst.reverse()
print(lst) # [3, 2, 1]
它只能用于列表。元组、字符串、字典都没有这个方法。
tup = (1, 2, 3)
tup.reverse() # AttributeError: 'tuple' object has no attribute 'reverse'
s = "abc"
s.reverse() # AttributeError: 'str' object has no attribute 'reverse'
因为元组和字符串是不可变的,原地修改根本不可能。所以它们没有reverse方法。
reverse()的时间复杂度是O(n),因为它需要交换列表两端的元素,遍历一半的长度。空间复杂度是O(1),不需要额外空间。
reversed():内置的反向迭代器函数
reversed()是Python的内置函数,接受任何可迭代对象(序列、列表、元组、字符串、range等),返回一个反向迭代器。
lst = [1, 2, 3]
it = reversed(lst)
print(it) # <list_reverseiterator object at 0x...>
print(list(it)) # [3, 2, 1]
注意:reversed()返回的不是列表,是一个迭代器。迭代器只能遍历一次。
it = reversed([1, 2, 3])
print(list(it)) # [3, 2, 1]
print(list(it)) # [],已经耗尽了
如果你需要多次使用反转结果,必须转成列表:
reversed_list = list(reversed([1, 2, 3]))
print(reversed_list) # [3, 2, 1]
print(reversed_list) # [3, 2, 1],可以重复使用
reversed()不会修改原对象:
lst = [1, 2, 3]
list(reversed(lst))
print(lst) # [1, 2, 3],原列表没变
它适用于任何可迭代对象,包括元组和字符串:
print(list(reversed((1, 2, 3)))) # [3, 2, 1]
print(list(reversed("abc"))) # ['c', 'b', 'a']
print(list(reversed(range(5)))) # [4, 3, 2, 1, 0]
对于字符串,reversed()返回的是字符的反向迭代器。如果你想要反转后的字符串,需要再拼回去:
s = "abc"
reversed_s = "".join(reversed(s))
print(reversed_s) # "cba"
坑一:把 reverse() 的返回值赋给变量
这是最常见的错误,小孙踩的就是这个。
nums = [1, 2, 3]
nums = nums.reverse() # nums 变成 None
正确的写法是直接调用,不接收返回值:
nums = [1, 2, 3]
nums.reverse()
print(nums) # [3, 2, 1]
或者用reversed():
nums = [1, 2, 3]
nums = list(reversed(nums))
print(nums) # [3, 2, 1]
这个错误和之前讨论过的sort()是同一个问题:原地修改的方法返回None,这是Python的设计规则。所有原地修改的方法都不返回修改后的对象,而是返回None。
坑二:以为 reversed() 会修改原列表
反过来,有些人用了reversed(),以为原列表被反转了:
nums = [1, 2, 3]
reversed(nums)
print(nums) # [1, 2, 3],没变
reversed()返回的迭代器没有被使用,原列表纹丝不动。
正确的写法是接收迭代器并遍历,或者转成列表:
nums = [1, 2, 3]
nums = list(reversed(nums))
print(nums) # [3, 2, 1]
或者用reverse()原地反转:
nums = [1, 2, 3]
nums.reverse()
print(nums) # [3, 2, 1]
坑三:对元组或字符串用 reverse()
reverse()只能用于列表。如果你有一个元组想反转,只能用reversed():
tup = (1, 2, 3)
tup.reverse() # AttributeError
result = tuple(reversed(tup))
print(result) # (3, 2, 1)
字符串也一样:
s = "abc"
s.reverse() # AttributeError
result = "".join(reversed(s))
print(result) # "cba"
坑四:reversed() 返回迭代器,不能索引
reversed()返回的是迭代器,不是列表。迭代器不支持索引和切片:
it = reversed([1, 2, 3])
print(it[0]) # TypeError: 'list_reverseiterator' object is not subscriptable
如果你需要索引访问,必须先转成列表:
lst = list(reversed([1, 2, 3]))
print(lst[0]) # 3
坑五:在循环中多次使用同一个 reversed 迭代器
迭代器只能遍历一次。如果你在循环里用了reversed(),然后想再遍历一次,会发现第二次是空的。
it = reversed([1, 2, 3])
for x in it:
print(x) # 3, 2, 1
for x in it:
print(x) # 什么也没有
如果需要多次遍历,要么每次都调用reversed(),要么转成列表:
lst = list(reversed([1, 2, 3]))
for x in lst:
print(x)
for x in lst:
print(x)
性能差异
reverse()是原地反转,不需要额外空间,时间复杂度O(n)。
reversed()返回迭代器,不复制数据,也是O(1)空间(迭代器本身很小)。但如果你转成列表list(reversed(...)),就需要O(n)额外空间。
在时间上,reverse()和list(reversed(...))差不多,都是O(n)。但reversed()本身是惰性的,如果你只是遍历一次,不需要创建完整列表,可能更省内存。
import timeit
lst = list(range(10000))
t1 = timeit.timeit(lambda: lst.copy().reverse(), number=10000)
t2 = timeit.timeit(lambda: list(reversed(lst)), number=10000)
print(f"reverse(): {t1:.4f}")
print(f"list(reversed()): {t2:.4f}")
在我的机器上,两者都在0.5秒左右,差距很小。
什么时候用 reverse(),什么时候用 reversed()?
判断标准很简单:
需要原地反转列表,不保留原顺序:用reverse()。
需要保留原列表,或者处理非列表对象:用reversed()。
只需要遍历一次反向序列:用reversed(),不需要转列表。
需要多次使用反向结果,或者需要索引访问:用list(reversed(...))。
对元组、字符串、range等不可变对象反转:只能用reversed()。
一个常见的误解:reversed() 比 reverse() 慢很多
有些人觉得reversed()返回迭代器,每次遍历都要计算下一个元素,所以比reverse()慢。其实不然。
reverse()需要交换元素,遍历一半长度。reversed()的迭代器在每次next()时计算下一个位置,总共也是O(n)。两者时间复杂度相同。
真正的区别在于内存:reverse()原地修改,不占额外空间;reversed()如果不转列表,也不占额外空间。如果你转成列表,就需要O(n)空间。
所以选择哪个,主要看语义需求,而不是性能。
回到小孙的故事
小孙最终的代码是这样的:
messages = get_messages()
messages.reverse() # 原地反转
# 或者
messages = list(reversed(messages)) # 生成新列表
他选择了reverse(),因为他不需要保留原顺序,而且原列表只有他一个人在用。反转之后,他遍历messages,处理最新的消息,一切正常。
他后来在团队文档里写了一条:“reverse()原地反转列表,返回None。reversed()返回反向迭代器,不修改原对象。需要保留原顺序就用reversed(),不需要就用reverse()。别把reverse()的返回值赋给变量。”
这个区分看起来简单,但真到写代码的时候,很多人会随手写nums = nums.reverse(),然后在下一行打印时发现是None。或者用reversed()之后想用列表方法,发现迭代器没有那些方法。
记住那个书架的比喻:reverse()是直接把书架上的书倒过来,reversed()是给你一个反向阅读指南,书架本身不动。你要保留书架原样,就用reversed();你不在乎书架顺序,只想把它倒过来,就用reverse()。
逆序本身不难,难的是想清楚“这个逆序会不会影响别的地方”。想清楚了,reverse和reversed就不再是坑,而是两个各司其职的工具。