python中的数据类型(list,tuple,dict,set,None)

该系列文章:

1.背景知识

1.1.简化版python解释器如何运行源代码

python解释器主要包含了两大部分,一个部分相当于编译器,另一个部分相当于虚拟机。python解释器的编译器部分首先把程序编译成中间码(字节码),再由python解释器里的虚拟机部分(Python Virtual Machine (PVM))运行字节码。

1.2.简化版对象(object)与类(class)

我们知道,给函数提供输入(input),则函数会处理输入(input),返回结果(output)或者不返回。程序就是解决问题的一系列步骤,这被称为面向过程(Procedure Oriented) 的编程方式。后来,编程语言中出现了一种 面向对象(Object Orientend) 的思想,简单来说,对象借鉴了现实世界由一个个客体组成的概念,用一个个对象间的互动来组织起程序,跟现实世界的客体类似,对象有自己的特征(对象里的各种值),对象也有自己能够做到的事(通过对象里的各种方法)。对象里的各种值被叫做对象的字段(field) ,对象里的各种方法被叫做对象的方法(method) ,对象的字段跟方法统称为对象的属性(attribute)

对象的字段类似于编程语言里的普通变量,所不同的是对象的字段是对象独有的。如果一个对象叫a,a有个属性是b,我们如何访问b呢?答案是通过a.b这种形式的写法访问。a.b的意思就是a的属性b

对象的方法类似于编程语言中的函数,所不同的是对象的方法是对象独有的,如果一个对象叫cc有个方法是d,我们如何使用(调用)d呢?答案是通过c.d()这种形式的写法来是使用,d方法可以带参数,形如这样:c.d(a)c.d()的意思就是使用(调用)c的方法d

如何实现对象呢?有一种方法就是通过类(class)来实现。对象对应于现实中一个个具体的客体,这些客体各不相同,但是很明显,有一些客体是可以归到同一个阵营里去的,比如所有的人,所有的苹果,所有的猫,这里的人、苹果、猫是抽象的一般概念,程序中的类(class) 就是基于像这样的一般概念而抽象出来的某一类客体的模板,可以是人的类,苹果的类,猫的类。从类(模板)中可以构造出这一类客体的对象。从类到对象,相当于从蓝图中实现了一个对象,所以可以说某对象是某个类的一个实例(实现了的例子)。反过来,某个类规定了将要实现的对象的该有的属性跟方法,跟别的类实现的对象有了区别,所以对象的类型(type) 就是它所承袭的类。

1.3.简化版调用栈(call stack),堆(heap)

内存(memory):是指计算中的随机存取内存(random access memory (RAM))。可以认为,内存是一张很大的表,其中的每个表格(1个字节)有两个属性:地址和值。地址表示了某个表格在内存这张大表中的位置,因此我们可以通过地址找到该表格;一旦找到该表格,就可以对该表格的值进行擦除并重写的操作,或者只是读取该表格的值。具体的细节我们可以不用去考虑,我们需要更加抽象的思考地址和值,任意大小的一块内存都可以有一个(标识)地址来让我们在整个内存中找到它,该内存块中能存储复杂的值。

程序要运行时,操作系统就会给它分配一块可用的内存,或者由某高级语言虚拟机提供运行时环境。在该内存空间(运行时环境)里,首先会载入程序本身(机器码或者字节码),接下来会载入整个程序可以用的东西(全局(类)变量,模块等),除此之外的内存会划分为两种,一种是程序运行时的调用栈(call stack),另一种则是堆(heap)。在这里,内存并非计算机中真实的物理内存,而是由操作系统通过软硬件结合的一种技术分配的一块虚拟内存,该虚拟内存中的虚拟地址跟计算机中真实的物理内存地址之间有着映射的关系。在这样的虚拟内存空间里,地址是连续的,也就是说程序运行在某一块特定的虚拟内存中,可以想象成一个长块儿。

12915007-2b206c4073dab5e2
程序运行的内存块

当程序运行时,主函数(main函数)或者首先被执行的函数(方法)会被放入到调用栈中,因为调用栈中只有这一帧,所以它处在调用栈的顶层,一旦处在调用栈的顶层,就会被激活,被激活的帧(frame)得到程序的执行权,该帧中的代码被一步步执行,我们把这一帧姑且叫第0帧吧。第0帧中调用另一个函数(方法)时,被调用函数(方法)的帧被放入到调用栈的最顶层,我们把这一帧叫第1帧,现在,被激活的帧就变成了第1帧,程序的执行权转移到第1帧中,它首先会把被调用时传进来的参数(如果有)存储,接着就声明和初始化局部(实例)变量,操作变量……当第1帧调用另一个函数(方法)时,被调用函数(方法)的帧被放入到调用栈的最顶层,我们把这一帧叫第2帧,如前所述,第2帧被激活,得到程序执行权,当第2帧执行结束,返回值(如果有),这时候第2帧的内存被销毁,包括其中的局部(实例)变量、参数等等,第2帧在调用栈中不在存在,这时候,第1帧成为调用栈的顶层,程序的执行权又重新回到第1帧第1帧继续执行剩余的代码,当第1帧执行结束,返回值(如果有),第1帧被销毁,调用栈中最顶层的帧重新变成第0帧,如果第0帧执行结束,则调用栈空白……这其中,被调用函数(方法)放入调用栈最顶层被称为推入(push),而在调用栈中被销毁则被称为弹出(pop)

12915007-b8b0ba42dba687c2
image

调用栈(call stack)中的函数(方法)里存储着程序运行过程中该如何做的行为(动作、指令)和需要处理的局部(实例)变量,这些变量实际上是怎么被存储的呢?不同的编程语言有不同的存储方式,实际上有两种最为主流的做法。

我们从上篇文章《python入门,编程基础概念介绍(变量,条件,函数,循环)》已经知道,变量的值有各种各样的类型。像简单的数字,布尔值,字符串……,对于这些比较简单的值,一种方法就是把它们看做原始(内置)类型,直接在调用栈内存中分配出一块能容纳该类型值的内存,把该值存进去,通过变量名来使用该值。这时候变量仿佛就是这些值了。这种存储类型又被称为值类型(value type)。需要注意的是,静态类型语言中的变量是有类型的,一旦声明成某种类型的,则只能存储该类型的值;动态类型语言中的变量不存在类型,可以存储任何类型的值。

12915007-b93be1f8ec647649
image

对于简单的数字,布尔值,字符串……这些类型的值,另一种方法就是把它们都存在堆(heap)内存空间里。在调用栈中的变量里,实际存储的是堆(heap)内存空间里的某一块内存的地址。当一个变量被声明并且被赋值的时候发生了什么呢?实际上发生的是首先在堆(heap)内存空间中分配出一块能容纳该类型值的内存,把该值存进去;然后把该内存的地址绑定到变量上。这时候我们就说该变量引用(reference) 了该值。这种存储类型被称为引用类型(reference type)。有时候也能够看到这种说法:变量里存储的是值的引用,可以看到,值的内存地址跟值的引用的说法可以互换。

12915007-dbe199efb28d2a6e
image

前面我们简略介绍了对象,对象可以看做是种复杂类型的值,你可以想想,对象里的各种属性和各种方法。那么当我们有一个对象的时候,或者有某个复杂类型的值的时候,不同的编程语言都不约而同的选择了堆(heap)内存空间。这是为什么呢?因为调用栈中的值需要的内存块的大小一旦确定就不能改变,而堆则没有这个限制,有可能一个对象占用很大的一块内存,并且在程序运行过程中动态的变大或者变小。

2.数据类型

变量用来存储值,值可以被改变。正如我们之前好几次说到的那样,值的类型各种各样,但都携带了某种信息,并且这种信息可以被操作(处理),我们可以认为,它们都是数据(data)

数据的类型(type) 是各种各样的,在1.3小节中我们已经看到非常普遍的数据类型:数字,布尔值,字符串。对这些简单的数据类型,我们已经知道普遍存在的两种存储方式,那么在python中呢?在python中,一切数据类型都是对象,所以,我们存储任何数据类型的方式都是通过引用(reference)

在python中有个内置函数type(),可以通过它来检查一个对象的类型:

>>> type(1)
<class 'int'>
>>> type(1.1)
<class 'float'>
>>> type("int")
<class 'str'>
>>> type(True)
<class 'bool'>
>>> 

上面交互命令行中出现了四种简单的数据类型,分别是int,float,str,bool。如果两个变量同时引用了相同的对象,会发生什么呢?在python中有个内置的函数id(),这个函数返回对象的一个id,可以把该id看做该对象的引用(内存地址)。让我们看看两个变量同时引用相同的对象时发生了什么:

>>> a=1
>>> b=1
>>> id(a)
255681632
>>> id(b)
255681632
>>> 

可以看到,两个变量指向了同一个对象,那么当我们改变了其中某个变量引用的对象,是不是另一个变量引用的对象也同时改变了呢?从理论上讲会改变,对吧,因为是同一个对象。我们来看看:

>>> a=1
>>> b=1
>>> id(a)
255681632
>>> id(b)
255681632
>>> b=2 #改变b的值
>>> a
1
>>>

事实并不像我们认为的那样,另一个变量的对象并没有改变。这是因为,在python中,上面出现的这四种简单的数据类型都是不可变(immutable) 对象。举个数字的例子来理解这种不可变性:数字1是个对象,是个独立的客体,看起来这个对象简单到不能再简单,我们无法改变它,如果将变量的引用从数字1改变成数字2,那么,已经是另一个对象了,相当于是更新了变量的引用。

2.1.列表(list)

直到现在,我们处理过的数据类型都很简单,但是当我们想要存储更为复杂多变的数据,用我们目前知道的数据类型来存储就会越来越繁琐,容易出错了。比如我们想要在程序里一次性处理包含6个数字的内容(7,9,11,36,74,12),难道我们要给每个数字都提供一个变量名,并一一存储吗?我们还有更多选择,在python中,它提供了一种叫列表(list) 的数据类型,这种数据类型像个容器,可以装进去其他各种数据类型,甚至也可以将其他列表(list)嵌套进去。我们要把7,9,11,36,74,12放进一个列表(list)中,可以这么做:

#把这几个数字放进列表,并赋值给变量x
x=[7,9,11,36,74,12]  
#可以定义一个空列表
y=[]
#使用内置函数list()创建列表
a=list("abc")
b=list()

现在把数字放进一个列表了,那么我们怎么拿出某个数字呢?跟字符串类似,列表中的元素组成了一串,每个元素在列表中都是有顺序的。每个元素都被分配了一个位置索引(index)。我们可以通过特定的索引来访问对应的元素,根据惯例,索引总是从0开始。

x=[7,9,11,36,74,12]
z=len(x)
#从列表x中循环取出数字,并打印到命令行
for index in range(z):
    print(str(x[index]))

列表中的元素的值能不能改变呢?能不能增加或者减少元素?答案是可以的。我们说python中一切数据类型都是对象,列表也是对象,所有它有自己的专属方法。可以通过列表的append()方法来增加元素,增加的元素被追加到列表结尾。删除一个元素呢,可以通过del语句来删除,也可以通过列表的remove()方法或者pop()方法来删除。这里要注意,remove方法通过值来删除,pop方法通过索引来删除,并且remove方法没有返回值,而pop方法则返回要删除的值。如果我们不只想删除某一个元素,而是想清空整个列表,则可以使用列表的clear()方法。看下面:

a=['change me',['first',1,2],2019,True]
#以下注释都根据惯例,从0开始计数
#改变第0个元素的值
a[0]='changed'
print('列表a: {}'.format(a))
#改变第1个元素(list)中的第0个元素
a[1][0]=0
print('列表a: {}'.format(a))
#增加一个元素
a.append(2019)
print('列表a: {}'.format(a))
#删除一个元素,通过del语句
del a[0]
print('列表a: {}'.format(a))
#删除一个元素,通过remove方法
a.remove(True)
print('列表a: {}'.format(a))
#删除一个元素,通过pop方法,并将返回值赋给变量b
b=a.pop(2)
print("被删除的元素是{}".format(b))
print('列表a: {}'.format(a))
#清空列表
a.clear()
print('列表a: {}'.format(a))

以上代码中,用到了str的format()方法,这种方法通过在字符串保留一对{},来让format方法中的参数填充其中,参数可以是任意多个(需要与前面{}的数量一致),可以是各种数据类型。这个方法大大简化了我们想把其他数据类型加入到字符串的过程。运行结果如下:

列表a: ['changed', ['first', 1, 2], 2019, True]
列表a: ['changed', [0, 1, 2], 2019, True]
列表a: ['changed', [0, 1, 2], 2019, True, 2019]
列表a: [[0, 1, 2], 2019, True, 2019]
列表a: [[0, 1, 2], 2019, 2019]
被删除的元素是2019
列表a: [[0, 1, 2], 2019]
列表a: []

既然python中的数据类型都是对象,那么我们如何判断两个对象是否是同一个呢。答案用is操作符。比如我们想要判断a对象与b对象是否同一,则可以通过a is b来判断,其结果是布尔值。

我们看到,列表里的元素可以比一个多,字符串里的字符也可以比一个多,所以我们给这种其中的元素或者属性可以比一个多的对象运用in操作符(查看某元素是否属于该对象,这被称为成员检测),来提供给for循环语句或者别的语句,让这些语句逐个访问其中的元素或者属性,这个行为可以称为迭代。前面说到的内置函数list()可接受的参数就是可以被迭代的对象。上篇文章中讲到的for...in循环就是迭代的一个例子。

#in操作符的例子
>>> x='hello'
>>> 'h' in x
True
>>>

在讲列表(list)之前,我们说到了不可变对象。那么列表的情况又是如何呢?我们把那个例子中的数据类型换成列表来看看:

a=['change me',['first',1,2],2019,True]
#把同样的引用赋值给变量b
b=a
#看看引用是否相同,是否对象是同一个
print(a is b)
#通过变量b改变列表
b[0]='changed'
#现在打印出变量a,看是否有变化
print(a)

以上代码运行结果如下:

True
['changed', ['first', 1, 2], 2019, True]

从运行结果来看,列表是可以改变的,所以是可变(mutable)对象。现在我们已经把可变对象不可变对象的行为差不多摸清楚了。讲不可变对象的时候我们讲解了一个数字对象的例子来帮助理解不可变对象,现在我们来通过字符串的例子来进一步说明,假如我们把一个字符串“string”的引用赋值给变量a,那么我们是不能对其中的字符来进行修改的,如下图:

>>> a="string"
>>> a[0]="a"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

但是对于变量a来说,它是完全可以更新的,可以把另一个对象的引用重新赋值给它:

>>> a="string"
>>> a[0]="a"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment
>>> a="hello"
>>> a
'hello'
>>>

事实上不可变对象无论是看着无法改变的,还是直觉上感觉可以改变的(比如字符串),都是python中规定好了的。所以不必纠结于直觉,我们要做的是记住哪些数据类型是可变的,而哪些数据类型又是不可变的。下面将介绍一种新的数据类型,它跟列表类似,最大的不同是,它是不可变的。它叫元组(tuple)。

2.2.元组(tuple)

元组(tuple) 中可以装进去其他各种数据类型,甚至也可以将其他元组(tuple)嵌套进去。元组(tuple)的元素有索引(index),可以通过索引访问到。

#空元组
tuple1=()
#一个元素的tuple,写法特殊一点。如果在元素后面不加逗号
#则python解释器会当成元素的数据类型,而不认为是个元组
tuple2=(2,)
#另一个元组,它里面有个列表作为元素
tuple3=(2,"lalala",True,45,[4,5])
#使用内置函数tuple()创建元组,该函参数接受可迭代的对象
a=tuple()
b=tuple([1,2,3])

因为元组是不可变对象,所以它其中的元素是不能修改的。元素也不能增删。但整个元组是可以通过del语句删除的。但是当元组中的元素是可变对象时,比如元组中的某个元素是列表,那该列表能不能修改?因为该列表是可变对象,所以该列表中的元素是可以自然增删修改的,但该列表因为是不可变对象中的元素,所以无法删除,如下:

>>> d=(2, 'lalala', True, 45, [5, 5])
>>> d[4][0]=88
>>> print(d)
(2, 'lalala', True, 45, [88, 5])
>>> del d[4]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object doesn't support item deletion
>>> del d[4][0]
>>> print(d)
(2, 'lalala', True, 45, [5])
>>>

那么情况反过来呢?当可变对象中的元素是不可变对象时,比如当列表中的某元素是个元组,该元组是否能够被修改?因为该元组是不可变对象,所以该元组中的元素不能被修改,但是该元组本身是可变对象的元素,所以可以被删除,如下:

>>> e=['he!',0,(4,5,'last')]
>>> e[2][0]=5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> del e[2][0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object doesn't support item deletion
>>> del e[2]
>>> print(e)
['he!', 0]
>>>

2.3.序列(sequence)

一个数据集合里面的元素根据放入的先后顺序排成一串,这种形式的数据可以被称为序列。字符串,列表,元组都有着类似序列的结构,所以也就有类似的行为,它们都可以被索引,都可以被迭代,都能够通过索引访问到其中的元素;不仅仅是能够访问到其中的某一个元素,还能访问到其中的某几个元素,这种同时访问到好几个元素的行为,称为切片(也可以把被访问的这些元素称为数据的切片),因为这样做相当于从整个数据序列中切下来一部分,如下:

x='string'
y=[1,2,3,4,5,6]
z=(7,8,9,10,11,12)
#以下注释都根据惯例,从0开始计数
#打印出字符串的第3个字符
print(x[3])
#打印出字符串的倒数第2个字符
print(x[-2])
#打印出字符串的一部分,从第1个开始,到第3个字符,不包括第4个字符
print(x[1:4])
#打印出列表的倒数第3个元素
print(str(y[-3]))
#打印出列表的一部分,从第1个元素开始,到第4个元素,不包括第5个元素
print(str(y[1:5]))
#打印出列表的一部分,从第1个元素开始,直到结束
print(str(y[1:]))
#打印出元组的倒数第4个元素
print(str(z[-4]))
#打印出元组的一部分,从第1个元素开始,到第4个元素,不包括第5个元素
print(str(z[1:5]))
#打印出列表的一部分,从第1个元素开始,直到结束
print(str(z[1:]))

运行如下:

i
n
tri
4
[2, 3, 4, 5]
[2, 3, 4, 5, 6]
9
(8, 9, 10, 11)
(8, 9, 10, 11, 12)

切片中可以设置步长,就是说可以设置隔着几个元素的方式访问数据的一部分,默认步长为1,如下:

>>> a=(1,2,3,4,5,6,7,8,9,10)
>>> a[::1]
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
>>> a[::2]
(1, 3, 5, 7, 9)
>>>

当我们把数据的切片赋值给变量时会发生什么?一般来说,数据被切片部分会被复制一份副本,然后把副本的引用赋值给变量。但是,有个特殊情况,就是当数据的被切片部分是全部数据本身时,那该数据要看是可变对象还是不可变对象了。列表(可变对象)则会复制一份副本,然后把副本的引用赋值给变量,而字符串、元组(不可变对象)则会直接将该数据本身的引用直接赋值给变量,如下:

>>> a=[1,2,3,4]
>>> b=a[:]  #省略掉开头跟结尾index则切片结果是数据本身
>>> id(a)
140046385947272
>>> id(b)
140046386409992
>>> c=(1,2,[2,3])
>>> d=c[:]
>>> id(c)
140046385743768
>>> id(d)
140046385743768
>>> 

2.4.字典(dict)

前面我们介绍了序列,我们知道序列的索引隐含在数据类型里,不需要我们明确去定义,因为都是0,1,2,3,4……有顺序的排列下去,当可变对象里的元素被删除,保留下来的元素的索引会改变,因为序列里的索引永远都是顺序排列的数字,这些数字没有跟元素绑定,而只是允许我们通过索引的数字来访问该位置的元素。那如果我们想要自定义索引,用字符串,数字等来做索引,并希望这些索引能够跟特定的元素绑定在一起,我们该怎么办?更简单的说,就是我们希望索引是独特并且容易记住,有语义,我们该怎么办?python给我们提供了一种数据类型字典(dict),可以胜任这样的要求。我们把这样的索引可以看作是键 (keys),它与要存储的值绑定在一起,叫做键值对。

字典里存储键值对儿,索引独特,所以是没有顺序的。顺序已经无关紧要,我们只需要记住独特的键就行了。如何创建字典,如何存储键值对,看下面的例子:

>>> a={}    #创建一个空字典
>>> type(a)
<class 'dict'>
>>> b={"id":4667,"name":"john"}  #创建一个有两对键值对的字典
>>> b["id"]  #通过键"id"来获得对应的值
4667
>>> c=dict(id=4555,name="li")  #通过内置的dict函数创建字典
>>> print(c)
{'id': 4555, 'name': 'li'}
>>> 

字典是可变对象。为了通过键来查找值,就需要字典键(keys)保持唯一性,如果键用了可变对象来存储,会出现不可控因素,举个例子,假如两个键都是由列表来存储,则一旦把这两个列表修改相同,那么查找这两个键所对应的值时就会出现矛盾,所以键一定要用不可变对象来存储,包括数字,布尔值,字符串,元组(需要元组中的元素不包含可变对象)。又因为字典是可变对象,所以字典中键值对里的值是可以改变的。如下:

>>> c={("id",):46678,"name":"john_ss"}
>>> c[("id",)]
46678
>>> d=8
>>> e={d:8,"d":"8"}
>>> e[d]
8
>>> e["d"]
'8'
>>> e[d]=123
>>> print(e)
{8: 123, 'd': '8'}
>>> 

在字典中,也可以用字典的get()方法通过键获取值。如果要给字典里增加键值对,可以直接用方括号(下标)的方式增加,例如dict["key_word"]="some values"。可以用字典的pop()方法来删除键值对,要注意的是,pop()方法在删除键值对的同时会返回要移除的键值对,把返回值赋给变量,变量就会得到被移除的键值对:pair=dict.pop("id")。如果我们不只想删除某一个键值对,而是想清空整个字典,则可以使用字典的clear()方法。如果看下面的例子:

>>> a={1:1,2:2,3:3}
>>> a.get(1)
1
>>> a["appended"]="ok,then!"
>>> print(a)
{1: 1, 2: 2, 3: 3, 'appended': 'ok,then!'}
>>> a.pop(2)
2
>>> print(a)
{1: 1, 3: 3, 'appended': 'ok,then!'}
>>> a.clear()
>>> print(a)
{}
>>> 

当我们尝试像迭代序列那样直接迭代字典时,交互命令行显示结果如下,明显只迭代了键值对中的键(key):

>>> a={1: 1, 2: 2, 3: 3, 'appended': 'ok,then!'}
>>> for item in a:
...     print(item)
... 
1
2
3
appended
>>> 

这时候字典中有两个内置方法可以协助来完成迭代,分别是items()跟keys()。items()返回字典中无序的键值对,keys()返回字典中无序的键(keys)。如下:

>>> a={1: 1, 2: 2, 3: 3, 'appended': 'ok,then!'}
>>> for item in a.items():
...     print(item)
... 
(1, 1)   #可以看到把键值对装进了元组
(2, 2)
(3, 3)
('appended', 'ok,then!')
>>> for key in a.keys():
...     print(key)
... 
1
2
3
appended
>>> 

2.5.集合(set),frozenset

前面我们介绍了字典,跟字典类似,在python里还有一种无序的数据类型:集合(set)。基本上,这儿的集合跟数学上的集合的概念是一样的。其中的元素是无序的,并且每个元素都是唯一不可重复的。创建集合跟创建字典的符号一样,都是花括号“{}”,所以当创建空集合时,会跟创建空字典的符号有冲突,所以python里“{}”表示创建空字典,而创建空集合只能用内置函数set()来创建,如下:

>>> a={1,4,7,"string",("lalala",2,3,4)}  #创建集合
>>> print(a)
{1, 4, 7, 'string', ('lalala', 2, 3, 4)}
>>> b={}   #空字典
>>> type(b)
<class 'dict'>
>>> c=set()  #空集合
>>> type(c)
<class 'set'>
>>> 

集合是可变对象,但是它的元素则要求一定是不可变对象,根据集合的定义,每个元素都是唯一不可重复,那么一旦元素是可变对象,则有了可重复的可能,比如元素中有两个列表的话,通过一些操作有可能会让这两个列表成为一样的。如下:

>>> d={1,2,[1,2]}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> 

既然集合是可变对象,我们就可以通过add()方法增加元素,通过remove()、discard()方法、pop()方法删除元素,前提是所有元素都要是不同的,通过pop()方法删除元素时,不带参数,并且集合无序,所以无法预知哪个元素被丢掉,但pop()方法会返回被丢的元素:

>>> a={1,4,7,("string",55),"hello"}
>>> a.add("hello,world!") #add方法新增元素
>>> print(a)
{1, 4, 7, 'hello,world!', ('string', 55), 'hello'}
>>> a.remove(7) #remove方法删除元素
>>> print(a)
{1, 4, 'hello,world!', ('string', 55), 'hello'}
>>> a.discard(1) #discard方法删除元素
>>> print(a)
{4, 'hello,world!', ('string', 55), 'hello'}
>>> a.pop()   #通过pop方法删除随机某个元素并返回被删除的元素,可以使用变量来存储
4
>>> print(a)
{'hello,world!', ('string', 55), 'hello'}

可以使用update方法来增加多个元素,使用clear()方法来清空整个集合,接上面的例子:

>>> a.update([1,2,3,4])  #用列表来更新集合
>>> a.update((5,6,7))    #用元组来更新集合
>>> print(a)
{1, 2, 3, 4, 5, 6, 7, 'hello,world!', ('string', 55), 'hello'}
>>> a.clear()
>>> print(a)
set()   #代表空集合

集合可以实现数学上的并集,交集,差集,对称差,有两种方式:操作符跟方法。并集是两个集合中所有的元素组成的新集合,交集是两个集合中都存在的元素组成的新集合。差集是两个集合中在某个集合中存在,并且在另一个集合中不存在的元素组成的新集合。对称差集是两个集合中所有元素,除去在交集中的元素,由这样的元素组成的新集合。下面这个表显示python的操作方法:

操作名称 操作符 集合(比如A,B)内置方法
| A.union(B)
& A.intersection(B)
- A.difference(B)
对称差 ^ A.symmetric_difference(B)

通过例子来看看:

>>> A={1,2,4,6,7}
>>> B={1,3,4,5,8}
>>> print(A | B)
{1, 2, 3, 4, 5, 6, 7, 8}
>>> print(A.union(B))
{1, 2, 3, 4, 5, 6, 7, 8}
>>> print(A & B)
{1, 4}
>>> print(A.intersection(B))
{1, 4}
>>> print(A - B)
{2, 6, 7}
>>> print(A.difference(B))
{2, 6, 7}
>>> print(B - A)
{8, 3, 5}
>>> print(B.difference(A))
{8, 3, 5}
>>> print(A ^ B)
{2, 3, 5, 6, 7, 8}
>>> print(A.symmetric_difference(B))
{2, 3, 5, 6, 7, 8}
>>> 

从上面的例子可以看出来,两个集合的并集,交集,对称差集都是可以互换的,而差集不是,差集需要区分A-BB-A

在python中,还可以创建类似于tuple这样的不可变对象的set,那就是frozenset,frozen在英文中是冻结了的意思,顾名思义,frozenset就是冻结的集合。frozenset不能增加或者更新元素,删除或者清除元素,类似于只读文件。并集,交集,差集,对称差的操作对于frozenset同样适用。如下:

>>> a=frozenset([1,2,3])   #创建一个frozenset
>>> print(a)
frozenset({1, 2, 3})
>>> a.add(4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'frozenset' object has no attribute 'add'
>>> a.clear()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'frozenset' object has no attribute 'clear'
>>> b=frozenset((4,5,3))
>>> a.union(b)
frozenset({1, 2, 3, 4, 5})
>>> 

2.6.简化内置的help函数,获取python中各数据类型内置的方法及其简要说明

在python中,有个help()内置函数可以获取关于对象的说明文档。但说明文档中有关于该对象实现的细节,当我们只是想知道各数据类型的常用内置方法及其使用时,就会变得很不方便。我写了一个很短的程序,运行该程序可以将help函数的输出简化(删去实现对象的相关细节)并存入当前目录新建的一个文本文件中,并且该程序还有将方法的解释翻译成中文的可选功能。下面是该程序内容:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import re
#如果想将解释翻译成中文,把下面一行的注释取消
#from translate import Translator

def command_line_arguments(usge,len_of_arguments):
    if len(sys.argv)!=len_of_arguments+1:
        print(usge)
    return sys.argv

def stdout_to_file(dir,func,func_arguments):
    #把函数的标准输出存入文件中
    default_stdout = sys.stdout
    with open(dir,"w") as w:
        sys.stdout=w
        func(func_arguments)
    sys.stdout=default_stdout
    
def simpler_help(object):
    
    stdout_to_file("simpler_help_{}.txt".format(object),help,object)
    
    #把文件中的内容分行存入内存
    with open("simpler_help_{}.txt".format(object),"r") as r:
        lines=r.readlines()
    
    #将内存中的内容修改后写入同名文件,覆盖原有文件
    #mark1,为了将带有“__”的特殊函数不要写入,并将其下面的解释一并略过
    mark1=False
    #mark2,为了将“---”下面的内容略过
    mark2=False
    with open("simpler_help_{}.txt".format(object),"w") as w:
        for line in lines:
            
            #将带有“__”的特殊方法不要写入,并将其下面的解释一并略过
            if "__" in line:
                mark1=True
                continue  
            elif mark1==True:
                result=re.search("[A-Za-z]+",line)
                if result==None:
                    mark1=False
                    continue
                

            #将“---”下面的内容略过
            elif "---" in line:
                mark2=True
                continue
            elif mark2==True:
                pass
            
            #将行中的self跟“/”替换成空
            else:
                if ("self," in line) or ("/" in line):
                    if ("self," in line) and ("/" in line):
                        w.write(line.replace("self,","").replace("/","").replace(", )"," )").replace(" , "," "))
                    elif ("self," in line) and ("/" not in line):
                        w.write(line.replace("self,",""))
                    elif ("self," not in line) and ("/" in line):
                        w.write(line.replace("/","").replace(", )"," )").replace(" , "," "))          
                else:
                    w.write(line)

#如果想将解释翻译成中文,可以把下面的函数注释取消                
"""
def translation_of_help(object):
    translator= Translator(to_lang="zh")
    with open("simpler_help_{}_zh.txt".format(object),"r") as r:
        lines=r.readlines()
    with open("simpler_help_{}_zh.txt".format(object),"w") as w:
        line_process=""
        count_line=0
        for line in lines:
            if ("(" in line) and (")"in line) and ("." not in line):
                w.write(line)
            else:
                if count_line<=1:
                    w.write(translator.translate(line)+"\n")
                else:
                    line_process=line.replace("|","")
                    w.write(" |        "+translator.translate(line_process)+"\n")
            count_line=count_line+1
"""

len_of_arguments=1
arguments=command_line_arguments('''You need provide a name of object,for example:
python3 simpler_help.py list''',len_of_arguments)
if len(arguments)==len_of_arguments+1:
    simpler_help(arguments[1])
    #如果想将解释翻译成中文,可以把下面一行的注释取消
    #translation_of_help("list")      
    

将上面内容复制粘贴,并命名为simpler_help.py。举个例子,如果想知道列表中的内置方法,则在命令行中通过类似这样的python3 simpler_help.py list命令来运行,特别注意需要在文件名空一格后写上需要查询的数据类型名称。下面是程序创建的简化版的列表的help函数输出,保存在当前目录的simpler_help_list.txt中:

Help on class list in module builtins:

class list(object)
 |  list(iterable=() )
 |  
 |  Built-in mutable sequence.
 |  
 |  If no argument is given, the constructor creates a new empty list.
 |  The argument must be an iterable if specified.
 |  
 |  Methods defined here:
 |  
 |  append( object )
 |      Append object to the end of the list.
 |  
 |  clear( )
 |      Remove all items from list.
 |  
 |  copy( )
 |      Return a shallow copy of the list.
 |  
 |  count( value )
 |      Return number of occurrences of value.
 |  
 |  extend( iterable )
 |      Extend list by appending elements from the iterable.
 |  
 |  index( value, start=0, stop=9223372036854775807 )
 |      Return first index of value.
 |      
 |      Raises ValueError if the value is not present.
 |  
 |  insert( index, object )
 |      Insert object before index.
 |  
 |  pop( index=-1 )
 |      Remove and return item at index (default last).
 |      
 |      Raises IndexError if list is empty or index is out of range.
 |  
 |  remove( value )
 |      Remove first occurrence of value.
 |      
 |      Raises ValueError if the value is not present.
 |  
 |  reverse( )
 |      Reverse *IN PLACE*.
 |  
 |  sort( *, key=None, reverse=False)
 |      Stable sort *IN PLACE*.
 | 
 

通过程序创建的文件,可以大概了解各数据类型都有哪些内置方法,之后可以从网上通过关键词搜索出更为详细的介绍跟用例。另外,如果想将方法的解释翻译成中文,把simpler_help.py中相关内容的注释取消,并且首先需要在命令行中执行pip install translate

2.7.关于None

>>> type(None)
<class 'NoneType'>
>>>

None属于NoneType这个数据类型(对象),是它唯一的值。并且,NoneType对象并不像别的数据类型一样,可以有很多实例,而是从始至终都只能有一个实例。

>>> None==False
False
>>> None==1
False
>>> None==""
False
>>> None==0
False
>>> None=={}
False
>>> None==None
True
>>>

从上面的例子(做逻辑判断)可以看出来,None不等于其他任何值,None只等于None。None一般用来代表数据的缺失,看下面的例子:

def addTwo(a,b):
    if (type(a)==int or type(a)==float) and ((type(b)==int or type(b)==float)):
        return a+b
    else:
        return None

b=addTwo("ss",4)
print(b)

#OUTPUT应该为None,None代表函数没法合理的处理参数,只能返回None。

当我们拿到某个数据,我们想要判断它不是None的时候该如何做呢,这时候应该用is,因为None是数据类型,也是对象,所以我们一般会想要这样做:if an_object is not None:。此处not的位置跟成员检测if an_object not in an_object:的位置不同,需要特别注意。如下:

a=1

if a is not None:
    print("a is not None")
else:
    print("a is None")

#OUTPUT应该为"a is not None"。

有些时候我们会在别人的代码中看到if an_object:这样的写法,这种写法跟上面的判断某数据(对象)是不是None的写法没有什么联系,这种代码暗示了数据(对象)本身在做逻辑判断时会另外呈现出或真或假的布尔值,事实上也的确如此,在各种数据类型中,会有某些特殊的值在逻辑判断的时候布尔值表现为False,其余值表现为True。在python中,规定了一套规则,当我们拿某个数据本身来做逻辑判断的时候,解释器如何解确定其布尔值:

  • 布尔型,False表示False,其他为True
  • 整数和浮点数,0表示False,其他为True
  • 字符串和类字符串类型(包括bytes和unicode),空字符串表示False,其他为True
  • 序列类型(包括tuple,list,dict,set等),空表示False,非空表示True
  • None永远表示False

举个例子,当我们在理论上来说从某函数(方法)返回了一个列表list1,我们用if list1:来判断的时候,则会出现三种情况:list1Nonelist1是空列表,list1是非空列表,这时候list1在第一和第二种情况下表现出的布尔值为False,在第三种情况下表现出的布尔值为True。如下:

list1=[]

while True:
    if list1:
        print("true")
    else:
        if list1 != None:
            print("false,empty list")
            list1=None
        else:
            print("false,None")
            list1=[]

#OUTPUT应该永远在None跟空列表之间徘徊。交替打印“false,empty list”和“false,None”。

欢迎浏览我的个人博客,https://diwugebingren.github.io

12915007-4ff9c8bfe43855a9
欢迎关注我的公众号

转载于:https://www.jianshu.com/p/f1a58e84bc40

猜你喜欢

转载自blog.csdn.net/weixin_33736832/article/details/91319100