📚 文档目录
🎃 类型和对象 - 🎈 程序结构与函数编程 - 🎏 面向对象编程

由于页面中的某些内容嵌套在了多重标签插件下,使得宽度变得过于狭窄,特别是手机端体验会很差,建议在电脑端阅读

该笔记内容可能存在一些错误,如果您能够帮助指正,鄙人将非常感谢

对象的身份与类型

Python中一切皆对象,包括一些常见的基础数据类型,如数字、字符串、列表、字典等,还有用户所自定义的对象(对象也称为类型的“实例”)。对象具有三大特征:

  1. 身份(id),可以理解为在内存中的地址,具有唯一性,可以通过id(obj)查看
  2. 类型(type),对象都是由类实例化产生的,对象所属类型可通过type(obj)obj.__class__查看
  3. 值(value),不同对象具有不同值(体现在“属性”和“方法”的不同上),这也是其存在的现实意义

对象被创建后,其身份和类型就不可改变,根据其值是否可变,可以区分为“可变对象”与“不可变对象”

从身份、类型和值三个不同的角度分别比较对象:

1
2
3
4
5
6
7
def compare(a, b):
if a is b:
pass #a和b是同一个对象,或者说a和b指向同一内存地址
if type(a) is type(b):
pass #a和b属于同一类型
if a == b:
pass #a和b具有相同值

当使用==运算符时,请务必确保a==b中的左值对象a所属类型已重载了__eq__方法(特别提醒用户自定义对象,python中已有的基础对象类型都已经重载了该方法),否则比较是无实际意义的,须知a==b等同于执行a.__eq__(b),而一切对象继承自object类型,object类型的__eq__方法其实比较的是身份(return a is b)。一般来说is用得较少,因为在实际场景中,要比较两个对象是否相等,并不是通过内存地址来判断的,而是应该通过这两个对象的部分属性值,或者全部属性值来对比判断的
注:在描述时,obj泛指的是任意python对象,而object则特指万物始祖之基类

Python中全部8种比较运算:>(对应__gt__()), >=(对应__ge__()), <(对应__lt__()), <=(对应__le__()), ==, !==(对应__ne__()), is, is not。其他就不说了,is用在什么地方?
或许你已知道==None是不推荐使用的,应替换为is None,这基于两点理由:1)None在python中是一个单例对象(None作为对象也具有自己的类型,但是你无法用这个类型创造出另一个None对象),其内存地址唯一,而身份比较的一个好处就是速度快,因为无需对将要比较的对象本身进行检查,is操作符只需对对象所在的内存地址进行比较,因此采用is None速度更快,2)由于==是可重载的,导致某些非None对象也可能“等于”None,从而致使程序出错,譬如当重载定义为def __eq__(self,other): return True

类型的类型

既然一切皆对象,类型本身作为对象又是谁的实例呢?答:type类。上结论:

  • 一切类的类都是type,即一切类(包括type自身)都是type类的实例
  • object是最顶层基类,一切类(除了object自身)追本溯源都继承自object
1
2
3
4
5
print(type(object)) #<class 'type'> #基类object作为对象,它的类型是type
print(type.__base__) #<class 'object'> #type作为类,直接继承自object

print(list.__base__) #<class 'object'> #list、tuple、dict等以及自定义类都继承自object,类型为type
print(type(list)) #<class 'type'>
图1. object类和type类:图中虚线是实例关系、实线是继承关系[3]
isinstance()类型判断

早先在书上偶然看到一行代码isinstance(A,object) #返回True,其中A是一个自定义类,突然很不理解,没回过味来,还在SegmentFault上提问,好蠢/(ㄒoㄒ)/~~

根据帮助文档(help(isinstance))可知:isinstance(obj, class_or_tuple)判断对象obj是否是class_or_tuple中某个类的实例或者其子类的实例。假设A继承自Ba=A(),那么显然isinstance(a,A)返回True,由于BA的父类,所以Ba所属的更大的类的范畴,所以isinstance(a,B)也返回True,即aB的实例。根据上面object类和type类之间的关系很容易推知:一切类(包括objecttype)都是object的实例

1
2
3
4
5
6
7
8
9
10
>>> class A: pass
...
>>> isinstance(A(),object)
True
>>> isinstance(A,object)
True
>>> isinstance(object,object)
True
>>> isinstance(type,object)
True

官方建议当我们需要判断一个对象类型的时候应当使用isinstance(),虽然可以使用type()做“简单”类型判断,“简单”的原因在于,它无法判断一个对象是否属于其基类这一更大的范畴,譬如无法判断一只鹦鹉个体是否属于鸟类(谬论一),而只能判断它是鹦鹉类别

鹦鹉不是鸟:

1
2
3
4
5
6
7
8
9
10
11
>>> class Bird: pass
...
>>> class Parrot(Bird): pass
...
>>> a=Parrot()
>>> type(a) is Parrot
True
>>> type(a) is Bird
False
>>> isinstance(a,Bird)
True

另外在旧式类(完全没必要了解)中type()还具有一个缺陷,譬如两个完全不同的类实例竟然能得出“类型相同”的结果(谬论二),狗和猫是同类?示例如下(python2环境):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> class Dog: pass
...
>>> class Cat: pass
...
>>> d=Dog()
>>> c=Cat()
>>> type(d) is type(c)
True
>>> type(d)
<type 'instance'>
>>> isinstance(d,Cat)
False
>>> class Shiba(Dog): pass
...
>>> type(Shiba())
<type 'instance'>

对于新式类来说,实例化后的对象类型就是该类,这不容置疑,但是旧式类中没有基类object,实例化后的对象类型都是instance

对象的引用与复制

在程序进行像a=b这样的赋值时,就会创建一个对b的新引用,对于像数字和字符串这样的不可变对象,这种赋值实际上创建了b的一个副本,修改b并不影响a,然而,对于可变对象(如列表和字典),行为则完全不同,譬如:

1
2
3
4
5
6
7
8
9
10
11
>>> a = [1,2,3,4]
>>> b = a #b和a指向内存中同一位置的对象
>>> b is a #id(a)==id(b)
True
>>> b[2] = -100 #修改容器b中一个元素
>>> a #此时a仍指向b所指向的对象,所以a的内容也相应改变
[1, 2, -100, 4]
>>> c = d = 12
>>> c = 13
>>> d
12

也就是说当两个变量引用指向同一个可变对象时,修改其中任意一个变量都会影响到另一个,为了避免这种情况,必须创建对象的副本而不是新引用。事实上对于像列表和字典这样的容器对象,存在两种复制操作:浅复制和深复制。浅复制虽然创建了一个新对象(体现在id变化),但它包含的还是对原始对象中所包含的元素(假设这个元素是可变对象的话)的引用,譬如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> a=[1,2,[3,4]] #a中第三个元素是可变对象(所谓可变对象就是对象的值变了,但其在内存中的地址仍保持不变)
>>> b=list(a) #list工厂函数属于浅复制,还有切片操作也是浅复制
>>> b
[1, 2, [3, 4]]
>>> a[0]=8
>>> b
[1, 2, [3, 4]]
>>> a[2][0]=30 #b中第三个元素其实是对a中第三个元素的引用,当a中第三个元素的值发生变化,b中第三个元素也将相应改变
>>> b
[1, 2, [30, 4]]
>>> a[2].append(5)
>>> b
[1, 2, [30, 4, 5]]
>>> a[2]=[11,12] #该赋值使得a中第三个元素指向了内存中另一个区域的(一个列表)对象,从而对列表[30,4,5]的引用计数减1,还剩下b中第三个元素对它的唯一引用
>>> b
[1, 2, [30, 4, 5]]

深复制也将创建一个新对象,且会递归地遍历它所包含的所有对象并创建其副本,需借助标准库中的copy.deepcopy()函数完成此操作

基础数据类型

None类型表示一个空对象,如果一个函数没有显式地返回值,则返回该对象。None经常用作可选参数的默认值,以便让函数检测调用者是否为该参数实际传递了值。None没有任何属性,在布尔表达式中求值时为False

Python内置了5种数字类型(都是不可变对象):布尔型(bool)、整型(int)、长整型(long,目前好像没有long这一类型了,并入int)、浮点型(float)以及复数型(complex)。布尔值包括TrueFalse两个值(参与数值运算时分别映射为数值1和0)。整数表示范围在-2147483648和2147483647(对应0111,1111,1111,1111,1111,1111,1111,1111,其最高位为符号位)之间的所有整数。浮点型是用机器上浮点数的本机双精度(64bit)表示的,提供大约17位数的精度和范围从-308到308的指数。复数使用一对浮点数表示,复数的实部和虚部分别使用.real.imag访问,.conjugate()用于计算复共轭(a+bj的共轭是a-bj)

1
2
3
4
5
6
7
8
9
10
>>> d={}
>>> d[True] = "JavaScript"
>>> d[1] = "Ruby" #根据字典的原理,首先计算键1的哈希,于是找到了键True的存储位置,又发现True==1,于是认为这两个键是同一对象,于是直接将新值'Ruby'替换旧值'JavaScript'完事儿,于是现在字典中的数据变成了{True:'Ruby'}
>>> d[1.0] = "Python"
>>> d.keys()
dict_keys([True])
>>> d[True]
'Python'
>>> d[1.0]
'Python'

布尔类型int类型的子类(如isinstance(True,int)True),其满足True == 1 == 1.0 and False == 0 == 0.0True的条件,换句话说,True11.0这三个对象具有相同的哈希(同样False00.0也是如此),在将其用作字典的键时,将映射到同一个值

进制转换

先说一下int(x, base)方法,其根据给定的进制base(取值为0、2~36,为什么最高是36?因为26个英文字母加上10个数字总计36,譬如有一个16进制数:e1,其十进制结果为225)将数字字符串x转换为十进制整数,也可用于将一个浮点数转换为整型(非字符串时不能传递base参数)。特别的,base取值为0时,将按照字符串的字面意思进行解释,譬如给定的字符串为0o16,此时不必明确告知base=8,因为0o打头的就表示这是一个八进制数。默认的base值为10,即默认将一个十进制数字字符串转换为十进制整型(int('1314')=1314

1
2
3
4
5
6
7
#2、8、16进制转为10进制
>>> int('10010',2) #int('0b10010',0)
18
>>> int('32',8) #int('0o32',0)
26
>>> int('e1',16) #int('0xe1',0)
225

总而言之,int()只能用于将2~36进制的数(且为字符串类型)转换为10进制,如果你要将一个10进制数转换为2进制、8进制转为2进制、2进制转为8进制等等那它就无能为力了,下面介绍bin()oct()以及hex(),分别用于将一个整数(这个整数可以是10进制的,也可以是2进制、8进制和16进制的)转换为2进制、8进制以及16进制:

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
#10进制转2进制
>>> bin(18)
'0b10010'
#8进制转2进制
>>> bin(0o22)
'0b10010'
#16进制转2进制
>>> bin(0x12)
'0b10010'
#10进制转8进制
>>> oct(26)
'0o32'
#2进制转8进制
>>> oct(0b11010)
'0o32'
#16进制转8进制
>>> oct(0x1a)
'0o32'
#10进制转16进制
>>> hex(225)
'0xe1'
#2进制转16进制
>>> hex(0b11100001)
'0xe1'
#8进制转16进制
>>> hex(0o341)
'0xe1'

以上描述了2、8、10、16进制数之间的相互转换,那么任意进制(2~36)之间如何相互转换呢,可以借助10进制数作为中间状态,代码如下:

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
from functools import partial

def transform(x,base=None,to=None):
'''将base进制整数x转换为to进制数y,函数返回y
合法的进制范围为2~36,
x是一个字符串,是为了通用性考虑,因为整
数类型只涵盖了2、8、10、16进制,譬如16:
0b10000、0o20、16、0x10,但是其36进制为
g,只能以字符串形式表示,特别的,输入允
许以'0b'、'0o'、'0x'打头(此时base可以不
给或任意),也可以不是,但
是作为输出的2、8、16进制数字字符串一律不
会以'0b'、'0o'、'0x'打头。
转换原理是,将base进制先转换为10进制,再
将这个10进制转换为to进制
'''
x=x.lower()
x_10=0
if x[:2] in ['0b','0o','0x']:
x_10=int(x,0)
else:
for i,e in enumerate(x[::-1]):
x_10+=(pow(base,i)*(ord(e)-ord('a')+10 if 'a'<=e<='z' else int(e)))
y=''
while x_10>0:
t=x_10%to
y+=(chr(ord('a')+t-10) if t>9 else str(t))
x_10//=to
return y[::-1] if y!='' else '0'

transform('16',base=10,to=36) #g
f2t16=partial(transform,base=2,to=16) #from 2 to 16
f2t16('11100001') #e1

上述代码中十进制转任意进制部分,有人给出了递归写法(我用的循环):

1
2
3
4
5
6
7
8
9
def transform_10_to_any(n,base):
string='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' #''.join([*[str(i) for i in range(10)],*[chr(ord('A')+i) for i in range(26)]])
if n<base:
return string[n]
else:
return transform_10_to_any(n//base,base)+string[n%base]

print(transform_10_to_any(28,16)) #1C
print(transform_10_to_any(28**690,10)) #RecursionError: maximum recursion depth exceeded in comparison

由于存在递归深度限制,我将其改造成了尾递归版本,并使用@tail_call_optimized(见后,序列三大操作函数之reduce()案例3)进行修饰:

1
2
3
4
5
6
7
8
9
@tail_call_optimized
def transform_10_to_any(n,base,temp=''):
string='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
if n<base:
return string[n]+temp
else:
return transform_10_to_any(n//base,base,string[n%base]+temp)

print(transform_10_to_any(28**690,10))

注,任意进制转10进制的办法:按权相加,例如,将8进制数字 53627 转换成10进制:0o53627=5×84+3×83+6×82+2×81+7×80=224230o53627 = 5×8^4 + 3×8^3 + 6×8^2 + 2×8^1 + 7×8^0 = 22423 (十进制)。所谓“权”(即位权),对于一个8进制数,从左往右看,第1位的位权为 80=1,第2位的位权为 81=8,第3位的位权为 82=64,第4位的位权为 83=512,第5位的位权为 84=4096 …… 第n位的位权就为 8n-1,将各个位的数字乘以位权,然后再相加,就得到了十进制数值

将十进制转为任意N进制的办法,除N取余,逆序排列,例如将一个10进制数转换为8进制(36926 → 110076):

1
2
3
4
5
6
36926/8 = 4615(除数) ··· 6(余数)
4615 /8 = 576 ··· 7
576 /8 = 72 ··· 0
72 /8 = 9 ··· 0
9 /8 = 1 ··· 1
1 /8 = 0 ··· 1(除数为0时停止)

以上说的都是整数,那么如何对小数部分进行进制转换呢?例如将一个浮点数从8进制转换为10进制(还是按权相加):423.5176423.5176 (八进制) =4×82+2×81+3×80+5×81+1×82+7×83+6×84=275.65576171875= 4×8^2 + 2×8^1 + 3×8^0 + 5×8^{-1} + 1×8^{-2} + 7×8^{-3} + 6×8^{-4} = 275.65576171875 (十进制)。即小数部分和整数部分情况相反,要从左往右看,第1(小数)位的位权为 8-1=1/8,第2位的位权为 8-2=1/64,第3位的位权为 8-3=1/512,第4位的位权为 8-4=1/4096 …… 第m位的位权就为 8-m

另外如何将一个浮点数从10进制转换为8进制呢?规则为:乘N取整,顺序排列,示例如下(0.930908203125 → 7345):

1
2
3
4
0.930908203125×8 = 7.447265625 → 7(取整)
0.447265625 ×8 = 3.578125 → 3
0.578125 ×8 = 4.625 → 4
0.625 ×8 = 5.0 → 5(乘数结果小数部分为0时停止)

由于浮点计算的不精确性,无法在程序中判断小数点后是否为0,小数进制转换代码就不写了,可能需要借助Sympy符号运算库?

日常生活中逗号常见于大整数的表示中,如10,000,000,从python3.6开始支持整数中使用_标记,作用同逗号:a=10_000_000,也适用于十六进制或二进制:b=0b1_010_101

Python内置序列对象包括字符串、列表和元组,字符串是字符的序列,而列表和元组是任意对象的序列。字符串和元组均属于不可变对象,而列表支持插入、删除、替换元素操作。所有序列都支持迭代(见可迭代对象)

字符串的方法太多,请参阅文献[1]-p34

格式化字符串

有两种格式化方式:占位符(%)和format()方法

  1. 占位符(%)

    取模运算符(s % d)生成格式化的字符串,其中s是一个格式字符串,而d是一个元组或映射对象(字典),格式字符串包含两种类型:普通字符和转换字符(总是以%打头),格式化程序会根据转换字符将相关元组或映射中的元素转换为对应的输出格式并替换原始的转换字符。如果d是一个元组,转换字符的个数必须与d中对象的个数保持一致,如果d是一个映射,每个转换字符都必须与映射中的一个有效键名相关联

    转换字符
    转换字符 输出格式
    %c 单个字符
    %s 字符串,格式化对象时用str()生成字符串
    %r 字符串,格式化对象时用repr()生成字符串
    %d,%i (长)整数(十进制)
    %u 无符号(长)整数(十进制)
    %o (长)整数(八进制)
    %x (长)整数(十六进制)
    %X (长)整数(十六进制,大写字母)
    %e 浮点数(科学计数法,如[-]m.dddddde±xx)
    %E 浮点数(科学计数法,如[-]m.ddddddE±xx)
    %f 浮点数(如[-]m.dddddd)
    %g,%G 指数小于-4或更高精度时使用%e或%E,否则使用%f
    %p 指针(用十六进制表示对象的内存地址)
    %% 字面量百分号

    %和转换字符之间可以出现以下修饰符,并且只能按照下述顺序出现:

    1. 位于括号总的一个键名,用于从映射对象中选出一个具体项,如果不存在此键,则引发KeyError异常
    2. 下面所列的一个或多个
      • -,左对齐标志,默认为右对齐
      • +,表示应该包含数字符号(即使为正值也是如此)
      • 0,表示零填充(仅用于数字类型,如%d%f
    3. 一个指定最小宽度的数字,转换后的值将被打印在至少为这个宽度的字段中,并且在左边填充至满字段宽(如果指定了-标志,则填充在右边)
    4. 一个小数点,用于按照精度分割字段宽度
    5. 一个数字,指定要打印字符串中的最大字符个数,或者浮点数中小数点之后的位数,或者整数的最小位数
    1
    2
    3
    4
    5
    6
    7
    >>> 'my name is %s%c%c, my age is %d' % ('muggle','d',121,23) #%c还可以接受整数(具体参见ASCII码表,或通过ord()、chr()转换)并转换为对应字符
    'my name is muggledy, my age is 23'
    >>> d={'x':13, 'y':1.54321, 'z':'hello world'}
    >>> '%.5s' % d['z']
    'hello'
    >>> '%(x)05d, %(y).2f' % d
    '00013, 1.54'
    ord()chr()

    有些论坛不允许新人帖包含url链接,最简单的办法之一是将链接的每个字符转换成ASCII码,其他人再将ASCII转为字符。具体使用ord()函数返回单个字符的ASCII(0-255)以及chr()函数返回一个整数(0-255)对应的字符

    1
    2
    3
    4
    5
    >>> s='http://www.paperpass.com/'
    >>> [ord(i) for i in s] #编码
    [104, 116, 116, 112, 58, 47, 47, 119, 119, 119, 46, 112, 97, 112, 101, 114, 112, 97, 115, 115, 46, 99, 111, 109, 47]
    >>> ''.join([chr(i) for i in eval('[104,116,116,112,58,47,47,119,119,119,46,112,97,112,101,114,112,97,115,115,46,99,111,109,47]')]) #解码
    'http://www.paperpass.com/'
  2. format()方法

    高级字符串格式化,调用字符串sformat()方法:s.format(*args,**kwargs),其接受位置参数和关键字参数的任意组合,并使用它们的值来替换s中嵌入的占位符,形式为{n}的占位符(n为数字,某些情况下可以省略)将被format()方法的第n个位置参数替换,而形式为{name}的占位符将被名为name的关键字参数值替换,如果要输出一个{},必须使用{{}}的形式

    使用占位符还可以对位置参数或关键字参数进行数值或键索引,如{name[n]}n为索引数字)、{name[key]}key为非数字字符串),譬如:

    1
    2
    3
    4
    >>> 'my name is {1}, {0[0]}+{0[1]}={0[2]}, {2[x]}{2[y]}{2[z]}, {d}, {e[0]}, {e[1]}'.format([1,2,3],'dy',{'x':'a','y':'b','z':'c'},d=4,e=[5,'msy'])
    'my name is dy, 1+2=3, abc, 4, 5, msy'
    >>> '{d}, {},{}, {e}, {}'.format([1,2,3],'dy',{'x':'a','y':'b','z':'c'},d=4,e=[5,'msy']) #使用缺省的{}占位符,将和位置参数从左至右一一对应
    "4, [1, 2, 3],dy, [5, 'msy'], {'x': 'a', 'y': 'b', 'z': 'c'}"

    使用占位符还可以对位置参数或关键字参数进行属性查找,如{name.attr}

    1
    2
    >>> '{0.real} {0.imag}'.format(x)
    '1.0 2.0'

    另外,还可以指定格式说明符,对输出进行更加精确的控制,具体是用一个冒号(:)给每个占位符添加可选的格式说明符,形如{place:format_spec},使用这种说明符可以指定列宽、小数位和对齐方式,一般格式是[[fill]align][sign][0][width][.precision][type][]中的每个部分都是可选的,特别地,当给定fill时,也必须同时给定align),width说明符指定要使用的最小字段宽度,align说明符的可取值为<>^(分别代表左对齐、右对齐和居中对齐),fill是一个可选的填充字符,用于填充空白,type说明符表示数据的类型,可以不提供,默认值分别是s(字符串)、d(整数)、e(浮点数,科学计数),sign说明符可取值为+-或空格,用于在数值类型前加上符号,precision说明符指定十进制数的精度位数,另外如果在width前面加上一个0,就会使用0来填充数字前面的空白,看个例子:

    1
    2
    >>> '|{:#^10}|{age:^+10.2f}|'.format('dy',age=25.54321,)
    '|####dy####| +25.54 |'
    Formatted string literals(f-string)

    F-string是python3.6引入的新特性,使用方式形如:f’字符串’F’字符串’,字符串中使用大括号作为占位符标识要替换的字段,本质上f-string并非常量字符串,它将在运行时对其中的字段求值,f-string在设计上用于替代%format()这两种传统格式化方法,性能优于二者且形式更加简便

    • 简单使用:
      1
      2
      3
      4
      >>> name = "Eric"
      >>> age = 74
      >>> f"Hello, {name}. You are {age}."
      'Hello, Eric. You are 74.'
    • 表达式求值和函数调用:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      >>> f"{2 * 37}" #表达式求值
      '74'
      >>> def to_lowercase(input):
      . . . return input.lower()
      . . .
      >>> name = "Eric Idle"
      >>> f"{to_lowercase(name)} is funny." #函数调用
      'eric idle is funny.'
      >>> f"{name.lower()} is funny." #调用对象方法
      'eric idle is funny.'
      >>> import math
      >>> f'The answer is {math.log(math.pi)}'
      'The answer is 1.1447298858494002'
      >>> comedian = {'name': 'Eric Idle', 'age': 74}
      >>> f"The comedian is {comedian['name']}, aged {comedian['age']}."
      The comedian is Eric Idle, aged 74.

      注:如果占位符{...}中的对象非字符串,那么将调用对象的__str__()方法(和__repr__()方法相比,前者面向人类,后者面向计算机,一般来说,你甚至可以通过执行(调用eval()函数)repr()返回的字符串重新生成对象,如果只实现__repr__(),那么str()还是会调用对象的__repr__()方法),并以返回的字符串替换占位符(如果同时也定义了__repr__()方法,且希望格式化时使用该方法返回的字符串,则写成{...!r}),看个例子:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      class Comedian:
      def __init__(self, first_name, last_name, age):
      self.first_name = first_name
      self.last_name = last_name
      self.age = age

      def __str__(self):
      return f"{self.first_name} {self.last_name} is {self.age}."

      def __repr__(self):
      return f"{self.first_name} {self.last_name} is {self.age}. Surprise!"

      new_comedian = Comedian("Eric", "Idle", "74")
      print(f'{new_comedian}') #Eric Idle is 74.
      print(f'{Comedian("Yang","Dai",25)}') #Yang Dai is 25.
      print(>>> f"{new_comedian!r}") #'Eric Idle is 74. Surprise!'
      注1:字符串内的引号不能和字符串外的引号定界符冲突,可根据情况灵活切换'"(单引号和双引号),必要的时候,还可以使用三引号'''或者""",再不行,还可以使用斜杠\对特殊字符进行转义,譬如'i\'m dy.',但是f-string大括号内部不能使用斜杠\转义甚至说根本就不能出现任何斜杠

      注2,支持多重大括号写法,譬如双重大括号,f'{{5}}'将打印出字符串'{5}',但注意此时双重大括号中的内容是被当作普通字符串,譬如:

      1
      2
      3
      4
      5
      6
      7
      8
      >>> f'{temp_a}' #temp_a从未定义,没有这个变量
      Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      NameError: name 'temp_a' is not defined
      >>> f'{{temp_a}}'
      '{temp_a}'
      >>> f'{{70 + 4}}'
      '{70 + 4}'

      要当作变量或表达式或函数调用,则再加一层大括号,即三重大括号:

      1
      2
      3
      4
      5
      6
      >>> f'{{{70 + 4}}}'
      '{74}'

      #如果是四重大括号:
      >>> f'{{{{70 + 4}}}}'
      '{{70 + 4}}'

    • 多行f-strings:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      name = "Eric"
      profession = "comedian"
      affiliation = "Monty Python"
      message = (
      f"Hi {name}. "
      f"You are a {profession}. "
      f"You were in {affiliation}."
      )
      print(message) #Hi Eric. You are a comedian. You were in Monty Python.

      #上述写法需要在每一行首部添加标识f或F,如果不像这样可以使用三引号实现多行字符串:
      message = f"""
      Hi {name}.
      You are a {profession}.
      You were in {affiliation}.
      """

      注,多个(位于同一行的)独立字符串之间不需要使用任何连接符(如果不在同一行,需要在行末加上斜杠表示换行(任意语句如果太长,你都可以在一行结尾处使用斜杠,然后就可以在下一行继续书写上一条语句剩余的内容,且不受python正常的缩进规则限制),如果不想写斜杠,也可以在所有独立字符串的左右两端裹上小括号),它们同样会被自动拼接:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      s="Hello" " world!"
      print(s) #Hello world!

      s="dy " "is " \
      'a' ' ' \
      'muggle'
      print(s) #dy is a muggle

      #等同于:
      s=("dy " "is "
      'a' ' '
      'muggle')
      print(s) #dy is a muggle

      且多个独立字符串作为函数参数时,不论是否在同一行,都无需斜杠作为换行符:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      def f(a,b):
      print(a)
      print(b)

      f("muggle"
      "dy","dai" \
      "xiao""dong")

      '''OUTPUT
      muggledy
      daixiaodong
      '''
    • 格式精确控制(对齐、宽度、符号、补零、精度、进制等):
      类似format(),f-string使用形如{content:format_spec}进行格式的精确控制,其中content可以是变量、表达式或函数等,format_spec是格式描述符,采用默认格式时,则省略:format_spec,格式描述的使用顺序要注意,具体请参阅文章[2]相关部分
    print()函数

    当使用print()打印对象的时候,实际上是调用了标准输出并向其中写入字符串:sys.stdout.write(str(obj)+'\n')sys.stdout就是标准输出对象,默认是写入到控制台设备并追加一个换行符(可以通过指定print()函数的end参数修改所追加的字符或字符串),当然你也可以修改sys.stdout的值,以实现输出重定向,譬如将输出重定向至某个文件:

    1
    2
    3
    4
    5
    6
    7
    >>> __console__ = sys.stdout #将控制台对象引用保存下来以便于恢复
    >>> file = open('redir_print.txt', 'w')
    >>> sys.stdout = file
    >>> print('Hello,World!') #此时等同于:file.write(...)
    >>> file.flush() #打印的内容首先存在于缓冲区flush中,直到下一次打印时上次的打印(内容)才被写到文件中,使用flush()强制刷新缓冲区将其写进文件
    >>> sys.stdout = __console__
    >>> file.close()

    在python3中不需要像上面这样繁琐,指定print()函数的file参数和flush参数即可:

    1
    >>> print('Hello,World!', file = file, flush = True)

    实际上你可以往print()中传递任意数量的任意对象且是以位置参数的形式,因为__str__()具有默认实现,其返回一个尖括号包裹的字符串,包含类名和内存地址信息,不同对象的输出字符串会自动连接且以单个空格作为间隔符:

    1
    2
    3
    4
    >>> class A: pass
    ...
    >>> print('muggledy','+',...,'zj',A())
    muggledy + Ellipsis zj <__main__.A object at 0x000001B0358AB7B8>
  1. 增加
    • s.append(x)
    • s.extend(t)
      注:不同于append追加元素,extend将一个新的序列扩展到原始序列中,等同于[*s,*t](解包)
      解包与装包

      主要有三种用途:变量赋值,函数传参和简化容器添加

      1. 变量赋值
        • 将多个值或者说对象(其以逗号间隔)赋值给一个变量时,python会自动将这些值封装成元组,这种特性即称为“装包”:
          1
          2
          3
          >>> a=1,2,3
          >>> a
          (1, 2, 3)
        • 特别地,当函数返回多个数值时,也会进行装包:
          1
          2
          3
          4
          5
          def f(a,b,c):
          return a*1,b*2,c*3

          ret=f(1,2,3)
          print(ret,type(ret)) #(1, 4, 9) <class 'tuple'>
        • 将一个(可迭代)对象拆分成多个对象并依次赋值给多个变量,这种特性即称为“解包”,另外还可以用星号*修饰变量,使其接收多个拆解对象的组合(装包为列表类型):
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          >>> a=[1,2,3]
          >>> x,y,z=a
          >>> x
          1
          >>> y
          2
          >>> z
          3
          >>> x,*y,z=range(10) #非*修饰变量(x,z)只能接收一个拆解对象
          >>> x
          0
          >>> y
          [1, 2, 3, 4, 5, 6, 7, 8]
          >>> z
          9
        • 可以解包的对象类型包括元组、列表、字典、生成器以及其它可迭代对象:
          1
          2
          3
          4
          5
          6
          7
          8
          9
          >>> d={'a':1,'b':2,'c':3} #字典也是可迭代对象,等同于迭代其键序列,但是要注意键序列其实是“无序”的
          >>> x,y,z=d
          >>> x,y,z
          ('a', 'b', 'c')
          >>> x,y,z=d.items() #如果要同时得到字典的键值的话
          >>> x,y,z
          (('a', 1), ('b', 2), ('c', 3))
          >>> a=(i for i in range(3)) #生成器也是可迭代对象
          >>> x,y,z=a

          装包和解包常用于“对称性赋值”操作和for循环中的变量拆解:

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          x,y=6,8
          x,y=y,x
          print(x,y) #将变量x和y的值对调,不需要像C语言那样借助临时变量实现。等号右边会先进行封装,变成元组(y,x),然后元组被解包并依次赋值给x和y

          d={'a':1,'b':2,'c':3}
          for k,v in d.items():
          print('键:{}, 值:{}'.format(k,v))

          '''OUTPUT
          键:a, 值:1
          键:b, 值:2
          键:c, 值:3
          '''
        • 可以在变量赋值操作中对等号右边的可迭代对象直接使用星号*进行拆解或者说解包:
          1
          2
          3
          4
          >>> a=['dy',88,True]
          >>> v=1,2,*a,3
          >>> v
          (1, 2, 'dy', 88, True, 3)

          如果不想对右边的对象进行显式拆解,还可以在左边对将要接收的变量裹上一层或多层()[],以实现“多级嵌套解包”:

          1
          2
          3
          4
          5
          6
          7
          >>> a=['dy',88,True]
          >>> *_,(x,y,z),_=1,2,a,3 #写成*_,(x,y,z),*_=1,2,a,3是错误的,在同一级不能出现两个及以上的星号,什么意思?譬如写成*_,(x,*y),_=1,2,a,3则是正确的,这两个*不在同一级
          >>> x,y,z
          ('dy', 88, True)
          >>> _,(a,(b,c),d),_=1,[2,[3,4],5],6 #总之两边结构保持一致即可
          >>> b,c
          (3, 4)

          简单来说,星号出现在赋值号左边表示装包,出现在赋值号右边表示解包,不管是哪种,星号修饰符都必须位于一个序列中,可能不太好理解,看个例子即可:

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          >>> a=[1,2,3]
          >>> *x=a #报错
          File "<stdin>", line 1
          SyntaxError: starred assignment target must be in a list or tuple
          >>> *x,=a
          >>> (*x,)=a
          >>> [*x]=a #这三种改法都是正确的
          >>> x=*a #报错
          File "<stdin>", line 1
          SyntaxError: can't use starred expression here
          >>> x=*a,
          >>> x=(*a,)
          >>> x=[*a] #这三种改法都是正确的
      2. 函数传参
        • 装包传递(分两种,一是位置参数将被装包为一个元组,二是关键字参数将被装包为一个字典):
          1
          2
          3
          4
          5
          6
          7
          8
          9
          def f1(*t):
          print(t)

          f1(1,2,3,4,5) #(1, 2, 3, 4, 5)

          def f2(**d):
          print(d)

          f2(X='a',Y='b',Z='c') #{'X': 'a', 'Y': 'b', 'Z': 'c'}
        • 解包传递(也分为两种情况,一是元组解包传递给位置参数,二是字典解包传递给关键字参数):
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          def f3(x,y,z):
          print(x,y,z)

          t=(1,2,3)
          f3(*t) #等同于执行:f3(1,2,3)

          def f4(a,b=22,c=33):
          print(a,b,c)

          d={'a':1,'c':3}
          f4(**d) #等同于执行:f4(a=1,c=3)

          #特别地,如果调用f1(*t)或者f2(**d),将同时完成解包和装包传递
      3. 简化容器添加
        • 当容器为listtuple(虽然元组是不可变类型,但是没关系,能解包就行,这也并不涉及元组值的改变)、set类型时,同变量赋值操作中等号右边可迭代对象的拆解,使用单星号进行解包:
          1
          2
          3
          4
          5
          >>> a=(1,2,3)
          >>> (*a,4,5) #创建一个全新的元组
          (1, 2, 3, 4, 5)
          >>> {*a,4,5} #集合
          {1, 2, 3, 4, 5}
        • 当容器对字典时,使用的是双星号**进行解包:
          1
          2
          3
          >>> d={'1':1,'2':2,'3':3}
          >>> {**d,'4':4,'5':5}
          {'1': 1, '2': 2, '3': 3, '4': 4, '5': 5}

          但是需要注意的是,这虽然比通过dict.update()扩充字典的形式要简单一点,但须知,dict.update(another_dict)可以明确更新项目,而**字典解包则无法确保,譬如:

          1
          2
          3
          4
          5
          >>> d={'a':1,'b':2,'c':3}
          >>> {**d,'c':33}
          {'a': 1, 'b': 2, 'c': 33} #'c'键值更新了
          >>> {'c':33,**d}
          {'c': 3, 'a': 1, 'b': 2} #'c'键值没有更新

          另外就是效率问题,用***简化容器的添加操作,其性能和容器对象的update(),append()add()方法相比,谁更优?根据下面的测试,解包比append()方法慢了3倍:

          1
          2
          3
          4
          5
          6
          import timeit
          t1=timeit.Timer('a.append(100)','a=list(range(100))')
          print(t1.repeat()) #[0.0850489117379404, 0.09039714449200119, 0.08290944329400407]

          t2=timeit.Timer('b=[*a,100]','a=list(range(100))')
          print(t2.repeat()) #[0.2924700997522413, 0.2945020249114667, 0.2961987540175306]
    • s.insert(i,x)
  2. 删除
    • s.pop([i])
      注:函数定义或签名中的[]表示可选参数
    • s.remove(x)
    • del s[i]del s[i:j]del s[i:j:stride]
      注:这是可变序列的通用操作
  3. 查找
    • s.index(x,[,start[,stop]])
      注:返回满足s[start:stop][i]==x的最小i值,如果找不到,会抛出异常
    • s[i]s[i:j]s[i:j:stride]
      注:这是序列的通用操作。i:j其实是一个特殊的切片对象slice(i,j,None),且ijstride都是任意可省的,如s[::]
  4. 修改
    • s.reverse()
      注:这是原址操作,可以通过切片实现s[::-1](返回一个新对象,上面说过,属于浅复制)
    • s.sort([key[,reverse]])
      sort()方法

      基本形式:sorted(iterable[,cmp[,key[,reverse]]])(副本排序)、 iterable.sort(cmp[,key[,reverse]])(原址排序)

      参数:iterable为可迭代对象,cmp指定排序时进行比较的函数,key也是函数,指定待排序元素将要参与排序的特征,reverse实现降序,默认False为升序
      注:python3去掉了cmp参数

      简单示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      #1.原址排序(对原始的序列对象进行内部排序而不是返回一个新的已排的序列对象)
      n=[7,4,1,3,2,8,9]
      n.sort()
      print(n) #[1, 2, 3, 4, 7, 8, 9]

      #2.副本排序
      #两种:创建原始对象的一个副本,然后对副本做原址排序,或者使用sorted方法,其将返回一个类型为列表的副本
      x=[4,3,5,2,1]
      y=x[:] #对于列表对象来说,创建副本的方式有很多:切片、工厂函数、copy.copy(前三种是浅拷贝)、copy.deepcopy
      y.sort()
      print(x,y) #x未排序,y已排序

      m=[3,4,1,7,5,0,2]
      print(sorted(m))
      print(m) #sorted()不改变原始对象

      高级用法:

      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
      #1.自定义比较函数cmp(x,y)
      #既然python3不支持,就不说了

      #2.自定义待排序元素特征keys(接受一个参数,表示待排序的元素对象)
      #之所以在key后面加上s,是因为特征可以有多个,首先按照第一个特征对元素排序,然后在此基础之上,再用第二个特征排序,第一个特征优先级最高。此即为多级排序
      def keys(x):
      return len(x)

      array=['daiyang','sb','miaosiyu','jianglicheng','liuhan'] #按照字符串的长度进行排序(降)
      print(sorted(array,key=keys,reverse=True)) #由于取消了cmp比较函数,因此只能通过reverse设定升降顺序
      array.sort(key=lambda x:len(x),reverse=True)
      print(array) #['jianglicheng', 'miaosiyu', 'daiyang', 'liuhan', 'sb']

      #3.多级排序
      arrays_1=[(2,3,10),(1,2,3),(5,6,7),(2,5,10),(2,4,10)]
      arrays_2=[('2','3','10'),('1','2','3'),('5','6','7'),('2','5','10'),('2','4','10')]
      arrays_1.sort(key=lambda x:(x[2],x[1])) #例1 #[(1, 2, 3), (5, 6, 7), (2, 3, 10), (2, 4, 10), (2, 5, 10)]
      print(arrays_1)
      print(sorted(arrays_2,key=lambda x:list(map(int,(x[2],x[1]))))) #例2

      #将operator.itemgetter函数用在多级排序中
      from operator import itemgetter
      #itemgetter函数用于选择对象的指定维数的数据,注意itemgetter返回的是一个函数对象
      a=[1,2,3]
      gets=itemgetter(1,2) #获取“对象[1]”和“对象[2]”,这个对象是广义化的,由你任意指定,如下:
      ret=gets(a) #返回一个元组:(a[1],a[2])
      print(ret)

      #结果同例2:
      print(sorted(arrays_2,key=lambda x:list(map(int,itemgetter(2,1)(x)))))
      #结果同例1:
      print(sorted(arrays_1,key=itemgetter(2,1))) #体会key的作用,真神奇啊,源码是怎么实现的啊

      注,获取排序后的索引序列:

      1
      2
      3
      4
      5
      6
      7
      def argsort(arr):
      return [i for i,v in sorted(enumerate(arr),key=lambda x:x[1])]

      a=[1,3,2,9,4,2,0]
      inds=argsort(a)
      print(f'sorted-index:{inds}') #sorted-index:[6, 0, 2, 5, 1, 4, 3]
      print(f'sorted-value:{[a[ind] for ind in inds]}') #sorted-value:[0, 1, 2, 2, 3, 4, 9]

类似于列表,只不过元组一旦创建不能修改,比列表要省内存

首先新建一个链表类(目前就是一个普通的容器,可以存储数据,但是尚未实现__iter__方法,因此还不是可迭代对象),实现了链表的基本操作,索引、追加、插入、删除:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
__all__=['Link',]

class Node:
def __init__(self,val):
'''节点对象,有数据域和指针域(指向下一元素)'''
self.data=val
self.next=None

class Link:
def __init__(self):
'''初始化链表,初始化一个头节点,其数据域默认为空,可以添加链表长度等信息'''
head=Node(0)
self.pointer=head #语义:链表指针,指向头节点

def append(self,val):
'''在链表尾部追加新元素'''
node=Node(val)
p=self.pointer #语义:工作指针
while p.next: #获取指向最后一个元素的指针
p=p.next
p.next=node #看起来next“指针域”指向了node,因为next中保存的是对node的引用,而不是node的拷贝,这和C中的指针效果是一样的
self.pointer.data+=1

def printf(self):
'''打印链表中元素'''
print('[链表] ',end='')
p=self.pointer
while p.next:
print(p.next.data,end=', ')
p=p.next
print()

def delete(self,index,get=False):
'''删除链表中第index个元素,get参数用于get()函数代码复用,这个参数应该隐藏(譬如作为类属性)而不该放在函数定义中'''
if index<1 or index>self.len:
raise ValueError('下标溢出')
p=self.pointer
i=1
while p.next:
if i==index:
if not get:
p.next=p.next.next #断开的节点无需手动free,因为python有垃圾回收机制
else:
return p.next
break
p=p.next
i+=1
self.pointer.data-=1

def deleteall(self):
'''删除全部元素,即头节点指针域指空'''
p=self.pointer
p.next=None
self.pointer.data=0

def get(self,index):
'''获取第index个节点元素,index取值1~len(Link)'''
return self.delete(index,True).data

@property
def len(self):
'''链表长度,之前我们说头节点有空闲的数据域,我们可以存储链表长度信息,通过实时更新其值,可以避免硬计算'''
p=self.pointer
i=0
while p.next:
p=p.next
i+=1 # 硬计算
if self.pointer.data!=i: #为了避免错误,暂时我仍使用硬计算,并对头节点的数据域中的长度值进行检测
print('头节点错误')
return i

def insert(self,index,val):
'''插入元素,index合法取值1到len(Link)+1,特别地,当index取值len(Link)+1时,为在尾部追加(特殊插入)'''
node=Node(val)
if index<1 or index>self.len+1:
print('下标溢出')
return
if index==self.len+1:
self.append(node)
return
p=self.pointer
i=1
while p.next:
if i==index:
t=p.next
p.next=node
node.next=t
break
p=p.next
i+=1
self.pointer.data+=1

if __name__=='__main__':
L=Link() #空链表
L.append(1)
L.append(2)
L.append(3)
L.insert(1,0)
L.printf()
L.delete(1)
L.printf()
L.deleteall()
L.append('muggle')
L.append('daiyang')
L.printf()
print(L.get(2))

凡是可迭代对象都可以用for语句逐一遍历全部元素,这是检查一个对象是否可迭代的标准,要创建一个可迭代对象,必须实现__iter__()方法,其返回一个迭代器,迭代器内部持有一个状态,用于记录当前迭代所在的位置,迭代器必须实现__next__()用于迭代获取下一个元素并更新迭代状态,而迭代器也属于可迭代对象的范畴,因此也必须实现__iter__()方法,具体实现时返回自身即可

下面我们试着将上述不可迭代的链表对象(Link)转变为可迭代的列表对象(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
class List(Link):
def __iter__(self): #实现了__iter__方法之后,链表变成可迭代对象
return ListIteration(self)

class ListIteration: #简单来说,迭代器就相当于“普通容器+迭代状态”
def __init__(self,container):
self.data=container #迭代器由容器数据和状态构成
self.iterstate=1 #迭代器当前的迭代状态
self.stopstate=self.data.len #终止

def __iter__(self): #对一个迭代器获取其迭代器,就是其本身
return self

def __next__(self): #根据当前迭代状态迭代下一个元素,并更新状态
while self.iterstate<=self.stopstate:
self.iterstate+=1
return self.data.get(self.iterstate-1)
raise StopIteration #对超出范围的元素迭代需要返回StopIteration,在for语句中该异常将被捕捉

a=List() #创建一个列表(可迭代对象)
a.append(10)
a.append(12)
a.append(14)
a.append(16)
b=iter(a) #将自动调用a.__iter__()
print(b.__next__()) #10 #或者写next(b)
c=iter(b) #对迭代器调用iter(),返回自身,因此下面的c都可以替换为b
print(c.__next__()) #12
for i in c:
print(i) #14 16
for i in a:
print(i) #10 12 14 16
for循环原理
1
2
for x in [1,2,3]:
print(x)

等价于:

1
2
3
4
5
6
7
8
9
#1.首先通过iter()获取迭代器对象
iterator=iter([1,2,3])
#2.在循环中不断调用next方法获取迭代器对象的下一个值直至捕捉到StopIteration异常退出循环
while True:
try:
x=next(iterator)
print(x)
except StopIteration:
break

itertools模块提供了诸多具有特殊用途的迭代器,譬如count()返回一个无限循环的迭代器(既然无限循环为什么不直接使用while?因为count()可以很方便地记录当前迭代次序),再如cycle()从有限序列生成无限序列,用于循环遍历序列,等等,下面是cycle的实现(我随便写的,非官方实现):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class cycle:
def __init__(self,container,start=0): #start指定从序列的哪一个元素开始循环迭代
self.data=container
self.iterstate=start
self.len=len(container)

def __iter__(self):
return self

def __next__(self):
self.iterstate+=1
return self.data[(self.iterstate-1)%self.len]

a=cycle([1,2,3],) #不同于上面自定义的列表是一个可迭代对象(但非迭代器对象),此处cycle(...)直接就是迭代器对象,当然也就是可迭代对象。再如python几种内置类型list、tuple、dict、set、str等,也都是可迭代对象,但非迭代器对象(为何这样设计?道理很简单,迭代器对象都是用完即扔的,这实在是太浪费资源了,于是你需要不停在内存中创建和回收对象,但重复将可迭代对象转换为迭代器就很便宜了,无非就是将状态位归零罢了),譬如a=[1,2,3];next(a)将报错:TypeError: 'list' object is not an iterator
print(next(a),next(a),next(a),next(a),next(a),next(a),next(a)) #1 2 3 1 2 3 1
for i in a:
print(i) #无限循环,因为for循环只会在next(迭代器)抛出StopIteration(停止迭代异常)的时候终止

判断是否可迭代以及是否是迭代器:

1
2
3
4
5
>>> import collections
>>> isinstance([],collections.Iterable) #list是可迭代对象吗?
True
>>> isinstance([],collections.Iterator) #list是迭代器对象吗?
False
图2. 可迭代对象、迭代器、生成器关系[4]

再说说生成器,实际上生成器本质就是迭代器(因此生成器也是用完即弃),只要迭代的下一个元素是即时计算(惰性计算)的,那就是生成器,普通迭代器迭代下一个元素,是从某个容器中取出来,所有元素事先已经计算得出,其始终占用着一片内存区域(因此只有生成器才能实现“无限序列”)。在python2中range()就是一个普通迭代器(譬如range(10000)内存中真的就存在10000个数字),xrange()则是生成器(此时内存中就没有10000个数据占用),下面我们自行设计一个生成器版本的range(Range):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Range(object):
def __init__(self,*args):
if len(args)==1:
self.start,self.stop,self.step=0,args[0],1
elif len(args)==2:
self.start,self.stop,self.step=*args,1
elif len(args)==3:
self.start,self.stop,self.step=args
else:
raise ValueError('[参数错误] Range(start[,stop[,step]])')

def __iter__(self):
return self

def __next__(self):
while self.start<self.stop:
self.start+=self.step
return self.start-self.step
raise StopIteration

for i in Range(3,9,2):
print(i) #3, 5, 7
print(list(Range(5))) #[0, 1, 2, 3, 4],工厂函数list自动计算生成器各个元素的值并以列表返回

通常采用yield关键字构造生成器,在函数中使用yield关键字代替return关键字(称为生成器函数),函数调用将返回一个生成器对象,此时函数内部代码不会立即执行,除非对其调用next()方法,函数执行流将在yield位置中止,并返回yield语句中的表达式值,继续调用next()方法,函数执行流将从yield中止处继起并在下一次遇到yield时中止…,yield使得生成器的语法变得相当优雅(我们几乎从不使用上面Range示例的方式编写生成器,python本身也不认为那是一个生成器对象,如isinstance(Range(5),types.GeneratorType)会返回False),简单来说生成器函数就是在一个循环中不断计算并产生新值的过程且只在需要时计算(“惰性计算”,不调用next()方法则不计算),它定义了数据生产的流程,当我们需要将生产和消耗分离的时候,使用生成器可以方便地解耦,任意函数稍作修改便可转换为生成器函数,只需要将return关键字改成yield关键字,现在我们用yield方式重写Range:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def Range(*args):
if len(args)==1:
start,stop,step=0,args[0],1
elif len(args)==2:
start,stop,step=*args,1
elif len(args)==3:
start,stop,step=args
else:
raise ValueError('[参数错误] Range(start[,stop[,step]])')
while start<stop:
start+=step
yield start-step
raise StopIteration #生成器其实没必要在结束位置处显式抛出该异常,因为会自动抛,但是(非生成器的)迭代器必须手动抛出

for i in Range(3,9,2):
print(i)

print(list(Range(5))) #[0, 1, 2, 3, 4]
惰性计算

为了深入了解“惰性计算”,看个示例

1
2
3
4
g=(i for i in range(4))
for i in [1,10]:
g=(i+j for j in g)
print(list(g))

不少人(包括我)认为结果会是[11,12,13,14],但是解释器运行结果却是[20, 21, 22, 23],why?

对于一个生成器表达式(expression1 for variables in expression2)expression1只有在调用生成器对象的__next__()方法时才会执行,而for子句则是即时计算的,即expression2是即时计算的(expression2应返回一个可迭代对象),另注意,for子句可能是多层for循环嵌套的,只有最左边的for子句才会即时计算,譬如:

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
n=2
def bar():
return range(n)

g1=(i for i in bar()) #for子句表达式是即时计算的,此处等同于:g1=(i for i in range(2))
n=3
g2=((i,j) for i in bar() for j in bar()) #只有最左侧的for子句表达式才即时计算,此处等同于:g2=((i,j) for i in range(3) for j in bar())
n=4
print(list(g1))
print(list(g2)) #list遍历生成器元素,由于最后修改的n值为4,于是此处等同于:g2=((i,j) for i in range(3) for j in range(4))

'''OUTPUT
[0, 1]
[(0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2), (1, 3), (2, 0), (2, 1), (2, 2), (2, 3)]
'''

#如果expression2是一个变量引用,将变量引用指向其他对象不会改变生成器数值,如果变量引用指向一个可变对象,修改该可变对象则会改变生成器数值:
a=range(2)
g=(i for i in a)
a=range(10)
print(list(g)) #[0, 1]

a=[1,2,3,4]
g=(i for i in a)
a.append(5)
print(list(g)) #[1, 2, 3, 4, 5]

回到最初的问题,第一次循环,生成器g中的值其实就是[i+0,i+1,i+2,i+3](尚未计算),第二次循环,g变成[i+(i+0),i+(i+1),i+(i+2),i+(i+3)](尚未计算),最后通过list(g)遍历时,i=10,于是输出[20,21,22,23]。将其用生成器函数改写一下同时展开for循环,其执行过程相当于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def gen1():
for i in range(4):
yield i

i=1
def gen2():
for j in gen1():
yield i+j

i=10
def gen3():
for j in gen2():
yield i+j

print(list(gen3())) #[20, 21, 22, 23]

这就很明了了,或者再改写一下得到:

1
2
3
4
5
6
7
8
i=1

i=10
def gen3():
for k in range(4):
yield i+i+k

print(list(gen3())) #[20, 21, 22, 23]

另外如果不是通过list(g)输出生成器序列元素,而是用for循环替换之:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for i in g:
print(i)

'''OUTPUT
20
41
84
171
'''

#破解之法就是替换for循环变量
for k in g:
print(k)

'''OUTPUT
20
21
22
23
'''

要注意迭代变量的使用,如果使用i作为迭代变量,结果将会发生变化,这仍是惰性计算导致的,结合for循环原理原因就很清楚了(每次循环都会执行i=next(g),而i将影响生成器序列的数值计算)

GeneratorExit异常标志着生成器迭代过程的非正常终止,相反,正常终止不会引发该异常:

1
2
3
4
5
6
7
8
9
10
11
def f(n): #不断生产数据
try:
while n>0:
yield n
n-=1
except GeneratorExit:
print('exiting...')

g=f(3)
for i in g: #每产生一个数据就消耗一个数据,如果使用while循环是无法分离生产和消耗两个部分的,它们势必混合在一起
print(i) #3 2 1

如果生成器仅被部分使用,将在yield处引发GeneratorExit异常(但一般不用人为捕获),并自动调用close()方法关闭生成器,如果再次调用next()方法将引发StopIteration异常,它已不能继续产生新值了:

1
2
3
4
5
6
7
g=f(3)
for i in g:
if i>1:
print(i) #3 2 exiting...
else:
break
g.__next__() #出现StopIteration异常,不要再期望输出1

注:for语句能判断生成器函数是否全部完成,如果你是通过whilenext进行迭代是无法判断的(只有在整个程序终止时才会引发该异常),这个时候就需要显式关闭(但这不是必须的),鉴于你通常会忘记,一般我们只使用for循环来迭代生成器

手动执行close()方法亦将在yield处引发GeneratorExit异常(假设生成器已被使用过的话):

1
2
3
g=f(3)
print(g.__next__()) #3
g.close() #exiting...

删除生成器对象也会引发该异常:

1
2
3
g=f(3)
print(g.__next__()) #3
del g #exiting...

小结一下:

  1. 生成器在close()方法后就无法再进行迭代,再调用next()方法就会抛出StopIteration异常
  2. 生成器调用close()方法会在yield处抛出GeneratorExit异常,但前提是至少调用一次生成器的next()方法才会产生GeneratorExit异常,如果生成器没有启动则不会抛出异常
  3. 生成器内捕捉GeneratorExit异常后,可以继续执行剩余的代码,但剩余的代码中不能再包含yield语句,否则抛出RuntimeError异常
  4. GeneratorExit异常不能通过Exception捕捉,因为其直接继承自所有异常的基类BaseException

上面我们说了为什么内置的类型如listtupledictsetstr都被设计为可迭代对象而非迭代器(迭代器是“一次性”的),原因不止一条,譬如in关键字就不能用于迭代器对象,x in y语句的工作原理是,首先调用对象y上的iter()方法,然后不断执行next()方法获取下一个元素,直至x==next(y),返回True,否则返回False,表示容器y中不存在x,看一个错误范例就明了了:

1
2
3
4
y=(i for i in range(5))
x=1
print(x in y) #True
print(x in y) #False

第一次执行x in y时候,iter(y)返回y自身,执行两次next(y),生成器y中的前两个元素0和1被消耗(发现x存在于y中),结果返回True,第二次执行x in y的时候,iter(y)仍旧返回被部分消耗的y自身,继续调用next()方法,显然后面的元素2、3、4中不存在1,因此返回False。这种情况下如果要进行存在性判断,可以先将生成器转换为列表(list(y)),一个可迭代对象(而iter(可迭代对象)总是返回一个全新的迭代器对象)

yield与递归混用

yield与递归混用时要小心,很容易出错,先来看一个示例,遍历一个嵌套的列表(展平操作):

1
2
3
4
5
6
7
8
9
def flatten(lists):
for i in lists:
if isinstance(i,list):
flatten(i)
else:
print(i)

a=[0,[1,2,3],[4,5,[6,7]],[8,9,10]]
flatten(a) #0 1 2 3 4 5 6 7 8 9 10

改写为生成器(这个改写是错误的):

1
2
3
4
5
6
7
8
9
10
def flatten(lists):
for i in lists:
if isinstance(i,list):
flatten(i) #位置2
else:
yield i #位置1

a=[0,[1,2,3],[4,5,[6,7]],[8,9,10]]
for i in flatten(a):
print(i) #仅仅打印出0

这个结果似乎出人意料,分析一下,设g=flatten(a)for语句第一次调用gnext()方法,由于列表a的第一个元素是数字,因此函数执行流将到达位置1处中止并返回yield表达式值,即0,第二次调用next()方法,执行流从中止处继续,由于列表a的第二个元素是列表,因此到达位置2处,关键即在于此,位置2处的flatten(i)将返回一个生成器,然而生成器是惰性的,在没有调用next()方法的情况下生成器根本不会执行(修正的办法就是手动去迭代这个生成器),更何况返回的生成器对象还没有被赋值,没有外部引用的它随即会被当作垃圾回收,因此完全可以忽略它的存在,即可以将位置2处的语句替换成pass,于是执行流继续向后执行,而列表a中的后面的两个元素仍都是列表对象,因此执行流不会再到达位置1,生成器也不会再生产值,执行流结束将抛出StopIteration异常,分析结束,可知g这个迭代器的长度仅为1。修正:

1
2
3
4
5
6
7
def flatten(lists):
for i in lists:
if isinstance(i,list):
for j in flatten(i):
yield j
else:
yield i

序列操作三大函数:

  • map(func,seq1[,seq2,...])
    批量映射函数,map将序列中的元素映射或转换成其他元素,func充当转换器

    1. 当只有一个序列参数时,map(func,seq)等同于[func(e) for e in seq]
    2. 当有多个序列参数时,map(func,seq1,seq2,...)等同于[func(*t) for t in [*zip(seq1,seq2,...)]](这些序列的长度的长度可以不一致,按最短的来,在python2中,当funcNone时,map就等同于zip,但python3已不再允许func参数为None
      打个比方,如果将seq1seq2、… 各自比作飞机的不同的零件提供商,那么map函数就是批量制造飞机的工厂,func就是制造飞机的原理图纸
    zip函数

    简单应用:

    • 快速构建字典

      1
      2
      3
      4
      >>> dict(zip(range(5),range(5,10))) #通过dict类初始化函数或者说工厂函数
      {0: 5, 1: 6, 2: 7, 3: 8, 4: 9}
      >>> {k:v for k,v in zip(range(5),range(5,10))} #通过字典生成式
      {0: 5, 1: 6, 2: 7, 3: 8, 4: 9}
    • 利用zip完成二维矩阵转置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      a = [[11, 12, 13], [14, 15, 16], [17, 18, 19], [20, 21, 22]] #按行描述的矩阵,现将该矩阵按列描述,即为转置的结果
      for i in a: #原矩阵
      print(i)
      #利用列表生成式进行转置
      for i in [[row[col] for row in a] for col in range(len(a[0]))]:
      print(i)
      #这个生成式正是zip压缩的过程,因此直接用zip很简便:
      for i in zip(*a): #*为解包裹函数参数传递
      print(i)
    • 将序列按连续k个值分组

      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
      #假设是将序列a按连续两个值分组
      a = [1, 2, 3, 4, 5, 6, 7]

      #大佬写法1
      for i in zip(*[iter(a)]*2): #操作符优先级为:zip(*([iter(a)]*2)),这里的*是函数参数的解包裹传递方式,就是将一个序列解包为诸多的位置参数传给函数
      print(i)

      #普通写法2
      b=iter(a)
      for i in zip(b,b): #另注意不能构成长度为2的分组的部分元素(残值)将被丢弃
      print(i)

      '''OUTPUT
      (1, 2)
      (3, 4)
      (5, 6)
      '''

      #正常人写法3
      myslice=lambda a,k:[a[i:i+k] for i in range(0,len(a),k)] #缺点在于假设传入的a万一是一个迭代器(无法计算长度,无len()方法),那就需要转为列表或其它可计算长度的对象类型,相比之下,zip调用next()更鲁棒。这种写法不丢弃“残值”
      print(myslice(a,2)) #[[1, 2], [3, 4], [5, 6], [7]]

      #正常人写法4
      for i in zip(a[::2],a[1::2]): #相邻2个元素
      print(i)
      for i in zip(a[::3],a[1::3],a[2::3]): #相邻3个元素
      print(i)
      # 假设要相邻k个元素,就要进行k次切片,显然很麻烦,改写一下:
      myzip=lambda k:zip(*[a[i::k] for i in range(k)])
      for i in myzip(4):
      print(i)

      解释:先说一下zip函数的工作原理,函数接收任意数量的可迭代对象参数,第一步利用iter()将参数全部转换为迭代器对象,第二步在一个while循环中,每一轮,依次获取每个迭代器的下一项,然后组装成一个元组,并追加到返回值序列中,当最短的迭代器对象抛出StopIteration异常时,退出while循环。zip原理(看看就好):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      def myzip(*args):
      '''the principle of function zip'''
      m=list(map(iter,args))
      try:
      ret=[] #返回值序列
      while 1:
      li=[]
      for i in range(len(m)):
      li.append(m[i].__next__())
      ret.append(tuple(li))
      except StopIteration:
      pass
      return ret
      ret=myzip([1,2,3],[4,5,6,7],[8,9,10,11,12],[13,14])
      print(ret) #[(1, 4, 8, 13), (2, 5, 9, 14)]

      '''或者:
      def myzip(*args): #直接根据最短的列表长度确定轮回次数nloop
      m=list(map(iter,args))
      nloop=min([len(args[i]) for i in range(len(m))])
      #ret=[tuple([m[i].__next__() for i in range(len(m))]) for j in range(len(list(m[0])))] #和下面修改的区别在于:list(one iteration)会导致对迭代器(参数)的一遍迭代,结果想再调用next就会立即抛出异常,由此我们可以大概知道工厂函数list的原理:list(iterable obj),首先将可迭代参数对象转为迭代器,然后迭代(next)整个迭代器得到新的列表对象,其中的元素都是对参数对象中元素的引用
      ret=[tuple([m[i].__next__() for i in range(len(m))]) for j in range(nloop)]
      return ret
      '''

      现在回头看写法2就非常明了了,写法1就是换个写法而已,前提是你要知道[obj]*i将返回一个长度为i的序列[obj,obj,...],其中每一项都是对原始的obj对象的引用(或者说浅复制),如果是按连续k个值分组,那就将zip(*[iter(a)]*2)中的2换成k即可
      注:实际上zip()的返回值是一个类似生成器的对象,即用即弃

    • 滑动窗口

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      from itertools import islice
      def slicing(obj,n):
      #obj为可迭代序列,n为窗口大小或者说步长,每次滑动我们看见的序列内容都不一样,函数返回全部窗口滑动状态下的序列的内容视图
      return zip(*[islice(obj,i,None) for i in range(n)]) #注意obj只能是可迭代对象,不能是迭代器,因为islice会消耗迭代器
      for i in slicing([2,3,8,5,1],3):
      print(i)

      '''OUTPUT
      (2, 3, 8)
      (3, 8, 5)
      (8, 5, 1)
      '''
    • 反转字典

      1
      2
      3
      >>> m = {'a': 1, 'b': 2, 'c': 3, 'd': 4} #字典value必须都是不可变对象
      >>> dict(zip(*list(zip(*m.items()))[::-1]))
      {1: 'a', 2: 'b', 3: 'c', 4: 'd'}
  • filter(func,seq)
    序列过滤器,过滤掉不符合条件的元素,返回那些符合条件的元素所组成的新(子)序列,func充当筛选器(应返回TrueFalse)。等同于[e for e in seq if func(e)]

  • reduce(func,seq[,initial])
    该函数工作流程是:在迭代seq的过程中,首先将前两个元素传给func函数(可以通过initial参数额外指定首个元素值,相当于序列seq增加了一个元素,新序列为[initial,*seq]),函数加工后将返回值和seq序列的第三个元素重新传入func函数,再将返回值和seq的第四个元素一起传入func函数,以此类推,直至遍历到seq的最后一个元素,最后一次执行的func函数输出将作为reduce函数的返回值
    注:python3从全局空间移除了该函数,需要从标准库functools中引入
    reduce最大的特点在于,过程中每一次迭代都携带着上一次的迭代结果,这种对历史的可见性有时候很有用。记忆力是个好东西
    案例1(计算阶乘):

    1
    2
    3
    >>> from functools import reduce
    >>> reduce(lambda x,y:x*y,range(1,6)) # 也可以写成:import operator;reduce(operator.mul,range(1,6) #计算5!
    120

    案例2(将整数列表拼成整数):

    1
    2
    >>> reduce(lambda x,y:x*10+y,[1,2,3,4,5])
    12345
    案例3(计算斐波那契)

    已知斐波那契数列:1、1、2、3、5、8、13、21、34、……
    要求:输出数列第n项的值

    1
    2
    3
    4
    #根据数列规律:当前项的值等于前两项的和,即在迭代过程中只要保留最近的历史(前两项元素值)即可,因此基调确定,即reduce中的func参数应该返回一个二元组,而参数seq其实对计算没用,仅用于迭代。且实际假设斐波那契序列为(增加首项0):0,1,1,2,3,5,8,...,然后将前两项(0,1)作为reduce的initial参数,于是reduce首次迭代可以计算出第三项数值(前两项和)为1,并使func返回第二项和第三项的值(1,1),然后第二次迭代可以计算出第四项数值为2,并使func返回第三项和第四项的值(1,2),以此类推
    from functools import reduce
    fib=lambda n:1 if n==1 else reduce(lambda x,y:(x[1],x[0]+x[1]),range(n-1),(0,1))[1]
    print(fib(10)) #55
    其他实现

    普通递归:

    1
    2
    3
    4
    5
    6
    7
    8
    def Fibonacci(n=6):
    if n==1 or n==2:
    return 1
    else:
    return Fibonacci(n-2)+Fibonacci(n-1)

    ret=Fibonacci(10) #由于递归层数限制,如果n取值过大,会爆栈,所以严格来说,这种写法是错误的,另一个关键问题是递归计算实在是太耗时了,其具有指数级的时间复杂度!试试求取第35数值,已经明显迟钝,如果把上述递归树画出来,其实有大量结点是重复计算,关于速度的改进参见第二章节·装饰器
    print(ret)

    看看大佬们是怎么写的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def fib1(n):
    return n<=2 and 1 or fib1(n-1)+fib1(n-2) #秀我一脸

    #等价于:
    def fib2(n):
    return 1 if n<=2 else fib2(n-1)+fib2(n-2)

    #形式再简化:
    fib3=lambda n:1 if n<=2 else fib3(n-1)+fib3(n-2)
    andor

    andor执行布尔逻辑运算,但是在python中返回值并非TrueFalse,其返回的是两端的比较值之一

    运算规则:

    • and运算规则
      1. and左端表达式为真时,继续对右端表达式进行计算并返回该(右端)表达式的值
      2. and左端表达式为假时,立即返回该(左端)表达式的值
    • or运算规则
      1. or左端表达式为假时,继续对右端表达式进行计算并返回该(右端)表达式的值
      2. or左端表达式为真时,立即返回该(左端)表达式的值

    两规则的第二条体现了andor的截断作用(其右端表达式没有被计算),因此andor又被称为“短路运算符”(再次注意只有在and左边为假或者or左边为真才会表现出该特性)

    1
    2
    print(0 and (3-2)) #0
    print(1 or 0) #1

    简单使用:

    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
    #(一)and和or单独使用(多个and或者多个or)
    #and:从左到右扫描,返回第一个为假的表达式值,无假值则返回最后一个表达式值。简单说就是:and返回真,除非遇见一个假,则返回假
    print(-1 and 'dy' and ...) #注意负数、字符串都是逻辑为真,而...其实是一个Ellipse对象(该对象没有任何属性,python也没有任何内置类型使用该对象),其逻辑为真
    print(True if -1 and 'dy' and ... else False) #建议使用all([-1,'dy',...])替代
    #or:从左到右扫描,返回第一个为真的表达式值,无真值则返回最后一个表达式值。简单说就是:or返回假,除非遇见一个真,则返回真
    print(0 or 0 or 1 or 2 or 0) #1
    print(True if 0 or 0 or 1 or 2 or 0 else False) #建议使用any([0,0,1,2,0])替代

    #(二)and-or搭配使用
    #and和or是同级运算符,计算顺序从左到右
    #经典案例之逻辑表达式(condition and a or b):
    a,b="JAVA","PYTHON"
    print(1 and a or b) #JAVA
    print(0 and a or b) #PYTHON
    #我们可以通过控制condition的真假,控制输出a还是输出b。语义即为:“若 真 则返回 a,否则返回 b”,等同于C++中的条件运算符(bool? a:b),但是必须注意:a和b都必须是逻辑真的

    print(1 and 0 or a and b and 0 and a or b) #b
    #对于一个比较复杂的混合应用,没有别的办法,只能从左到右由内而外层层计算:((((((1 and 0) or a) and b) and 0) and a) or b)

    #由于(condition and a or b)中a和b都要满足逻辑真,这如何确保?先看个反例:
    a,b=[],1 #a是逻辑假的,此时and-or实现的条件运算将失效
    print(True and a or b) #1
    print(False and a or b) #1

    #还是有安全的解法的,如下(套个[]强制变成逻辑真,新的逻辑表达式变为(condition and [a] or [b])[0]):
    a=''
    b="MSY"
    print((0 and [a] or [b])[0])

    #通常情况下,还是建议if-else实现条件运算符的功能,以增强程序的可读性,装B除外
    print('daiyang' if False else 'daixiaodong') #daixiaodong

    上述小结(可跳过代码):

    and-or实现条件运算:(condition and [a] or [b])[0]等同于condition ? a : b

    all([condition1,condition2,...])等同于True if condition1 and condition2 and ... else False

    any([condition1,condition2,...])等同于True if condition1 or condition2 or ... else False


    全部逻辑运算
    运算 名称 逻辑函数表示 短释 实现
    A AND B 与门 A·B 所有输入为高时,才有高的输出,一低出低 A and B
    A OR B 或门 A+B 所有输入为低时,才有低的输出,一高出高 A or B
    NOT A 非门 Aˉ\bar{A} 根据输入的高低逆转后输出 not A
    A NAND B 与非门 AB\overline{A·B} 所有输入为高时,才有低的输出,一低出高 not (A and B)
    A NOR B 或非门 A+B\overline {A+B} 所有输入为低时,才有高的输出,一高出低 not (A or B)
    A XOR B 异或门 ABA\oplus B 输入为一高一低时,才有高的输出,否则出低 (A and not B) or (not A and B)
    A XNOR B 同或门 AB\overline {A\oplus B} 输入为一高一低时,才有低的输出,否则出高 (A and B) or (not A and not B)
    BUF A 是门 A 输出与输入相同的高低状态 A
    A IMPLY B 蕴含门 ABA\rightarrow B 第一输入为低时输出高,否则输出同第二输入 (not A) or B
    A NIMPLY B 蕴含非门 AB\overline {A\rightarrow B} 第一输入为低时输出低,否则输出与第二输入相反 A and (not B)

    注1,表中的A、B均为二值型数据,要么是0,要么是1。最后一列是python代码实现,但不仅适用于二值0和1或布尔值TrueorFalse,且适用于其他任何自定义对象,只要实现__bool__()方法即可

    注2,上表中除了前三个门是基础外,其他门都可以用这三个门实现,譬如:“异或门”AB=AB+ABA\oplus B=A·\overline B+\overline A·B、“同或门”AB=AB+AB\overline {A\oplus B}=A·B+\overline A·\overline B、“蕴含门”AB=A+BA\rightarrow B=\overline A+B、“蕴含非门”AB=AB\overline {A\rightarrow B}=A·\overline B。记忆:“与非门”是对“与门”输出取反,“或非门”是对“或门”输出取反,“同或门”是对“异或门”输出取反,“蕴含非门”是对“蕴含门”输出取反

    在某些语言如C中可以使用尾递归解决递归爆栈问题,虽然python本身不支持尾递归,但是存在一些解决方案可以间接实现尾递归,下面我先给出尾递归形式的写法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def fib5(n): #计算原理同下面循环版本的fib4
    def fib_iter(n,n1,n2):
    if n==0: return n2
    else: return fib_iter(n-1,n2,n1+n2)
    return fib_iter(n,1,0)

    #lambda形式的尾递归
    fib6=lambda n,n1=1,n2=0:n2 if n==0 else fib6(n-1,n2,n1+n2) #同fib5

    #and-or形式的尾递归
    fib7=lambda n,n1=1,n2=0:n==0 and n2 or fib7(n-1,n2,n1+n2) #同fib6
    python实现尾递归

    在计算机中,函数调用是通过“栈”这种数据结构实现的,每调用一个函数,就会往栈中压入一个栈帧对象,其中保存着函数所对应的可执行代码(即代码对象,通常由compile()函数返回,可通过栈帧对象framef_code属性访问)、变量命名空间(包含frame.f_locals局部变量、frame.f_globals全局变量和frame.f_builtins内置名称)以及其他一些属性(如frame.f_back,表示对当前调用者而言的上一个栈帧对象),当函数执行结束,就会从栈中删除当前栈帧,如果函数没有结束就又发生了其他函数调用,栈中的帧数就会线性增加,由于栈的大小不是无限的,对于递归函数,如果递归过深,很容易导致栈溢出错误

    1
    2
    3
    >>> import sys
    >>> sys.getrecursionlimit #python允许的最大递归深度
    1000

    那么旧的栈帧能不能删除呢?一般来说递归调用的返回结果是要为上层函数所使用的,即递归的回溯过程通常不可避免,所以不能删除。尽管如此,由于尾递归的特殊性(尾调用由于是函数执行流的最后一步,调用结果不需要反馈给调用者,因此在这种情况下旧的栈帧可以直接销毁,此即为“尾调用优化”),决定了这种递归形式在执行过程中是不需要回溯的,于是可以把原本需要线性复杂度栈内存空间的执行过程用常数复杂度的空间完成,实现该优化的解释器或编译器可以使得递归本身无论调用多少次,都只占用一个栈帧。本质上尾递归和循环没什么区别,要效率,还是建议循环

    大佬实现的TCO(Tail-Call Optimization)装饰器(原理简单来说就是当发现尾递归调用的函数在栈帧中重复出现时,手动抛出异常并携带着最新调用的函数参数返回上层,从而使之后重复调用的函数栈帧被销毁):

    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
    class TailRecurseException(Exception):
    def __init__(self, args, kwargs):
    self.args = args
    self.kwargs = kwargs

    def tail_call_optimized(g):
    """
    This function decorates a function with tail call
    optimization. It does this by throwing an exception
    if it is it's own grandparent, and catching such
    exceptions to fake the tail call optimization.

    This function fails if the decorated
    function recurses in a non-tail context.
    """
    def func(*args, **kwargs):
    f = sys._getframe()
    if f.f_back and f.f_back.f_back \
    and f.f_back.f_back.f_code == f.f_code: #位置1
    raise TailRecurseException(args, kwargs)
    else:
    while 1:
    try:
    return g(*args, **kwargs) #位置2
    except TailRecurseException as e:
    args = e.args
    kwargs = e.kwargs
    func.__doc__ = g.__doc__
    return func

    使用案例(尾递归实现的阶乘计算):

    1
    2
    3
    4
    5
    6
    7
    8
    @tail_call_optimized
    def factorial(n, acc=1):
    "calculate a factorial" #函数__doc__文档
    if n == 0:
    return acc
    return factorial(n-1, n*acc) #位置3

    print(factorial(1000))

    解读:首先得知道装饰器的基本原理,于是有factorial=tail_call_optimized(factorial),即变量名factorial实际指向函数func,而func内部的变量g实际指向函数factorial。在执行factorial(1000)时,由函数func构成的栈帧将首次被压入函数调用栈中,程序执行到位置1,不符合if条件,于是执行到位置2,再一次发生函数调用,此时由函数factorial构成的栈帧将被压入栈中,程序继而执行到位置3并函数调用,于是由函数func构成的栈帧将再次被压入栈中,然后执行到位置1发现满足条件(当前栈帧的前两个栈帧存在且上上个栈帧的代码对象和当前栈帧的代码对象相同,即发生重复调用),于是向上层抛出异常(具体返回到哪一层就看哪一层捕捉到了该异常),同时携带着最新的调用参数,显然程序将返回到第一个栈帧(即第一个函数调用)的位置2处(此时栈内就只有这一个帧了),此处异常被捕捉,由于处在一个while循环中,于是又来到位置2并发生函数调用,函数factorial构成的栈帧将被压入栈中,来到位置3并发生函数调用,函数func构成的栈帧将被压入栈中,判断if条件,再次满足条件,抛出异常…,依此类推

    之前的斐波那契问题,使用TCO修饰:

    1
    2
    3
    4
    5
    6
    def fib5(n):
    @tail_call_optimized
    def fib_iter(n,n1,n2):
    if n==0: return n2
    else: return fib_iter(n-1,n2,n1+n2)
    return fib_iter(n,1,0)

    注意,TCO只能用于尾递归,否则出错,示例如下:

    1
    2
    3
    4
    5
    6
    7
    @tail_call_optimized
    def f(n=6): #此递归函数功能:每深入调用一次递归函数,计数器增1,即函数返回递归的深度
    if n==0:
    return 0
    return 1+f(n-1)

    print(f()) #0 #去掉TCO时返回6

    最正确且效率最高的当属循环:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #能用循环解决的问题,尽量不要使用递归,有时候递归可以极大简化问题,那也应当在合理的递归深度范围内,否则意味着错误必将发生
    def fib4(n):
    n1,n2=1,0
    while n:
    n1,n2,n=n2,n1+n2,n-1
    return n2

    print(fib4(10000))

    #顺便给出打印完整数列的方法,稍微改造上述fib函数即可
    from itertools import count
    def all_fib(n):
    n1,n2=0,1
    for i in count():
    if i==n:break
    print(n2,end=',')
    n1,n2=n2,n1+n2

    all_fib(6) #1,1,2,3,5,8,13,21,34,55

字典又名哈希表、散列表,实现一个哈希表需要讨论两大基本问题:地址映射和冲突处理

  1. 地址映射
    一个好的hash方法是在对象不相同的情况下能产生不相等的哈希值,这是最理想的情况,退而求次,hash方法应尽力将数据集合中不相同的对象均匀分布到所有可能的地址上面,即通过哈希函数应能得到一个“尽可能随机的地址”

  2. 冲突处理
    如果两个不同对象映射到了相同的地址,将产生冲突,无论怎样精心构造hash函数,这种冲突都不可避免,但是可以解决,如开放定址(探测再散列)、再哈希、链式、公共溢出区,其中链表法是比较常见的冲突处理办法,python采用开放定址办法

因此当我们对字典成员对象进行操作时,需要能够计算对象的哈希并比较对象之间的异同,特别是用户自定义类型,需要用户自己实现。先说说几个特殊方法(也叫“魔法方法”,以双下划线开头且以双下划线结尾,这些方法会在进行特定操作时被自动调用,譬如运算符重载方法,如上面提到过的对象比较==相当于调用对象的__eq__()方法、加法+相当于调用对象的__add__()方法等):

  • object.__hash__(self)

    注:这是object基类所定义的哈希方法签名,下同

    __hash__()会在以下情况被调用:1)由内置方法hash()调用(hash(obj)即等同于obj.__hash__()),2)对散列集合成员进行操作时被调用,散列集合类型包括dictsetfrozenset。注意__hash__()应当返回一个整数,对于用户自定义类型,我们建议将对象的不同组件打包进元组,然后计算元组的哈希值,譬如:

    1
    2
    def __hash__(self):
    return hash((self.name, self.sex, self.age))
  • object.__getattribute__(self, name)

    实例属性访问时始终会调用__getattribute__()方法(类属性访问则不会调用之,很明显一个道理,__getattribute__()是实例方法嘛,怎么可能会调用它,当类属性不存在时也将触发AttributeError异常,而这就更不太可能触发调用__getattr__()了),如果还定义了__getattr__(),则除非__getattribute__()显式调用它或者其中引发了AttributeError异常(属性不存在的默认行为),否则不会调用后者。为了防止在此方法中陷入无穷递归,其实现应始终调用具有相同名称的基类方法来访问其所需的任何属性,如object.__getattribute__(self, name)(之所以要传入当前实例self,是因为在类上访问实例方法返回的是“非绑定方法”(即原始函数对象),而在实例上访问方法返回的则是“绑定方法”,所谓绑定,即绑定实例,实例方法访问返回的其实是一个闭包对象,譬如x.method返回的相当于partial(method,self=x)),不过一般还是使用super().method(arg)实现基类方法调用。下面的错误示例将导致无限递归(但受最大递归层数限制):

    1
    2
    3
    4
    5
    6
    7
    8
    class A:
    def __init__(self,name):
    self.name=name

    def __getattribute__(self,item):
    return self.__dict__[item] #__dict__中存放着对象上绑定着的实例属性,若你想从中取值,self.__dict__又将调用self.__getattribute__()方法,从而陷入无穷递归

    print(A('a').name) #RecursionError: maximum recursion depth exceeded while calling a Python object
    修改:
    1
    2
    def __getattribute__(self,item):
    return super(A,self).__getattribute__(item) #在python3中也可以简写为super(),完全等同于super(A,self)
    遇到的问题

    我写了下面的代码,但是对输出感到困惑:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class A:
    def __init__(self,name):
    self.name=name

    def __getattribute__(self,item):
    print('属性访问:',item)
    print('object的__getattr__方法:',super(A,self).__getattr__) #?
    print(super(A,self).__getattr__(item)) #super到底发生了什么?
    return super(A,self).__getattribute__(item)

    def __getattr__(self,item):
    return 'daiyang'

    print(A('a').name)

    '''OUTPUT
    属性访问: name
    daiyang
    '''

    首先我们来看一下super(…)的原理(仅用于理解,因为super本身是一个类):

    1
    2
    3
    def super(cls, inst):
    mro = inst.__class__.mro()
    return mro[mro.index(cls) + 1]

    MRO(method resolution order)列表记录了类继承体系中的成员解析顺序,每个类都有这样的属性,可以用来解决python中钻石继承的难题,核心就在于它将非线性的父类继承顺序通过C3算法转换成线性顺序。因此以后不要再单纯地认为super()返回的就是当前类的父类了,这不一定(单继承不用怀疑,肯定是父类啦,指的是多继承的情况下不一定),也可能是兄弟类(当然了,不管是“父类”还是“兄弟类”都是不严谨的说法,因为返回的是super类实例),看个例子:

    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
    class Base(object):
    def __init__(self):
    print("enter Base")
    print("leave Base")

    class A(Base):
    def __init__(self):
    print("enter A")
    #print(self.__class__.mro()) #[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Base'>, <class 'object'>]
    super(A,self).__init__() #根据MRO列表可知,super(A,self)返回的是类B,注意了,这也是为什么说super()返回的不一定是父类,此处返回的是类A的兄弟B
    print("leave A")

    class B(Base):
    def __init__(self):
    print("enter B")
    #print(self.__class__.mro()) #[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Base'>, <class 'object'>]
    super(B,self).__init__() #根据MRO列表可知,super(B,self)返回的是类Base
    print("leave B")

    class C(A,B):
    def __init__(self):
    print("enter C")
    #print(self.__class__.mro()) #[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Base'>, <class 'object'>]
    super(C,self).__init__() #根据MRO列表可知,super(C,self)返回的是类A,准确来说返回的是绑定了self实例的super类对象(bound super object),可以通过__self__属性查看绑定的实例,super(C,self).__self__
    print("leave C")

    c=C()

    '''OUTPUT
    enter C
    enter A
    enter B
    enter Base
    leave Base
    leave B
    leave A
    leave C
    '''

    事实上,super(cls,inst)有两种用法,一是上述所说,参数1是类,参数2是实例,且必须满足isinstance(inst,cls)True的条件,二是参数2也可以是一个类,此时则必须满足issubclass(inst,cls)True的条件,两者返回的都是绑定了inst(应该是绑在__self__属性中)的super对象,注意并不是预期的父类哦,如果绑定的inst是实例,那么该super对象可以调用父类的实例方法,如果绑定的inst是类,那么该super对象则可以调用父类的类方法,当然也可以访问父类的其他类属性(包括类中定义的静态方法、非方法属性),如果父类中找不到的话,则会到父类的基类中继续搜寻,如果一直没有找到将抛出AttributeError

    回到原题,super(A,self).__getattr__理论上应该返回基类object相应的绑定方法(绑定了类A实例self),问题在于object类并未定义__getattr__()方法啊(参考书[1]出来挨打O(∩_∩)O),当访问一个不存在的属性将引发AttributeError异常,注意这个异常是在类A__getattribute__()内部引发的,而由于还定义了__getattr__(),因此程序最终输出了字符串daiyang。我们可以捕捉该异常:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class A:
    def __init__(self,name):
    self.name=name

    def __getattribute__(self,item):
    print('属性访问:',item)
    try:
    print('object的__getattr__方法:',super(A,self).__getattr__)
    except AttributeError as e:
    print('异常捕捉:',e)
    return super(A,self).__getattribute__(item)

    def __getattr__(self,item):
    return 'daiyang'

    print(A('a').name)

    '''OUTPUT
    属性访问: name
    异常捕捉: 'super' object has no attribute '__getattr__'
    a
    '''
  • object.__getitem__(self,key)
    执行self[key]时被调用,对于序列类型,接受的键应该是整数和切片对象,请注意,负索引的特殊解释(如果类希望模拟序列类型)取决于__getitem__()方法,如果key是不合适的类型,则可能引发TypeError,如果是序列的索引集合之外的值,则应引发IndexError,对于映射类型,如果缺少键(不在容器中),则应引发KeyError
  • object.__setitem__(self,key,value)
    执行self[key]=value时被调用

一般我们使用内置的标准字典或集合,甚至不需要子类化进行任何定制,即使有其它方面的需求,也有第三方库提供的数据结构供我们使用,譬如defaultdictOrderedDict。但是自定义成员对象并通过字典等存取时,基本都得重写__hash__()__eq__()方法,这一点很重要,现假设我们要向集合中存放自定义类对象,如:

1
2
3
4
5
6
7
8
9
class man:
def __init__(self,name,sex,age):
self.name=name
self.sex=sex
self.age=age

man1=man('muggle','male',22)
man2=man('muggle','male',22)
hash(man1)==hash(man2) #False

如你所见,两个“相同”(只要姓名、性别、年龄都一样,我们就认为两个人是同一人啊)对象的哈希值不一样,将它们存放到字典s=set([man1,man2])中的后果就是,字典s中竟然“有两个一毛一样的人”,这实在是匪夷所思,原因即在于,执行映射类容器相关操作的时候(譬如向字典中添加一个键值对,或查询某键的值),会先后调用键(此处man实例对象就是“键”)对象的__hash__()以及__eq__(),现在我们两个对象man1man2的哈希值不一样,而且默认的__eq__方法也判断两个对象不相同,尽管哈希值不一样的键也有可能落到同一个“桶”里面,但是现在这两对象也不equal啊,于是这两个对象必然不同且同时存在于集合中,解决的办法就是为man类重写__hash__()__eq__()(一般修改其中任意一个方法,另一个也要相应修改),它们必须满足以下两大规则:

  1. 如果两个对象相同,即obj1.equal(obj2)==True,则两个对象的哈希地址映射也必须相同,即hash(obj1)==hash(obj2)(否则对象明明存在于容器中,却找不到)
  2. 如果两个对象的哈希地址映射不同,即hash(obj1)!=hash(obj2),则两个对象必须不相同,即obj1.equal(obj2)==False(否则容器中将存在同一对象的多个“副本”,浪费存储资源)

修改后的man类已满足上述规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class man:
def __init__(self,name,sex,age):
self.name=name
self.sex=sex
self.age=age

def __hash__(self): #用姓名、性别和年龄判断两个人是否相同,若相同,此时他们的哈希也必然相同,但是相同的哈希也可能对应不同的人,此时就要靠equal方法来判断了
return hash((self.name, self.sex, self.age))

def __eq__(self,other):
if self.name==other.name and self.sex==other.sex and self.age==other.age:
return True
else:
return False

关于__hash__()__eq__(),官方文档中有非常明确的指示:

  1. 如果重写了__eq__(),必须重写__hash__(),因为两个“相同”对象的哈希地址映射不一定相同,为了确保这一点,必须重写__hash__()加以保证。反之,如果重写了__hash__(),也必须重写__eq__(),因为两个哈希地址相同的对象,如果是相同对象,__eq__()也必须相同,否则不同,若两个对象哈希地址不同,则__eq__()必须不同,否则出错

  2. 如果只重写了__eq__(),那么该类对象将不能用于映射类容器,因为此时__hash__()将隐式地被赋为None,即此时__hash__方法失效,该类实例将不可哈希。如果想使用父类的__hash__(),需要明确说明:

    1
    2
    3
    4
    class B(A):
    __hash__=A.__hash__
    def __eq__(self,other):
    pass
  3. 如果没有重写__eq__(),又希望使__hash__()失效,应明确赋值为None

    1
    2
    3
    4
    5
    class A():
    __hash__=None

    s=set()
    s.add(A()) #TypeError: unhashable type: 'A'
  4. 如果可变对象的类实现了__eq__(),就不必再为之重写__hash__()了,因为即使你重写了一个看似不会抛异常的__hash__(),但是由于对象可变,它总是会出错(可变对象就不能作为字典或集合的“键”)。举个例子,我们子类化列表,重写__eq__(),规定只要新列表的第一个元素相同,那么两个新列表就相同,同时重写__hash__(),使它返回新列表第一个元素的哈希值,这一设定是合理的,并假设新列表的第一个元素我们总是使它是可哈希的,我们创建一个新列表的实例a=['muggle'],设对象a的哈希地址为addr1,将其添加到集合s中,于是s={['muggle'],},我们修改a的第一个元素值为'jingjing',结果就是addr1地址上存放的字符串从muggle变成了jingjing,现在新建一个新列表实例b=['jingjing'],设对象b的哈希地址为addr2,且addr1!=addr2,将b也添加到s中,于是s={['jingjing'],['jingjing'],},可是根据我们的__eq____hash__方法,显然有a==b以及hash(a)==hash(b)成立啊,即这两个对象“完全相同”,而不该在集合中重复出现!代码如下:

    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
    class MyArray(list):
    def __init__(self,*args):
    for i in args:
    self.append(i)

    def set(self,index,value):
    self[index]=value

    def __hash__(self):
    return hash(self[0])

    def __eq__(self,other):
    if self[0]==other[0]:
    return True
    else:
    return False

    a=MyArray('muggle')
    s=set()
    s.add(a) #s={['muggle']}
    a[0]='jingjing' #{['jingjing']}
    b=MyArray('jingjing')
    s.add(b) #{['jingjing'], ['jingjing']}
    hash(a)==hash(b) #True
    a==b #True
  5. 用户自定义类默认的__hash__()__eq__()继承自基类object,它们都是非常严格的,判断两个实例相不相同是根据唯一身份标识id,哈希也是对身份标识进行哈希

字典的使用和一些注意点:

  1. Python2中判断某个key是否存在字典中可使用has_key()方法,另外一种方式是使用in关键字(key in dict),推荐使用后者,因为in的处理速度更快,另一个原因是has_key()这个方法在Python3被移除了

  2. 获取字典中某个键的值,两种方式:dict[key]dict.get(key[, default]),前者简便但如果键不存在会导致KeyError异常,为了避免异常不得不先进行键存在性的判断,或者捕捉异常,如果不想手动捕捉异常那就子类化字典并重新定义字典的__missing__()方法以避免异常的抛出,该特殊方法只有在使用dict[key]key不存在时才会被调用。一般我们使用dict.get()方法,键不存在则返回None(可通过default参数修改该默认值),但并不会修改底层数据,这不同于defaultdict类或dict.setdefault(key,default),示例:

    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
    class MyDict(dict):
    def __missing__(self,key):
    #self[key]=0 #通常都会这么做,修改底层数据,否则就没有意义了,因为还不如直接通过dict.get(key,default)设置缺省值
    return 0

    d=MyDict()
    for k in '11422134431': #计数器
    d[k]+=1
    print(d) #{'1': 4, '4': 3, '2': 2, '3': 2}

    #或者:
    d=dict()
    for k in '11422134431':
    d[k]=d.get(k,0)+1
    print(d) #{'1': 4, '4': 3, '2': 2, '3': 2}

    #或者:
    d={}
    for k in '11422134431':
    d[k]=d.setdefault(k,0)+1 #setdefault(key,default=None)和get(key,default=None)方法用法类似,区别是,如果键不存在于字典中,将会添加键并将值设为默认值,即会直接修改底层数据。如果字典中包含有给定键,则返回键所对应的值,否则返回为键设置的default值
    print(d) #{'1': 4, '4': 3, '2': 2, '3': 2}

    #或者:
    from collections import defaultdict

    d=defaultdict(int) #defaultdict类的初始化函数接受一个工厂函数作为参数,作用是当key不存在时,返回的是工厂函数的默认值,比如list对应[],str对应的是空字符串,set对应set(),int对应0
    for k in '11422134431':
    d[k]+=1
    print(d) #defaultdict(<class 'int'>, {'1': 4, '4': 3, '2': 2, '3': 2})
  3. 创建字典的几种方式:

    • dict()实例化字典对象(默认无参数时创建一个空字典,也可以直接写d={}),可以传递任意数量的关键字参数,或者是一个二元元组列表以及任意数量的关键字参数,再或者是一个字典和任意数量的关键字参数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      >>> dict()
      {}
      >>> data=[("animal", "bear"),("animal", "duck"),("plant", "cactus"),("vehicle", "speed boat"),("vehicle", "school bus")]
      >>> dict(data) #可以传入一个二元元组列表,实际上只要是可迭代的对象且对象元素也是可迭代对象且这些元素长度一律为2的都可以
      {'animal': 'duck', 'plant': 'cactus', 'vehicle': 'school bus'}
      >>> dict(zip(['a','b','c'],[1,2,3])) #使用zip构造二元组序列
      {'a': 1, 'b': 2, 'c': 3}
      >>> dict((['a',1],['b',2]),c=3) #可以传入关键字参数
      {'a': 1, 'b': 2, 'c': 3}
      >>> dict({1:2,3:4},last=6) #也可以传入一个字典,实际上只要是mapping类型都可以,python标准mapping就是dict
      {1: 2, 3: 4, 'last': 6}
    • dict.fromkeys(seq[,initial])根据序列seq构造字典,列表中的元素将作为键,需另外指定初始值initial,若未指定则值全部为Nonefromkeys()完全可以用字典生成式实现

      1
      2
      3
      >>> d=['first','second','third']
      >>> dict.fromkeys(d,8)
      {'first': 8, 'second': 8, 'third': 8}
    • 字典生成式,现在有两个序列,分别为一一对应的键值序列,将其组装为字典:

      1
      2
      3
      4
      >>> a=['first','second','third']
      >>> b=[1,2,3]
      >>> d={k:v for k,v in zip(a,b)} #for子句中有一个变量解构操作
      {'first': 1, 'second': 2, 'third': 3}
  4. 字典更新

    • d.update(other_d),用另一个字典other_d更新原字典d,对于原字典中不存在的键值对直接增添,若存在,则更新旧值为新值

      1
      2
      3
      >>> d={'1':1,'2':2,'3':3}
      >>> d.update({'1':11,'4':4,'5':5})
      {'1': 11, '2': 2, '3': 3, '4': 4, '5': 5}
    • d[key]=value方式用于添加一个新的键值对或者更新某个键的旧值

  5. 字典元素删除

    • 使用d.pop(key[,default])删除键值对,可以指定默认的返回值,可以避免由于键不存在而导致的KeyError

      1
      2
      3
      4
      5
      6
      7
      >>> d = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
      >>> d.pop('e')
      Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      KeyError: 'e'
      >>> d.pop('e',None)
      None
    • popitem()返回并删除字典中的最后一对键和值(这里没搞懂,最后一对键值是怎么确定的?):

      1
      2
      3
      4
      >>> d = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
      >>> d.popitem()
      >>> d
      {'a': 1, 'b': 2, 'c': 3}
    • 使用del执行删除,既可以删除整个字典,也可以删除字典的某个键值对,如果删除的键不存在,则会报错

      1
      2
      3
      >>> d = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
      >>> del d['a']
      >>> del d
  6. 字典遍历
    d.keys()d.values()d.items(),其中items()多用于for循环遍历:

    1
    2
    for k,v in d.items():
    print(k,v)

    事实上,字典本身也是可迭代对象,且迭代字典对象等同于迭代字典的键序列:

    1
    2
    3
    >>> d={'a':1,'b':2,'c':3}
    >>> [k for k in d]
    ['a', 'b', 'c']

和字典一样,集合虽然可以迭代,但它们都是无序的,不支持切片和index(数值下标)索引(虽然集合不属于“映射类型”,但由于都是基于散列表实现,所以放在一起描述)

  • 创建集合
    1. 创建一个空集合:
      1
      2
      >>> set()
      {}
    2. 使用现有数据初始化一个集合,set(iterable)构造方法接受一个可迭代对象,如列表:
      1
      2
      >>> set([1,2,3])
      {1,2,3}
  • 查询集合
    集合是无序容器,不能通过下标索引元素,只能遍历:
    1
    2
    for e in set([1,2,3]):
    print(e)

    使用innot in检查某一元素是否存在于集合中:

    1
    2
    >>> 1 in set([1,2])
    True
  • 添加元素
    1. add(item)向集合中添加单项元素:
      1
      2
      3
      4
      >>> s=set()
      >>> s.add('msy')
      >>> s
      {'msy'}

      注:鉴于集合的原理(散列表),向集合中添加的元素既是value又是key,因此添加的元素必须是可哈希的,即集合元素必须是不可变类型

    2. update(iterable)使用一系列元素(可迭代序列)更新集合(s1.update(s2)等同于“并集”操作s1|=s2):
      1
      2
      3
      >>> s={12,13}
      >>> s.update(['muggle',101,'msy'])
      {'muggle', 101, 12, 13, 'msy'}
  • 删除元素
    1. remove(item)从集合中删除元素item,无返回值,如果元素不存在则引发keyError
      1
      2
      3
      4
      5
      6
      >>> s={'msy'}
      >>> s.remove('msy')
      >>> s.remove('msy')
      Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      KeyError: 'msy'
    2. discard(item)remove(item),但是元素不存在时不会引发异常
    3. pop()从集合中随机删除一个元素,由于集合是无序的容器,无法通过下标索引查询元素,因此pop()不接受任何参数(可对比列表的pop()方法),该方法会返回被(某种程度可以认为是“随机”)删除的元素:
      1
      2
      3
      4
      5
      6
      7
      8
      >>> s=set()
      >>> s.update('1a2b3c4d')
      >>> s
      {'1', '2', '3', '4', 'a', 'b', 'c', 'd'}
      >>> s.pop()
      'a'
      >>> s.pop()
      'c'
    4. clear()删除集合中的全部元素,无返回值
  • 交并集数学运算(非原址)
    1. s1.intersection(s2)返回集合s1s2的交集,该操作对应的运算符为s1 & s2
      1
      2
      3
      4
      5
      6
      >>> s1=set([1,2])
      >>> s2=set([2,3])
      >>> s1.intersection(s2)
      {2}
      >>> s1 & s2
      {2}
    2. s1.union(s2)返回集合s1s2的并集,该操作对应的运算符为s1 | s2
      1
      2
      3
      4
      5
      6
      >>> s1=set([1])
      >>> s2=set([2])
      >>> s1.union(s2)
      {1, 2}
      >>> s1 | s2
      {1, 2}
    3. s1.difference(s2)返回集合s1s2的差集(简单来说就是返回集合s1中有的而集合s2中所没有的那些元素),该操作对应的运算符为s1 - s2
      1
      2
      3
      4
      5
      6
      >>> s1=set([1,2,3])
      >>> s2=set([2,3])
      >>> s1.difference(s2)
      {1}
      >>> s1 - s2
      {1}
    4. s1.symmetric_difference(s2)返回集合s1s2的对称差集(简单说就是返回集合s1中有的而集合s2中所没有的,以及集合s2中有的而集合s1中所没有的那些元素,等同于(s1-s2) | (s2-s1)),该操作对应的运算符为s1 ^ s2
      1
      2
      3
      4
      5
      6
      >>> s1=set([1,2,3])
      >>> s2=set([2,3,4])
      >>> s1.symmetric_difference(s2)
      {1, 4}
      >>> s1 ^ s2
      {1, 4}
    注1,并集、交集、差集和对称差集的非运算符版本(non-operator,如s1.union(s2))允许接受任何可迭代对象作为参数,但它们的运算符版本要求操作数必须都是集合类型:
    1
    2
    3
    4
    5
    6
    >>> set([1]).union([2]) #合法
    {1, 2}
    >>> set([1]) | [2] #非法
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    TypeError: unsupported operand type(s) for |: 'set' and 'list'

    注2,我们知道在执行s1-s2的时候,将触发执行s1.difference(s2),但操作执行完毕后s1的值仍保持不变,属于非原址操作,同样-=运算符也适用于两个集合对象,在计算差集之后会立即更新第一个操作数(即完成两个步骤:t=s1-s2; s1=t),属于原址操作,而它实际上将触发执行s1.difference_update(s2)

    1
    2
    3
    4
    5
    6
    7
    >>> s1=set([1,2,3])
    >>> s2=set([2,3,4])
    >>> s1-s2
    {1}
    >>> s1.difference_update(s2) #s1-=s2
    >>> s1
    {1}

    其它原址操作:

    • s1.intersection_update(s2)交集更新操作,对应运算符为s1 &= s2
    • s1.update(s2)并集更新操作,对应运算符为s1 |= s2
    • s1.symmetric_difference_update(s2)对称差集更新操作,对应运算符为s1 ^= s2
  • 父集和子集判断
    1. s1.issubset(s2)判断集合s1是否是集合s2的子集,对应运算符为s1 <= s2
      1
      2
      3
      4
      5
      6
      >>> s1=set([1,2])
      >>> s2=set([1,2,3])
      >>> s1.issubset(s2)
      True
      >>> s1 <= s2
      True
    2. s1.issuperset(s2)判断集合s1是否是集合s2的父集,对应运算符为s1 >= s2
      1
      2
      3
      4
      >>> s2.issuperset(s1)
      True
      >>> s2 >= s1
      True
    注,两个集合只有在这种情况下是相等(s1 == s2,不等关系测试则使用!=)的:集合中的每一个元素同时也是另一个集合中的元素(或者说二者互为子集)。一个集合比另一个集合小(s1 < s2,用于严格意义上的子集测试):只有在第一个集合是第二个集合的子集,且第二个集合不是第一个的子集时成立。一个集合比另一个集合大(s1 > s2):只有在第二个集合是第一个集合的子集,且第一个集合不是第二个的子集时成立
  • “冰冻”集合
    Python提供一个集合的不可变版本frozenset,创建的方式同set,譬如:
    1
    >>> fs=frozenset([1,2,3])

    两者的区别主要在于,set是可变对象,不可哈希,有add()remove()等方法,frozenset则是不可变的,存在哈希值,因此它可以作为字典的键,也可以作为其它集合的元素,缺点是一旦创建便不能更改,没有add()remove()等方法,但是和普通集合一样,支持union()difference()等交并运算以及issubset()等关系运算

    普通集合和冰冻集合可以混用,用于交并运算时,第一个操作数的类型还决定了返回值的类型,譬如第一个操作数是frozenset,第二个是set,那么返回值就是frozenset

    1
    2
    >>> frozenset([1,2,3])-set([1])
    frozenset({2, 3})

    collections模块提供了两个集合类型,SetMutableSet,后者用于判断是否是可变集合


Sponsor