作者: Jim Wang 公众号: 巴博萨船长

列表与操作方法

列表list是Python语言中的标准数据类型,进一步讲list是一种可变的序列类型,不可变的有tuple和range(注:Python 3.x中xrange已经不存在了)。可变序列类型可变就是长度可以改变,所含元素数值可以改变,元素数据类型可以改变;序列意指成员有序排列,可以通过索引访问元素。既然是序列类型,就一定可以迭代(for 循环),所以列表是可迭代的对象,请注意可迭代迭代器的差异。

列表的常用操作包括,索引,切片,改变与排序。而这几种操作要属改变最多样。改变包括,一、长度改变,成员的添加(常用方法append,insert和extend)与删除(常用方法pop与remove)。二、数值改变,通过索引访问与重新赋值来实现。

列表操作的延伸思考

由于上一篇文章是与运算符相关,所以在准备文字的时候就思索,作为标准数据类型的列表,能和它组合使用的运算符都有什么呢?今天我就和大家一起总结,共同学习一下。

一般情况下,Python中列表只能与➕操作符组合使用,前提是➕号左右都是序列类型。可能别的教程里面规定➕左右都应该是一样的列表类型。我们今天扩展一下,定义:➕号左右都是序列类型。

  • 当加号➕右侧为列表,或者range()时,代码表现符合正常预期,请参看下面例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
>>> m2 = [1, 2, 3, 4]
>>> m3 = [5, 6, 7, 8]
>>> m2 + m3
[1, 2, 3, 4, 5, 6, 7, 8]
>>> m2 + range(5, 9)
[1, 2, 3, 4, 5, 6, 7, 8]
>>> ss = range(5, 9)
>>> type(ss)
<type 'list'>
>>> m2 + (5, 6, 7, 8)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "tuple") to list

List作为唯一的可变序列类型,而且与加号左侧数据类型一直,它的加法运算正常且容易理解。range, str与tuple是不可变的数据类型,理论上与列表相加时肯定会出错的,但是在Python 2.x 中却没有问题,结果如上。其原因为: Python 2.x 中range返回的是一个列表。而在Python 3.x 中这样写就会引发异常,参考如下代码:

1
2
3
4
5
6
7
8
>>> m2 = [1, 2, 3, 4]
>>> m2 + range(5, 9)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "range") to list
>>> ss = range(5, 9)
>>> type(ss)
<class 'range'>

因为Python 3.x 中返回的是一个真正的range类实例,是一个不可变的序列类型。

  • 当加号➕右侧为其它序列类型,即不可变序列类型,如 tuple 或 str 时,请参看下面例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> m2 = [1, 2, 3, 4]
>>> m2 += "hello"
>>> m2
[1, 2, 3, 4, 'h', 'e', 'l', 'l', 'o']
>>> m2 = [1, 2, 3, 4]
>>> m2 = m2 + "hello"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "str") to list

>>> m2 = [1, 2, 3, 4]
>>> m2 += (5, 6, 7, 8)
>>> m2
[1, 2, 3, 4, 5, 6, 7, 8]
>>> m2 = m2 + (1, 2, 3, 4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "tuple") to list

理论上肯定不行的,毕竟左右两侧的数据类型不同。但是总会有些意外,有些惊喜。虽然在运算符的章节中,我们了解到自加操作符与加法和赋值的组合等效,即 a += 1 等效于 a = a + 1。但是参照上方的例子,我们发现应该在解析运行的时候。两种数据操作方式还是有些不一样的,比如在参与的数据有列表是,自加时,因为列表(等号左侧)为可变对象,原则上仅需要本地对象(这里指m2)进行修改,不需要产生新的对象。那么在修改的时候解释器会讲右侧的对象尝试数据类型转换,上述的例子中讲字符穿转换为字符列表,将tuple转换为列表,进而对本地m2对象进行修改,所以我们就实现了所有序列类型的加法操作,注意非序列的数据类型不行,是不是有点神奇。

进一步延伸思考

上一节,我们通过总结可知:1. 列表的加法操作,并不是全效的加法,因为对数据只能对序列操作,那么如果仅需要添加一个成员的时候,加法操作就不再适用。2. 列表没有减法操作。那么有解决方法吗?了解到Python万物皆对象(必定不单身)。那么如果继承列表基本类,创建自有字类,通过重写方法就是实现Python的全效加法于减法。有想法之后就实现一下,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/env python
#-*- coding: utf-8 -*-

class MyList(list):

def __add__(self, obj):
#Function is called by + command
self.addObj(obj)
return

def __sub__(self, obj):
#Function is called by - command
if isinstance(obj, list) or isinstance(obj, tuple):
#if isinstance(obj, list) or isinstance(obj, tuple) or isinstance(obj, range): # Python 3.x
for o in obj:
return self.__class__([item for item in self if item not in obj])
else:
return self.__class__([item for item in self if item != obj])


def __lshift__(self, obj):
#Function is called by << command
self += obj
return self

def __iadd__(self, obj):
#Function is called by += command
if isinstance(obj, list) or isinstance(obj, tuple):
#if isinstance(obj, list) or isinstance(obj, tuple) or isinstance(obj, range): # Python 3.x
for o in obj:
self.addObj(o)
else:
self.addObj(obj)
return self

def addObj(self, obj):
self.append(obj)

在使用需要注意,由于Python默认的列表类为list,传统的列表赋值方式只能产生list的实例对象,那么如果初始化玩MyList类的实例后,再为实例变量使用传统方式赋值时,那么就会产生新的对象,而且新类的属性方法将丢失。

1
2
3
4
5
6
7
8
9
10
>>> from test import MyList
>>> ml = MyList(["a", "b", "c"])
>>> type(ml)
<class 'test.MyList'>
>>> ml = MyList()
>>> type(ml)
<class 'test.MyList'>
>>> ml = ["a", "b", "c"]
>>> type(ml)
<type 'list'>

实际使用结果如下,因为在加法运算时,我们返回类实例本身,所以列表的值在做加法是会发生变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
>>> ml = MyList(["a", "b", "c"])
>>> ml
['a', 'b', 'c']
>>> ml + ['e', 'f', 'g']
>>> ml
['a', 'b', 'c', ['e', 'f', 'g']]
>>> ml = MyList(["a", "b", "c"])
>>> ml + ['e', 'f', 'g']
>>> ml
['a', 'b', 'c', ['e', 'f', 'g']]
>>> ml = MyList(["a", "b", "c"])
>>> ml += ['e', 'f', 'g']
>>> ml
['a', 'b', 'c', 'e', 'f', 'g']
>>> ml = MyList(["a", "b", "c"])
>>> ml
['a', 'b', 'c']
>>> ml + "e"
>>> ml
['a', 'b', 'c', 'e']
>>> ml + 1
>>> ml
['a', 'b', 'c', 'e', 1]
>>> ml - 1
['a', 'b', 'c', 'e'] # 新的列表对象
>>> ml
['a', 'b', 'c', 'e', 1]
>>> ml - ['c', 1]
['a', 'b', 'e'] # 新的列表对象
>>> ml
['a', 'b', 'c', 'e', 1]
>>> ml << "e"
['a', 'b', 'c', 'e', 1, 'e']
>>> ml << ['d', 'f']
['a', 'b', 'c', 'e', 1, 'e', 'd', 'f']
>>> tt = ml - ['c', 1]
>>> type(tt)
<class 'test.MyList'>

而在做减法运算时,我们创建并返回一个新的类实例,所以减数,即原始列表值不发生改变,请参见上述代码中带注释的内容。有心的同学可能也发现了,我们重写__iadd__(), 实现了我们预期的自加,重写__add__(), 实现了我们预期的加法,重写__sub__(), 实现了我们预期的列表减法运算和重写__lshift__(), 实现了我们预期的列表的左移运算,怎么样是不是也有一点成就感。

小结

在之前的文章中我们介绍了如何动态重写类方法,那么我们的上述的方法能不能也动态地写入基类list中,使其所有实例都有新的方法呢?感兴趣的童鞋可以自己可以试一下。我把结果写出来, 就不再举例分析。是不行的,由于基类是由别的语言实现的属于built-in类型,虽然也是动态的类,但是却因为别的语言限制了访问,所以其__iadd__()方法,在实例中是仅可读的。所以就没办法实现list基类,这些方法的动态绑定和重写。


版权声明:
文章首发于 Jim Wang's blog , 转载文章请务必以超链接形式标明文章出处,作者信息及本版权声明。