Part0.BulitIn 小知识 处理大数据
如果是类 ->
减少内存使用__slots__
如果是数值类型 使用Numpy
array
memoryview
为什么说repr是开发者模式的str 因为eval(repr(obj))
–> obj
如果class中有self.__attr
私有变量, 则无法通过eval(repr(obj))
获取obj
1 2 3 4 5 6 7 def repr (obj ): """ Return the canonical string representation of the object. For many object types, including most builtins, eval(repr(obj)) == obj. """ pass
self|cls python
中cls
代表的是类的本身,相对应的self则是类的一个实例对象。
因为cls
等同于类本身,类方法中可以通过使用cls来实例化一个对象。
cls
通常于@classmethod
搭配使用用于实例化类对象
反射 python的反射,它的核心本质其实就是利用字符串的形式去对象(模块)中操作(查找/获取/删除/添加)成员 ,一种基于字符串的事件驱动!
林奈结构 没有采用一贯的下行二分法体系(系统)而是采用在一个界(kingdom)
之内只含有四个阶元层次:纲(class),目(order),属(genus),种(species) 的等级结构的体系。
帮助 想知道某个类或者函数有方法以及其例子时, 可以使用dir()或者help()
比如想了解pandas.read_excel
如何读取其title
和data
1 2 3 4 5 6 7 8 9 10 import pandas as pdfile = pd.read_excel("./students.xls" ) columns = list (file.columns) for data in file.values: print(data) >> ['Star' 46599201 85 20 ] ['Drink' 36784554 90 21 ] ['Fire' 12492483 96 23 ]
想了解一个函数或者对象要做什么事情可以使用__annotation__
__doc__
inspect.signature
annotation
1 {'dtype': 'DtypeArg | None', 'storage_options': 'StorageOptions'}
doc
signature
1 (io, sheet_name=0, header=0, names=None, index_col=None, usecols=None, squeeze=False, dtype: 'DtypeArg | None' = None, engine=None, converters=None, true_values=None, false_values=None, skiprows=None, nrows=None, na_values=None, keep_default_na=True, na_filter=True, verbose=False, parse_dates=False, date_parser=None, thousands=None, comment=None, skipfooter=0, convert_float=None, mangle_dupe_cols=True, storage_options: 'StorageOptions' = None)
下划线
模式
例子
含义
单前导
_var
命名约定, 内部私有变量, 解释器不强制执行, 除import *
外
单末尾
var_
命名约定, 避免与关键字冲突
双前导
__var
类的上下位中使用, 名称修饰为_class__var
避免属性被意外修改
双前导|双末尾
__var__
magic method
单下划
_
占位符
数字
格式
输出
描述
3.1415926
{:.2f}
3.14
保留小数点后两位
3.1415926
{:+.2f}
+3.14
带符号保留小数点后两位
-1
{:+.2f}
-1.00
带符号保留小数点后两位
2.71828
{:.0f}
3
不带小数
5
{:0>2d}
05
数字补零 (填充左边, 宽度为2)
5
{:x<4d}
5xxx
数字补x (填充右边, 宽度为4)
10
{:x<4d}
10xx
数字补x (填充右边, 宽度为4)
1000000
{:,}
1,000,000
以逗号分隔的数字格式
0.25
{:.2%}
25.00%
百分比格式
1000000000
{:.2e}
1.00e+09
指数记法
13
{:>10d}
13
右对齐 (默认, 宽度为10)
13
{:<10d}
13
左对齐 (宽度为10)
13
{:^10d}
13
中间对齐 (宽度为10)
11
'{:b}'.format(11) '{:d}'.format(11) '{:o}'.format(11) '{:x}'.format(11) '{:#x}'.format(11) '{:#X}'.format(11)
1011 11 13 b 0xb 0XB
进制
^ , <**, **> 分别是居中、左对齐、右对齐,后面带宽度, : 号后面带填充的字符 ,**只能是一个字符[即只能填充一次]**,不指定则默认是用空格填充。
1 2 3 4 5 str = "1{:15}" .format ("&" )print(string_1, len (str )) |1 & | 16
+ 表示在正数前显示 **+**,负数前显示 **-**; (空格)表示在正数前加空格
b、d、o、x 分别是二进制、十进制、八进制、十六进制。
1 2 3 4 5 6 ROW_FMT = "{0:2d} @ {1:2d} {2} {0:<2d}" print(ROW_FMT.format (needle, posotion, offset))
Dis常用字节码
Name
args
Info
LOAD_GLOBAL
namei
加载名称为 co_names[namei]
的全局对象推入栈顶。
LOAD_FAST
var_num
将指向局部对象 co_varnames[var_num]
的引用推入栈顶。
LOAD_CONST
consti
将 co_consts[consti]
推入栈顶。
CALL_FUNCTION
argc
调用一个可调用对象并传入位置参数。 argc 指明位置参数的数量。 栈顶包含位置参数,其中最右边的参数在最顶端。 在参数之下是一个待调用的可调用对象。 CALL_FUNCTION
会从栈中弹出所有参数以及可调用对象,附带这些参数调用该可调用对象,并将可调用对象所返回的返回值推入栈顶。在 3.6 版更改: 此操作码仅用于附带位置参数的调用。
POP_TOP
删除堆栈顶部(TOS)项
STORE_FAST
var_num
将 TOS 存放到局部对象 co_varnames[var_num]
。
RETURN_VALUE
返回 TOS 到函数的调用者。
Attention 函数|方法返回 python 会对operation返回return None来表示对对象的就地更改, 即不会id(obj)不会变
用返回None来表示就地改动有一个弊端, 即调用者无法将其串联起来; 而返回一个新对象的方法(比如说str里的所有方法)则正好相反, 他们可以串联起来调用
sort sort分为两种, 一种是value sort即根据值进行排序, 另一种是time sort即就算两个元素比不出大小, 但在每次排序的结果里他们的相对位置是固定的
推导式
Q&A 1.python中list和array的区别
list是python的内置数据类型
list中的数据类不必相同的
array的中的类型必须全部相同
在list中的数据类型保存的是数据存放的地址,简单的说就是指针,并非数据
这种保存方式增加了存储和CPU的消耗例如list1=[1,2,3,’a’]需要4个指针和四个数据
数组在背后存的并不是float对象,而是数字的机器翻译,也就是字节表述。这一点和c语言中的数组一样
array创建的数组不适用于数字操作(比如矩阵和矢量运算)。另外+=和*=运算符可以用于array的添加。
从python3.4开始,数组(array)类型不再支持诸如list.sort()这种就地排序方法。要给数组排序的话,得用sorted函数新建一个数组:
Magic Method 表1:跟运算符无关的 magic method
类别
方法名
字符串/字节序列表示形式
repr str format bytes
数值转换
abs bool complex int float hash index
集合模拟
len getitem setitem delitem contains
迭代枚举
iter reversed next
可调用模拟
call
上下文管理
enter exit
实例创建/销毁
new init del
属性管理
getattr getattribute setattr delattr dir
属性描述符
get set delete
跟类有关的服务
prepare instancecheck subclasscheck
表2:跟运算符相关的 magic method
类别
方法名和对应的运算符
一元运算符
neg - pos + abs abs()
众多比较运算符
lt < le <= eq == ne != gt > ge >=
算术运算符
add + sub - mul * truediv / floordiv // mod % divmod divmod() pow **|pow() round round()
反向算术运算符
radd rsub rmul rturediv rfloordiv rmod rdivmod rpow
增量赋值算术运算符
iadd isub imul itruediv ifloordic imod ipow
位运算符
invert ~ lshift << rshift >> and & or | xor ^
反向位运算符
rlshift rrshift rand rxor ror
增量赋值位运算符
ilshift irshift iand ixor ior
1 注: 以下所有的keyword默认为 self.__xx__():
bool|len 默认情况下我们定义的类的实例总是被认为True
,除非在define class
中有实现bool或者len
bool()
的背后其实是调用x.__bool__()
的结果;如果x.__bool__()
不存在, 则调用len()
即x.__len__()
,如果为0-False other-True
调用优先级为bool > len
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class temp : def __init__ (self ): self.value = 1 def __len__ (self ): return 0 !def __bool__ (self ): ! return True > bool (temp) False ? bool(temp) True
and|mul and --> + add
mul --> * multiple
str|repr 两者的却别在于str will called by str() and print(), repr can only called by print()
如果两者特殊方法保留一个repr
会是更好的选择, 因为在没有str
时, 解释器会调用repr
优先级str > repr
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class temp : def __init__ (self ): self.value = 1 def __repr__ (self ): return f"repr value is {self.value} " def __str__ (self ): return f"str value is {self.value} " test = temp() print(test) H:\Python\python.exe H:/MY_TXM/test.py str value is 1
Moudle recude 对参数序列中元素进行累积。
函数将一个数据集合(链表,元组等)中的所有数据进行下列操作:用传给 reduce 中的函数 function(有两个参数)先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。
1 2 3 sum2 = reduce(lambda x, y: x+y, [1 ,2 ,3 ,4 ,5 ]) >> 15 = 1 + 2 + 3 + 4 + 5
os listdir
列出目录下的所有文件和文件夹不包括.
..
隐藏目录
1 2 3 4 import osprint(os.listdir("H:\MY_TXM" )) >> ['.git' , '.gitignore' , '.idea' , 'static' , 'venv' ]
locale
locale 是根据计算机用户所使用的语言,所在国家或者地区,以及当地的文化传统所定义的一个软件运行时的语言环境。通常情况下它可以按照涉及使用习惯分为12大类:
语言符号及其分类(LC_CTYPE)
数字(LC_NUMBERIC)
比较习惯(LC_COLLATE)
时间显示格式(LC_TIME)
货币单位(LC_MONETARY)
信息主要是提示信息,错误信息,状态信息,标题,标签,按钮和菜单等(LC_MESSAGES)
行么书写方式(LC_NAME)
地址书写方式(LC_ADDRESS)
电话号码书写方式(LC_TELEPHONE) -度量衡表达方式(LC_MEASUREMENT)
默认纸张尺寸大小(LC_PAPER)
对locale 自身包含信息的概述(LC_IDENTIFICATION)
除此之外还有一个LANGUAGE参数,它与LC_MESSAGES相似
time time process_time process_time_ns perf_counter perf_counter_ns time()
精度上相对没有那么高,而且受系统的影响,适合表示日期时间或者大程序程序的计时。
perf_counter()
适合小一点的程序测试,会计算sleep()
时间。
process_counter()
适合小一点的程序测试,不会计算sleep()
时间。
此外Python3.7
开始还提供了以上三个方法精确到纳秒
的计时。分别是:
1 2 3 time.perf_counter_ns() time.process_time_ns() time.time_ns()
arrary 数组所接受的typecode以及其对应的存储数据类型
Type code
C Type
Python Type
Minimum size in bytes
Notes
‘b’
signed char
int
1
‘B’
unsigned char
int
1
‘u’
Py_UNICODE
Unicode character
2
(1)
‘h’
signed short
int
2
‘H’
unsigned short
int
2
‘i’
signed int int
2
‘I’
unsigned int
int
2
‘l’
signed long
int
4
‘L’
unsigned long
int
4
‘q’
signed long long int
8
(2)
‘Q’
unsigned long long
int
8
(2)
‘f’
float
float
4
‘d’
double
float
8
bisect bisect是一个根据二分算法写的库,其中主要的俩个方法是bisect和insert,根本上根据二分算法算出有序序列的索引,可以当做快速定位index使用
bisect|bisect_left 接受一个有序序列,一个元素,返回该元素在有序序列的索引,索引以前全≤该元素,bisect(bisect_right)|bisect_left
主要区别是如果俩对比元素相等,前者返回index是从右边插入,后者左边
1 2 3 4 5 6 7 8 9 import bisecttemp_sequence = [0 , 2 , 3 ] index = bisect.bisect(temp_sequence, 2 ) _index = bisect.bisect_left(temp_sequence, 2 ) print(index) print(_index) 2 1
insort 根据bisect或者bisect_left返回的index进行插入,接受有序序列,返回有序序列
groupby 和nlargest() nsmallest()
一样支持key=
【聚合】
有时候我们需要给一个sequence
按照某个属性分组,可以借助groupby来实现,groupby常常和lambda map operator.itemgetter
一起使用,因为在分组前,大多希望相关的数据聚集在一起, 这样对于groupby来说分组才有意义
语法:
1 2 3 4 5 groupby(seqence, key) args: sequence must iterable key sort the sequence 聚合 return iterator contains key, data --> type tuple
注意:
如果groupby中的key=None,那么group后的key是被group中的element, 如果key=fun那么group后的key是fun(element)
返回的数据
type(key) type(element)
type(data) iterator
实列:
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 a = [{'severity' : '严重' }, {'severity' : '严重' }, {'severity' : '不严重' }, {'severity' : '严重' }] for key, data in groupby(a): print(key, list (data)) """ {'severity': '严重'} [{'severity': '严重'}, {'severity': '严重'}] {'severity': '不严重'} [{'severity': '不严重'}] {'severity': '严重'} [{'severity': '严重'}] """ a.sort(key=itemgetter("severity" )) for key, data in groupby(a): print(key, list (data)) """ {'severity': '不严重'} [{'severity': '不严重'}] {'severity': '严重'} [{'severity': '严重'}, {'severity': '严重'}, {'severity': '严重'}] """ a.sort(key=itemgetter("severity" )) for key, data in groupby(a, key=itemgetter("severity" )): print(key, list (data)) """ 不严重 [{'severity': '不严重'}] 严重 [{'severity': '严重'}, {'severity': '严重'}, {'severity': '严重'}] """
operator itemgetter
使用[]运算符, 不仅支持序列还支持映射和任何实现了__getitem__
方法的类
operator模块提供的itemgetter函数主要用于获取某一对象 特定维度的数据, 其中的参数为特定维度的索引
operator.itemgetter函数获取的并不是某一个数值,而是某一个函数 常常可以使用lambda
函数替换
用法:
1 2 3 4 itemgetter(var) args: var can accept index and key return fun()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import operator a = [{"age": 10, "socre": 90}, {"age": 9, "socre": 98}, {"age": 0, "socre": 50}, {"age": 15, "socre": 56}] # 定义一个获取维度数据的函数 # 获取sequence的index=0&1的数据 condition = operator.itemgetter(0, 1) # 将sequence传入获取函数中 res = condition(a) print(res, type(a)) ({'age': 10, 'socre': 90}, {'age': 9, 'socre': 98}) <class 'tuple'> # 对a的age进行排序 a.sort(key=itemgetter("age")) a.sort(key=lambda x: x["socre"], reverse=True) print(a) [{'age': 0, 'socre': 50}, {'age': 9, 'socre': 98}, {'age': 10, 'socre': 90}, {'age': 15, 'socre': 56}] # 先对age排序在对score排序 a.sort(key=itemgetter("age", "socre"), reverse=True)
headq nlargest nsmallest 和groupby()
一样支持key=
介绍:
这两个函数可以帮助我们在某个集合中找出最大或最小的N个元素
语法:
1 2 3 4 def nlargest (n, iterable, key=None ): """Find the n largest elements in a dataset. Equivalent to: sorted(iterable, key=key, reverse=True)[:n] """
random
注意如果在random.method之前使用了seed,那么random.method每次都将按照一定规则返回相同的数值
shuffle 语法:
1 2 3 4 5 random.shuffle(iterable) args: iterbale a list or tuple (sequence) return : the origin sequence but the position of element is random ==> None
实列:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import randoma = [1 , 2 , 3 , 4 , 5 ] print(a, id (a)) random.shuffle(a) print(a, id (a)) >>> [0 , 1 , 2 , 3 , 4 ] 2200356865544 None [0 , 3 , 4 , 1 , 2 ] 2200356865544
seed 调用 random.random()|shuffle
生成随机数时,每一次生成的数都是随机的。 但是,当使用 random.seed(x)
设定好种子之后,其中的 x
可以是任意数字,这个时候,先调用seed
的情况下,使用 random()
生成的随机数将会是同一个
randrange 从指定范围内,按指定基数递增的集合中 获取一个随机数。
random.randrange([start], stop[, step])
1 2 random.randrange(10 , 100 , 2 )
sample random.sample
的函数原型为:random.sample(sequence, k)
从指定序列中随机获取指定长度的片断。sample函数不会修改原有序列。
1 2 3 4 5 6 7 8 9 temp = [i for i in range (10 )] print(temp, id (temp)) random_choice = random.sample(temp, 3 ) print(random_choice, id (random_choice)) print(id (temp)) [0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] 2483596469896 [7 , 8 , 3 ] 2483594327944 2483596469896
dis dis.dis Python代码在执行的时候,会被编译为Python字节码,再由Python虚拟机执行Python字节码。有时候就我们执行python文件的时候会生成一个pyc文件,这个pyc文件即用于存储Python字节码指令,而这些字节码是一种类似于汇编指令的中间语言,但是每个字节码对应的不是机器指令,而是一段C代码。
而dis模块,就是用于查看这些字节码的运行轨迹,因此我们可以用dis模块判断两个函数的内存占用谁会更大,谁会更消耗CPU性能,不仅如此,通过指令,我们还可以知道Python中一些内置函数、变量的取值过程、运行逻辑,对于我们代码性能并优化代码很有帮助。
1 2 3 4 5 6 7 8 9 10 11 12 13 import disdis.dis("ori_tuple[index] += mutable_sequence" ) 1 0 LOAD_NAME 0 (ori_tuple) 2 LOAD_NAME 1 (index) 4 DUP_TOP_TWO 6 8 LOAD_NAME 2 (mutable_sequence) 10 INPLACE_ADD 12 ROT_THREE 14 STORE_SUBSCR 16 LOAD_CONST 0 (None ) 18 RETURN_VALUE
字节码操作:
假设
ori_tuple = (1, 2, [10, 20])
index = 2
mutable_sequence = [30, 40]
6 BINARY_SUBSCR 将 ori_tuple[index]
的值存入TOS
(Top Of Stack)
10 INPLACE_ADD 计算ori_tuple[index] += mutable_sequence
, 这一步骤可以完成, 是因为TOS
指向的是一个可变对象
14 STORE_SUBSCR 赋值, 这一步骤失败, 是因为tuple属于不可变序列(immutable)
我们从这个操作中可以得到三个教训:
不要把可变对象放在元组中
增量赋值(+=)不是一个原子操作, 在计算完后的赋值阶段如果抛出异常, 计算仍然会完成
譬如:
1 2 3 4 5 6 7 b = (1, 2, [10, 20]) b += [30, 40] Traceback (most recent call last): File "<input>", line 1, in <module> TypeError: can only concatenate tuple (not "list") to tuple print(b) (1, 2, [10, 20])
在上述实列中, 即使在最后的赋值阶段tuple抛出异常, 但是此时tuple的数据依然发生了改变!
Python字节码 name(variable)
执行操作
LOAD_NAME
将与 co_names[namei]
相关联的值推入栈顶。
DUP_TOP_TWO
复制堆栈顶部的两个引用,使它们保持相同的顺序。
BINARY_SUBSCR
实现 TOS = TOS1[TOS]
。
INPLACE_ADD
就地实现 TOS = TOS1 + TOS
。
ROT_THREE
将第二个和第三个堆栈项向上提升一个位置,顶项移动到位置三。
STORE_SUBSCR
实现 TOS1[TOS] = TOS2
。
LOAD_CONST
将 co_consts[consti]
推入栈顶
RETURN_VALUE
返回 TOS 到函数的调用者。
sys getsizeof 1 2 getsizeof(object, default) -> int Return the size of object in bytes .
获取对象所消耗的内存大小
argv 是一个从程序外部获取参数的桥梁,从外部取得的参数可以是多个,所以获得的是一个列表(list),也就是说sys.argv其实可以看作是一个列表,所以才能用[]提取其中的元素。其第一个元素是程序本身,随后才依次是外部给予的参数。
memoryview memoryview**[内存视图]是一个内置类, 它能让用户在不复制内容的情况下操作同一个数组的不同切片,其实内存试图时泛化和去数学化的 numpy数组**,它允许你在不复制内容的前提下,在数据结构之间共享内存, 其中数据结构可以是任何形式,这个功能在处理大型数据集合的时候十分重要
memoryview 语法:
参数说明:
返回元组列表(物理地址)。
.cast memoryview.cast 的概念和数组模型差不多,能用不同的方式读写同一块内存地址,而且内容字节不会随意移动,和C语言中类型转换的概念相似.cast会把同一块内存里的内容打包成一个全新的memoryview对象给你
Method all|any 都接受一个可迭代对象, 其中all是当可迭代对象中的元素都是True时返回True, any是有一个为True时则返回True
1 2 3 4 5 6 7 d = all (map (lambda x: x > 10 , [1 , 10 , 11 ])) print(d) f = any (map (lambda x: x > 10 , [1 , 10 , 11 ])) print(f) >> False True
eval 描述
eval() 函数用来执行一个字符串表达式,并返回表达式的值。
语法
以下是 eval() 方法的语法:
1 eval(expression[, globals[, locals]])
参数
expression – 表达式。
globals – 变量作用域,全局命名空间,如果被提供,则必须是一个字典对象。
locals – 变量作用域,局部命名空间,如果被提供,可以是任何映射对象。
返回值
返回表达式计算结果。
isatty
isatty() 方法检测文件是否连接到一个终端设备,如果是返回 True,否则返回 False。
语法
isatty() 方法语法如下:
assert
python断言和try-except用法相反
断言函数是对表达式布尔值的判断,要求表达式计算值必须为真。可用于自动调试。
如果表达式为假,触发异常;如果表达式为真,不执行任何操作。
1 2 3 assert conditionif not condition: raise AssertionError()
reverse reversed
s.reverse()
就地排序, id(s)
不变
reversed(s)
返回s
的倒序迭代器
id 返回对象的物理存储地址, 一般用作操作符是或否创建了一个新对象, 比较两个对象的物理地址是否相同使用is
map 提供的函数对指定序列做映射。
1 map (function, iterable, ...)
filter filter() 函数用于过滤序列,过滤掉不符合条件的元素,返回由符合条件元素组成的新列表。
Python2.7 返回列表,Python3.x 返回迭代器对象
1 2 filter (function, iterable)
Python2.x 中返回的是过滤后的列表, 而 Python3 中返回到是一个 filter 类。
filter 类实现了 iter 和 next 方法, 可以看成是一个迭代器, 有惰性运算的特性, 相对 Python2.x 提升了性能, 节约内存。
abs|fabs[math] Python
中 fabs(x)
方法返回 x
的绝对值。虽然类似于 abs()
函数,但是两个函数之间存在以下差异:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import matha = -1 b = -1.3232 c = b d = 1 + 1.0j e = 3 + 4.0j print("a的绝对值是:" , abs (a)) print("b的绝对值是:" , abs (b)) print("c的绝对值是:" , math.fabs(c)) print("d的的绝对值是:" , abs (d)) print("e的绝对值是:" , math.fabs(e)) Traceback (most recent call last): File "H:/MY_TXM/test.py" , line 80 , in <module> print("e的绝对值是:" ,math.fabs(e)) TypeError: can't convert complex to float
hypot[math] hypot() 返回欧几里德范数 sqrt(x*x + y*y)
hypot()
是不能直接访问的,需要导入 math
模块,然后通过 math
静态对象调用该方法。
1 2 3 4 5 6 7 8 import matha = int (input ("请输入直角三角形的一条直角边长:" )) b = int (input ("请输入直角三角形的另一条直角边长:" )) print("该直角三角形的斜边长为: " , int (math.hypot(a, b))) print("Done" )
bool bool() 函数用于将给定参数转换为布尔类型,如果没有参数,返回 False
用于判断真假
bool
是 int
的子类。
1 2 >>> issubclass (bool , int ) True
issubclass issubclass(class, classinfo) --> return True|False
issubclass() 方法用于判断参数 class
是否是类型参数classinfo
的子类
Part1.DataModel 1.具名元组 Python元组的升级版本 – namedtuple(具名元组)
因为元组的局限性:不能为元组内部的数据进行命名,所以往往我们并不知道一个元组所要表达的意义,所以在这里引入了 collections.namedtuple 这个工厂函数,来构造一个带字段名的元组。具名元组的实例和普通元组消耗的内存一样多,因为字段名都被存在对应的类里面。这个类跟普通的对象实例比起来也要小一些,因为 Python 不会用 dict 来存放这些实例的属性。
namedtuple 对象的定义如以下格式:
collections.namedtuple(typename, field_names, verbose=False, rename=False)
返回一个具名元组子类 typename,其中参数的意义如下:
typename:元组名称
field_names: 元组中元素的名称
rename: 如果元素名称中含有 python 的关键字,则必须设置为 rename=True
verbose: 默认就好
下面来看看声明一个具名元组及其实例化的方法:
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 import collections两种方法来给 namedtuple 定义方法名 User = collections.namedtuple('User' , ['name' , 'age' , 'id' ]) User = collections.namedtuple('User' , 'name age id' ) user = User('tester' , '22' , '464643123' ) print(user) collections.namedtuple('User' , 'name age id' ) 创建一个具名元组,需要两个参数,一个是类名,另一个是类的各个字段名。后者可以是有多个字符串组成的可迭代对象,或者是有空格分隔开的字段名组成的字符串(比如本示例)。具名元组可以通过字段名或者位置来获取一个字段的信息。 输出结果: User(name='tester' , age='22' , id ='464643123' ) 具名元组的特有属性: 类属性 _fields:包含这个类所有字段名的元组 类方法 _make(iterable):接受一个可迭代对象来生产这个类的实例 实例方法 _asdict():把具名元组以 collections.OrdereDict 的形式返回,可以利用它来把元组里的信息友好的展示出来 from collections import namedtupleUser = namedtuple('User' , ['name' , 'sex' , 'age' ]) user = User(name='Runoob' , sex='male' , age=12 ) print( user._fields ) user = User._make(['Runoob' , 'male' , 12 ]) print( user ) print( user.name ) print( user.sex ) print( user.age ) user = user._replace(age=22 ) print( user ) print( user._asdict() ) 以上实例输出结果为: ('name' , 'sex' , 'age' ) User(name='Runoob' , sex='male' , age=12 ) Runoob male 12 User(name='Runoob' , sex='male' , age=22 ) OrderedDict([('name' , 'Runoob' ), ('sex' , 'male' ), ('age' , 22 )])
2.列表表达式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 print([(i, j) for i in range (1 , 10 ) for j in range (11 , 20 )]) res = [] for i in range (1 , 10 ): for j in range (11 , 20 ): res.append((i, j)) print(res) fun() for -in -loop_1 for -in -loop_2 <==> for -in -loop_1: for -in -loop_2: fun()
3.random choice 1 2 3 from random import choicechoice(obj) -> obj: can iterable ==> this module can choice a random element from obj
4.Magic Method 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 obj[key] 本质上调用的是__getitem__ magic method 又称为 dunder method 及双下滑线魔法方法 for -in -loop==> __contains__ 迭代通常是隐式的 obj[key] --> self.__getitem__ 迭代通常是隐式的,如果一个集合类型没有实现slef.__contains__, 那么in 运算符就会按照顺序做一次迭代搜索 len(obj) --> self.__len__ 注意如果obj是python内置的类型比如(list 、str 、bytearry)等Cpython会抄个近路,__len__实际上会直接返回PyVarObject的self.ob_size这个属性,而不是执行self.__len__这个方法 for item in x: --> self.__iter__ x 必须是可迭代的
5.迭代器与生成器 5.1迭代器 迭代是Python最强大的功能之一,是访问集合元素的一种方式。
迭代器是一个可以记住遍历的位置的对象。
迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。
迭代器有两个基本的方法:iter() 和 **next()**。
字符串,列表或元组对象都可用于创建迭代器:
1 2 3 4 5 6 list =[1 ,2 ,3 ,4 ]it = iter (list ) print (next (it)) >1 print (next (it))>2
迭代器对象可以使用常规for语句进行遍历 :
1 2 3 4 5 6 7 8 list =[1 ,2 ,3 ,4 ]it = iter (list ) for x in it: print (x, end=" " ) > 1 2 3 4
也可以使用 next() 函数:
1 2 3 4 5 6 7 8 9 10 11 12 import sys list =[1 ,2 ,3 ,4 ]it = iter (list ) while True : try : print (next (it)) except StopIteration: sys.exit()
5.2创建一个迭代器 把一个类作为一个迭代器使用需要在类中实现两个方法 iter () 与 next ()
在定义类的时候都有一个构造函数,Python 的构造函数为 init (), 它会在对象初始化的时候执行。
iter () 方法返回一个特殊的迭代器对象 , 这个迭代器对象实现了 next () 方法并通过 StopIteration 异常标识迭代的完成。
next () 方法(Python 2 里是 next())会返回下一个迭代器对象。
创建一个返回数字的迭代器,初始值为 1,逐步递增 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 32 33 34 35 36 class Numbers (): """ 抽象一个迭代器对象,初始属性【value=0每次next自增1,并通过【value>20】来rais StopInteration """ def __iter__ (self ): self.value = 0 return self def __next__ (self ): if self.value <= 20 : self.value += 1 return self.value else : raise StopIteration temp = Numbers() temp_iter = iter (temp) for item in temp_iter: print(item) > 1 2 ... 21 print(f"the obj is {temp} , type is {type (temp)} " ) print(f"the iter is {temp_iter} , type is {type (temp)} " ) >> the obj is <__main__.Numbers object at 0x0000016FFBD51088 > , type is <class '__main__ .Numbers '> the iter is <__main__.Numbers object at 0x0000016FFBD51088> , type is <class '__main__.Numbers'>
5.3生成器 在 Python 中,使用了 yield 的函数被称为生成器(generator)。
跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。
在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。
调用一个生成器函数,返回的是一个迭代器对象。
以下实例使用 yield 实现斐波那契数列:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import sys def fibonacci (n ): a, b, counter = 0 , 1 , 0 while True : if (counter > n): return yield a a, b = b, a + b counter += 1 f = fibonacci(10 ) while True : try : print (next (f), end=" " ) except StopIteration: sys.exit()
5.4迭代器和生成器对比
6.len为什么不是普通方法 如果x是一个python内置类型的实例, 那么len(x)中Cpython会直接调用这个类的属性,解释器将不走__len__()
这个方法
杂谈
数据模型|对象模型
python文档中大多使用数据模型, 而大多数喜欢使用对象模型
对象模型:计算机编程语言中对象的属性<==>数据模型
魔法方法
python用这个方法来提供更多的元对象协议,目的是让语言的使用者和开发者拥有并使用同样的工具
元对象
the art of the MetaObject Protocal | AMOP
元对象协议:那些对构建语言本身来讲很重要的对象, 以此为前提, 协议也可以看成是接口, 即是说, 元对象协议是对象模型(数据模型)的同义词, 他们的本质都是构建核心语言的API
Part2.DataStructure 2.0前言 在创建poython
之前Guido
曾为ABC
语言贡献过代码, ABC
语言是一个致力于为初学者设计变成环境的长达10的研究项目, 其中很多点子在现在看来任然具有python的特性如:
序列的泛型操作
内置的元组
映射类型
用缩进来构建源码
无需变量声明的强类型
python也从ABC继承了用统一风格处理序列数据
这一特点, 无论是中数据结构(string|list|bytes|tuple|array|xml|element|database query)他们都公用一套操作:
迭代
切片
排序
拼接
2.1内置序列类型 python使用C实现了两大类序列类型:容器序列 扁平序列
两者的却别前者支持存储不同类型的数据; 后者仅支持一种类型的数据
容器序列 : list tuple collection.deque
存放的是任意类型的对象的引用
支持存储不同数据类型
非连续内存存储
扁平序列 : str bytes bytearray memoryvivew arrary.arrary
存放的是值而不是引用
只能存储一种数据类型[字符 字节 数值]
连续存储
根据是否可以被修改可以分为:可变序列 不可变序列
从UML图中可以看出两者之间的部分继承关系,但是内置的序列 类型不是直接sequence
和mutale sequence
这两个抽象基类继承来的Abstarct Base Class | ABC
immutable sequence
mutable sequence
*getitem
y
*setitem
y
*delitem
y
*contains
y
*iter
y
*reversed
y
*iadd
y
index
y
insert
y
append
y
reverse
y
extend
y
pop
y
remove
y
count
y
2.2 列表推导和生成器表达
list comprehension and generator expression
[]: 列表推导式
(): 生成器表达式
列表推导式: 快速构建list
使用原则:
列表推导式的作用只有一个:生成列表
创建新列表
尽量保持简短
在py3.x
中修复了列表推导是中的变量泄露问题(通过局部作用域修复)
使用filter + map
也可以达到相同效果
注意: filter
返回的是一个迭代器
两者的效率根据不同情况表现不一样
1 2 3 4 5 6 7 temp = '!@#$%' m1 = [ord (s) for s in temp if ord (s) > 35 ] print(m1) m2 = list (filter (lambda x: x > 35 , map (ord , temp))) print(m2)
2.2.1 笛卡尔积 1 2 3 4 5 6 7 8 9 col_1 = ["A" , "B" , "c" ] col_2 = ["!" , "@" , "#" ] col_3 = [(i, j) for i in col_1 for j in col_2] print(col_3) for i in col_1: for j in col_2: print(i, j)
列表推导式和双层for
循环俩者执行顺序一样
2.2.2生成器表达式 使用列表推导也可以初始化元组|数组|其他序列,但是生成器表达式是一个更好的选择, 因为背后支持迭代器协议,可以逐个产出元素而非先建立一个完整的list
,可以更好的节省内存
生成器表达式和列表推导式的区别是:
2.3 元组
元组: 是一个不可变的list; 记录一定的维度[位置]的数据信息
元组的拆包
本质上是将元组中的数据提取
元组的拆包方式在任意可迭代对象中都是通用的
for-loop
*
平行赋值
a, b = b, a
本质上也是拆包
可以使用_
占位符来过滤掉不需要的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 a = (1 , 2 , 3 ) for i in a: print(i) i, *j = a b = (3 , 4 ) divmod (*b)i, j, m = a
2.3.1 具名元组 collections.namedtuple
构建的类的实例所消耗的内存和元组一样, 因为字段名都存在了相应的类里
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 from collections import namedtuplestudent = namedtuple('student' , ['name' , 'age' , 'score' ]) drink = student('Drink' , '21' , '90' ) starfire = student('starfire' , '23' , '92' ) spark = student('spark' , '24' , '95' ) print(drink) print(starfire) print(spark) student(name='Drink' , age='21' , score='90' ) student(name='starfire' , age='23' , score='92' ) student(name='spark' , age='24' , score='95' ) print(drink[0 ]) print(drink.name) Drink Drink
除了从普通元组继承的属性之外, 具名元组还有独特的属性:_fields类属性|类方法|_make(iterable)和实列方法_asdict()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ... print(drink._fields) ('name' , 'age' , 'score' ) drink_dict = drink._asdict() for key, value in drink_dict.items(): print(key, ":" , value) name : Drink age : 21 score : 90 data = ("star" , "20" , "60" ) star = student._make(data) print(star) student(name='star' , age='20' , score='60' )
2.3.2 不可变列表[元组]
元组支持列表的大多数操作除了增删改元素之外
2.4切片
切片支持大多数可变序列
切片的数学表达式为[<= <)
区间运算start default=0
s[a:b:c] start end step
其中step
可以为负即反向取值
本质上在使用slice取值时, python会调用seq.__getitem__(slice(start, end, step))
或者说slice()
是一个切片对象, 可以改切片命名就像excel--sheet
一样
1 2 3 4 5 name_slice = slice (0 , 5 ) a = "drink 21 98" print(a[name_slice]) drink
2.4.1 多维切片|省略
多维切片所支持的数据序列也是多维度,而python内置的数据序列都是一维的
本质上多维切片seq[i, j]| seq[m:n, i:j]
调用的是seq.__getitem__((i, j))
或者seq.__setitem((i, j))
省略...
在python解释器中是一个符号,其实本质上Ellipsis
对象的别称
如果x
是四维数组,那么x[i:...]
就是x[i:::]
的缩写
2.4.2 切片赋值
2.5 对序列使用+和*
+
和*
都遵守一条规律即不修改原有的操作对象 而是创建一个全新的序列
+
通常是相同类型的数据
在操作中python会创建一个包含相同类型数据的序列作为拼接的结果
注意在使用seq * n
操作时, 如果seq
里的元素是对其他可变对象的引用的话, 虽然* n
拼接了, 但本质上还是对seq
的引用, 对其进行操作时很可能会出现”多重操作”
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 a = [["_" ] * 3 for i in range (3 )] b = [["_" ] * 3 ] * 3 a[1 ][2 ] = "X" b[1 ][2 ] = "O" print(a) print(b) [['_' , '_' , '_' ], ['_' , '_' , 'X' ], ['_' , '_' , '_' ]] [['_' , '_' , 'O' ], ['_' , '_' , 'O' ], ['_' , '_' , 'O' ]]
其中b
时三个指向同一对象的引用
b
所犯的错误和下面的错误一样
1 2 3 4 5 6 7 8 9 10 11 row = ["_" ] * 3 board = [] for i in range (3 ): board.append(row) board = [] for i in range (3 ): row = ["_" ] * 3 board.append(row)
2.6 序列的增量赋值
即就地增量 += *= (仅针对可变序列), 不可变序列增量拼接的话实现的是 + * 每次都会创建一个新对象,效率会很低(str除外, 因为对于str的拼接操作过于频繁, CPython对其进行优化)
在调用这些运算符中, python优先调用__iadd__|__imul__
如果没有会次级调用__add__|__mul__
需要注意的是:
如果a
实现了__iadd__
那么+=
会实现就地增量赋值,像a.extend(iterator)
否则会调用__add__
, 此时不是就地增加,而是首先计算a + b
得到一个新的对象, 然后再将对象赋值给a
,即是否实现就地
完全取决于这个类型有没有实现__iadd__
1 2 3 4 5 6 7 8 9 10 11 12 13 14 aa = [1 , 2 , 3 ] print(id (aa)) aa += [1 , 23 ] print(id (aa)) ss = "123" print(id (ss)) ss += "qwer" print(id (ss)) 1168530238600 1168530238600 1168257348464 1168257157616
对不可变序列进行重复拼接操作:
创建一个新对象
将原来对象的元素复制到新对象中
追加寻元素
str有优化:在为str初始化内存的时候,程序会为他流出额外的可扩展空间,因此进行增量操作的时候,并不会涉及复制原有字符串到新位置的这一操作
2.6.1 元组的增量赋值 1 2 3 4 5 6 7 8 t = (1 , 2 , [1 , 2 ]) t[2 ] += [3 , 4 ] Traceback (most recent call last): File "<input>" , line 2 , in <module> TypeError: 'tuple' object does not support item assignment print(t) (1 , 2 , [1 , 2 , 3 , 4 ])
元组中的元素被成功拼接
解释器抛出异常
其实写成t[2].extend([3, 4])就不会抛出异常了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 dis.dis('tuple[i] += iter' ) 1 0 LOAD_NAME 0 (tuple ) 2 LOAD_NAME 1 (i) 4 DUP_TOP_TWO 6 BINARY_SUBSCR 8 LOAD_NAME 2 (iter ) 10 INPLACE_ADD 12 ROT_THREE 14 STORE_SUBSCR 16 LOAD_CONST 0 (None ) 18 RETURN_VALUE
BINARY_SUBSCR: 将值存入stack (DONE)
INPLACE_ADD 完成 += (DONE)
STORE_SUBSCR 赋值 (ERROR)
可以得出:
不要把可变对象放在元组里
增量赋值不是一个原子操作
2.7 排序.sort和sorted()
两者都是排序, 且内部算法使用的是Timsort
,它是一种自适应算法,会根据原始数据的顺序特点交替使用插入排序和并归排序,而且Timsort
算法是稳定的(相等元素的相对位置保持不变)
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 import randomfrom itertools import groupbyfrom operator import itemgetterfrom heapq import nlargest, nsmallestrandom.seed(7 ) a = [i for i in range (5 )] random.shuffle(a) print(a) a.sort() print(a) print(nlargest(3 , a)) print(nsmallest(3 , a)) temp_data = [ {"name" : 1 , "age" : 1 }, {"name" : 2 , "age" : 32 }, {"name" : 1 , "age" : 45 }, ] temp_data.sort(key=itemgetter("name" )) for name, data in groupby(temp_data, key=itemgetter("name" )): print(f"name---[{name} ]-----> [{list (data)} ]" ) [4 , 0 , 3 , 1 , 2 ] [0 , 1 , 2 , 3 , 4 ] [4 , 3 , 2 ] [0 , 1 , 2 ] name---[1]-----> [[{'name': 1, 'age': 1}, {'name': 1, 'age': 45}]] name---[2]-----> [[{'name': 2, 'age': 32}]]
2.8 bisect管理有序序列
bisect
模块包含两个主要的函数bisect
和insort
, 两者都是通过二分法获取index
并进行操作
bisect(haystack, needle)
, 在haystack
中搜索needle
其中:
haystack
必须是有序的
return index
bisect
可以建立一个用数字作为索引的查询表格, 比如把表格和成绩对应起来
1 2 3 4 5 6 7 8 import bisectdef grade (socre, grades=None , breakpoints="FDCBA" ): if grades is None : grades = [50 , 60 , 70 , 80 , 90 ] socre_index = bisect.bisect(grades, score) return breakpoints[socre_index]
bisect.insort(seq, item)
把变量item
插入到有序序列seq
中[本质上是通过二分法获取index后插入]
如果只是处理数字列表的话, array
是一个更好的选择
2.9 当list不是首选时 list
可以处理多个数据类型,但当面对大量的数字类型时, array
是一个更好的选择
数组存储的不是num
对象,而是数字的机器翻译(字节表述)
和C语言的数组一样, 如果需要频繁的对序列进行进出操作, deque
的速度会更快
在处理包含
操作时,set
是个更好的选择,python对此进行过优化,需要注意的是,set
不是序列,他是无序的
2.9.1 数组 array.array
在存储数字方面效率胜于list
且支持list
的基本操作,此外数组还提供从文件读取(.frombytes)
和存入文件(.tofile)
的快速方法
python中的数组和C一样, 在创建数组时都需要一个类型码,以表明要存储的数据类型
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 from array import arrayfrom random import randomfloats = array("d" , [random() for _ in range (10 ** 7 )]) print(floats[1 ]) file_path = "./floats.bin" fp = open (file_path, "wb" ) floats.tofile(fp) fp.close() floats2 = array("d" ) op = open (file_path, "rb" ) floats2.fromfile(op, 10 **7 ) print(floats2[1 ])
method区别:
数组不支持浅拷贝|但支持__deepcopy__
数组支持seq.reverse()
但不支持seq.__reversed__()
数组不支持就地排序seq.sort()
从python3.4开始数组不再支持就地排序seq.sort(), 如果需要排序得用sorted函数重新建立一个数组
a = array.array(a.typecode, sorted(a))
2.9.2 内存视图 memoryview
是一个内置类, 可以让用户在不复制内容的情况下操作同一数组的不同切片
本质上是泛化和去数学化的Numpy
数组
可以在不需要复制内容的前提下,在任何数据结构之间共享内存
处理大数据很实用 –> 节省了数据copy内存和时间
需要注意是对原数据直接进行操作
memoryview.cast
能用不同的方式读写同一块内存数据 ,并且内容字节不会随意移动,会把同一块内存里的内容打包成一个全新的memoryview.cast
对象给你
不同方式 :可以用不同的读写方式操作数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 temp_array = array('h' , [1 , 2 , 3 , 4 , 5 , 6 ]) view = memoryview (temp_array) cast = view.cast("B" ) temp = cast.tolist() cast[1 ] = 6
2.9.3 NumPy|SciPy
NumPy和SciPy提供了高阶数组和矩阵操作, python内置数据类型都是一维数据
维度: row
每个维度所含元素: col
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import numpya = numpy.arange(12 ) print(a.shape) (12 ,) a.shape = 4 , 3 [[ 0 1 2 ] [ 3 4 5 ] [ 6 7 8 ] [ 9 10 11 ]] a[1 , 2 ] a[:, 1 ] a.transpose()
2.9.4 双向队列
用列表和append pop(0)可以实现队列 先进先出, 但是删除list中的第一个或者在第一元素前添加一个元素都是十分耗时的, 因为这涉及到所有元素的移动
collection.deque
双端队列实现了队列两端元素操作的优化, 但是涉及到中间元素的操作依然会耗时, 并且是一个thread safe
类
快速从两端对元素进行操作
存储最近使用的几个元素
collection.deque
可以实现简单的过期机制<通过设置maxlen>
append(item)|popleft(index)
都是原子操作, 因此deque
可以在多线程程序中安全地当作先进先出
的栈
使用, 而且使用者不需要担心资源锁
问题
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 from collections import dequedq = deque(range (10 ), maxlen=10 ) dq.append(10 ) dq.appendleft(0 ) dq.rotate(3 ) dq.extend([10 , 11 , 12 ]) dq.extendleft([-1 , -2 , -3 ])
2.10 线程安全
除了collections.deque
外还有其他python
标准库也有对队列的实现
queue
提供了同步线程安全
类Queue|LifoQueue|PriorityQueue
,不同的线程 可以利用这些数据类型来交换信息
线程通信
三个类构造时都有一个可选参数maxsize
用来限制队列大小
在满员时这些类并不会丢掉旧的元素腾出位置
而是会锁住资源, 直到另外的线程移除了某个元素
适合控制活跃线程的数量
multiprocessing
这个包实现了自己的Queue
和queue.Queue
类似, 是设计给进程 通信用的还有一个multiprocessiong.JoinableQueue
asyncio
python3.4
及以上提供, 里面有Queue
LifoQueue PriorityQueue
和JoinableQueue
这些类受到queue
和multiprocessing
的影响,用于异步变成任务管理
heapq
和上面三个模块不同, heapq
没有队列类,而是提供了heappush
和heappop
方法, 可以让用户把可变序列
当作堆队列或者优先队列使用
本章小结 可变序列 不可变序列
容器序列: 存储引用 非连续存储
扁平序列: 存储值 连续存储 原子性数据
列表: 列表中数据最好是具有相同特性的数据
拆包: 获取元组内数据最安全可靠的方法,*
拆包的利器
具名元组: 将元组与数据关联
namedtuple()._asdict
将具名元组转化成ordereddoct
dict(zip(iter_key, iter_value))
普通元组转化dict
序列切片是python中最受欢迎的语言特性之一
对切片赋值是一个修改可变序列的捷径
重复拼接n*seq
要在正确的前提下使用!!!!!
增量赋值+= 和 *=
的操作行为和序列本身有关(可变序列|不可变序列)
PS: string类型有优化不会重复复制原数据
元组的鼻祖是ABC语言中的compounds, tuple更准确的说是frozenlist
key参数
再使用默认函数key进行排序时Python
总会比较两个键, 但是这一计算阶段发生在C
语言层, 这也是比调用用户自定义比较函数快的原因
当ele
是int
或者str
时, key
支持int
或者str
int
: 比较整个数值
str
: 只比较第一位数值
1 2 3 4 5 6 temp_1 = [3 , 45 , 2 , "10" , "34" ] print(sorted (temp_1, key=int )) print(sorted (temp_1, key=str )) [2 , 3 , '10' , '34' , 45 ] ['10' , 2 , 3 , '34' , 45 ]
Part3.Dict|Set
dict
的内置函数_builtins_.dict_
, Python对字典实现了高度优化 –> hashtable
3.1 泛映射类型 collections.abc
中含有Mapping
和MutableMapping
这俩个抽象类,为dict
和其他类似的类型定义形式接口
非抽象类一般不会直接继承这些抽象基类, 而是直接对dict
或者collections.User.Dict
进行扩展,这些抽象基类的主要作用是作为形式化的文档, 定义构建一个映射类型所需要的最基本接口.而且还可以与isinstance
一起使用来判断某个数据是否是广义上的mapping
类型
1 2 my_dict = {} isinstance (my_dict, abc.mapping)
使用isinstance
而不是type
可以判断除dict
外的广义映射类型
标准库的所有映射类型都是通过dict来实现的,他们有个共同的限制, 即key必须是可散列的
可散列数据
1 An object is hashable if it has a hash value which never changes during its lifetime (it needs a __hash__() method), and can be compared to other objects (it needs an __eq__() method). Hashable objects which compare equal must have the same hash value.
含有__hash__
散列
含有__eq__
比较key
哈希值在整个声明周期不会变化
散列对象相等==>散列值一定相等
可散列的数据类型:
str
bytes
数值类型
frozenset
tuple(所含的元素全为可散列)
一般用户自定义的类型的对象都是可散列的, 其中散列值就是id(obj)
, 所以所有这些对象在比较时都是不相等的. 如果一个对象实现了__eq__
方法, 并且在方法中用到了这个对象的内部状态的话, 那么只有当所有这些内部状态都是不可变的情况下, 这个对象才是可散列的
构造字典方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 >> dict (name="Drink" , age="23" , score="98" )>> {"name" : "Drink" , "age" : "23" , "score" : "98" } >> dict (zip (["name" , "age" , "score" ], ["Drink" , "23" , "98" ]))>> dict ([("name" , "Drink" ), ("age" , "23" ), ("score" , "98" )])>> dict ({"name" : "Drink" , "age" : "23" , "score" : "98" })>> {key, value for key, value in iterator(key-value-pair)}
3.2 字典推导
推导式的作用:
创建新的数据对象
过滤作用
3.3 常见的映射方法 映射类型的方法很丰富, dict
defalutdict
和OrderedDict
的常见方法, 后面两个数据类型是dict
的变种,位于collections
模块内
default_factory
不是一个方法而是一个可调用对象, 他的值在defaultdict
初始化中由用户设置
OrderedDict.popitem()
会移除最先插入的元素**(先进先出), 如果.popitem(last=True)
则会移除最后插入的元素 (后进先出)**
d.update(m, [**kargs])
, 在处理m
时, 函数首先检验m
是否有keys
方法,
有–>update
会把它当作映射对象处理
无–>update
会把它当作含有(key-vaule)
键值对元素的迭代器
python
大多数映射类型的构建都采用了相似的逻辑
可调用对象
将类当作函数调用instance()
类中实现__call__
快速调用带有某一实例化属性的类的函数
1 2 3 4 5 6 7 8 9 class eval_some : def __init__ (slef, g ): self.g = g def __call__ (self, t ): return (self.g * 2 ) / t earth = eval_some(9.8 ) print(earth(10 ))
3.3.1 更新字典kV 用setdefault处理找不到的键
d[k] --> getitem
找不到键时, 会有异常抛出KeyError
符合python的快速失败哲学
可以使用d.get(k, default)
代替异常
当在更新某个键对应的值的时候, 以上俩个方式效率都很低
如果要更新字典的一个键值对(包括不存在)使用setdefault(key, default) [operate]
这样只会query 1
,如果使用if else []
则会查询2~3
次
1 2 3 4 5 6 7 8 9 10 11 a = {"name" : "Drink" } a.setdefault("things" , []).append("reading" ) if "things" not in a: a["things" ] = [] a["things" ].append("reading" )
3.4 映射的弹性键查询
在映射中查找不存的key时, 我们希望可以返回一个default value而不是error, 以下俩途径可以实现弹性键查询
通过defaultdict
这个类而不是普通的dict
自己定义一个dict
子类, 并在在子类中实现__missing__
方法
3.4.1 defaultdict处理不存在的key
defaultdict
是collections
中的一个类, 在实例化它时,可以接受一个defaultfoctory
不接受参数(一定是可调用的且不接受参数的方法类|函数)
在调用d[key]
中如果__getitem__
找不到对应的key
会调用__missing__
查看是否定义了defaultfactory
将defaultfactory
返回的值赋值给d[key]
更新dict
1 2 3 4 5 6 7 8 from collections import defaultdicttemp = defaultdict(list ) temp["name" ] print(temp) >> defaultdict(<class 'list'>, {"name": []})
如果defaultfactory=None
在没有找到对应的key
时会抛出KeyError
defaultfactory
只会在__getitem__
中调用, 比如dict[key]
才会被调用, 其中连接两者桥梁的时__missing__
3.4.2 特殊方法missing
所有的映射类型在找不到key的时候都会牵扯到missing, python基类虽然直到这个方法,但并没有定义这个方法, 不过, 如果一个class继承了dict并且这个类提供了missing方法,那么在getitem
找不到值的时候不会抛出异常
特点:
missing
方法只会被getitem
调用
不会对.get()
__contains__
方法产生影响
dict[key] -- __getitem__ -- __missing__
dict.get() -- get -- try except
如果要自定义一个映射类型, 更合适的策略是继承collections.UserDict
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class StrKeyDict (dict ): def __missing__ (self, key ): if isinstance (key, str ): raise KeyError(key) return self[str (key)] def get (self, key, default=None ): try : return self[key] except KeyError: return default def __contains__ (self, key ): return key in self.keys() or str (key) in self.keys()
像k in dict.keys()
操作在python3
中是十分快速的, 即使映射类型对象很庞大, 因为:
dict.keys()
返回的值是一个视图, 就像集合, 在里面查找一个元素是十分快的
key in dict
扫描的是一个list, 处理大数据效率低dict.keys()
3.5 字典的变种 都是基于collections
库
除了bultin中定义的dict外, 其余字典的变种都是collections
库中的
UserDict
比较特殊的一种dict, 这个类其实是把builtin中的dict用纯Pyhton又实现了一遍
一般用于用户自定义子类的继承
OrderedDict
这个类型会在添加键的时候保持顺序, 因此键的每次迭代次序总是一致的
OrderedDict
的popitem
方法默认删除并返回的时字典里的最后一个元素(先进后出), 如果popitem(last=False)
则会删除并返回第一个元素(先进先出)
ChainMap 什么是chainmap?为什么要搞chainmap
该类型可以容纳数个不同的映射对象,然后在进行键查找操作时, 这些对象会被当作一个整体逐个进行查找,直到键被找到
在给有嵌套功能作用域的语言做解释器的时候很有用, 可以用一个映射对象来代表一个作用域的上下文
ChainMap
可以将多个字典合并为一个独有的字典,这样的操作 并不是对源数据的拷贝,而是指向源数据,假如原字典数据修改,ChainMap
映射也会改变;如果对ChainMap
的结果修改,那么原数据一样也会被修改
使用update进行合并字典,是对源数据的拷贝
ChainMap
可接受多个映射然后在逻辑上使它们表现为一个单独的映射结构;它只是维护了一个记录底层映射关系的列表,然后去重定义常用的字典操作
如果有重复的键,会采用第一个映射中键对应的值
修改 ChainMap
映射结构,会同时作用在自己和原始字典结构上
可以使用字典的 update()
方法,来替代上面的合并方案;但是这就需要创建一个新的字典对象(或者修改原字典,破坏了原始数据),并且原始字典做了修改,并不会反映到新建的字典上
ChainMap
使用的就是原始字典,因此原字典变,它也会改变。
如果在使用 ChainMap 合并多个字典时,字典中有重复的 key 值,默认取第一个字典中 key 对应的 value ,从原理上面讲,ChainMap 实际上是把放入的字典存储在一个队列中,当进行字典的增加删除等操作只会在第一个字典上进行,当进行查找的时候会依次查找(以查找的第一个key-value返回)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 m1 = {"name" : "Drink" , "age" : 23 } m2 = {"name" : "Drinkle" , "score" : 93 } m3 = {"name" : "Spark" , "age" : 23 , "score" : 98 , "sex" : "man" } cm = collections.ChainMap(m1, m2, m3) print(cm) print(cm["name" ]) for key, value in cm.items(): print(key, " <--> " , value) ChainMap({'name' : 'Drink' , 'age' : 23 }, {'name' : 'Drinkle' , 'score' : 93 }, {'name' : 'Spark' , 'age' : 23 , 'score' : 98 , 'sex' : 'man' }) Drink name <--> Drink age <--> 23 score <--> 93 sex <--> man
Counter
这个类型会给键准备一个整数计数器, 每次更新一个键的时候都会增加这个计数器.因此这个类型可以用来给可散列对象计数(str bytes frozenset 数值型)或者当成多重集来用–>多重集合就是集合里的元素可以出现不止一次, Counter实现了+ - 运算符来和并记录
还有像most_common(num)
这类方法, 会按照次序返回映射里最常见的num个键和他们的计数
1 2 3 4 5 6 7 8 9 10 11 counter = collections.Counter("asdxzcsdfwerewrfewzsqawd" ) print(counter) counter.update("asdsassd" ) print(counter.most_common()) Counter({'w' : 4 , 's' : 3 , 'd' : 3 , 'e' : 3 , 'a' : 2 , 'z' : 2 , 'f' : 2 , 'r' : 2 , 'x' : 1 , 'c' : 1 , 'q' : 1 }) [('s' , 7 ), ('d' , 5 )]
3.6 子类化UserDict
自定义mapping类型,以UserDict
为基类更加方便,因为UserDict
是原生的方法, 而dict
可能会走写捷径,导致我们不得不在子类中重写这些方法, 而UserDict
则不会出现这个问题
需要注意的是, UserDict
并不是Dict
的子类,但在data
属性是基于Dict
的实例实现的, 这个属性实际上是UserDict
最终存储数据的地方
UserDict
的子类在实现__setitem__
避免了不必要的递归
可以让__contains__
的代码更加简洁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import collectionsclass StrKeyDict (collections.UserDict ): def __missing__ (self, key ): if isinstance (key, str ): raise KeyError (key) return self[str (key)] def __contains__ (self, key ): return str (key) in self.data def __setitem__ (self, key, value ): self.data[str (key)] = value
UserDict
继承的是MutableMapping
,所以StrKeyDict
里剩下的映射类型方法都是从UserDict
MutableMapping
和 Mapping
这些超类中继承的
Mapping
类虽然是一个抽象基类ABC
,但它提供了许多实用方法
1.MutableMapping.update
可以直接使用dict.update(obj)
定义在__init__
中, 实列化时可以接受多个不同类型的参数构造dict
本质上调用的是__setitem__
–> self[key]=value
2.Mapping.get
.get()
–> def get()
–> try except
3.7 不可变映射类型
接受一个mapping类型, 返还一个动态只读视图, 在原数据上更改可以反映到视图上,但不能对视图进行写操作, 只能读取 types.MappingProxyType
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from types import MappingProxyTypetemp = {"name" : "Drink" } proxy_dict = MappingProxyType(temp) >> proxy_dict["name" ] proxy_dict["age" ] proxy_dict["score" ] = 90 temp["age" ] = 23 proxy_dict >> <Dirnk> <KeyError: 'age' > <TypeError: 'mappingproxy' object does not support item assignment> <SUCCESS> <{'name' : 'Drink' , 'age' : 23 }>
3.8 集合论
集合从2.3以module出现, 到2.6才成为built-in type, 包括set
frozenset
集合的本质是许多唯一对象的聚集(去重)
拥有极快的查找速度 ==> 散列表
空集 –> set()
{}
–> dict 本质上是一个空字典
集合内元素必须是can hash
即可散列的
集合的操作
|
–> ∪
&
–> ∩
–> set.intersection(set)
-
–> 差集
3.8.1 集合字面量「仅set」
除了空集合set()
外, 集合的字面量如同{1} {1, 2}
字面量语句构造集合的方法比set([1, 2, "3"])
更快, 后者python
需要新建一个列表, 最后吧这个列表传入到构造方法中, 如果是{1, 2, "3"}
的字面量构造, python
会利用一个BUILD_SET
的字节码创建集合
使用反汇编函数查看字节码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from dis import disdis("{1}" ) dis("set([1])" ) 0 LOAD_CONST 0 (1 )2 BUILD_SET 1 4 RETURN_VALUE 0 LOAD_NAME 0 (set )2 LOAD_CONST 0 (1 )4 BUILD_LIST 1 --6 CALL_FUNCTION 1 --8 RETURN_VALUE
python
对于frozenset
没有特殊字面量句法, 只能采用构造的方式
3.8.2 集合推导
同列表推导字典推导一样
创建一个新的序列
起到过滤条件的作用
3.8.3 集合的操作
1 2 3 4 5 6 7 8 9 10 a = {1 , 2 , 3 } a.remove(4 ) a.pop() a.discard(4 )
下表中, 缀运算符需要两侧的对象都是集合类型, 但是其他的方法则只要求所传入的参数是可迭代对象
集合的比较运算符, 返回布尔值
集合的其他操作方法
3.9 dict和set的原理
principles of dict and set
, 了解python
里的字典和集合类型,他们背后所涉及的hash table
是必不可少的
这节将会回答以下几个问题
python
中的dict
和set
效率有多高?
为什么他是无序且不稳定的?
为什么并不是所有的python
对象都可以当作dict
的key
或set
里的number
?
为什么dict
的key
和set
的number
的顺序是根据它们被添加的次序而定的,以及为什么在映射对象的生命周期中,这个顺序并不是一成不变的?
为什么不应该迭代循环dict
或set
的同时往里面添加元素?
1 2 3 4 5 6 7 8 temp_list = [1 , 2 , 3 , 4 ] a = {"name" : "Drink" }.fromkeys(temp_list, "" ) print(a) dict .fromkeys(iterator, values){1 : '' , 2 : '' , 3 : '' , 4 : '' }
3.9.1 效率实验 使用in set(&)
操作符,分别在set dict list
查询是否含有某个元素
其中结果如下:
最快的是&
最慢的是list in
由于列表背后没有散列表来支持in
运算符, 每次搜索都会扫描一次完成的列表 [说明了, dict
和set
在使用散列表存储的时候使用了类似排序的功能]
3.9.2 dict中的散列表
这一节只是笼统的描述python如何使用hashtable来实现dict, 其中Cpython对dict有过优化
散列表
本质上是一个稀疏数组(总是有空白元素的数组称为稀疏数组)
散列表中的单元叫做表元(bucket)
在dict的散列表中, 每个键值对占用一个表元,一个表元有俩部分, 分别是对值的引用和对键的引用
表元大小一致, 可以通过偏移量读取特定的表元
python会保证散列表中大约三分之一的表元是空的,如果达到阈值, 原有的散列表会被复制 到一个更大的空间中
把对象放入散列表, 首先使用hash()
计算这个元素键的散列值
有关散列表的概念 :
1.散列值和相等性
hash()
==
hash()
可以用于任何内置类型对象, 自定义对象调用自定义实现的__hash__
如果俩对象在比较时是相等的, 则它们的散列值一定相等(但对象的类型和存储结构不一定相等)
为了让散列值可以当作散列表的索引, 越是相似但不相等的对象,他们散列值的差别就越大
从python3.3开始 str bytes datetime对象的散列值计算过程中多了随机加盐
所加盐值是python进程内的一个常量每次启动python解释器都会生成一个不同的盐值,目的是为了防止DOS
攻击而采取的安全措施
2.散列表算法
以dict[key]
为例:
python首先会调用hash(key)
计算key的散列值
把计算的散列值最低的几位数字当作偏移量, 在散列表中查找表元bucket
具体取几位要看散列表的大小
若对应的表元为空 –> KeyError
若非空 –> 表元里会有key-value-pairs
python会校验key-value-pairs
中的key
与输入的key
是否相等==
相等 –> 返回对应的value
不等 –> 散列冲突
散列冲突: 因为散列表所做的是把随机的元素映射到只有几位的数字上,而散列表本身的索引又依赖于这个数字的一部分
为了解决冲突,算法会在散列值中另外再取几位数字用特殊方法处理后把新的数字当作索引寻找表元,并重复之前的操作
添加新元素:
发现表元为空时会放入一个新元素
更新元素 :
找到表元后会更新元素
需要注意的是,python会按照散列表的拥挤程度来决定是否要重新分配内存为它扩容,如果增加散列表的大小,那散列值所占的位数和用作索引的位数会增加,减少了散列冲突
**注意:**Cpython中规定如果有一个整型对象,而且他能存进入一个机器字中,那么他的散列值就是它本身
3.9.3 dict的实现和限制
主要讨论hashtable给dict带来的优势和限制
1.键必须是可散列的
包括: str bytes frozenset 数值类型
一个可散列的对象必须包括
支持hash()
函数并且通过__hash__
所得到的散列值不变(在他的生命周期中)
支持通过__eq__
方法检测相等性
若a==b
那么hash(a)==hash(b)
所有由用户自定义的对象默认都是可散列的,因为他们的散列值都是id()
来获取的,而且他们不相等
**注意:**如果自定义的类实现了_eq_
,并且希望他是可散列的, 那么必须定义一个恰当的_hash_
保证a==b
时hash(a)==hash(b)
另一方面,如果一个含有自定义_eq_
依赖的类处于可变状态, 那么就不要在这个类中实现_hash_
,因为他的实例是不可散列的
2.字典在内存中开销大
字典使用散列表实现
散列表是稀疏的
在自定义类中_slots_
属性可以改变实例属性的存储方式, 由dict-->tuple
optimization
优化往往和可维护相对立maintain
3.键查询很快
空间换时间
hash table
实现了无视数据量大小的快速访问(在数据能放入内存的前提下)
4.键的次序取决于添加次序
在添加新键时, 可能会由哈希冲突, 此时新键会被安排到另一个表元(bucket)中
但是和俩个值相同的键添加顺序有关
虽然逻辑和面向用户表现形式上如此,在判断俩上依旧是相等的
1 2 3 4 5 6 a = {"name" : "Drink" , "age" : 23 } b = {"age" : 23 , "name" : "Drink" } if a == b: print("you are right" ) >> right
5.添加新键会改变已有键的顺序
尽量不要边遍历边添加新键
添加新键
python解释器为字典扩容
新键散列表并把已有元素添加到新表中
这个过程可能会发生散列冲突,导致新散列表中键的次序变化
如果在迭代一个字典的所有键的过程中同时对字典进行修改,那么这个循环可能会跳过一些键甚至是字典中已有的键
因此:不要对字典同时进行迭代和修改,如果扫描并修改一个字典,最好分成俩步 :
[迭代–得出要添加的内容–并把内容放到新字典]
[对原有字典进行更新]
不过在python3
中对此进行了优化,.keys()
.items()
.values()
方法返回的都是字典视图, 换句话说这些方法返回的更像set
而不是list
,此外视图还可以动态的反馈字典的变化
3.9.4 set的实现和限制
set 和 frozenset的实现也依赖于散列表
在散列表中只存放元素的引用(类似只存放key的引用)
集合中的元素必须是可散列的
集合很消耗内存
可以很高效的判断元素是否在集合中
元素的次序取决于被添加到集合中的顺序
在集合里添加元素会改变集合里已有元素的次序
本章小结 dict
是python
的基石, 除了dict
外builtin
里还提供了特殊映射比如collections
里的defaultdict
OrderDict
ChainMap
Counter
UserDict
以及types
里的MappingProxyTypes
映射类型的重要方法
1.setdefault
更新字典里存档的可变值(比如列表)
避免重复搜索键
2.update
3.missing
collections.abc
模块提供了Mapping
和MutableMapping
俩大抽象基类,利用它可以进行类型查询或者引用,此外还有Set
和MutableSet
俩大抽象基类
Part4.TextAndBytes
human use text and computer use bytes.
本章节主要讨论:
4.1 字符问题
字符串:一个字符序列, 本质上是一个序列; 问题是什么是字符?
2015年字符的定义为Unicode字符(面向人)
Unicode标准把字符的标识-和具体的字节表述进行了区分:
字符的标识即码位 :是01114111的数字(10进制)在unicode
标准中以46个十六进制数字标识,并且加前缀U+
字符的具体表述取决于所用的编码 :编码是在码位和字节序列之间转换时使用的算法
把码位转化成字节序列的过程时编码,反之是解码
简单的理解:
码位 是数字映射后的准备编码的字符标识 ;
字节序列 是通过一定编码规则(算法)编码后的用于计算机存储和传播的字节序列
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 a = 'advdv我' print(len (a)) b = a.encode('utf-8' ) print(b, len (b)) print(b.decode('utf-8' )) >> 6 b'advdv\xe6\x88\x91' 8 advdv我
python3
的str
类型基本相当于python2
的unicode
类型, 但是前者的bytes
类型却不是把str
类型换个名字那么简单
4.2 字节概要 python
内置了两个基本的二进制序列类型其中有py3
引入的不可变bytes
类型和py2.6
添加的可变bytesarray
类型
bytes
和bytearray
对象的各个元素是介于0~255(含)
之间的整数而不是py2
的str
对象那样的单个字符.
需要注意的是二进制序列的切片始终是同一类型的二进制序列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 cafe = bytes ("cafの" , encoding="utf_8" ) print(cafe) print(cafe[0 ]) print(cafe[:1 ]) cafe_arr = bytearray (cafe) print(cafe_arr) print(cafe_arr[-1 :]) >> b'caf\xe3\x81\xae' 99 b'c' bytearray (b'caf\xe3\x81\xae' )bytearray (b'\xae' )
二进制序列本质上整数序列,但是他们的字面量表示含有ASCII文本,因此各个字节的值会有以下三种不同的方式显示 –> 例如b'xxxxx\xx\xx\t'
可打印的ASCII范围内的字节(从空格到~)使用ASCII字符本身
制表符 换行符 回车符和\ 使用转义序列 \t
\n
\r
\\
其他字节的值,使用十六进制转义序列(\x00
空字节)
1 2 3 4 5 6 7 a = bytes .fromhex('31 4b ce a9' ) print(a) b'1K\xce\xa9' 31(16进制) --> 49(10进制) --> ASCII(1K) """ 在ASCII中K代表10进制 """
4.2.1 str方法 1.casefold
lower()
只对 ASCII 也就是 'A-Z'
有效,但是其它一些语言里面存在小写的情况就没办法了。文档里面举得例子是德语中'ß'
的小写是'ss'
:
1 2 3 s = 'ß' s.lower() s.casefold()
汉语 & 英语环境下面,继续用 lower()
没问题;要处理其它语言且存在大小写情况的时候再用casefold()
2.isdecimal
判断string
是否是十进制, 返回bool
空字符为False
3.isidentifier
Python 对各种变量、方法、函数等命名时使用的字符序列称为标识符
检查string
是否是python
标识符
标准为:
对应keyword.iskeyword(str)
检查是否是python
保留的关键字
4.isnumeric
isnumeric()
方法检测字符串是否只由数字组成,数字可以是Unicode
数字,全角数字(双字节) 罗马数字 汉字数字。指数类似 ² 与分数类似 ½ 也属于数字。返回bool
5.isprintable
判断是否是可打印字符,不可打印的有:
6.translate
s.transelate(table)
table
用str.maketrans(in-str, out-str)
生成用于创建字符映射的转换表
转换表本质上整数映射
1 2 3 print(transtable) {115 : 109 , 111 : 97 , 109 : 110 , 101 : 121 }
以table
作为基础替换s
中所有命中的字符
1 2 3 4 5 6 7 8 a = "i like something relaxing!" in_str = "some" out_str = "many" transtable = str .maketrans(in_str, out_str) print(a.translate(transtable)) >> i liky manything rylaxing!
str
类型的其他方法都支持bytes
和bytearray
类型
二进制序列并不是指字节序列全是二进制编码,而是指用于计算机存储和传播的二进制字节序列即字节序列,其本质上还是整数序列
构建bytes
或bytearray
实例还可以调用各自的构造方法,传参如下:
一个str
对象(码位)和encoding
关键字参数(编码规则)
一个可迭代对象, 提供0~255
数值
一个实现缓冲协议的对象(bytes bytearray memoryview array.array)
此时把源对象中的字节序列复制到新建的二进制序列中
注意使用缓冲类对象构建二进制序列是一个底层操作,可能涉及类型转换
下面是将array.array
转化成字节序列表示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import arraynumbers = array.array('h' , [-2 , -1 , 0 , 1 , 2 ]) to_bytes = bytes (numbers) print(to_bytes) >> b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00' temp_array = array.array('h' ) print(temp_array.frombytes(to_bytes)) print(temp_array) >> None array('h' , [-2 , -1 , 0 , 1 , 2 ])
需要注意
使用缓冲类对象(bytes bytearray array.array memoryview)
创建bytes
bytearray
对象时, 始终复制的是源对象中的字节序列
不过memoryview
对象允许在二进制数据结构之间共享内存
从二进制序列提取结构化信息需要struct
模块
4.2.2 结构体和内存视图 struct
模块提供了一些函数, 把打包的字节序列转换成不同类型字段组成的元组,还有一些函数用于执行反向转换,把元组转化成打包的字节序列.可以处理bytes
bytearray
memoryview
对象
注意 :
memoryview
类不是用于创建或存储字节序列的, 而是共享内存,让你访问其他二进制序列 打包的数组和缓冲中的数据切片,而无需复制字节序列
memoryview 对象的切片是一个新 memoryview 对象,而且不会复制字节序列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import structfmt = "<3s3sHH" whith open ("filter.gif" , "rb" ) as fp: img = memoryview (fp.read()) header = img[:10 ] bytes (header)struct.unpack(fmt, header) del headerdel img
如果经常处理二进制数据推荐阅读:
memoryview
struct interpret bytes as packed binary data
4.3 基本的编解码器
python自带了超过100种编解码器(编码算法)用于在文本和字节之间相互转换
其中后三者是可变长度的多字节编码, 其余都是单字节编码
图中星号表示某些编码(ASCII
和多字节编码GB2312
)不能表示所有的Unicode
字符, UTF
编码的设计目的是处理每一个Unicode
码位
4.4 编解码问题
编解码涉及到unicode
或者string
或称码位
与byte
的编解码转换
编解码异常有三种:
1.UnicodeEncodeError
str
转byte
异常
2.UnicodeDecodeError
byte
转str
异常
3.SynatxError
如果在编解码中,源码的编码与预期不符, 在加载模块时会抛出语法异常
4.4.1 UnicodeEncodeError 4.4.2 UnicodeDecodeError 4.4.3 SyntaxError
多数非UTF
编解码器只能处理Unicode
字符的一小部分子集, 把文本转化成字节序列时, 如果目标编码中没有定义某个字符就会抛该异常, 处理方式是把errors
参数传给编码方法或函数,对错误进行特殊处理
不是每个字节都包含有效的ASCII字符,也不是每个字符序列都是有效UTF-8
或UTF-16
因此,在把二进制序列转化成文本时, 如果假设是这俩个编码中的一个, 遇到无法转换的字节序列时会抛出UnicodeDecodeError
str.en|decode(encoding=””, errors=””)
errors
接收四个参数
需要注意的是errors接受的参数是可以扩展的, 你可以位errors参数注册额外的字符串方法codecs.register_error
函数
4.4.4 找出字节序列编码
简单的说一般找不出字节序列的编码, 有些通讯协议和文件格式, 比如 HTTP和XML包含明确指明内容编码的首部 . 可以肯定的是, 某些字节流不是ASCII, 因为其中包含大于127的字节值, 而且制定UTF-8和UTF-16的方式也限制了可用的字节序列.即使如此我们也无法根据特定的位模式来确定二进制文件的编码时ASCII或UTF-8
统一字符编码侦测包Chartdetect
,可以识别30中编码
二进制序列编码通常不会指明自己的编码, 但是UTF
格式可以在文本内容的开头添加一个字节序列标记
1 2 3 4 5 import chardeta = chardet.detect(b"sddssfsd" ) print(a) {'encoding' : 'ascii' , 'confidence' : 1.0 , 'language' : '' }
4.4.5 BOM有用的鬼符
BOM (bytes-Order-Mark)字节序列标记, 指明编码时使用的是IntelGPU的小子节序
utf-16
有俩种编码格式UTF-16LE
小子节序UTF-6BE
大字节序
小子节序各个码位的最低有效字节在前面, 大字节序相反(Unicode码位)
因此在utf-16
编码中使用开头的BOMb'\xff\xfe'
十进制(255 254)表示小子节编码
utf-8
无论设备使用哪种字节序,生成的字节序列始终一致, 因此不需要BOM
4.5 处理文本文件
处理文本文件最好的方式是三明治处理法, 首先将输入的字节序列转码成字符串, 在对字符串进行处理, 最后把处理好的字符串编码成字节序列进行输出
Python3
中的Open
函数会在读取文本进行必要的解码, 在以文本模式写入文件时也会进行必要的编码, 所以my_file.read()
方法得到的以及传递给my_file.write(text)
方法的都是字符串对象
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 fp = open ("text.txt" , "w" , encoding="utf-8" ) print(fp) <_io.TextIOWrapper name='text.txt' mode='w' encoding='utf-8' > fp.write("Drink" ) fp.close() import osprint(os.stat("text.txt" ).st_size) 5 fp2 = open ("text.txt" ) print(fp2.encoding) cp936 fp2.read() fp3 = open ("text.txt" , encoding="utf-8" ) fp3.read() fp4 = open ("text.txt" , "rb" ) print(fp4) <_io.BufferedReader name='text.txt' > fp4.read()
除非想要判断编码否则不要使用二进制模式打开文本文件, 即便想要查看编码类型也不要重复发明轮子, 可以使用chardet
通常不建议使用默认编码方式取处理二进制或文本流
编码默认值 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 import localeimport sysexpressions = """ locale.getpreferredencoding() type(my_file) sys.stdout.isatty() sys.stdout.encoding sys.stdin.isatty() sys.stdin.encoding sys.stderr.isatty() sys.stderr.encoding sys.getdefaultencoding() sys.getfilesystemencoding() """ my_file = open ('dummy' , 'w' ) for expression in expressions.split(): value = eval (expression) print(expression.rjust(30 ), "-->" , repr (value)) sys.stdin.isatty() sys.stderr.isatty()
1 2 3 4 5 6 7 8 9 10 locale.getpreferredencoding() --> 'cp936' type(my_file) --> <class '_io.TextIOWrapper'> sys.stdout.isatty() --> False sys.stdout.encoding --> 'UTF-8' sys.stdin.isatty() --> False sys.stdin.encoding --> 'UTF-8' sys.stderr.isatty() --> False sys.stderr.encoding --> 'UTF-8' sys.getdefaultencoding() --> 'utf-8' sys.getfilesystemencoding() --> 'utf-8'
sys.getfilesystemencoding()
用于编解码文件名而非文件内容, 把字符串参数作为文件名传给open()
函数会使用它, 如果是字节序列则直接传给OS API
locale.getprefferedencoding()
返回的编码即是打开文件的默认编码,同时也是重定向文件的sys.stdout/stdin/stderr
不要依赖默认编码尤其是windows用户
4.6 规范化Unicode字符
因为Unicode有组合字符(变音字符和附加到前一个字符上的记号, 打印时作为一个整体)所以字符串比较起来比较复杂
比如é
这个词, 是e
后加U+0301
得来的, 在unicode
中称为标准等价物, 但是python
看到的是不同的码位序列, 因此判断两者不相等, 这时就需要一个统一的标准, 来规范化Unicode
就像规范两种方言一样
使用unicodedata.normalize
函数
第一个参数:
NFC (Normalization Form C)
使用最少的码位构成等价的字符串
把组合字符分解成 [基字符] 和 [单独的组合字符]
1 2 3 4 5 6 7 8 9 10 11 12 13 s1 = "café" s2 = "cafe\u3001" print(unicodedata.normalize("NFC" , s1)) print(unicodedata.normalize("NFC" , s2)) print(unicodedata.normalize("NFD" , s1)) print(unicodedata.normalize("NFD" , s2)) café cafe、 café cafe、
用户输入的文本默认是NFC格式, 在保存文本前, 最好使用normalization(“NFC”, text)清洗字符串
需要注意的是, 在使用NFC清洗字符串时, 有些单字符会被规范成另一个单字符, 比如电阻的单位Ω会被规范成欧米茄, 在视觉上是一样的, 但在比较时并不相等
这俩个规范化方法是兼容性规范化, 这些规范化更适合用在用户搜索或索引上, 需要注意的是, 经过NFKC或者NFKD兼容性规范化后的字符可能会丧失原本的意义, 尽量不要用于数据的持久化
1 2 3 s3 = "¼" print(unicodedata.normalize("NFKC" , s3)) >> 1 /4
4.6.1 大小写折叠
即将所有的字符转化成小写, 相比于lower()只支持Unicode, casefold()支持更多的大小写转化, 尽管如此, 两者不同的字符仅有116个码位, 占比0.11%
4.6.2 规范化文本匹配实用函数
NFC和NFD可以很合理的比较Unicode字符串, 并且NFC是最好的规范化形式, 不区分大小写应该使用casefold()
1.去掉变音字符号
unicodedata.combining (unichr) 将分配给Unicode字符 unichr 的规范组合类 返回 为整数 。 如果未定义组合类,则返回 0 。
1 2 3 4 5 6 7 8 9 10 11 12 13 from unicodedata import normalizedef shave_marks (txt ): """ 去掉所有的变音符号 """ norm_txt = unicodedata.normalize("NFD" , txt) shaved = "" .join(c for c in norm_txt if not unicodedata.combining(c)) return unicodedata.normalize("NFC" , shaved)
2.比较字符是否相等|忽略大小写
1 2 3 4 5 6 7 8 9 10 from unicodedata import normalizedef nfc_equal (str1, str2 ): return normalize("NFC" , str1) == normalize("NFC" , str2) def fold_equal (str1, str2 ): return (normalize("NFC" , str1).casefold() == normalize("NFC" , str2).casefold())
4.7 Unicode文本排序
对于ASCII字符串来说比较的是码位, 但在非ASCII字符时会使用locale.strxfrm函数, 这个函数会把string转化成适合的区域进行比较
需要注意的是, 一般情况下, 变音符不会对排序造成影响, 如果有影响, 变音符号常常排在常规词后面
在使用locale.strxfrm函数作为排序key前, 要先调用setlocale(LC_COLLATE, your_locale)
需要注意:
区域设置是全局的, 不建议库中调用, 且应用或框架应该在进程中调用
操作系统必须支持区域设置
使用unicode排序算法排序
pyuca
Python Unicode Collator Algorithm
1 2 3 4 5 6 import pyucacoll = pyuca.Collator() fruits = ["apple" , "alle" , "banana" , "bba" ] sorted_fruits = sorted (fruits, key=coll.sort_key) print(sorted_fruits)
4.8 Unicode 数据库
本质上已经一个关于unicode以及个中映射关系的数据库, 就如码位与string bytes的映射一样, 同样基于此提供了许多双模式API对外调用, 出名的有unicodedata
regex库正在替代re库
双模式API是一个趋势 : 即支持字符和字节参数,并根据参数的格式做出自适应的调整
4.9 支持str和bytes的双模式API
标准库中一些函数能接受字符串或字节序列作为参数, 然后根据类型自适应做不同的行为, 其中re和os为例
4.9.1 正则str和bytes
unicode
bytes
次数|注
*
{0,}
+
{1,}
?
{0,1}
\d
匹配任何一个十进制数字, 包括[0-9]和其他数字字符
只匹配[0-9]
和re.ASCII
\w
任何Unicode字符, 所有语言|数字|下横线
[a-zA-A0-9]
和re.ASCII一样
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 import rere_numbers_str = re.compile (r"\d+" ) re_words_str = re.compile (r"\w+" ) re_numbers_bytes = re.compile (rb"\d+" ) re_words_bytes = re.compile (rb"\w+" ) text_str = "Drink see \u0be7\u0be6\u0be5\u0be4" "as 1999 = 1000 + 999" text_bytes = text_str.encode("utf-8" ) print("TEXT" , repr (text_str), sep="\n " ) print("NUMBERS" ) print(" str :" , re_numbers_str.findall(text_str)) print(" bytes:" , re_numbers_bytes.findall(text_bytes)) print("WORDS" ) print(" str :" , re_words_str.findall(text_str)) print(" bytes:" , re_words_bytes.findall(text_bytes)) >> TEXT 'Drink see ௧௦\u0be5\u0be4as 1999 = 1000 + 999' NUMBERS str : ['௧௦' , '1999' , '1000' , '999' ] bytes : [b'1999' , b'1000' , b'999' ] WORDS str : ['Drink' , 'see' , '௧௦' , 'as' , '1999' , '1000' , '999' ] bytes : [b'Drink' , b'see' , b'as' , b'1999' , b'1000' , b'999' ]
4.9.2 os中的字符串和字节序列
GNU/linux内核不理解Unicode, 因此文件名中使用字节序列都是无效的
为了方便处理字符串或字节序列形式的文件名或路径名, os模块提供了特殊的编码和解码函数
fsencode(filename)
当filename是str时使用sys.getfilesystemencoding()
编码成bytes
fsdecode(filename)
当filename是bytes时使用sys.getfilesystemencoding()
解码成str
在unix及其衍生平台中使用surrogateescape
错误处理方式, 避免碰到意外字节卡住, windows使用strict, 这种处理方式在py3.1时被引进
这种错误的处理方式实际上是将无法编码的字节替换成Unicode
中U+DC00
到U+DCFF
之间的码位(low surrogate area)
, 这些码位是保留的,没有分配字符, 只供程序内部使用
4.10 本章小结 1.一个字符不等于一个字节, 两者的对应关系取决于编码所用的算法
2.chardet包可以在没有元数据的情况下检测编码的大概方法
3.在str-bytes的编解码中, 避免使用系统默认编解码
4.文本比较由于编解码的不同, 在比较之前需要进行规范化和大小写折叠
5.双模式API的自适应调整是以后的趋势
杂谈
Q1:纯文本是什么?
A: 由特定标准的码位序列组成的没有任何字节表示除文本之外的信息的结构化或非结构化计算机编码文本
Unicode从广义上讲比较复杂, 因为要面对多种实际情况, 但在日常使用中不需要有这种担心
Q2:在RAM中表示字符串
A: 在内存中, 使用固定数量的字节序列存储字符串的各个码位, 在py3.3之前, 编译Cpython时可以配置在内存中使用16|32位, 存储各个码位
16: 窄构建 sys.maxunicode 65535
32: 宽构建
从py3.3后, 创建str对象时, 解释器会检查里面的字符并自适应选择最适合的内存布局
Part5 Fun is Obj 5.0 1st Fun 在python中函数被称为一等对象
在运行时创建
能赋值给变量或数据结构中的元素
能作为参数传给函数
能作为函数的返回结果
我们熟悉的int float string dict都属于一等对象
在python中所有函数都是一等对象
DocStrings 文档字符串是一个重要工具,用于解释文档程序,帮助你的程序文档更加简单易懂。
我们可以在函数体的第一行使用一对三个单引号 ‘’’ 或者一对三个双引号 “”” 来定义文档字符串。
你可以使用 doc (注意双下划线)调用函数中的文档字符串属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def test (n ): """ test function """ return 1 if n < 2 else n * test(n-1 ) print(test.__doc__) fact = test print(fact(5 )) for i in map (fact, [10 , 1 ]): print(i) >> test function
5.2 Higher Order Fun
接受函数为参数或者把函数作为结果返回的函数都是高阶函数(Higher-Order-Functions), 常见的有map sorted filter reduce apply(已经过时|使用不定量的参数调用函数, 已经使用解包代替)
为了使高阶函数具有更多的可读性, 一般会在高阶函数中使用匿名函数
reduce() 函数会对参数序列中元素进行累积。
函数将一个数据集合(链表,元组等)中的所有数据进行下列操作:用传给 reduce 中的函数 function(有两个参数)先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。
注意: Python3.x reduce()
已经被移到 functools
模块里,如果我们要使用,需要引入 functools
模块来调用 reduce()
函数:
此函数多用求和
现在已经出现sum()
内置函数代替此函数
1 from functools import reduce
5.3 匿名函数
lambda用于创建匿名函数
使用lambda
创建
多用作高阶函数的传参函数
lambda
函数十分简单, 无法在定义体中赋值, 也不能使用while
try
等函数
建议除了给高级函数作为传参函数外, 不要过度使用匿名函数
需要注意: lambda
和def
一样都会创建一个函数对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import functoolsa = [1 , 3 , 45 , 5 , 4 , 23 ] b = filter (lambda x: x > 19 , a) for i in b: print(i) c = functools.reduce(lambda x, y: x + y, [1 , 2 , 3 , 4 ]) print(c) d = all (map (lambda x: x > 10 , [1 , 10 , 11 ])) print(d) f = any (map (lambda x: x > 10 , [1 , 10 , 11 ])) print(f) >> 45 23 10 False True
5.4 可调用对象
除了用户自定义的函数, 调用运算符()
还可以用在其他对象上, 如果要判断一个函数是否可以调用, 可以使用callable()
python中的可调用对象可以分为以下几类:
自定义的def
或 lambda
内置函数: Cpython
实现的函数如len
time.strftime
内置方法: 使用C
语言实现的, 比如dict.get
类定义体中定义的函数
类: 调用类会运行类的__new__
创建一个实例, 之后运行__init__
初始化实例, 最后把实例返回给调用方
类的实例: 在类中定义了__call__
生成器函数: yield
的关键字函数或方法, 生成器函数往往返回生成器对象
注意的是: 生成器函数在很多方面与其他可调用对象不同, 生成器函数还可以作为协程来使用
5.5 定义的可调用类型
python其实模糊了类和函数的界限, 要实现内部call的magic method, 在实现这个magic mthod时需要在内部维护一个状态, 让它在调用之间可用, 通常使用俩种方法 –> 闭包 | 装饰器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import randomclass BingoCage : def __init__ (self, items ): self._elems = list (items) random.shuffle(self._elems) def pick (self ): try : return self._elems.pop() except IndexError: return LookupError("pick from empty BingoCage!" ) def __call__ (self ): return self.pick() bingo = BingoCage(range (5 )) print(bingo.pick())
5.6 函数内省
内省: 即通过函数名, 可以得到函数的更多信息, 通常需要借助fun_name.__doc__
dir(fun_name)
5.7 定位参数|关键词参数 1 def tag (name, *content, cls=None , **attrs ):
name
: 定位参数, 当没有key
时按照位置赋值
*content
: 接受除定位参数后的所有参数, 一般是一个可迭代对象, 并将这些参数解包成元组
cls=None
: 关键词参数, 一般需要设置一个default_value
**attrs
: 关键词参数, 可以接受多个关键词, 通常使用字典封装
定位参数
arg
*args
common –> tuple(iterator)
关键词参数
key=value
**dict
common –> dict(Obj)
py3接受了一个特性, 只接受关键词参数|即只接受特定的key
1 2 3 4 5 6 7 8 9 def f (a, *, b ): print(a, b) f(1 , 2 ) >> TypeError: f() takes 1 positional argument but 2 were given ---------- you should modify this function --> f(1, b=2)
5.8 获取参数的信息
获取函数对象的参数信息__code__
可以获取参数的名字以及其他属性, 它本身返一个对象, 也有其他属性, __defaults__
返回一个元组, 里面存储的是函数定位参数的缺省值, __kwdefaults__
返回一个字典, 里面存储的关键词参数的缺省值
python中的代码对象是实现代码自省的重要概念, 代码对象通过fun.__code__
访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def temp (x=1 , y=2 , *, z=3 ): return x, y, z temp(1 , 32 , z=3 ) print(temp.__defaults__) print(temp.__kwdefaults__) print(temp.__code__.co_argcount) print(temp.__code__.co_varnames) >> (1 , 2 ) {'z' : 3 } 2 ('x' , 'y' , 'z' )
代码对象
代码对象 code object
是一段可执行的 Python
代码在 CPython
中的内部表示。 可执行的 Python
代码包括:
当你运行一段代码时,它被解析并编译成代码对象,随后被 CPython
虚拟机执行。 代码对象包含一系列直接操作虚拟机内部状态的指令。 这跟你在用 C
语言编程时是类似的,你写出人类可读的文本,然后用编译器转换成二进制形式,二进制代码C 的机器码或者是 Python 的字节码
被 CPU对于 C 语言来说
或者 CPython
虚拟机虚拟的 CPU 直接执行。
代码对象除了包含 指令,还提供了虚拟机运行代码所需要的一些 额外信息。
在Python 2
中,函数的代码对象通过 函数.func_code
来访问;而 Python 3
中,则需要通过 函数.__code__
来访问。 Python 3 的代码对象增加了一个新属性 co_kwonlyargcount
,对应强制关键字参数 keyword-only argument
。
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 def clip(text, max_len=80): """ clip the text in the first whitplace :param text: :param max_len: :return: """ end = None if len(text) > max_len: space_before = text.rfind(" ", 0, max_len) if space_before >= 0: end = space_before space_after = text.rfind(" ", max_len) if space_after >= 0: end = space_after if end is None: end = len(text) return text[:end].rstrip() # 获取函数对象 print(clip.__code__) # 获取函数的局部变量名 print(clip.__code__.co_varnames) # 获取函数的定位参数数量 print(clip.__code__.co_argcount) # 获取函数的字节码 print(clip.__code__.co_code)
即使我们通过函数对象__code__
实现了函数自省, 但是其数据和效率并不高, 而且所获取到的信息可读性比较差
inspect模块
为了解决通过函数对象实现函数自省的效率和可读性问题, 我们可以通过inspect
模块
1 2 3 4 5 6 7 8 9 10 from inspect import signaturesig = signature(clip) print(sig) for name, param in sig.parameters.items(): print(param.kind, ":" , name, "=" , param.default) >> (text, max_len=80 ) POSITIONAL_OR_KEYWORD : text = <class 'inspect ._empty '> POSITIONAL_OR_KEYWORD : max_len = 80
inspect.signature
函数返回一个inspect.Signature
对象, 这个对象有个很重要的属性是parameters
, 是一个有序映射, 可以把参数名和inspect.Parameters
对应起来.
Signature.Parameters 1 2 3 4 5 6 7 print(sig.parameters) a = sig.parameters.items() >> OrderedDict([('text' , <Parameter "text" >), ('max_len' , <Parameter "max_len=80" >)]) odict_items([('text' , <Parameter "text" >), ('max_len' , <Parameter "max_len=80" >)])
各个Parameters
也有自己独立的属性(封装在inspect.signature.parameters
的有序映射里的对象体)
OrderedDict([('text', <Parameter "text">)]
每个<Parameter>
kind
: 函数对象的变量种类
name
: 函数对象的变量名字
default
: 函数对象的默认值
annotation
: 注解属性|提供函数签名元数据
Signature.bind
inspect.Signature
对象有一个bind
方法, 可以将任意参数绑定到签名中的形参上, 所用的规则与实参到形参的匹配方式一样, 可以使用此方法进行调用函数前的验参(验证是否少参|多参)
1.inspect.Signature
获取函数签名
2.讲特定函数签名通过Signnature.bind(**params)
绑定进行验参
3.终端输出是否存在少参多参的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def check_sign (name, age, sex, *, book="Drink" , title ): return name, age, sex, book, title params = {"name" : "Drink" , "age" : 21 , "sex" : "man" , "book" : "StarFire" , "title" : "SINUO" , "temp" : 1 } check_params = signature(check_sign) try : temp_sign = check_params.bind(**params) print(temp_sign) for name, value in temp_sign.arguments.items(): print(name, "=" , value) except Exception as e: print(f"-----error---> something error {e} " ) >> -----error---> something error got an unexpected keyword argument 'temp'
5.9 函数注解
函数注解是python3的一种语法, 用于声明函数中的参数和返回值附加的元数据, 注解并不会对代码逻辑做任何处理, 只是将其作为[可视化]的元数据存储在函数对象的__annotations__
属性里(表现形式是字典)
注解如下:
1 2 3 4 def clip (text, max_len=80 ):def clip (text:str , max_len:'int > 0' =80 ) -> str:
注解加在par:
后, 一般声明参数类型
如果有默认值, 注解放在par:
和=
之间
python对注解所作的唯一件事是将其放在fun obj的__annotation__
属性里
注解对python解释器没有任何意义, 注解只是元数据,供IDE
框架和装饰器使用
1 2 3 4 5 6 def test_ann (name: str , age: int , book: str = 'StarFire' ) -> str: return name, age, book print(test_ann.__annotations__) >> {'name': <class 'str'>, 'age': <class 'int'>, 'book': <class 'str'>, 'return': <class 'str'>}
1 2 3 4 5 6 7 8 9 10 11 sign = signature(test_ann) print(sign.return_annotation) for param in sign.parameters.values(): note = param.annotation print(note, ":" , "=" , param.default) >> <class 'str '> <class 'str '> : = <class 'inspect ._empty '><class 'int '> : = <class 'inspect ._empty '><class 'str '> : = StarFire
1.signature
函数返回一个Signature
对象, 这个对象拥有return_annotation
属性和parameters
属性
2.return_annotation
返回函数对象注解
3.parameters
属性返回一个字典, 其中字典的key是参数, value是Parameter
对象
4.每个Parameter
对象有独属于自己的属性
5.10 函数式编程
Python的不目标并不是称为函数式编程语言, 但是operator
和functools
等包的支持让其函数式风格十分快捷
Operator
其中绝大多数方法, 用于<算术运算>和<代替匿名函数从序列中取出元素或读取对象>, 即大多数其方法式实列化的匿名函数
其用法就如该目录一样即提供函数式编程|换句话说是所返回的实例本质上式函数|创建函数
1.算数运算
1 2 3 4 5 6 7 8 9 10 from operator import mulfrom functools import reducedef fact (n ): return reduce(lambda a, b: a * b, range (1 , n + 1 )) def fact (n ): return reduce(mul, range (1 , n + 1 ))
2.itemgetter取值
1 from operator import itemgetter
3.attrgetter取值
1 2 3 4 5 6 7 8 9 10 11 12 from operator import attrgetterimport pandas as pdheader = ["name" , "id" , "score" , "age" ] temp = [] for item in sorted (students, key=attrgetter("id" , "score" , "age" ), reverse=True ): temp.append([item.name, item.id , item.score, item.age]) file_path = "./students.xls" print(temp) df = pd.DataFrame(temp, columns=header) df.to_excel(file_path, index=False )
1 2 3 4 name id score age 0 Drink 68491562 90 21 1 Star 21719561 85 20 2 Fire 10069164 96 23
4.methodcaller
methodcaller(fun_name, fun_args)
fun_name: str
fun_args: *args **args
本质上式访问的参数的类属性(get atteration)
1 2 3 4 5 6 7 8 9 10 11 12 from operator import methodcallers = "The time has come" upcase = methodcaller("upper" ) print(upcase(s)) replace_case = methodcaller("replace" , " " , "-" ) print(replace_case(s)) >> THE TIME HAS COME The-time-has-come
又称为偏函数, 即根据一个函数通过functools
创建一个可调用对象, 其中实现了原函数的功能, 但是只接受其参数的子集, 否则抛出exception
functools
提供了一系列的高阶函数, 其中使用频率最多的是reduce
和partial
以及其变体partialmethod
使用partial冻结参数类似于将函数的部分参数设置了一个可变的缺省值
基于一个函数创建一个新的可调用对象, 并把原函数的某些参数固定, 使这个函数可以接受一个或者多个参数的函数改编成需要回调的API
1 2 3 4 5 6 7 8 9 from operator import mulfrom functools import partialtriple = partial(mul, 3 ) triple(7 ) >> 21 list (map (triple, range (1 , 10 )))[3 , 6 , 9 , 12 , 15 , 18 , 21 , 24 , 27 ]
partial
的第一个参数接受一个可调用对象, 后面需要跟任意一个要绑定的定位参数和关键字参数
functools
提供的高阶函数除了reduce
partial
partialmethod
之外, 还有可用于缓存的lru_cache
以及装饰器singledispatch
和wraps
以及构造选择的函数itemgetter
attrgetter
5.11 本章小结 函数的一等性
可赋值给变量
传给其他函数
存储在数据结构中
方位函数的属性
供框架和一些工具使用
规约函数
函数调用规约,是指当一个函数被调用时,函数的参数会被传递给被调用的函数和返回值会被返回给调用函数 。
偏函数
即根据一个函数通过functools
创建一个可调用对象, 其中实现了原函数的功能, 但是只接受其参数的子集, 否则抛出exception
可调用对象
0.函数即是对象, 对象也是函数
1.lambda
实现的方法
2.自定义或实现__call__
的函数或对象
Part6 Imp DP Use 1 FunObj 6.0 抽象基类 定义一个接口或抽象类,并且通过执行类型检查来确保子类实现了某些特定的方法 运用 abc
模块实现抽象基类
抽象方法: 表示基类的一个方法,没有实现,所以基类不能实例化,子类实现了该抽象方法才能被实例化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from abc import abstractmethod, ABCMetaclass Http_Base (metaclass=ABCMeta ): @abstractmethod def get (self ): pass @abstractmethod def set (self, key, value ): pass class Http (Http_Base ): def get (self ): pass def set (self ): pass
抽象类的一个特点是它不能直接被实例化
抽象类的目的就是让别的类继承它并实现特定的抽象方法
Implement design patterns using first-class functions
上下文
把一些计算委托给实现了不同算法的可互换组件, 提供服务
策略
实现不同算法的组件共同的接口
具体策略
策略的子类, 用于实现不同的算法
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 from abc import ABC, abstractmethodfrom collections import namedtupleCustomer = namedtuple("Customer" , "name fidelity" ) class LineItem : def __init__ (self, product, quantity, price ): self.product = product self.quantity = quantity self.price = price def total (self ): return self.price * self.quantity class Order : def __init__ (self, customer, cart, promotion=None ): self.customer = customer self.cart = list (cart) self.promotion = promotion def total (self ): if not hasattr (self, "__total" ): self.__total = sum (item.total() for item in self.cart) return self.__total def due (self ): if self.promotion is None : discount = 0 else : discount = self.promotion.discount(self) return self.total() - discount def __repr__ (self ): fmt = "<Order total: {:.2f} due: :{:.2f}>" return fmt.format (self.total(), self.due()) class Promotion (ABC ): @abstractmethod def discount (self, order ): """ 返回折扣金额(正值) """ class FidelityPromo (Promotion ): """ 第一个具体的策略 为积分1000以及上的顾客提供5%折扣 """ def discount (self, order ): return order.total() * .05 if order.customer.fidelity >= 1000 else 0 class BulkItemPromo (Promotion ): """ 第二个策略 单个商品为20个及以上时提供10%折扣 """ def discount (self, order ): discount = 0 for item in order.cart: if item.quantity >= 20 : discount += item.total() * .1 return discount class LargeOrderPromo (Promotion ): """ 第三个具体策略 订单中的额不同商品达到10个及以上时提供7%折扣 """ def discount (self, order ): distinct_items = {item.product for item in order.cart} if len (distinct_items) >= 10 : return order.total() * .07 return 0
需要注意的是: 我们把Promotion
定义为抽象基类(ABCMeta)
, 这么做是使用@abstractmethod
装饰器除此之外还可以使用class Promotion(metaclass=ABCMeta)
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 joe = Customer("john doe" , 0 ) ann = Customer("ann smith" , 1100 ) cart = [ LineItem("banana" , 4 , .5 ), LineItem("apple" , 4 , 1.5 ), LineItem("watermellon" , 5 , 5.0 ) ] print(Order(joe, cart, FidelityPromo())) print(Order(ann, cart, FidelityPromo())) >> <Order total: 33.00 due: 33.00 > <Order total: 33.00 due: 31.35 >
可以完全使用一等对象函数来实现这种策略, 并且代码更少
6.1 函数实现策略模式
在上面的例子中, 每一个实例化的抽象类Promotion
都只实现了一个方法, 我们对此重构如下 使用作为一等对象的函数, 可以使某些设计模式进行简化
什么是策略模式: 定义一系列算法, 把他一一封装起来, 并且可以使他们相互替换(使算法可以独立于使用它的客户而变化)
策略模式对类的编排
策略模式示例:
电商策略模式:
1000积分的顾客, 每单5%折扣
同一个订单, 单个商品数量达到20个, 10%折扣
订单中不同商品达到10, 7%折扣
假设每个订单只享受一个折扣
使用order类和函数实现的折扣策略
把具体的策略换成简单的函数, 去除抽象类
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 from collections import namedtupleCustomer = namedtuple("Customer" , "name fidelity" ) class LineItem : def __init__ (self, product, quantity, price ): self.product = product self.quantity = quantity self.price = price def total (self ): return self.price * self.quantity class Order : def __init__ (self, customer, cart, promoition=None ): self.customer = customer self.cart = list (cart) self.promotion = promoition def total (self ): if not hasattr (self, "__total" ): self.__total = sum (item.total() for item in self.cart) return self.__total def due (self ): if self.promotion is None : discount = 0 else : discount = self.promotion(self) return self.total() - discount def __repr__ (self ): fmt = "<Order total: {:.2f} due: {:.2f}>" return fmt.format (self.total(), self.due()) def fidelity_promo (order ): return order.total() * .5 if order.customer.fidelity >= 1000 else 0 def bulk_item_promo (order ): discount = 0 for item in order.cart: if item.quantity >= 20 : discount += item.total() * .1 return discount def lager_order_promo (order ): distinct_items = {item.product for item in order.cart} if len (distinct_items) >= 10 : return order.total() * .07 return 0
计算折扣只需要调用self.promotion()
再次表明了函数是一等对象:可以作为参数传递
取消了抽象类
各个策略都是函数
为了把策略用到实例上, 只需要传入对应的策略函数即可
策略对象通常是很好的享元(flyweight)
享元是可以共享的对象, 可以同时在多个上下文中使用
享元不需要在每个上下文实例化, 减少消耗
显然易见, 即使是使用了函数作为策略实现, 但是在选择策略时,任然时人工选择, 接下来我们考虑实现 元策略 , 即让它自动化实现最优方案
1 2 3 4 5 6 promos = [fidelity_promo, bulk_item_promo. large_order_promo] def meta_promo (order ): """auto select best promo""" return max (promo(order) for promo in promos)
promos
列出函数实现的各个策略
于其他*_promo
函数一样, meta_promo
函数的参数是一个order
的实例
使用生成器表达式, 将order
作为参数传给fun list
这里的promos是一个存储函数对象的列表, 当你深刻意识到了函数的一等对象性, 你就不难发现, 这样构建函数列表的simple and right
1.作为参数传递
2.可以被数据结构化存储
如下例所示:
1 2 3 4 5 6 7 8 def temp (): pass fun_list = [temp] print(fun_list) >> [<function temp at 0x000001D96DA478C8 >]
6.1.1 find all strategy in module
在python中, 模块也属于一等对象, 并且标准库提供了许多处理模块的函数
Globals()
返回一个字典, 表示当前的全局符号表, 这个符号表始终针对当前模块(对函数或方法来说是定义他们的模块而不是调用他们的模块)
动态收集促销品折扣函数更简单的一个方式是使用简单的装饰器
6.2 命令模式
命令模式也可以通过把函数作为参数而传递
主要解决的是解耦调用者和接收者, 通过中间对象command
对象实现|如果要实现保存函数状态, 可以使用函数闭包
1 2 3 4 5 6 7 8 9 class Macrocommand : def __init__ (self, commands ): self.commands = list (commands) def __call__ (self ): for command in self.commands: command()
6.3 本章小结 Recommand Books
Learning python design patterns
Part7 Decorators And Closuer
装饰器用于在源码中标记函数, 用于增强函数行为, 装饰器的基础时闭包(回调式异步编程和函数式编程的基础)
7.0 Target
如何计算装饰器句法
如何判断变量不是局部的
闭包存在的原因和工作原理
nonlocal解决的问题
行为良好的装饰器如何实现
标准库中常见的装饰器
实现参数化装饰器
7.1 DecoratorBasic
装饰器是一个可调用对象, 其参数是一个被装饰的函数, 其结果可能会处理被装饰的函数并将其返回, 或者将其替换成一个函数或可调用对象
在运行时改变程序的行为
装饰器
接受: 函数
返回: 处理后的函数|替换的函数 |可调用对象
特性:
能把被装饰的函数换成其他函数
装饰器在加载模块时立即执行
在被装饰函数定义之后立即运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def deco (func ): def inner (): print("running inner()" ) return inner @deco def target (): print("running target()" ) return target() print(target)
7.2 Py何时执行装饰器
在被装饰函数定义后立即执行, 通常是在导包时(加载模块)中执行
在import
或running code
时装饰器会首先运行, 被装饰的函数只有在调用时才运行
通常情况下, 装饰器独自写成一个模块, 被其他模块通过import
调用
大多数情况下装饰器会在内部定义一个函数
7.3 装饰器改进策略模式
使用装饰器改进策略模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 promos = [] def promotion (promo_func ): promos.append(promo_func) return promo_func @promotion def prom_1 (order ): return @promotion def prom_2 (order ): return @promotion def prom_3 (order ): return def best_promos (): for item in promos: print(item) best_promos()
策略函数不需要一个特别名称
装饰器突出了被装饰的函数的作用, 可以临时禁用或者注释, 只需要注释掉装饰器即可
可以在其他模块中定义促销函数, 只需要添加装装饰器
转装饰器如果使用内部函数, 必须依赖于闭包实现
7.4 变量作用域规则
variable scoping rule
1.python在定义函数中, 默认函数中的变量都是local
, 如果在执行中没有在local
中找到对应的变量, 或者变量没有绑定值, python将会通过code crash的方式告诉用户, 而非像JavaScript一样找global
代替
1 2 3 4 5 6 7 8 9 def test (a ): print(a) print(b) b = 1 test(1 ) >> 1 1
1 2 3 4 5 6 7 8 9 10 def test (a ): print(a) print(b) b = 2 b = 1 test(1 ) >> 1 UnboundLocalError: local variable 'b' referenced before assignment
两个函数的字节码如下
可以看到俩这的差别, 一个是将b
视为FAST
另一个视为GLOBAL
将fun_block中的变量加载为global可以使用global关键词声明variable
7.5 闭包
只有涉及到嵌套函数时才会有闭包, 通常情况出现在定义的函数体中使用了lambda
函数, 或者定义的装饰器内部含有函数
闭包的本质: 闭包是指延伸了作用域的函数, 其中包含函数定义体中引用, 但是不在定义体中定义的非全局变量, 闭包的关键问题不是匿名函数, 而是它能访问定义体之外定义的非全局变量
函数中创建的变量是一个局部变量。
局部变量的生命周期 是等局部作用结束之后就会被释放掉。
如果内函数使用了外函数的局部变量,那么这个变量就与闭包函数发生了绑定关系,就延长该变量的生命周期。实际上就是内存给它存储了这个值,暂时不释放。
要实现一个函数不断计算系列值的均值
需要记录器历史值
方法一 类实现
使用类中和属性存储历史数值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Averager : def __init__ (self ): self.series = [] def __call__ (self, new_value ): self.series.append(new_value) return sum (self.series) / len (self.series) avg = Averager() print(avg(10 )) print(avg(18 )) print(avg(17 )) print(avg(22 )) print(avg.series)
方法二 高阶函数实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def make_averager (): series = [] def average (new_value ): series.append(new_value) return sum (series) / len (series) return average avg = make_averager() avg(2 ) avg(12 )
我们可以在fun.__code__
(表示编译后的函数定义体 )属性中查看变量作用域
需要注意的是
series
是make_averager
函数的局部变量, 因为在函数的定义体中已经初始化了series: series = []
, 可是在调用avg(10)
时, make_averager
函数已经返回了, 而且它的本地作用域也消失了
在averager
函数中, series
是自由变量freevariable
, 指未在本地作用域中绑定的变量
averager
的闭包延伸到那个函数的作用域外, 包含自由变量series
的绑定
1 2 3 4 5 6 7 print(avg.__code__.co_varnames) print(avg.__code__.co_freevars) >> ('new_value' ,) ('series' ,)
series
的绑定在返回的avg
函数的__closure__
属性中, avg.__closure__
中的各个元素对应的avg.__code__.co_freevars
中一个名称, 这些元素都是一个cell
对象, 可以通过cell_contents
属性访问保存的值
1 2 3 4 5 6 avg.__code__co_freevars >> ("series" , ) avg.__closure__ >> (<cell at 0x00000193478E8AF8 : list object at 0x00000193479273C8 >,) avg.__closure__[0 ].cell_contents >> [19 , 20 ]
闭包本质上是一个函数, 它会保留定义函数时存在的自由变量的绑定, 这样在调用函数时, 虽然定义作用域不可用了, 但是使用那些绑定
需要注意的是, 只有嵌套在其他函数中的函数才可能需要处理不在全局
7.6 NoncalDeclaration
改进计算累加均值的方式, 只记录总值不再记录历史数据
nonlocal declaration not allowed at module level
1 2 3 4 5 6 7 8 9 def make_average (): count = 0 total = 0 def average (new_value ): count += 1 total += new_value return total / count return total / count
由于str int tuple
属于不可变类型, +=
本质上var = var + other
, 因此这样会隐式创建一个局部变量 , 这样count total
就无法作为自由变量 存在于闭包中, 因此在实例化函数对象时会失败
1 2 3 4 5 6 7 8 9 10 def make_average (): count = 0 total = 0 def average (new_value ): nonlocal count, total count += 1 total += new_value return total / count return total / count
nonlocal
会把变量标记为自由变量 , 即使在函数中为变量赋予了新值, 也会称为自由变量
nonlocal的作用可以简单理解为: 允许在闭包定义的变量从局部变量转变为自由变量, 从而实现调用定义体外的变量. 从宏观上看延长了变量的生命周期(调用完函数即释放), 并使变量具有记忆
7.7 实现一个简单的装饰器 1.decorator(func)
等价于
2.被装饰器装饰的函数, 其引用指向对应装饰器的对应内部函数
实例
这里实现了装饰器一个典型的行为, 把被装饰的函数替换成新函数, 两者接受一样的参数, 并且返回被装饰的函数本应该返回的值, 同时在这一个过程中会进行额外的操作
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 import timedef clock (func ): def clocked (*args ): t0 = time.perf_counter() result = func(*args) elapsed = time.perf_counter() - t0 name = func.__name__ args_str = ", " .join(repr (arg) for arg in args) print("[%0.8fs] %s(%s) -> %r" % (elapsed, name, args_str, result)) return result return clocked @clock def factorial (n ): return 1 if n < 2 else n * factorial(n-1 ) factorial(1 ) factorial(3 ) print(factorial.__name__) >> [0.00000060s] factorial(1) -> 1 [0.00000030s] factorial(1) -> 1 [0.00000860s] factorial(2) -> 2 [0.00001510s] factorial(3) -> 6 clocked
需要注意的是:
1.装饰器会抹去被装饰器的__name__
和__doc__
属性, 可以使用functools.wraps
装饰器把接受参数(func)
的属性复制到被装饰函数中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def clock (func ): @functools.wraps(func ) def clocked (*args ): t0 = time.perf_counter() result = func(*args) elapsed = time.perf_counter() - t0 name = func.__name__ args_str = ", " .join(repr (arg) for arg in args) print("[%0.8fs] %s(%s) -> %r" % (elapsed, name, args_str, result)) return result return clocked @clock def factorial (n ): return 1 if n < 2 else n * factorial(n-1 )
7.8 标准库中的装饰器
标准库中常见的装饰器函数有:
property
classmethod
staticmethod
lru_cache
singledispatch
7.8.1 lru_cache备忘
functools.lru_cache实现了备忘(memoization)的功能, 是一项优化技术, 它将耗时的函数结果保存起来, 避免传入相同参数时重复计算
LRU(least Recently Used)
最近使用, 表示缓存不会无限增长, 一段时间不用会自动丢掉
lru_cache装饰器非常适合递归函数使用, 而且从web中获取信息也有很大优势
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 import functoolsimport timedef clock (func ): @functools.wraps(func ) def clocked (*args ): t0 = time.perf_counter() result = func(*args) elapsed = time.perf_counter() - t0 name = func.__name__ args_str = ", " .join(repr (arg) for arg in args) print("[%0.8fs] %s(%s) -> %r" % (elapsed, name, args_str, result)) return result return clocked @functools.lru_cache() @clock def factorial (n ): return 1 if n < 2 else n * factorial(n - 1 ) factorial(1 ) factorial(9 ) factorial(10 ) print(factorial.__name__) >> [0.00000050s] factorial(1) -> 1 ... [0.00000040s] factorial(10) -> 3628800 factorial
显示调用了10次
不加装饰器会调用(19次)
必须像常规函数一样调用lru_cache()
, 加括号的原因是其可以接受配置参数
叠放装饰器@lru_cache()
在@clock
之前
需要注意的是lru_cache
可以使用两个可选参数, 签名如下:
1 2 3 4 from inspect import signatureprint(signature(functools.lru_cache)) >> (maxsize=128 , typed=False )
maxsize
:指定最大存储缓存数目, 满了之后旧的会被丢弃(为了最佳性能, maxsize应该设置成2的幂)
typed
: 为True
时会把不同参数类型得到的结果分开保存(即会把通常认为相等的浮点和整形分开保存)
lru_cache
使用字典储存结果, key
根据传入的定位参数和关键字参数创建, 所以被lru_cache
都必须时hashable
1 2 3 4 5 6 7 8 9 @functools.lru_cache() @clock def factorial (n ): m = n.pop return 1 if m < 2 else m * factorial(n) factorial([1 , 10 ]) >> TypeError: unhashable type : 'list'
7.8.2 单分派泛函数
分派函数: 对于不同的传参类型进行不同的处理输出, 一般使用(if-else), 时间一长就会显得十分笨拙, 而且各个模块的耦合度逐渐增高
泛函数: 根据第一个传参类型进行自适应处理输出函数组
单分派函数: 只根据第一个传参类型进行自适应判断的泛函数
多分派函数: 根据多个参数进行自适应判断的泛函数
python3.4
新增的functools.singledispatch
装饰器会多个函数组合成一个函数组, 甚至可以为无法修改的类提供专门的函数, 使用@singledispatch
装饰的普通函数会被成泛函数
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 from functools import singledispatch from collections import abc import numbers import html # 装饰器标记处理object类型的基函数 @singledispatch def htmlize(obj): content = html.escape(repr(obj)) return f"<pre>{content}<pre>" # 各个专门函数使用@<base_function>.register(<type>)装饰 @htmlize.register(str) # 使用占位符代替不必要的函数名 def _(text): content = html.escape(text).replace("\n", "<br>\n") return f"<p>{content}</p>" # 为每个需要特殊处理的类型注册一个函数, numbers.Integral是int的虚拟超类 @htmlize.register(numbers.Integral) def _(n): return f"<pre>{n}</pre>" # 可以叠放多个装饰器让同一个函数处理多个类型 @htmlize.register(tuple) @htmlize.register(abc.MutableSequence) def _(seq): inner = "</in>\n<li>".join(htmlize(item) for item in seq) return "<ul>\n<li>" + inner + "</li>\n<ul>"
只要可能, 注册的专门函数应该处理抽象基类(numbers.Integral) 和(abs.MutaleSequence), 不要处理具体实现(int | list) , 这样代码具有更高的兼容性
@singledispatch
装饰器不是把java重载函数带入pyhton. 实际上在一个类中为同一个方法定义多个重载变体,比在函数中使用if-elif-elif块更好用, 但是他们有个共同的缺陷, 都将代码单元(类或函数)承担了太多责任, 装饰器的好处是支持模块化扩展:各个模块可以为它支持的各个类型注册一个专门的函数
装饰器是函数, 因此可以组合起来使用类似于泛函数
连接两者的桥梁是[fun oneself] – params – [regsiter fun]
7.9 StackDecorator
有时需要对一个函数进行多次装饰以满足现实的需求
7.10 参数化装饰器
解析源码中的装饰器时, python会把被装饰的函数作为第一个参数传递给装饰器, 如果想将其他参数传递给装饰器, 我们需要创建一个装饰器工厂函数把参数传递给工厂函数, 他将返回一个带参装饰器(函数), 再将这个装饰器运用即可
装饰器的参数化必须通过函数工厂的模式实现
7.10.1参数化注册装饰器
启用或禁用register执行的函数注册功能, 提供一个active参数, 设置为false, 不注册被装饰函数, 从概念上看, register函数不是装饰器, 而是装饰器工厂函数
如果装饰器工厂函数带有 [定位参数], 在使用装饰器时, 必须使用()带参, 否则装饰器并不会运行
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 registry = set () def register (active ): def decorate (func ): print(f"running register {func} --> {active} " ) if active: registry.add(func) else : registry.discard(func) return func return decorate def f1 (): print(f"running 1" ) return False print("running main" ) print(f"registry -> {registry} " ) >> >> >>
@register工厂函数必须作为函数调用,并且传入所需参数
即使不传入参数, register也必须作为函数@register()
调用, 即要返回真正的装饰器decorate
, 而不是作为函数对象调用@register
我们需要调用装饰器工厂函数返回的装饰器函数, 而不是调用装饰器对象
关键在于使用@register(param)
返回一个函数作为装饰器而不是使用@register
的函数对象装饰
使用@register
装饰器, 装饰函数时等价于register()(func)
本质上是register()
返回一个函数对象在将(func)
返回带入返回的函数对象**(装饰器)**
参数化的装饰器原理相当复杂, 参数化装饰器通常会把被装饰的函数替换, 而且结构上要多一层嵌套(形成装饰器工厂函数)
7.10.2 参数化的clock装饰器
在调用装饰器时, 是否使用函数化调用@decorate()
取决于两个方面
1.装饰器是否需要传参
2.是否需要调用装饰器函数(否则调用装饰器函数对象)
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 import timeDEFAULT_FMT = "[{elapsed:0.8f}s] {name} ({args}) -> {result}" def clock (fmt=DEFAULT_FMT ): def decorate (func ): def clocked (*_args ): t0 = time.time() _result = func(*_args) elapsed = time.time() - t0 name = func.__name__ args = ", " .join(repr (arg) for arg in _args) result = repr (_result) print(fmt.format (**locals ())) return _result return clocked return decorate if __name__ == "__main__" : @clock() def snooze (seconds ): time.sleep(seconds) for i in range (3 ): snooze(.123 )
装饰器最好的是通过__call__
方法的类实现
7.11 本章小结 阅读清单
杂谈
把函数作为一等对象, 就必须要考虑一个问题: 作为一等对象的函数在某个作用域中定义, 但可能会在其他作用域中调用, 如何计算自由变量呢? python给出的答案是”词法作用域”(根据定义函数的环境计算自由变量)
Part8 ObjectOriented
本章目标如下:
8.1 变量不是盒子
变量不存储数据, 其是marker, 用来标记数据
多个变量标记同一个对象, 称为alias
赋值=, 先执行右边的对象计算, 对象在右边创建或获取, 在此之后创建变量并将变量标记(绑定)到对象上
8.2 标识|相等性|别名 标识 : 对象在内存中的地址, 一个对象一旦创建, 在其生命周期内, 其标识不会改变, 返回标识的整数表示:id()
判断标识是否相等: is
别名 : 多个变量绑定到同一个对象上, 这些变量统称为这个对象的别名, 别名具有等幂性(is == id)三等
相等性 : python
有两种相等性 数值相等通过对象定义的__eq__ or __len__
定义以及内存地址相等__id__
, 两者的运算符分别是==
is
1 2 3 4 5 6 7 8 9 a = {1 , 2 , 3 } b = {3 , 1 , 2 } c = a print(b == c, b is c, a is c) print(id (a), id (c), id (b)) >> True False True 2478949962920 2478949962920 2478949963144
对象的id在不同的实现中有所不同, Cpython中表示内存地址, 但其他的解释器中可能是别的值, 其本质是保证id唯一且在对象的声明周期内不可更改
8.2.1 is | ==
is: 比较两个对象的标识
==: 比较两个对象的值(存储的数据)
在变量与单例值的比较中使用is
如与None
的比较(查看变量绑定的值是否是None
)
多数情况下, is
运算比==
快很多, 因为前者不能重载, 两者本质上的魔法方法是__id__
和__eq__ + define by oneself
8.2.2 元组的相对不可变性
relative immutability of tuple
元组和多数python collection
一样其内部存储的都是对象的引用, 如果引用的元素本身是可变的, 即使元组不可变其元素依然可变
元组的相对不变性指的是:tuple数据结构的物理内容(保存的引用)不可变[id()], 与引用的对象无关
str bytes array.array等单一类型序列是扁平的, 他们保存的不是引用而是在连续的内存中保存数据本身
元组中不变的是元组的标识id(), 与其内部的对象引用本身无关, 因此有些元组是不可以散列的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 t1 = (1 , 2 , [1 , 10 ]) t2 = (1 , 2 , [1 , 10 ]) print(id (t1)) print(t1 == t2) t1[-1 ].append(100 ) print(t1) print(t1 == t2) print(id (t1)) >> 2375749450792 True (1 , 2 , [1 , 10 , 100 ]) False 2375749450792
8.3 默认做潜复制
shallow copy by default
Python operations perform shallow copies by default, no special declaration is required
复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 a = [1 , [2 , 3 ], (4 , 5 , 6 )] b = list (a) a.append(7 ) a[1 ].remove(2 ) print(f"a: {a} " ) print(f"b: {b} " ) b[1 ] += [4 , 5 ] b[2 ] += (7 , 8 ) print(f"a: {a} " ) print(f"b: {b} " ) >> a: [1 , [3 ], (4 , 5 , 6 ), 7 ] b: [1 , [3 ], (4 , 5 , 6 )] a: [1 , [3 , 4 , 5 ], (4 , 5 , 6 ), 7 ] b: [1 , [3 , 4 , 5 ], (4 , 5 , 6 , 7 , 8 )]
构造方法或[:]都是浅复制(复制了最外层容器, 副本中的元素是源容器的引用), 如果容器内部的元素都是不可变的, 那么不会出现异常且会节约内存, 如果容器内的元素有可变类型的, 对容器内的元素进行操作会有异常现象
浅复制容易操作, 但对于可变类型来说会出现异常情况
8.3.1 DeepCopy
sometime, we will use deep copy to avoid exception.
1 2 3 4 5 6 7 import copycopy.copy(iterable obj) copy.deepcopy(iterable obj)
一般情况下深复制不是一件简单的事情, 因为有嵌套引用的存在, 但有时深复制可能太深, 对象会引用不该复制的外部资源或单例值, 我们可以是实现__copy__()
__deepcopy__()
magic method 来自定义copy以及deepcopy的行为
8.4 函数的参数作为引用时
python唯一支持的参数传递方式是<共享传参>, 并且大多数面向对象语言都采用这一模式
共享传参指函数的各个形式参数获得实参中各个引用的副本, 换句话说函数内部的形参是实参的别名, 这种方式可能会修改作为参数传入的可变对象, 但是无法修改那些对象的标识(不能把一个对象替换成另一个对象)
函数对改变任何传入的可变参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def f (a, b ): a += b return a x, y = 1 , 1 a, b = [1 , 2 ], [3 , 4 ] c, d = (1 , 2 ), (3 , 4 ) f(x, y) f(a, b) f(c, d) print(x, y) print(a, b) print(c, d) >> 1 1 [1 , 2 , 3 , 4 ] [3 , 4 ] (1 , 2 ) (3 , 4 )
8.4.1 不要使用可变类型作为参数的默认值
可选参数设置默认值可以使API在进化的同时保证向后兼容, 但是我们应该避免使用可变参数作为参数的默认值
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 HanutedBus : """this is a haunted bus""" def __init__ (self, passengers=[] ): self.passengers = passengers def pick (self, name ): self.passengers.append(name) def drop (self, name ): self.passengers.remove(name) def __repr__ (self ): return f"{self.passengers} " bus1 = HanutedBus(["Drink" , "Star" ]) bus1.pick("Fire" ) bus1.drop("Drink" ) print(bus1) bus2 = HanutedBus() bus2.pick("Haunt" ) print(bus2) bus3 = HanutedBus() print(bus3) >> ['Star' , 'Fire' ] ['Haunt' ] ['Haunt' ]
在实例化对象时, 如果passengers为空, self.passengers变成了passengers参数默认值的别名, 这是因为默认值在定义函数时计算(通常在加载模块中), 因此默认值变成了函数对象的属性
我们可以通过审查HauntedBus.__init__
对象, 查看其中的__defaults__
属性中的haunted passengers
1 2 3 4 5 print(dir (HanutedBus.__init__)) print(HanutedBus.__init__.__defaults__) >> ['__annotations__' , ..., 'defaults' ] (['Haunt' ],)
通过验证得出bus2.passengers
是一个别名, 它绑定到了HauntedBus.__init__.__defaults__
属性的第一个元素上:
1 2 3 4 HauntedBus._-init__ --> return >>function HanutedBus.__init__.__defaults__[0 ] is bus2.passengers >> True
8.4.2 Defense Var Paras
预防可变参数是指: 是否将在函数|类中对参数的处理体现到函数外部
防御可变参数最好的方式是: 类或函数自己维护参数, 通过built-in constructor
构造自己的参数
1 2 3 4 5 6 7 8 class HanutedBus : """this is a haunted bus""" def __init__ (self, passengers=None ): if passengers is None : self.passengers = [] else : self.passengers = list (passengers)
除非希望这个方法修改传入的参数对象, 否则在类中直接把参数赋值给实例变量之前一定要深思, 因此这样会为参数对象创建别名, 如果不确定就使用<创建副本>的方式取变量
8.5 Del & GC
del: 删除名称, 通过删除名称减少对象引用, 进而会导致删除对象(引用为0)
对象不会自行销毁; 然而无法得到对象时, 可能会被当作垃圾回收
对象销毁情况:
对象引用为0
对象仅存在相互引用(垃圾回收程序会判断两者都无法获取进而销毁)
重新绑定对象导致引用为0
无法得到对象
__del__
特殊方法不会销毁实例, 不应该在代码中调用, 但在即将销毁实例时Python
解释器会调用__del__
给实例释放外部资源的机会
Cpython
垃圾回收算法:
引用计数
每个对象都会统计有多少引用指向自己
当引用数为0时, 调用__del__
(如果定义了)并销毁对象
释放分配给对象的资源(内存)
分代垃圾回收算法 [Cpy2.0]
检测引用循环中涉及的对象组
如果一组对象之间全是相互引用, 即使在出色的引用方式也会导致组中的对象不可获取
更复杂的收回算法
也有不依赖引用的垃圾回收算法, 这意味者对象引用数量为0时, 可能不会立即调用__del__
方法
使用弱引用可以监控对象的生命周期
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import weakrefa = {1 , 2 , 3 } b = a def bye (): print("Gone with the wind..." ) ender = weakref.finalize(a, bye) print(ender.alive) del aprint(ender.alive) del bprint(ender.alive) >> True True Gone with the wind... False
8.6 weakrefence
弱引用不会增加对象的引用数量, 不会妨碍对象被当作垃圾回收, 往往用在缓存中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import weakrefa_set = {1 , 2 , 3 } wref = weakref.ref(a_set) print(wref) print(wref()) a_set = [1 , 2 ] print(wref) print(wref()) print(wref() is None ) >> <weakref at 0x0000026B7900EF98 ; to 'set' at 0x0000026B791F1E48 > {1 , 2 , 3 } <weakref at 0x0000026B7900EF98 ; dead> None True
wref
重新监测弱引用对象是否被释放|未释放返回对象|否则返回None
wref()
获取最近监测的弱引用对象
需要注意的是: weakref.ref
类是一个底层接口, 供高级用途使用, 多数程序最好使用weakref
集合和finalize
WeakKeyDictionary
WeakValueDictionary
WeakSet
finalize
(在内部使用弱引用)
不建议手动创建并处理weakref.ref
实例
8.6.1 WeakValueDict
此类实现的是可变映射, 里面的value
是对象的weakreference
, 当被引用的对象被当作垃圾回收后, 对应的键会自动删除, 因此其常常用作缓存cache
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import weakrefstock = weakref.WeakValueDictionary() catalog = [cheese("Red" ), cheese("Blue" ), cheese("Green" ), cheese("White" )] for cheese in catalog: stock[cheese.kind] = cheese print(list (stock.values())) print(list (stock.keys())) del catalogprint(list (stock.values())) del cheeseprint(globals ()) print(list (stock.values())) >> [-Red, -Blue, -Green, -White] ['Red' , 'Blue' , 'Green' , 'White' ] [-White] {'stock' : <WeakValueDictionary at 0x1e8719b5a88 >} []
在del catalog
后查看stock
发现仍有一个white
没有deleted
这是因为还有一个临时变量cheese
引用了cheese("white")
可以通过global()
验证, 临时变量如果是局部变量则会在函数返回时被销毁, 但是在此次例子中, cheese
是for loop
中的全局变量, 需要手动(显式)删除
与WeakValueDictionary
相对应的是WeakKeyDictionary
, 后者的键是弱引用, 其用途为:
可以为应用中其他部分拥有的对象附加数据, 这样无需为对象添加属性(这对覆盖属性访问权限的对象尤其有用)
WeakSet
保存元素弱引用的集合类, 元素没有强引用时, 集合会将它删除, 如果一个类需要知道所有实例, 一种好的方案是创建一个weakset类型的类属性, 保存实例的引用, 直到实例被回收, 否则其存在时间和python进程一样长
weakkey|valuedict|set例子参考
weakfinalize
8.6.2 Limitions(WF)
the limitions of weak reference
不是所有python
对象都可以作为弱引用的目标, list
dict
实例就不可, 但是它们的子类可以解决这个问题
具体限制如下(受于Cpython的实现细节, 其他解释器可能有所不同), 这些局限时内部优化导致的结果
list -> 其子类可以弱引用
dict -> 其子类可以弱引用
int -> 不可引用|子类也不可
tuple -> 不可引用|子类也不可
1 2 3 4 5 6 7 8 9 10 11 12 class MyList (list ): """""" a_list = MyList(range (10 )) wref_to_a_sub_list = weakref.ref(a_list) temp = [1 , 2 , 3 ] wref_to_a_list = weakref.ref(temp) >> wref_to_a_list = weakref.ref(temp) TypeError: cannot create weak reference to 'list' object
8.7 Py对不可变类型的实现
Cpython
为了优化将不可变类型的copy
[:]
等不创建副本而是指向同一个引用
收到影响的有tuple
int
str
bytes
frozenset
1 2 3 4 5 6 7 8 a = "121" b = "121" print(a == b) print(a is b) >> True True
共享字符串字面量是一种优化措施, 称为驻留
这些trick
的目的时节省内存提升解释器的速度
8.8 Summary Python对象三要素
标识(id)
不可变
一般是整数内存地址, 通过id()获取, is
比较
通过构造创建副本id()不同
类型(type)
可变但不推荐变化
通过__class__
属性指定其他类
值(value)
可变
通过==
比较
赋值 | 引用
一般俩个变量具有相同的值== -> True
, 有两种情况, 副本或者别名(共同引用), 对于不可变对象没啥区别, 如果涉及到可变变量需要小心
变量保存的是引用
简单的赋值不创建副本, 副本一般通过[:]
构造list()..
copy.deepcopy()
副本和引用不同的区别是, 是否开辟了内存创建了一个新对象(id不同)
+=
*=
的增量赋值, 如果左边的变量绑定的是不可变对象, 则会创建一个新对象赋值, 否则是就地修改
对现有的变量赋予新值, 不会修改之前绑定的变量(重新绑定: 现有的变量绑定了其他对象), 需要注意的是, 如果该变量是该对象的唯一绑定那么, 绑定后该对象会被当作垃圾回收
以别名的形式给函数传参, 函数会修改通过参数传入的可变对象, 除非在函数中创建副本或传入不可变对象
使用可变类型作为函数参数的默认值有很大风险, 不推荐使用
在某些情况下需要保存对象的引用, 但不留存对象本身, 可以使用弱引用
Other ==
比较数值
is
比较引用
可变性
1.python
是面向对象编程语言, 在定义类的属性时可以是可变的或不可变的
2.可变对象会导致多线程编程难以实现, 因为一个线程修改了数据如果没有及时同步就会损坏数据, 但是过度同步会导致死锁
对象析构和垃圾回收
python
没有直接销毁对象的机制, 而是通过引用, 避免销毁时存在强引用
Cpython
主要用过引用计数 实现垃圾回收, 但是在碰到循环引用时容易泄露内存**(由于疏忽或错误造成程序未能释放已经不再使用的 内存)在Cpython2.0
后引入了 分代垃圾回收**(可以将引用循环中不可获取的对象销毁)
注意
1 open ("test.txt" , "wt" , encoding='utf-8' ).write("123" )
这行代码是安全的, 因为文件对象的引用数量会在write
后归零, Python
在销毁内存中表示文件的对象前, 会先关闭文件, 但是我们仍然提倡使用显示关闭文件, 即使用with
语句, 他能保证文件一定会被关闭, 即使打开文件时抛出了异常也无妨
考虑到(其他环境|语言):
1 2 with open ("test.txt" , "wt" , encoding='utf-8' ) as fp: fp.write("123" )
参数传递:引用传递 传递方式有两种:
按值传递:函数得到的是参数副本
按引用传递: 函数得到的参数的指针(引用)
在python
中, 函数得到是参数的副本,但是参数始终是引用, 即参数引用的副本(共享参数)
Part9 Python-Style Obj
Never use two leading underscores, it is annoyingly selfish
本章我们将自定义类并实现内置对象的结构和行为包括:
支持用于生成对象其他表示形式的内置函数repr()
bytes()
使用一个类方法实现备选构造方法
扩展内置的format()
和str.format()
实现只读属性
将对象变为可散列的, 以便在set
或作为dict
的 key
利用__solt__
节省内存
使用@classmethod
和@staticmethod
装饰器
python
的私有变量和受保护属性的用法约定和局限
9.1 Obj Representation
每种面向对象语言都有一种获取对象的字符串表示形式的标准方式, python提供了str()
repr()
对象表示函数
表示类型
内置函数
返回类型
str()
字符串表示
__str__()
str (Unicode-str)
repr()
字符串表示
__repr__()
str (Unicode-str)
bytes()
字节序列表示
__bytes__()
bytes
format()
特殊格式的字符串表示
__format__()
str (Unicode-str)
str.format()
特殊格式的字符串表示
__format__()
str (Unicode-str)
9.2 Vector Class 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 import mathfrom array import arrayclass Vector : type_code = "d" def __init__ (self, x, y ): self.x = float (x) self.y = float (y) def __iter__ (self ): return (i for i in (self.x, self.y)) def __repr__ (self ): class_name = type (self).__name__ return f"{class_name} ({self.x} , {self.y} )" def __str__ (self ): return str (tuple (self)) def __bytes__ (self ): return (bytes ([ord (self.type_code)]) + bytes (array(self.type_code, self))) def __eq__ (self, other ): return tuple (self) == tuple (other) def __abs__ (self ): return math.hypot(self.x, self.y) def __bool__ (self ): return bool (abs (self))
9.3 Alternative Constructions Method
实现将Vector
的字节序列转成Vector
实例
1 2 3 4 5 6 7 8 9 10 @classmethod def frombytes (cls, octets ): type_code = chr (octets[0 ]) memv = memoryview (octets[1 :]).cast(type_code) return cls(*memv)
9.4 classmethod|staticmethod @classmethod
类方法装饰器|定义备选构造方法, 用它装饰的第一个函数参数必须是cls
本身, 而非self
类实例, 常用于创建新的类实例
@staticmetho
静态装饰器, 装饰的函数将会与类失去交互, 常用在和类相关联的函数定义中, 但实际效果和定义在类的上下文一样, 意义更在于一种提示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Demo : @classmethod def test_class (*args ): return args @staticmethod def test_static (*args ): return args print(Demo().test_static()) print(Demo().test_static("sss" )) print(Demo().test_class()) print(Demo().test_class("sss" )) >> () ('sss' ,) (<class '__main__.Demo'>,) (<class '__main__.Demo'>, 'sss')
9.5格式化显示
内置的format()
和str.format()
方法本质上调用的是.__format__(format_spec)
格式规范微语言
{0.mass:5.2e}
0.mass
: 在代换字段句法种是字段名,
:
: 分隔符
5.2e
: 是格式说明符
格式规范微语言为一些内置类型提供了专用的表示代码, 比如
b
-> 二进制
x
-> 十六进制的int
型
f
表示float
类型
%
百分数
1 2 3 4 5 6 7 8 9 print(format (42 , "b" )) print(format (42 , "x" )) print(format (42 , ".2f" )) print(format (2 /3 , ".2%" )) >> 101010 2a 42.00 66.67 %
格式规范微语言是可扩展的, 各个类可以自行决定如何解释format_spec
参数, 并且一个类如果没有定义__format__
方法, 从Obj
继承的方法会返回str(my_obj)
格式规范微语言的本质上将类的部分参数以一定的格式显示
9.6 可散列的类
可散列的类是指类实现了__hash__
__eq__
方法, 且类的实例在其生命周期内hash()
不变, 有时还要实现属性的只读
1.eq
使用==
或其他方式判断两个类实例是否相等
1 2 def __eq__ (self, other ): return tuple (self) == tuple (other)
2.hash
最好使用位运算符异或运算, 混合各分量的散列值
1 2 def __hash__ (self ): return hash (self.x) ^ hash (self.y)
3.属性只读
init属性时使用self.__x
双前导保证属性的私有
在获取属性时, 返回私有属性并添加装饰器@property
把读值标记为特性
1 2 3 4 5 6 7 def __init__ (self, x, y ): self.__x = x self.__y = y @property def x (self ): return self.__x
@property
python @property 装饰器使一个方法可以像属性一样被使用,而不需要在调用的时候带上()
[example]
4.不变性
我们有时需要实现类中的某些属性具有不可变性, 以便实现__hash__
中的id()
不变
创建hashable类型, 不一定要实现[@property]特性, 也不一定要保护实例属性, 只需要正确的实现__hash__
和__eq__
方法即可, 但是, 实例的散列值绝不应该变化, 因此我们借机实现了只读属性
9.7 私有属性和受保护属性
首先明确两个概念, python虽然可以设置所谓的私有属性通过two leading underscores
但其并不是真正的private attribute
而是一种的name mangling
<命名改写>, 仅仅是一种保护装置, 通过将属性name mangling
成_CLS__attr
, 保护属性在继承或获取时隐藏, 但是我们依然可以使用instance._cls__attr
获取属性或者对属性进行update
python
不像Java
使用private
修饰符创建私有属性, 但是python
可以通过self.__attr
将属性通过name mangle
改写成fake-private-attr
, 起到保护作用
这样改写在一些人看来是自私的, 也有人建议通过命名soft-constraint
进行约束, 即约定one leading downscore -> self._x
, 如果担心命名冲突应该明确使用_Cls_attr
进行属性命名
name mangle <名称改写>是一个安全措施, 其保护的是意外改写, 不并不防止故意改写
1 2 3 4 5 6 7 8 9 10 11 12 13 from array import arrayimport mathclass Vector : type_code = "d" def __init__ (self, x, y ): self.__x = x self.__y = y vector = Vector(1 , 2 ) vector._Vector__x = 3
python
解释器不会对单个下划线的属性名做特殊处理, 这仅仅是一种约定, 程序员应该遵守约定, 不再类外部访问这种属性, 就像使用全大写字母编写常量
但是在顶层名称使用一个前导下划线的话, 确实会有影响, 对from mymod import *
, mymod
中前缀为下划线的名称并不会导入, 但是依然可以使用from mymod import _private_func
导入
python
文档中在类中使用self._x single downsoce
很常见, 表示此属性为类的私有属性(很少有人称为受保护属性), 尽在内部使用(尽管是一个口头约定)
总结:
私有属性 : 使用self.__x
, 通过name mangle
实现, 为了防止类在inherit
时属性丢失以及外部访问, 但是它从本质上是一个取巧装置, 我们依然可以通过_Cls_attr
外部访问并修改值
受保护属性 : 使用self._x
, 完全通过口头约定, 的soft sonstraint
9.8 使用slots类属性节省内存
默认情况下, python
在各个实例中名为__dict__
的字典里存储实例属性, 这样做的目的是为了使用底层的散列表提升访问速度, 但是字典会大量消耗内存, 如果要处理数百万个属性不多的实例, 通过__slots__
类属性, 能节省大量内存, 其本质上是使用tuple
存储实例属性而不是dict
继承自超类的__slots__
属性没有效果, Python
只会使用各个类中定义的__slots__
属性
定义__slots__
的方法是, 创建一个类属性, 使用__slots__
这个名字, 并把它的值设为一个字符串构成的可迭代对象, 元素为每个实例属性, 建议使用元组避免信息发生变化
1 2 3 4 5 6 7 8 class Temp : __slots__ = ("x" , "y" , "__name" ) def __init__ (self, x, y, name ): self.x = x self.y = y self.__name = name
类中__slots__
属性是告诉解释器, 类中的所有实例属性都在此, 这样python
会在各个实例中使用类似元组的结构存储实例变量, 从而避免使用消耗内存的__dict__
属性, 这样可以节省大量内存, 缓解服务器压力 (56%)
__slots__
不仅可以降低内存消耗, 而且运行速度也会提升
__slots__
定义属性后, 实例不再有所列之外的属性, 这是slots
的副作用, 其本质目的是为了提升内存使用率, 而不是为了约束内存, 也不推荐这样使用
如果在__slots__
中添加__dict__
, 那节省的内存会再次被吃掉, 实例会在元组中保存属性, 此外还支持动态创建属性, 这些动态属性会出存在__dict__
中
如果要使对象支持弱引用weak ref
, 必须在__slots__
中添加__weakref__
如果套处理数百万个数值对象, 应该使用Numpy
数组, 其能够高效使用内存并对数值处理函数进行了高度优化
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 class vector : __slots__ = ("x" , "y" ) def __init__ (self, x, y, name ): self.x = x self.y = y self.name = name vector1 = vector(1 , 2 , "Drink" ) print(vector1.name) >> self.name = name AttributeError: 'vector' object has no attribute 'name' ... __slots__ = ("x" , "y" , "__dict__" ) ... >> Drink print(vector1.__dict__) print(vector1.__slots__) >> {'name' : 'Drink' } ('x' , 'y' , '__dict__' )
slots的问题
__slots__
: 静态属性
不可滥用, 其存储的使静态属性, __dict__
:存储动态属性
本质上是为了优化内存, 而不是约束属性获取
如果要同时实现对象的弱引用, 需要在其中添加__weakref__
每个子类都要继承__slots__
属性, 因为解释器会忽略继承
可以创建 禁止创建动态属性 和 不支持弱引用的类
9.9 OverrideClsAttr
在定义class
的属性时, 可以在__init__
之外定义class
属性而非instance
属性, 并且这个属性可以直接通过cls.attr
访问, 即是没有初始化实例对象, 也会生成对应的cls.attr
类属性作为实例属性的默认值, 一旦被实例化, 类属性从用法上将转化为实例属性
修改类属性必须通过类修改, 实例无法修改类属性
类属性也可以在子类中实现Override
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 class Temp : a = 1 b = 2 def __init__ (self, x, y ): self.x = x self.y = y print(Temp.a) print(Temp.b) print(Temp.x) >> 1 2 AttributeError: type object 'Temp' has no attribute 'x' ... a = 1 b = 2 __slots__ = ("x" , "y" ) ... print(Temp.a) print(Temp.b) print(Temp.x) >> 1 2 <member 'x' of 'Temp' objects>
python
中class
与instance
关于attribute
的注意事项
在定义cls
属性时, 相当于给instance
属性做了default value
, 当两者属性key
冲突时, 解释器取instance
属性
在未定义instance
属性并slef.attr
获取时, 解释器回去取cls
属性(default value)
instance
可以更新自己的cls
属性
可以通过在类中添加__slots__
声明, 将属性变为静态属性, 即只能通过cls
对属性进行更新, instance
拿到的是only-read
类属性有个特点: 被实例化后变成<实例属性>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Temp : a = 1 b = 2 __slots__ = ("x" , "y" ) def __init__ (self, x, y ): self.x = x self.y = y def __repr__ (self ): return f"a ->{self.a} " temp = Temp(3 , 4 ) print(temp.a) temp.a = 10 temp2 = Temp(3 , 4 ) print(temp.a) >> 1 AttributeError: 'Temp' object attribute 'a' is read-only
9.10 Summary 本章主要实现了python style
的对象
有关对象的magic method
将对象用str
bytes
展示:
__str__
__repr__
__format__
__bytes__
备选构造方法 通过对方法使用@classmethod
装饰器, 将接收cls
参数, 并重构cls
返回一个类**(cls)**
1 2 3 4 5 6 7 8 9 10 11 12 ... @classmethod def frombytes (cls, octets ): type_code = chr (octets[0 ]) memv = memoryview (octets[1 :]).cast(type_code) return cls(*memv) ...
可散列对象
实现__hash__
__eq__
要保证对象在其生命周期内hash(obj)
不会变化, 通常会选择cls
中的部分属性, 将其private_attr
并以特性@property
变为私有只读属性, 并用异或^ + hash(attr)
实现__hash__
节省内存
使用__slots__
节省类的内存使用, 但是其本身也有其他作用
不能隐式继承
声明的是静态属性与__dict__
相反(动态属性)
会阻止不再声明内的属性的访问
如果要同时实现对象的弱引用, 需要在其中添加__weakref__
属性的覆盖
类属性是实例属性的前置默认值, 类被实例化后, 类属性在表现形式上为实例属性
类属性无法通过实例修改
类属性在继承或实例化后可以Override
类属性 + __slots__
可以实现only-read
__index__
作用: 将对象强制转化成整数索引
用途: 在特定的序列切片场景使用, 满足Numpy
需求, 或需要新键一种数值类型, 并想把它作为参数传递给__getitem__
方法
特性 -> [私有属性一起使用]
@property
def attr()
将类中的方法以属性的方式访问, 如果方法和类同名, 可以避免属性被意外更新(avoid accidental updates)
和私有属性一起使用
在python
中我们可以大胆定义属性, 在之后使用特性, 避免属性被意外更改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Test : def __init__ (self, x ): self.__x = x @property def x (self ): return self.__x test = Test(1 ) print(test.x) test.x = 2 print(test.x) >> ori code 1 AttributeError: can't set attribute
将@x.setter开打后
将属性变为公开属性 self.__x
=> self.x
初始化不成功
1 RecursionError: maximum recursion depth exceeded
私有属性的安全和隐私
严格意义上私有属性没有所谓的安全和隐私
私有属性:
self.__x
soft constraint
通过instance._Cls__private
访问和修改
受保护属性