由于页面中的某些内容嵌套在了多重标签插件下,使得宽度变得过于狭窄,特别是手机端体验会很差,建议在电脑端阅读
该笔记内容可能存在一些错误,如果您能够帮助指正,鄙人将非常感谢
对象的身份与类型
Python中一切皆对象 ,包括一些常见的基础数据类型,如数字、字符串、列表、字典等,还有用户所自定义的对象(对象也称为类型的“实例”)。对象具有三大特征:
身份(id),可以理解为在内存中的地址,具有唯一性,可以通过id(obj)
查看
类型(type),对象都是由类实例化产生的,对象所属类型可通过type(obj)
或obj.__class__
查看
值(value),不同对象具有不同值(体现在“属性”和“方法”的不同上),这也是其存在的现实意义
对象被创建后,其身份和类型就不可改变,根据其值是否可变,可以区分为“可变对象”与“不可变对象”
从身份、类型和值三个不同的角度分别比较对象:
1 2 3 4 5 6 7 def compare (a, b ): if a is b: pass if type (a) is type (b): pass if a == b: pass
当使用==
运算符时,请务必确保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 6 7 print (type (object )) print (type (type )) print (object .__base__) print (type .__base__) print (list .__base__) print (type (list ))
isinstance()
类型判断
早先在书上偶然看到一行代码isinstance(A,object) #返回True
,其中A
是一个自定义类,突然很不理解,没回过味来,还在SegmentFault 上提问,好蠢/(ㄒoㄒ)/~~
根据帮助文档(help(isinstance)
)可知:isinstance(obj, class_or_tuple)
判断对象obj
是否是class_or_tuple
中某个类的实例或者其子类的实例。假设A
继承自B
,a=A()
,那么显然isinstance(a,A)
返回True
,由于B
是A
的父类,所以B
是a
所属的更大的类的范畴,所以isinstance(a,B)
也返回True
,即a
是B
的实例。根据上面object
类和type
类之间的关系很容易推知:一切类(包括object
和type
)都是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 ParrotTrue >>> type (a) is BirdFalse >>> 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
补充 :
1 2 3 4 5 6 7 8 9 >>> from collections import Iterator>>> class A :... def __iter__ (self ):... ...... def __next__ (self ):... ...... >>> isinstance (A(), Iterator)True
由此可见,python是根据“鸭子类型”的思想来进行类型判断的,即当看到一只鸟走起来像鸭子、游泳起来像鸭子、声音叫起来也像鸭子,那么这只鸟就可以被认为是鸭子,上述示例尽管A
直接继承自object
,但由于它实现了__iter__()
和__next__()
,因而A()
被认为是一个迭代器对象
对象的引用与复制
在程序进行像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 is a True >>> b[2 ] = -100 >>> a [1 , 2 , -100 , 4 ] >>> c = d = 12 >>> c = 13 >>> d12
也就是说当两个变量引用指向同一个可变对象时,修改其中任意一个变量都会影响到另一个,为了避免这种情况,必须创建对象的副本而不是新引用。事实上对于像列表和字典这样的容器对象,存在两种复制操作:浅复制和深复制。浅复制虽然创建了一个新对象(体现在id变化),但它包含的还是对原始对象中所包含的元素(假设这个元素是可变对象的话)的引用,譬如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 >>> a=[1 ,2 ,[3 ,4 ]] >>> b=list (a) >>> b[1 , 2 , [3 , 4 ]] >>> a[0 ]=8 >>> b[1 , 2 , [3 , 4 ]] >>> a[2 ][0 ]=30 >>> b[1 , 2 , [30 , 4 ]] >>> a[2 ].append(5 )>>> b[1 , 2 , [30 , 4 , 5 ]] >>> a[2 ]=[11 ,12 ] >>> b[1 , 2 , [30 , 4 , 5 ]]
深复制也将创建一个新对象,且会递归地遍历它所包含的所有对象并创建其副本,需借助标准库中的copy.deepcopy()
函数完成此操作
基础数据类型
None
类型表示一个空对象,如果一个函数没有显式地返回值,则返回该对象。None
经常用作可选参数的默认值,以便让函数检测调用者是否为该参数实际传递了值。None
没有任何属性,在布尔表达式中求值时为False
Python内置了5种数字类型(都是不可变对象):布尔型(bool
)、整型(int
)、长整型(long
,目前好像没有long
这一类型了,并入int
)、浮点型(float
)以及复数型(complex
)。布尔值包括True
和False
两个值(参与数值运算时分别映射为数值1和0)。整数表示范围在-2147483648和2147483647(对应32位二进制数
0111,1111,1111,1111,1111,1111,1111,1111 ,其最高位为符号位)之间的所有整数。浮点型是用机器上浮点数的本机双精度(64bit)表示的,提供大约17位数的精度和范围从-308到308的指数。复数使用一对浮点数表示(complex(1,2)
),复数的实部和虚部分别使用.real
和.imag
访问,.conjugate()
用于计算复共轭(a+bj的共轭是a-bj)
1 2 3 4 5 6 7 8 9 10 >>> d={}>>> d[True ] = "JavaScript" >>> d[1 ] = "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.0
为True
的条件,换句话说,True
、1
、1.0
这三个对象具有相同的哈希(同样False
、0
、0.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 >>> int ('10010' ,2 ) 18 >>> int ('32' ,8 ) 26 >>> int ('e1' ,16 ) 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 >>> bin (18 )'0b10010' >>> bin (0o22 )'0b10010' >>> bin (0x12 )'0b10010' >>> oct (26 )'0o32' >>> oct (0b11010 )'0o32' >>> oct (0x1a )'0o32' >>> hex (225 )'0xe1' >>> hex (0b11100001 )'0xe1' >>> 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 partialdef 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 ) f2t16=partial(transform,base=2 ,to=16 ) f2t16('11100001' )
上述代码中十进制转任意进制部分,有人给出了递归 写法(我用的循环):
1 2 3 4 5 6 7 8 9 def transform_10_to_any (n,base ): string='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' 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 )) print (transform_10_to_any(28 **690 ,10 ))
由于存在递归深度限制,我将其改造成了尾递归版本,并使用@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进制:0 o 53627 = 5 × 8 4 + 3 × 8 3 + 6 × 8 2 + 2 × 8 1 + 7 × 8 0 = 22423 0o53627 = 5×8^4 + 3×8^3 + 6×8^2 + 2×8^1 + 7×8^0 = 22423 0 o 5 3 6 2 7 = 5 × 8 4 + 3 × 8 3 + 6 × 8 2 + 2 × 8 1 + 7 × 8 0 = 2 2 4 2 3 (十进制)。所谓“权”(即位权),对于一个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.5176 423.5176 4 2 3 . 5 1 7 6 (八进制) = 4 × 8 2 + 2 × 8 1 + 3 × 8 0 + 5 × 8 − 1 + 1 × 8 − 2 + 7 × 8 − 3 + 6 × 8 − 4 = 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 = 4 × 8 2 + 2 × 8 1 + 3 × 8 0 + 5 × 8 − 1 + 1 × 8 − 2 + 7 × 8 − 3 + 6 × 8 − 4 = 2 7 5 . 6 5 5 7 6 1 7 1 8 7 5 (十进制)。即小数部分和整数部分情况相反,要从左往右看,第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()
方法
占位符(%
)
取模运算符(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
指针(用十六进制表示对象的内存地址)
%%
字面量百分号
在%
和转换字符之间可以出现以下修饰符,并且只能按照下述顺序出现:
位于括号中的一个键名,用于从映射对象中选出一个具体项,如果不存在此键,则引发KeyError
异常
下面所列的一个或多个
-
,左对齐标志,默认为右对齐
+
,表示应该包含数字符号(即使为正值也是如此)
0
,表示零填充(仅用于数字类型,如%d
、%f
)
一个指定最小宽度的数字,转换后的值将被打印在至少为这个宽度的字段中,并且在左边填充至满字段宽(如果指定了-
标志,则填充在右边)
一个小数点,用于按照精度分割字段宽度
一个数字,指定要打印字符串中的最大字符个数,或者浮点数中小数点之后的位数,或者整数的最小位数
1 2 3 4 5 6 7 >>> 'my name is %s%c%c, my age is %d' % ('muggle' ,'d' ,121 ,23 ) '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/'
format()
方法
高级字符串格式化,调用字符串s
的format()
方法:s.format(*args,**kwargs)
,其接受位置参数和关键字参数的任意组合,并使用它们的值来替换s
中嵌入的占位符,形式为{n}
的占位符(n
为数字,某些情况下可以省略)将被format()
方法的第n
个位置参数替换,而形式为{name}
的占位符将被名为name
的关键字参数值替换,如果要输出一个{
或}
,必须使用{{
或}}
的形式
使用占位符还可以对位置参数或关键字参数进行数值或键索引,如{name[n]}
(n
为索引数字,对应name
是列表)、{name[key]}
(key
为非数字字符串,对应name
是字典),譬如:
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} ' ) print (f'{Comedian("Yang" ,"Dai" ,25 )} ' ) print (>>> f"{new_comedian!r} " )
注1:字符串内的引号不能和字符串外的引号定界符冲突,可根据情况灵活切换'
和"
(单引号和双引号),必要的时候,还可以使用三引号'''
或者"""
,再不行,还可以使用斜杠\
对特殊字符进行转义,譬如'i\'m dy.'
,但是f-string大括号内部不能使用斜杠\
转义甚至说根本就不能出现任何斜杠
注2,支持多重大括号写法,譬如双重大括号,f'{{5}}'
将打印出字符串'{5}'
,此时双重大括号中的内容是被当作普通字符串,再譬如:
1 2 3 4 5 6 7 8 >>> f'{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) 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) s="dy " "is " \ 'a' ' ' \'muggle' print (s) s=("dy " "is " 'a' ' ' 'muggle' )print (s)
且多个独立字符串作为函数参数时,不论是否在同一行,都无需斜杠作为换行符:
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.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 >
增加
s.append(x)
s.extend(t)
注:不同于append
追加元素,extend
将一个新的序列扩展到原始序列中,等同于[*s,*t]
(解包)
解包与装包
主要有三种用途:变量赋值,函数传参和简化容器添加
变量赋值
将多个值或者说对象(其以逗号间隔)赋值给一个变量时,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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 >>> a=[1 ,2 ,3 ]>>> x,y,z=a>>> x1 >>> y2 >>> z3 >>> x,*y,z=range (10 ) >>> x0 >>> y[1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ] >>> z9
可以解包的对象类型包括元组、列表、字典、生成器以及其它可迭代对象:
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) 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('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] #这三种改法都是正确的
函数传参
装包传递(分两种,一是位置参数将被装包为一个元组,二是关键字参数将被装包为一个字典):
1 2 3 4 5 6 7 8 9 def f1 (*t ): print (t) f1(1 ,2 ,3 ,4 ,5 ) def f2 (**d ): print (d) f2(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) def f4 (a,b=22 ,c=33 ): print (a,b,c) d={'a' :1 ,'c' :3 } f4(**d)
简化容器添加
当容器为list
、tuple
(虽然元组是不可变类型,但是没关系,能解包就行,这也并不涉及元组值的改变)、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' :33 ,**d}{'c' : 3 , 'a' : 1 , 'b' : 2 }
另外就是效率问题,用*
和**
简化容器的添加操作,其性能和容器对象的update()
,append()
或add()
方法相比,谁更优?根据下面的测试,解包比append()
方法慢了3倍:
1 2 3 4 5 6 import timeitt1=timeit.Timer('a.append(100)' ,'a=list(range(100))' ) print (t1.repeat()) t2=timeit.Timer('b=[*a,100]' ,'a=list(range(100))' ) print (t2.repeat())
s.insert(i,x)
删除
s.pop([i])
注:函数定义或签名中的[]
表示可选参数
s.remove(x)
del s[i]
、del s[i:j]
、del s[i:j:stride]
注:这是可变序列的通用操作
查找
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)
,且i
、j
、stride
都是任意可省的,如s[::]
修改
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 n=[7 ,4 ,1 ,3 ,2 ,8 ,9 ] n.sort() print (n) x=[4 ,3 ,5 ,2 ,1 ] y=x[:] y.sort() print (x,y) m=[3 ,4 ,1 ,7 ,5 ,0 ,2 ] print (sorted (m))print (m)
高级用法:
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 def keys (x ): return len (x) array=['daiyang' ,'sb' ,'miaosiyu' ,'jianglicheng' ,'liuhan' ] print (sorted (array,key=keys,reverse=True )) array.sort(key=lambda x:len (x),reverse=True ) print (array) 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 ])) print (arrays_1)print (sorted (arrays_2,key=lambda x:list (map (int ,(x[2 ],x[1 ]))))) from operator import itemgettera=[1 ,2 ,3 ] gets=itemgetter(1 ,2 ) ret=gets(a) print (ret)print (sorted (arrays_2,key=lambda x:list (map (int ,itemgetter(2 ,1 )(x)))))print (sorted (arrays_1,key=itemgetter(2 ,1 )))
注,获取排序后的索引序列:
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} ' ) print (f'sorted-value:{[a[ind] for ind in inds]} ' )
类似于列表,只不过元组一旦创建不能修改,比列表要省内存
首先新建一个链表类(目前就是一个普通的容器,可以存储数据,但是尚未实现__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 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 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 ): 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 a=List () a.append(10 ) a.append(12 ) a.append(14 ) a.append(16 ) b=iter (a) print (b.__next__()) c=iter (b) print (c.__next__()) for i in c: print (i) for i in a: print (i)
for
循环原理
1 2 for x in [1 ,2 ,3 ]: print (x)
等价于:
1 2 3 4 5 6 7 8 9 iterator=iter ([1 ,2 ,3 ]) 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 ): 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 ],) print (next (a),next (a),next (a),next (a),next (a),next (a),next (a)) for i in a: print (i)
判断是否可迭代以及是否是迭代器:
1 2 3 4 5 >>> import collections>>> isinstance ([],collections.Iterable) True >>> isinstance ([],collections.Iterator) False
再说说生成器,实际上生成器本质就是迭代器(因此生成器也是用完即弃),只要迭代的下一个元素是即时计算(惰性计算)的,那就是生成器,普通迭代器迭代下一个元素,是从某个容器中取出来,所有元素事先已经计算得出,其始终占用着一片内存区域(因此只有生成器才能实现“无限序列”)。在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) print (list (Range(5 )))
通常采用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 )))
惰性计算
为了深入了解“惰性计算”,看个示例 :
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
是即时计算的(根据for
原理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()) n=3 g2=((i,j) for i in bar() for j in bar()) n=4 print (list (g1))print (list (g2)) '''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)] ''' a=range (2 ) g=(i for i in a) a=range (10 ) print (list (g)) a=[1 ,2 ,3 ,4 ] g=(i for i in a) a.append(5 ) print (list (g))
回到最初的问题,第一次循环,生成器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()))
这就很明了了,或者再改写一下得到:
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()))
另外如果不是通过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 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: print (i)
如果生成器仅被部分使用,将在yield
处引发GeneratorExit
异常(但一般不用人为捕获),并自动调用close()
方法关闭生成器,如果再次调用next()
方法将引发StopIteration
异常,它已不能继续产生新值了:
1 2 3 4 5 6 7 g=f(3 ) for i in g: if i>1 : print (i) else : break g.__next__()
注:for
语句能判断生成器函数是否全部完成,如果你是通过while
和next
进行迭代是无法判断的(只有在整个程序终止时才会引发该异常),这个时候就需要显式关闭(但这不是必须的),鉴于你通常会忘记,一般我们只使用for
循环来迭代生成器
手动执行close()
方法亦将在yield
处引发GeneratorExit
异常(假设生成器已被使用过的话):
1 2 3 g=f(3 ) print (g.__next__()) g.close()
删除生成器对象也会引发该异常:
1 2 3 g=f(3 ) print (g.__next__()) del g
小结一下:
生成器在close()
方法后就无法再进行迭代,再调用next()
方法就会抛出StopIteration
异常
生成器调用close()
方法会在yield
处抛出GeneratorExit
异常,但前提是至少调用一次生成器的next()
方法才会产生GeneratorExit
异常,如果生成器没有启动则不会抛出异常
生成器内捕捉GeneratorExit
异常后,可以继续执行剩余的代码,但剩余的代码中不能再包含yield
语句,否则抛出RuntimeError
异常
GeneratorExit
异常不能通过Exception
捕捉,因为其直接继承自所有异常的基类BaseException
上面我们说了为什么内置的类型如list
、tuple
、dict
、set
、str
都被设计为可迭代对象而非迭代器(迭代器是“一次性”的),原因不止一条,譬如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) print (x in y)
第一次执行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)
改写为生成器(这个改写是错误的):
1 2 3 4 5 6 7 8 9 10 def flatten (lists ): for i in lists: if isinstance (i,list ): flatten(i) else : yield i a=[0 ,[1 ,2 ,3 ],[4 ,5 ,[6 ,7 ]],[8 ,9 ,10 ]] for i in flatten(a): print (i)
这个结果似乎出人意料,分析一下,设g=flatten(a)
,for
语句第一次调用g
的next()
方法,由于列表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
充当转换器
当只有一个序列参数时,map(func,seq)
等同于[func(e) for e in seq]
当有多个序列参数时,map(func,seq1,seq2,...)
等同于[func(*t) for t in [*zip(seq1,seq2,...)]]
(这些序列的长度的长度可以不一致,按最短的来,在python2中,当func
取None
时,map
就等同于zip
,但python3已不再允许func
参数为None
)
打个比方,如果将seq1
、seq2
、… 各自比作飞机的不同的零件提供商,那么map
函数就是批量制造飞机的工厂,func
就是制造飞机的原理图纸
zip
函数
简单应用:
快速构建字典
1 2 3 4 >>> dict (zip (range (5 ),range (5 ,10 ))) {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) 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 = [1 , 2 , 3 , 4 , 5 , 6 , 7 ] for i in zip (*[iter (a)]*2 ): print (i) b=iter (a) for i in zip (b,b): print (i) '''OUTPUT (1, 2) (3, 4) (5, 6) ''' myslice=lambda a,k:[a[i:i+k] for i in range (0 ,len (a),k)] print (myslice(a,2 )) for i in zip (a[::2 ],a[1 ::2 ]): print (i) for i in zip (a[::3 ],a[1 ::3 ],a[2 ::3 ]): print (i) 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) '''或者: 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 islicedef slicing (obj,n ): return zip (*[islice(obj,i,None ) for i in range (n)]) 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 } >>> dict (zip (*list (zip (*m.items()))[::-1 ])){1 : 'a' , 2 : 'b' , 3 : 'c' , 4 : 'd' }
filter(func,seq)
序列过滤器,过滤掉不符合条件的元素,返回那些符合条件的元素所组成的新(子)序列,func
充当筛选器(应返回True
或False
)。等同于[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 )) 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 from functools import reducefib=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 ))
其他实现
普通递归:
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 ) 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 )
and
与or
and
和or
执行布尔逻辑运算,但是在python中返回值并非True
或False
,其返回的是两端的比较值之一
运算规则:
and
运算规则
当and
左端表达式为真时,继续对右端表达式进行计算并返回该(右端)表达式的值
当and
左端表达式为假时,立即返回该(左端)表达式的值
or
运算规则
当or
左端表达式为假时,继续对右端表达式进行计算并返回该(右端)表达式的值
当or
左端表达式为真时,立即返回该(左端)表达式的值
两规则的第二条体现了and
和or
的截断作用(其右端表达式可能并没有被计算),因此and
和or
又被称为“短路运算符”(再次注意只有在and
左边为假或者or
左边为真才会表现出该特性)
1 2 print (0 and (3 -2 )) print (1 or 0 )
简单使用:
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 print (-1 and 'dy' and ...) print (True if -1 and 'dy' and ... else False ) print (0 or 0 or 1 or 2 or 0 ) print (True if 0 or 0 or 1 or 2 or 0 else False ) a,b="JAVA" ,"PYTHON" print (1 and a or b) print (0 and a or b) print (1 and 0 or a and b and 0 and a or b) a,b=[],1 print (True and a or b) print (False and a or b) a='' b="MSY" print ((0 and [a] or [b])[0 ])print ('daiyang' if False else '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} A ˉ
根据输入的高低逆转后输出
not A
A NAND B
与非门
A ⋅ B ‾ \overline{A·B} A ⋅ B
所有输入为高时,才有低的输出,一低出高
not (A and B)
A NOR B
或非门
A + B ‾ \overline {A+B} A + B
所有输入为低时,才有高的输出,一高出低
not (A or B)
A XOR B
异或门
A ⊕ B A\oplus B A ⊕ B
输入为一高一低时,才有高的输出,否则出低
(A and not B) or (not A and B)
A XNOR B
同或门
A ⊕ B ‾ \overline {A\oplus B} A ⊕ B
输入为一高一低时,才有低的输出,否则出高
(A and B) or (not A and not B)
BUF A
是门
A
输出与输入相同的高低状态
A
A IMPLY B
蕴含门
A → B A\rightarrow B A → B
第一输入为低时输出高,否则输出同第二输入
(not A) or B
A NIMPLY B
蕴含非门
A → B ‾ \overline {A\rightarrow B} A → B
第一输入为低时输出低,否则输出与第二输入相反
A and (not B)
注1,表中的A、B均为二值型数据,要么是0,要么是1。最后一列是python代码实现,但不仅适用于二值0和1或布尔值True
orFalse
,且适用于其他任何自定义对象,只要实现__bool__()
方法即可
注2,上表中除了前三个门是基础外,其他门都可以用这三个门实现,譬如:“异或门”A ⊕ B = A ⋅ B ‾ + A ‾ ⋅ B A\oplus B=A·\overline B+\overline A·B A ⊕ B = A ⋅ B + A ⋅ B 、“同或门”A ⊕ B ‾ = A ⋅ B + A ‾ ⋅ B ‾ \overline {A\oplus B}=A·B+\overline A·\overline B A ⊕ B = A ⋅ B + A ⋅ B 、“蕴含门”A → B = A ‾ + B A\rightarrow B=\overline A+B A → B = A + B 、“蕴含非门”A → B ‾ = A ⋅ B ‾ \overline {A\rightarrow B}=A·\overline B A → B = A ⋅ B 。记忆:“与非门”是对“与门”输出取反,“或非门”是对“或门”输出取反,“同或门”是对“异或门”输出取反,“蕴含非门”是对“蕴含门”输出取反
在某些语言如C中可以使用尾递归解决递归爆栈问题,虽然python本身不支持尾递归,但是存在一些解决方案可以间接实现尾递归,下面我先给出尾递归形式的写法:
1 2 3 4 5 6 7 8 9 10 11 def fib5 (n ): 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 ) fib6=lambda n,n1=1 ,n2=0 :n2 if n==0 else fib6(n-1 ,n2,n1+n2) fib7=lambda n,n1=1 ,n2=0 :n==0 and n2 or fib7(n-1 ,n2,n1+n2)
python实现尾递归
在计算机中,函数调用是通过“栈”这种数据结构实现的,每调用一个函数,就会往栈中压入一个栈帧对象,其中保存着函数所对应的可执行代码(即代码对象,通常由compile()
函数返回,可通过栈帧对象frame
的f_code
属性访问)、变量命名空间(包含frame.f_locals
局部变量、frame.f_globals
全局变量和frame.f_builtins
内置名称)以及其他一些属性(如frame.f_back
,表示对当前调用者而言的上一个栈帧对象),当函数执行结束,就会从栈中删除当前栈帧,如果函数没有结束就又发生了其他函数调用,栈中的帧数就会线性增加,由于栈的大小不是无限的,对于递归函数,如果递归过深,很容易导致栈溢出错误
1 2 3 >>> import sys>>> sys.getrecursionlimit 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: raise TailRecurseException(args, kwargs) else : while 1 : try : return g(*args, **kwargs) 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" if n == 0 : return acc return factorial(n-1 , n*acc) 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 ): if n==0 : return 0 return 1 +f(n-1 ) print (f())
最正确且效率最高的当属循环:
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 ))from itertools import countdef 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 )
案例4(增量求平均):
记x n ‾ \overline{x_n} x n 为序列{ x 1 , . . . , x n } \{x_1, ..., x_n\} { x 1 , . . . , x n } 的均值,由x n ‾ − x n − 1 ‾ = x 1 + . . . + x n n − x n − 1 ‾ = x n − 1 ‾ ( n − 1 ) + x n n − x n − 1 ‾ = x n − x n − 1 ‾ n \overline{x_n}-\overline{x_{n-1}} = \frac{x_1+...+x_n}{n} - \overline{x_{n-1}} = \frac{\overline{x_{n-1}}(n-1)+x_n}{n} - \overline{x_{n-1}} = \frac{x_n - \overline{x_{n-1}}}{n} x n − x n − 1 = n x 1 + . . . + x n − x n − 1 = n x n − 1 ( n − 1 ) + x n − x n − 1 = n x n − x n − 1 ,则x n ‾ = x n − 1 ‾ + x n − x n − 1 ‾ n \overline{x_n}=\overline{x_{n-1}} + \frac{x_n - \overline{x_{n-1}}}{n} x n = x n − 1 + n x n − x n − 1 ,于是有:
1 2 3 >>> avg = lambda arr : reduce(lambda avg_num, xn : (avg_num[0 ] + (xn - avg_num[0 ])/(avg_num[1 ] + 1 ), avg_num[1 ] + 1 ), arr, (0 ,0 ))[0 ]>>> avg([1 ,2 ,3 ,4 ])2.5
字典又名哈希表、散列表,实现一个哈希表需要讨论两大基本问题:地址映射和冲突处理
地址映射
一个好的hash
方法是在对象不相同的情况下能产生不相等的哈希值,这是最理想的情况,退而求次,hash
方法应尽力将数据集合中不相同的对象均匀分布到所有可能的地址上面,即通过哈希函数应能得到一个“尽可能随机的地址”
冲突处理
如果两个不同对象映射到了相同的地址,将产生冲突,无论怎样精心构造hash
函数,这种冲突都不可避免,但是可以解决,如开放定址(探测再散列)、再哈希、链式、公共溢出区,其中链表法是比较常见的冲突处理办法,python采用开放定址办法
因此当我们对字典成员对象进行操作时,需要能够计算对象的哈希并比较对象之间的异同,特别是用户自定义类型,需要用户自己实现。先说说几个特殊方法(也叫“魔法方法”,以双下划线开头且以双下划线结尾,这些方法会在进行特定操作时被自动调用,譬如运算符重载方法,如上面提到过的对象比较==
相当于调用对象的__eq__()
方法、加法+
相当于调用对象的__add__()
方法等):
object.__hash__(self)
注:这是object
基类所定义的哈希方法签名,下同
__hash__()
会在以下情况被调用:1)由内置方法hash()
调用(hash(obj)
即等同于obj.__hash__()
),2)对散列集合成员进行操作时被调用,散列集合类型包括dict
、set
和frozenset
。注意__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] print (A('a' ).name)
修改:
1 2 def __getattribute__ (self,item ): return super (A,self).__getattribute__(item)
遇到的问题
我写了下面的代码,但是对输出感到困惑:
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)) 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" ) super (A,self).__init__() print ("leave A" ) class B (Base ): def __init__ (self ): print ("enter B" ) super (B,self).__init__() print ("leave B" ) class C (A,B): def __init__ (self ): print ("enter C" ) super (C,self).__init__() 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
时被调用
一般我们使用内置的标准字典或集合,甚至不需要子类化进行任何定制,即使有其它方面的需求,也有第三方库提供的数据结构供我们使用,譬如defaultdict
、OrderedDict
。但是自定义成员对象并通过字典等存取时,基本都得重写__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)
如你所见,两个“相同”(只要姓名、性别、年龄都一样,我们就认为两个人是同一人啊)对象的哈希值不一样,将它们存放到字典s=set([man1,man2])
中的后果就是,字典s
中竟然“有两个一毛一样的人”,这实在是匪夷所思,原因即在于,执行映射类容器相关操作的时候(譬如向字典中添加一个键值对,或查询某键的值),会先后调用键(此处man
实例对象就是“键”)对象的__hash__()
以及__eq__()
,现在我们两个对象man1
和man2
的哈希值不一样,而且默认的__eq__
方法也判断两个对象不相同,尽管哈希值不一样的键也有可能落到同一个“桶”里面,但是现在这两对象也不equal
啊,于是这两个对象必然不同且同时存在于集合中,解决的办法就是为man
类重写__hash__()
和__eq__()
(一般修改其中任意一个方法,另一个也要相应修改),它们必须满足以下两大规则:
如果两个对象相同,即obj1.equal(obj2)==True
,则两个对象的哈希地址映射也必须相同,即hash(obj1)==hash(obj2)
(否则对象明明存在于容器中,却找不到)
如果两个对象的哈希地址映射不同,即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 ): 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__()
,官方文档中有非常明确的指示:
如果重写了__eq__()
,必须重写__hash__()
,因为两个“相同”对象的哈希地址映射不一定相同,为了确保这一点,必须重写__hash__()
加以保证。反之,如果重写了__hash__()
,也必须重写__eq__()
,因为两个哈希地址相同的对象,如果是相同对象,__eq__()
也必须相同,否则不同,若两个对象哈希地址不同,则__eq__()
必须不同,否则出错
如果只重写了__eq__()
,那么该类对象将不能用于映射类容器,因为此时__hash__()
将隐式地被赋为None
,即此时__hash__
方法失效,该类实例将不可哈希。如果想使用父类的__hash__()
,需要明确说明:
1 2 3 4 class B (A ): __hash__=A.__hash__ def __eq__ (self,other ): pass
如果没有重写__eq__()
,又希望使__hash__()
失效,应明确赋值为None
:
1 2 3 4 5 class A (): __hash__=None s=set () s.add(A())
如果可变对象的类实现了__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) a[0 ]='jingjing' b=MyArray('jingjing' ) s.add(b) hash (a)==hash (b) a==b
用户自定义类默认的__hash__()
和__eq__()
继承自基类object
,它们都是非常严格的,判断两个实例相不相同是根据唯一身份标识id
,哈希也是对身份标识进行哈希
字典的使用和一些注意点:
Python2中判断某个key
是否存在字典中可使用has_key()
方法,另外一种方式是使用in
关键字(key in dict
),推荐使用后者,因为in
的处理速度更快,另一个原因是has_key()
这个方法在Python3被移除了
获取字典中某个键的值,两种方式: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 ): return 0 d=MyDict() for k in '11422134431' : d[k]+=1 print (d) d=dict () for k in '11422134431' : d[k]=d.get(k,0 )+1 print (d) d={} for k in '11422134431' : d[k]=d.setdefault(k,0 )+1 print (d) from collections import defaultdictd=defaultdict(int ) for k in '11422134431' : d[k]+=1 print (d)
创建字典的几种方式:
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) {'animal' : 'duck' , 'plant' : 'cactus' , 'vehicle' : 'school bus' } >>> dict (zip (['a' ,'b' ,'c' ],[1 ,2 ,3 ])) {'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 ) {1 : 2 , 3 : 4 , 'last' : 6 }
dict.fromkeys(seq[,initial])
根据序列seq
构造字典,列表中的元素将作为键,需另外指定初始值initial
,若未指定则值全部为None
,fromkeys()
完全可以用字典生成式实现
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)} {'first' : 1 , 'second' : 2 , 'third' : 3 }
字典更新
字典元素删除
使用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
字典遍历
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(数值下标)索引(虽然集合不属于“映射类型”,但由于都是基于散列表实现,所以放在一起描述)
创建集合
创建一个空集合:
使用现有数据初始化一个集合,set(iterable)
构造方法接受一个可迭代对象,如列表:
1 2 >>> set ([1 ,2 ,3 ]){1 ,2 ,3 }
查询集合
集合是无序容器,不能通过下标索引元素,只能遍历:
1 2 for e in set ([1 ,2 ,3 ]): print (e)
使用in
或not in
检查某一元素是否存在于集合中:
1 2 >>> 1 in set ([1 ,2 ])True
添加元素
add(item)
向集合中添加单项元素:
1 2 3 4 >>> s=set ()>>> s.add('msy' )>>> s{'msy' }
注:鉴于集合的原理(散列表),向集合中添加的元素既是value又是key,因此添加的元素必须是可哈希的,即集合元素必须是不可变类型
update(iterable)
使用一系列元素(可迭代序列)更新集合(s1.update(s2)
等同于“并集”操作s1|=s2
):
1 2 3 >>> s={12 ,13 }>>> s.update(['muggle' ,101 ,'msy' ]){'muggle' , 101 , 12 , 13 , 'msy' }
删除元素
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'
discard(item)
同remove(item)
,但是元素不存在时不会引发异常
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'
clear()
删除集合中的全部元素,无返回值
交并集数学运算(非原址)
s1.intersection(s2)
返回集合s1
和s2
的交集,该操作对应的运算符为s1 & s2
:
1 2 3 4 5 6 >>> s1=set ([1 ,2 ])>>> s2=set ([2 ,3 ])>>> s1.intersection(s2){2 } >>> s1 & s2{2 }
s1.union(s2)
返回集合s1
和s2
的并集,该操作对应的运算符为s1 | s2
:
1 2 3 4 5 6 >>> s1=set ([1 ])>>> s2=set ([2 ])>>> s1.union(s2){1 , 2 } >>> s1 | s2{1 , 2 }
s1.difference(s2)
返回集合s1
和s2
的差集(简单来说就是返回集合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 }
s1.symmetric_difference(s2)
返回集合s1
和s2
的对称差集(简单说就是返回集合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{1 }
其它原址操作:
s1.intersection_update(s2)
交集更新操作,对应运算符为s1 &= s2
s1.update(s2)
并集更新操作,对应运算符为s1 |= s2
s1.symmetric_difference_update(s2)
对称差集更新操作,对应运算符为s1 ^= s2
父集和子集判断
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 <= s2True
s1.issuperset(s2)
判断集合s1
是否是集合s2
的父集,对应运算符为s1 >= s2
:
1 2 3 4 >>> s2.issuperset(s1)True >>> s2 >= s1True
注,两个集合只有在这种情况下是相等(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
模块提供了两个集合类型,Set
和MutableSet
,后者用于判断是否是可变集合