前言
在上一篇文章《[Python]切片完全指南(语法篇)》中,我着重从语法层面介绍了怎样使用切片,并用语言解释了切片的行为,例如我们拓宽了有效索引范围的概念,并把缺省值解释成拓宽范围中的无穷大。但显然,这些解释不是计算机所能理解的,那么在Python内部,切片操作到底是如何运作的呢?本篇文章将会进行深入探究。
注:本文的讨论基于Python3,与python2中略有差异。
魔术方法__getitem__
和__setitem__
在Python中,[]
常用于对对象进行索引,包括对单个位置的下标索引,以及本文关注的对某个范围的切片索引:
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> print(l[0])
0
>>> l[0] = 10
>>> print(l)
[10, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> print(l[:5])
[10, 1, 2, 3, 4]
在上面的例子中,实际上包含着两种不同的索引,一种是取值,另一种是赋值。
在Python中,这两类索引是依靠两个不同的魔术方法分别实现的:取值实际上调用了__getitem__
方法,赋值实际上调用了__setitem__
方法,我们现在在自定义类中重载这两个函数,来展示他们是何时以及如何被调用的。
class A():
def __getitem__(self, key):
print('__getitem__ is called')
print('key={}'.format(key))
def __setitem__(self, key, val):
print('__setitem__ is called')
print('key={}, val={}'.format(key, val))
如上所示,__getitem__
接受一个参数key
,Python实际上将[]
中的索引值作为该参数传入,在A
的实现中,__getitem__
在被调用时将打印提示信息以及key
的值;__setitem__
接受两个参数key
和val
,Python实际上将[]
中的索引值作为key
传入,将要赋的值作为val
传入,在A
的实现中,__setitem__
在被调用时将打印提示信息以及key
和val
的值。下面是例子:
>>> mySlice = A()
>>> mySlice[3]
__getitem__ is called
key=3
>>> mySlice[5] = 7
__setitem__ is called
key=5, val=7
这是我们跨出的第一步:索引操作本身是怎样运作的。
切片在__getitem__
和__setitem__
中的行为
当[]
中是切片语法,例如[start:stop]
,[start:stop:step]
等时,上述两个魔术方法是怎样运作的呢?Python通过语法解析,实际上传入了一个内置类型slice
。
>>> mySlice[0:3]
__getitem__ is called
key=slice(0, 3, None)
>>> mySlice[:5]
__getitem__ is called
key=slice(None, 5, None)
>>> mySlice[:100:5]
__getitem__ is called
key=slice(None, 100, 5)
可以看到,python实际上向__getitem__
传入了一个slice
类的对象,这个类有start
, stop
, step
三个属性,缺省值都是None
。
事实上,切片同样可以进行赋值:
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[:5] = [0, 0, 0, 0, 0]
>>> l
[0, 0, 0, 0, 0, 5, 6, 7, 8, 9]
此时__setitem__
传入的同样是一个slice
类对象:
>>> mySlice[0:3] = [1, 2, 3]
__setitem__ is called
key=slice(0, 3, None), val=[1, 2, 3]
下面我们详细介绍slice
类是什么。
slice
类和indices
方法
slice
是Python的一个内置类,其具体实现应该参看Cython源码,这里我们只介绍它的接口。Python的内置函数dir()
可用于查看对象的属性、方法列表。
>>> dir(slice)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'indices', 'start', 'step', 'stop']
可以看到,除了被双下划线包围的魔术方法外,slice
类只有四个成员,其中start
, stop
, step
是成员变量,indices
类是成员函数,根据官方文档:
length and computes information about the slice that the slice object would describe if applied to a sequence of length items. It returns a tuple of three integers; respectively these are the start and stop indices and the step
indices
接受一个整数参数length
,返回一个整数三元组,分别代表start
, stop
, step
. 但与原来slice
中的三个参数不同的是:
- 返回的一定是具体整数值,不会再有缺省值
None
。 - 返回的整数范围是为参数
length
“定制”,即一定是[0, length]
范围的有效索引。
下面的代码最直观地说明了indices
函数的作用:
>>> a = slice(5, 10, 2)
>>> b = a.indices(20)
>>> b
(5, 10, 2)
>>> list(range(*b))
[5, 7, 9]
indices
所返回的三个值是为其参数20
量身定制的,即假设用[5:10:2]
去对一个长度为20
的列表进行切片,其真正的start
, stop
, step
分别是多少。这为我们省去了自己编写slice
的扩展范围索引和缺省值规则的麻烦。并且,这三个值恰好可作为内置函数range()
的参数,得到索引下标的列表。
另外,上述代码中我们用到了Python的内置函数slice()
,顾名思义,就是用来返回一个slice
类的。
自定义支持索引的类
我们之前已经定义过一个重载了__getitem__
和__setitem__
的类,以支持[]
操作符索引,并打印出相关信息,这里我们进一步,判断是否是切片操作,如果是,打印出实际被索引的下标列表。
class A():
def __init__(self):
self.data = 'Python'
def __getitem__(self, key):
print('__getitem__ is called')
if isinstance(key, slice):
start, stop, step = key.indices(len(self.data))
index_list = list(range(start, stop, step))
print('keys={}'.format(index_list))
else:
print('key={}'.format(key))
def __setitem__(self, key, val):
print('__setitem__ is called')
if isinstance(key, slice):
start, stop, step = key.indices(len(self.data))
index_list = list(range(start, stop, step))
print('keys={}, vals={}'.format(index_list, val))
else:
print('key={}, vals={}'.format(key, val))
假设所定义的类内部被索引的数据是一个字符串Python
,我们先判断传入的key
类型,如果是slice
类,则利用其indices
方法得到实际的start
, stop
, step
,进而利用range
得到实际的下标列表。
>>> mySlice = A()
>>> mySlice[3]
__getitem__ is called
key=3
>>> mySlice[5] = 7
__setitem__ is called
key=5, vals=7
>>> mySlice[:3]
__getitem__ is called
keys=[0, 1, 2]
>>> mySlice[4:] = ['a', 'b']
__setitem__ is called
keys=[4, 5], vals=['a', 'b']
>>> mySlice[::-1]
__getitem__ is called
keys=[5, 4, 3, 2, 1, 0]
总结
本文介绍了Python进行索引和切片的原理,主要是利用两个魔术方法__getitem__
和__setitem__
,以及内置类型slice
及其方法indices
。