range() 函数
1. 何为 range()
首先我们需要知道 range() 函数返回的到底是一个什么东西:
from collections import Iterable
from collections import Iterator
x = range(100)
print(isinstance(x, Iterable))
print(isinstance(x, Iterator))
结果如下:
这说明 range() 返回的是一个可迭代对象,但不是一个迭代器。这个有点和列表 list 一样,但是在 Python3 中,他并不是一个 list。
x = range(100)
x_list = list(x)
print(type(x))
print(type(x_list))
结果如下:
可以看到,range() 函数返回的是一个 range 类的对象。
打印 range() 函数返回的对象:
x = range(100)
print(x)
结果如下:
到这里我回想起迭代器和生成器的惰性计算性质,我又想知道 range() 对象是否是惰性计算的。测试代码如下:
import os
import psutil
import gc
def show_memory_info(hint):
pid = os.getpid()
p = psutil.Process(pid)
info = p.memory_full_info()
memory = info.uss/1024/1024
print('{} memory used: {} M'.format(hint, memory))
def test_iterable():
show_memory_info('initing iterable')
my_iterable = [x * x for x in range(100000000)]
show_memory_info('after iterable initiated')
print('********')
def test_range():
show_memory_info('initing range')
my_iterable = range(100000000)
show_memory_info('after range initiated')
print('********')
test_iterable()
test_range()
神奇的一幕发生了,range 居然是惰性运算的,我猜测这也就是 Python3 把 Python2 的 xrange() 覆盖 range() 方法的原因,因为 Python2 中 range() 产生的是一个列表,如果长度太长,太占内存空间,而 Python3 的 range() 则是惰性计算。进一步观察:
x = range(100)
print(dir(x))
查看一下 range 对象的方法,我发现确实没有 __next__ 方法,也就不是一个迭代器,确实只是一个可迭代对象。这让我认识到了 python 的惰性计算并不是按照可迭代对象,迭代器,生成器来决定的。range 对象可以看为一个惰性可迭代对象
2. range 的用法:
range(start, stop, step)
常规的用法大家应该都知道,我这里主要看一些特例,也就是我很纠结的地方。(以下结果为了方便观察,默认使用 list() 函数转为列表)
1. range(-10): 空列表
2. range(-1, -10, -1): 和期望一样
3. range(0, -10)
到这里我终于可以确定了,range() 函数的步长默认就是 1,并不会在一些情况下自动变为 -1,需要手动去设置,然后如果参数完全不合逻辑,返回是一个空的可迭代对象。
3. 实现一个自己的 range 类
这里有一个有趣的地方,就是如果只传递一个参数,我们去赋值给的是 stop 而不是第一个位置的 start,但是如果是传递两个参数,那么就是按顺序赋值给 start 和 stop。
还有两个特点:
x = range(10)
print(len(x))
print(x[2])
print(x[-10])
print(x[-11])
结果如下:
说明 range 支持获取长度和其中第几个元素,而且还支持负索引,但是负索引超过了长度就会报错。另外就是步长 step 不能为 0:
x = range(0, 10, 0)
因此实现如下:
class MyRange:
def __init__(self, start, stop=None, step=1):
if step == 0:
raise ValueError('range() arg 3 must not be zero')
if stop is None:
start, stop = 0, start
if step > 0:
self.__length = max(0, ((stop - start - 1) // step) + 1)
else:
self.__length = max(0, ((start - stop - 1) // abs(step)) + 1)
self.__start = start
self.__stop = stop
self.__step = step
def __iter__(self):
num = self.__start
if self.__step > 0:
while num < self.__stop:
yield num
num += self.__step
else:
while num > self.__stop:
yield num
num += self.__step
def __len__(self):
return self.__length
def __getitem__(self, k):
if k < 0:
k += self.__length
if 0 <= k < self.__length:
return self.__start + k * self.__step
else:
raise IndexError('range object index out of range')