xyzpython切片工具 python 切片原理_Python


前言

在上一篇文章《[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__接受两个参数keyval,Python实际上将[]中的索引值作为key传入,将要赋的值作为val传入,在A的实现中,__setitem__在被调用时将打印提示信息以及keyval的值。下面是例子:


>>> 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