第1章 python数据模型
python的写法是由背后的魔法方法实现的,比如obj[key],解释器实际调用的是obj.__getitem__(key)
作者把魔法方法叫做双下方法,因为有两个下划线
collections.namedtuple可以用来创建只有少数属性但没有方法的对象,比如
beer_card = Card('7', 'diamonds')
random.choice和random.sample不一样的地方在于,sample是返回序列,choice是返回元素,当使用sample(list, 1)[0]的时候,不如直接使用choice(list)
deck[12::13],是指先抽出索引是12的那张牌,然后每向后数13张牌拿一张
实现了__getitem__让对象变得可迭代了
sorted(deck, key=spades_high) python sorted函数
suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0) def spades_high(card): rank_value = FrenchDeck.ranks.index(card.rank) return rank_value * len(suit_values) + suit_values[card.suit]
特殊方法的存在是为了被Python解释器调用的
PyVarObject是表示内存中长度可变的内置对象的C语言结构体。list或str或bytearray的__len__实际上返回的PyVarObject.ob_size属性,这个比调用一个方法要快的多
len之所以不是一个普通方法,是为了让python自带的数据结构可以走后门,abs也是同理
很多时候调用__init__方法的目的是,在你自己的子类的__init__方法中调用超类的构造器
abs,如果输入是整数或者浮点数,它返回的是输入值的绝对值;如果输入是复数,那么返回这个复数的模。
如果没有实现__repr__,得到的字符串可能就会是<Vector object at 0x10e100070>,就没有字符串表示
__repr__和__str__二选一的话,__repr__更好,因为如果一个对象没有__str__函数,解释器会用__repr__作为替代
python对象的一个基本要求就是它得有合理的字符串表示形式,这就是数据模型中存在特殊方法__repr__和__str__的原因
为了判定一个值x为真还是为假,python会调用bool(x),它的背后是调用x.__bool__()。如果不存在,就会调用x.__len__(),返回0为Flase,非0为True
python通过运算符重载这一模式提供了丰富的数值类型,除了内置那些,还有decimal.Decimal和fractions.Fraction
第2章 序列组成的数组
无需变量声明的强类型
python标准库用C实现了丰富的序列类型
容器序列 list tuple collections.deque, 扁平序列 str bytes bytearray memoryview array.array
容器序列存的是对象的引用,扁平序列存的是值(字符、字节和数值这种基础类型)
不可变序列 seq str bytes, 可变序列 list bytearray array.array collection.deque memoryview
列表推导,就是指a = [x for x in something]这种写法
生成器表达式用于生成列表外的其他类型的序列
它跟列表推导的区别仅仅在于方括号换成圆括号
如tuple(x for x in something)
array.array('I', x for x in something) array构造方法的第一个参数指定了数组中数字的存储方式
for tshirt in [c, s for c in colors for s in sizes]
列表推导会一次性生成这个列表,存储在内存中,占用资源
for tshirt in ('%s %s' for c in colors for s in sizes)
生成器表达式只在循环时逐个产出元素,避免额外的内存占用,省掉了运行for循环的开销
lax_coordinates = (1, 2) latitude, longitude = lax_coordinates # 元祖拆包
另外一个很优雅的写法当属不使用中间变量交换两个变量的值:
b, a = a, b
元祖拆包有2个字符可以使用,_和*
# os.path.split()会返回路径+文件名的元祖(path, last_part) _, filename = os.path.split('/home/luciano/.ssh/idrsa.pub') # 这比下面的写法地道多了 x = os.path.split('/home/luciano/.ssh/idrsa.pub') filename = x[1]
# *把一个可迭代对象拆开作为函数参数 def divmod(x, y) ... t = (20, 8) divmod(*t)
*的用法,需要掌握
>>> a, b, *rest = range(5) >>> a, b, rest (0, 1, [2, 3, 4]) >>> *head, a, b = range(5) >>> head, a, b ([0, 1, 2], 3, 4)
格式化
>>> print('{:15} | {:^9} | {:^9}'.format('', 'lat.', 'long.')) >>> | lat. | long.
^, <, > 分别是居中、左对齐、右对齐,后面带宽度, : 号后面带填充的字符,只能是一个字符
嵌套元祖拆包,很简单
some = [(1, 2, 3, (4, 5))] a, b, c, (d, e) = some
创建一个具名元祖需要2个参数,1是类名,2是各字段名字(多个字符串可迭代对象或空格分隔的多个字符串),如
from collections import namedtuple City = namedtuple('City', 'name country population coordinates') c = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667)) print(c[0]) print(c.name)
具名元祖,有3个特有方法:_fields(字段名),_make()(入参),_asdict()(字典形式输出)
元祖除了跟增减元素相关的方法之外,支持列表的其他所有方法
切片对象slice(a, b, c),执行seq[start, stop, step]时,python会调用
seq.__getitem__(slice(start, stop, step))
# 切片对象slice的命名,提高代码可读性 name = slice(2, 10) print(item[name]
切片对象可以赋值或del
x[2:5] = [20, 30] # 赋值时右边必须为可迭代对象 del x[1:2]
重复拼接 序列a * n,如果a中的元素是对其他可变对象的引用,那么结果可能会出乎意料。比如x = [[]] * 2,是不能初始化为列表组成的列表的,得到列表里包含的2个元素是2个引用,而且这2个引用指向的是同一个列表
board = [['_'] * 2] * 2 board[0][1] = '0' print(board) # [['_', '0'], ['_', '0']] # 指向同一列表的引用都被赋值为'0' # 正确应用列表推导式 board = [['_'] * 2 for i in range(3)] board[0][1] = '0' print(board) # [['_', '0'], ['_', '_']]
a += b,是修改a,还是a + b后生成新对象赋值给a,取决于a是否实现了__iadd__方法,*=对应__imul__,一般可变序列是实现了的,不可变是没实现的。意味着list是可以这样用,tuple这样用效率就会低
增量赋值不是原子操作,即使抛出了异常,还是会完成操作
如果一个函数或方法对对象进行的是就地改动,那它就会返回None,好让调用者知道未产生新的对象,比如list.sort() random.shuffle
sorted会生成一个新的列表作为返回值
可选参数key可以用在这些方法中,指定规则:list.sort() sorted() min() max() itertools.groupby() heapq.nlargest(),如
key=str.lower忽略大小写排序 ken=len基于字符串长度排序 key=int把字符看作数值 这操作666
二分查找 搜索bisect.bisect/bisect_left 插入bisect.insort/insort_left left指若相等取左边,默认取右边
一个很牛逼的把分数和成绩对应起来的例子:
def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'): i = bisect.bisect(breakpoints, score) # 返回索引:插入score后还能保持升序,比如77应该插入索引2的位置,返回2 return grades[i] print([grade(score) for score in [33, 99, 77, 70, 89, 90, 100]]) # ['F', 'A', 'C', 'C', 'B', 'A', 'A']
存放1000万个浮点数,用array 所以NumPy是用的array哈哈(散列表是稀疏数组,dict和set是基于散列表的)
频繁对序列做先进先出,用deque
检查一个元素是否出现在集合中的频率很高,用set
从文本读浮点数略慢是因为会使用内置的float把每一行文字转成浮点数,array.fromfile或pickle则不会,故效率更高;写的话,文本会占用更多的空间,而每个浮点数只占8个字节
memoryview NumPy SciPy
计时器
from time import pref_counter as pc t = pc() time.sleep(1) print(pc() - t)
第3章 字典和集合
一般用户自定义类型的对象都是可散列的,散列值就是id()函数的返回值,如果2个可散列对象相等,那么它们的散列值一定是一样的
a = dict(one=1, two=2, three=3) b = {'one': 1, 'two':2, 'three': 3} c = dict(zip(['one', 'two', 'three'], [1, 2, 3])) d = dict([('two', 2), ('one', 1), ('three', 3)]) e = dict({'three': 3, 'one': 1, 'two': 2}) print(a == b == c == d == e) # True
dict collections.defaultdict collections.OrderedDict
d[k],k不存在的处理:d.get(k, default),给找不到的键一个默认的返回值,还有更高效的setdefault
my_dict.setdefault(key, []).append(new_value) # 等价于 if key not in my_dict: my_dict[key] = [] my_dict[key].append(new_value) if 'headers' not in p.keys(): p['headers'] = {"x": "y"} # 等价于 p.setdefault('headers', {"x": "y"}) # 就不用写if了
default_factory设置字典找不到键时 的默认类型
# 平时是这样 a = {} a["x"] = 1 # {"x": 1} # 假如要a["x"].append(1) {"x": [1]}就不得行 # 使用defaultdict import collections a = collections.defaultdict(list) # 传参的list就是default_factory a["x"].append(1) # 就可以了
dict的__getitem__方法碰到找不到的键的时候,python会自动调用__missing__方法,而不是直接抛出KeyError异常
__missing__只会被__getitem__调用(d[k]会调用__getitem__)
python3中k in my_dict.keys()是很快的,因为返回值是一个“视图”
可以在count的时候使用collections.Counter,跟mysql的count类似
字典排序,就用collections.OrderedDict
如果想提供一个只能查看不能修改的dict,那可以用MappingProxyType
from types import MappingProxyType d = {1: 'a'} d_proxy = MappingProxyType(d) # 相当于视图
set可用于去重,6不6
x = ['a', 'a', 'b'] y = list(set(x)) # ['a', 'b']
代码优化片段,很实用,这就是python进阶
# 统计出现次数 # 优化前 found = 0 for n in needles: if n in haystack: found += 1 # 优化后 found = len(set(needles) & set(haystack)) # 借助set交集来实现
集合是大括号包起来的{1, 2, 3},但是注意空集合必须用set(),{}表示空字典
散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组)
dict和set由于是基于散列表的,查询效率很高
dict是比较占内存的,空间换时间
往字典里添加新键可能会改变已有键的顺序
.keys() .items() .values()返回的都是字典视图
字典的散列表存的是key+value,集合的散列表存的只有元素的引用(也就是key),set就像只有key的dict
python和json在拼写上有出入的值就是true、false和null
第4章 文本和字节序列
编码和解码
# 编码,为bytes对象 b = s.encode('utf8') # b'caf\xc3\xa9' # 解码,为str对象 s = b.decode('utf8') # 'cafe'
python自带了100多种编解码器(codes, encoder/decoder)
utf8有别名 utf_8 utf-8 utf8 U8
出现与Unicode有关错误时,首先要明确异常的类型,是UnicodeEncodeError、UnicodeDecodeError还是如SyntaxError等其他错误
UnicodeEncodeError,文本转换字节序列时,如果目标编码中没有定义某个字符,抛出异常
# 解决 s.encode('utf8', errors='ignore') # 忽略 s.encode('utf8', errors='replace') # 替换成? s.encode('utf8', errors='xmlcharrefreplace') # 替换成xml实体
UnicodeDecodeError,二进制序列转换文本时,遇到无法转换的字节序列,抛出异常。因为不是每一个字节都包含有效的ASCII字符,也不是每个字符序列都是有效的UTF-8或UTF-16
# 解决 b.decode('utf8', errors='replace')
SyntaxError可以尝试用 # coding=utf-8 来解决
同一个字节序列,如b'\xe9',不同编解码器decode,结果字符是不一样的
文本文件是bytes,open过程是bytes->str,write过程是str->bytes,这就是Unicode三明治
bytes -> str 解码输入的字节序列
100% str 只处理文本
str -> bytes 编码输出的文本
需要在多台设备中或多种场合下运行的代码,一定不能依赖默认编码。打开文件时始终应该明确传入encoding=参数,因为不同的设备使用的默认编码可能不同,有时隔一天也会发生变化
尤其是write方法
除非想判断编码,否则不要在二进制模式中打开文本文件。只应该使用二进制模式打开二进制文件,如光栅图像
在GNU/Linux和OS X中,编码默认值都是UTF-8,windows则不一定了,所以格外注意
locale.getpreferredencoding方法返回的只是猜测的编码,因为某些系统可能无法通过编程方式设置
与str.lower()类似的,str.casefold()大小写折叠,二者对有些字符的翻译不一样
检测默认编码:locale.getpreferredencoding()、sys.getfilesystemencoding()、sys.getdefaultencoding(),以及标准I/O文件(如sys.stdout.encoding)的编码
第5章 一等函数
在python中,所有函数都是一等对象
接受函数是参数,或者把函数作为结果返回的函数是高阶函数
lambda关键字在python表达式内创建匿名函数,除了作为参数传给高阶函数之外,python很少使用匿名函数
只需实现实例方法__call__,任何python对象都可以表现得像函数,比如class A实现了,那么a()可以像函数一样直接调用
函数形参中,一个*代表多个可有可无的形参,后面如果紧跟一个不带*的形参b,那b叫做”仅限关键字参数“,必须显示指定才能用
def tag(name, *content, cls=None) def f(a, *, b) # cls 和 b 必须指定才能传参
函数装饰器就是在def上面的@代码
可以用inspect模块来获取函数的参数信息
高阶函数:sum all any sorted min max functools.partial,要学会用起来精简代码呀
比如for循环去断言,用all any就搞定了
要使用lambda前,先看看operator模块和functools.partial函数
第6章 使用一等函数实现设计模式
- 别去编写只有一个方法的类,再去实现另一个类声明的单函数接口,而是要用函数,减少重复创建多个对象的运行时消耗
- globals() 返回一个字典,表示当前的全局符号表,这个符号表始终针对当前模块(对函数或方法来说,是指定义它们的模块,而不是调用它们的模块)
- 可以使用闭包在调用之间保存函数的内部状态
- 每个python可调用对象都实现了单方法接口,这个方法就是__call__
- 函数和模块都是一等对象
第7章 函数装饰器和闭包
函数装饰器用于在源码中”标记“函数,以某种方式增强函数的行为
装饰器是可调用对象,其参数是另一个函数(被装饰的函数)
@decorate def target(): print('running target()') # 等价于 def target(): print('running target()') target = decorate(target)
装饰器的一大特性是,能把被装饰的函数替换成其他函数。第二个特性是,装饰器在加载模块时(也就是import)立即执行
装饰器是在导入时执行,被装饰函数是在运行时被调用才执行,注意不要混淆,装饰器在执行时,并不会直接执行被装饰函数,具体执行什么内容,要看装饰器的实现代码怎么写的
python不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量
b = 9 def f(): # global b global声明 才是全局变量 print(b) b = 3 # 赋值了就是局部变量 if __name__ == '__main__': f()
会报错:UnboundLocalError: local variable 'b' referenced before assignment,因为b在f()中赋值了
自由变量,指未在本地作用域中绑定的变量。如果在函数定义体赋值,变成局部变量,就不是自由变量了,非要这样做的话,就只能使用nonlocal进行变量声明,即使赋值了也强制为自由变量
闭包是一种函数,函数中嵌套的函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定
只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量
闭包的出现,让装饰器成为了可能,哈哈哈
装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装饰的函数本该返回的值,同时还会做些额外操作
可以叠加装饰器
@functools.lru_cache() # @lru_cache()应用到@clock返回的函数上 @clock def fibonacci(n):
@functools.lru_cache(),Least Recently Used,把耗时的函数结果保存起来,避免传入相同的参数时重复计算,相当于缓存,可以用作性能优化
使用@singleddispatch装饰的普通函数会变成泛函数:根据第一个参数的类型,以不同方式执行相同操作的一组函数
@singleddispath def base_function(obj): # 基函数 @base_function.register(str) # @<<base_function>>.register(<<type>>) 可以叠加支持不同类型 def _(text): # 各个专门函数,随意命名,_最简单明了 # 专门函数建议处理抽象基类,兼容更广泛,如numbers.Integral是int的抽象基类
@singleddispatch相比于方法重载和ifelse的优点是支持模块化扩展:各个模块可以为它支持的各个类型注册一个专门函数
装饰器也可以添加参数,称为装饰器工厂函数,注意函数这2个字,意味着
# 添加的装饰器必须调用 @register() @register(active=True) # 不能这样 # @register # 因为装饰器工厂函数,返回的是装饰器本身,需要调用一次来返回装饰器
装饰器最好通过实现__call__方法的类实现,不应该像示例那样通过函数实现
第8章 对象引用、可变性和垃圾回收
python变量类似于java中的引用式变量,因此最好把它们理解为附加在对象上的标注,也就是说,python的变量不是盒子,是便利贴,欧耶
对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注。忘掉盒子吧!
a == b 等价于 a.__eq__(b)
浅复制,复制的只是引用,得到的结果可能并不是你想要的
# 拿list举例 a2 = list(a1) a2 = a1[:] # 这都是浅复制
深复制就用copy模块
python唯一支持的参数传递模式是共享传参,指函数的各个形式参数获得实参中各个引用的副本,也就是说,函数内部的形参是实参的别名,注意是引用的副本,引用的副本,引用的副本。这对不可变对象没影响,但是会影响可变对象
def f(a, b): a += b return a x = 1 y = 2 f(x, y) # x不变,因为x引用的1是常量 # 元组也不会变 # 列表会变 # dict会变
不要使用可变类型如[] {}作为参数的默认值,用None
除非这个方法确实想修改通过参数传入的对象,否则在类中直接把参数赋值给实例变量之前一定要三思,因为这样会为参数对象创建别名。如果不确定,那就创建副本。这样客户会少些麻烦
del语句删除名称,而不是对象,但是执行del操作后可能会导致对象不可获取(一般指被引用为0),从而被删除,如果del后还存在其他引用,则对象本身还会存在着
某些情况下,可能需要保存对象的引用,但不留存对象本身。例如,有一个类想要记录所有实例。这个需求可以使用弱引用实现,这是一种低层机制,是weakref模块中WeakValueDictionary、WeakKeyDictionary和WeakSet等有用的集合类,以及finalize函数的底层支持
类存在的时间与python进程一样长,除非显示删除类
对+=或*=所做的增量赋值来说,如果左边的变量绑定的是不可变对象,会创建新对象;如果是可变对象,会就地修改
==比较对象的值,is比较引用
python的None是一个正常的对象,与java的null不同
python不是函数式语言
第9章 符合python风格的对象
对象表示形式 repr() str() 也就是__repr__ __str__ ,还有__bytes__ __format__
classmethod最常见的用途是定义备选构造方法,类方法的第一个参数名为cls
其实,静态方法就是普通的函数,只是碰巧在类的定义体中,而不是在模块层定义
格式规范微语言
只读属性
class Vector2d: def __init__(self, x, y) self.__x = float(x) @property def x(self): return self.__x
使用__slots__类属性节省空间
@staticmethod不太有用,使用模块层函数更简单
要构建符合python风格的对象,就要观察真正的python对象的行为
第10章 序列的修改、散列和切片
*t 作为形参,是指多个可选参数;*t 作为实参代表元祖拆包,比如 t = (1, 2, 3)
协议是非正式的接口,只在文档中定义,在代码中不定义。比如,序列协议,只要类中实现了__len__和
__getitem__这两个方法,那就认为它是序列
为了创建符合python风格的对象,我们要模仿python内置的对象
属性查找失败后,解释器会调用__getattr__方法
如果想允许修改属性,可以使用__setitem__方法,支持v[0] = 1.1这样的赋值,以及实现__setattr__方法,支持v.x = 1.1这样的赋值
operator模块以函数的形式提供了python的全部中缀运算符,从而减少使用lambda表达式 漂亮
python3中,map函数是惰性的,它会创建一个生成器,按需产出结果
大多数时候,如果定义了__getattr__方法,那么也要定义__setattr__方法,这样才能避免行为不一致
第11章 接口:从协议到抽象基类
python语言没有interface关键字,而且除了抽象基类,每个类都有接口:类实现或继承的公开属性(方法或数据属性),包括特殊方法,如__getitem__或__add__
对python程序员来说,”X类对象“ “X协议” “X接口”都是一个意思
鸭子类型。如果遵守既定协议,很有可能增加利用现有的标准库和第三方代码的可能性,这得益于鸭子类型:对象的类型无关紧要,只要实现了特定的协议即可,从而避免过多的isinstance,直接try抛出异常
鸭子类型的由来:近代,属和种基本上是根据表型系统学分类的,表征学关注的是形态和举止的相似性,就像怎么来区分鸭科一样
猴子补丁:在运行时修改类或模块,而不改动源码
def set_card(deck, position, card): deck.cards[position] = card FrenchDeck.__setitem__ = set_card # 不用修改源码
滥用抽象基类会造成灾难性后果,表明语言太注重表面形式,这对以实用和务实著称的python可不是好事
collections.abc模块定义了16个抽象基类,还有numbers包
自己定义的抽象基类要继承abc.ABC
抽象方法使用@abstractmethod装饰器标记,而且定义体重通常只有文档字符串,也就是三个双引号
抽象基类可以包含具体方法,但具体方法只能依赖抽象基类定义的接口(即只能使用抽象基类中的其他具体方法、抽象方法或特性)
抽象方法可以有实现代码,但实现了,子类必须覆盖抽象方法,在子类中可以使用super()函数调用抽象方法,为它添加功能,而不是从头开始实现
python异常类的层次结构
BaseException +-- SystemExit +-- KeyboardInterrupt +-- GeneratorExit +-- Exception +-- StopIteration +-- StandardError | +-- BufferError | +-- ArithmeticError | | +-- FloatingPointError | | +-- OverflowError | | +-- ZeroDivisionError | +-- AssertionError | +-- AttributeError | +-- EnvironmentError | | +-- IOError | | +-- OSError | | +-- WindowsError (Windows) | | +-- VMSError (VMS) | +-- EOFError | +-- ImportError | +-- LookupError | | +-- IndexError | | +-- KeyError | +-- MemoryError | +-- NameError | | +-- UnboundLocalError | +-- ReferenceError | +-- RuntimeError | | +-- NotImplementedError | +-- SyntaxError | | +-- IndentationError | | +-- TabError | +-- SystemError | +-- TypeError | +-- ValueError | +-- UnicodeError | +-- UnicodeDecodeError | +-- UnicodeEncodeError | +-- UnicodeTranslateError +-- Warning +-- DeprecationWarning +-- PendingDeprecationWarning +-- RuntimeWarning +-- SyntaxWarning +-- UserWarning +-- FutureWarning +-- ImportWarning +-- UnicodeWarning +-- BytesWarning
@abstractmethod和def语句之间不能有其他装饰器,但@abstractmethod上面可以叠加装饰器
白鹅类型的一个基本特性:即便不继承,也有办法把一个类注册为抽象基类的虚拟子类,但注册的类不会从抽象基类中继承任何方法或属性
可以使用抽象基类明确声明接口,类可以子类化抽象基类或使用抽象基类注册,宣称它实现了某个接口
可以通过__mro__(Method Resolution Order)来查看类的继承关系,按顺序列出类及其超类
如果一门语言很少隐式转换类型,说明它是强类型语言;如果经常这么做,说明它是弱类型语言
在编译时检查类型的语言是静态类型语言,在运行时检查类型的语言是动态类型语言
静态类型使得一些工具(编译器和IDE)便于分析代码、找出错误和提供其他服务(优化、重构,等等);动态类型便于代码重用,代码行数更少,而且能让接口自然成为协议而不提早实行
python是动态强类型语言
第12章 继承的优缺点
不要直接子类化内置类型(如dict、list或str),因为内置类型的方法通常会忽略用户覆盖的方法。应继承collections模块中的类,例如UserDict、UserList和UserString,这些类做了特殊设计,易于扩展
直接在类型调用示例方法时,必须显示传入self参数,如C.pong(d) A.ping(self)
多继承声明决定了搜索顺序,class D(C, B),会先搜索C类,再搜索B类
如果一个类的作用是为多个不相关的子类提供方法实现,从而实现重用,但不体现“是什么”关系,应该把那个类明确地定义为混入类(mixin class)。混入不定义新类型,只是打包方法,便于重用。混入类绝对不能实例化,而且具体类不能只继承混入类。强烈推荐在名称中加入...Mixin后缀
具体类的超类中除了这一个具体超类之外,其余的都是抽象基类或混入
class MyconcreteClass(Alpha, Beta, Gamma): # Alpha是具体类,Beta和Gamma必须是抽象基类或混入 """这是一个具体类,可以实例化"""
提供一个类,把多个类结合起来,称为聚合类,不包含任何代码
第13章 正确重载运算符
不能重载内置类型的运算符
不能新建运算符,只能重载现有的
某些运算符不能重载——is、and、or和not(不过位运算符&、|、和~可以)
~x == -(x + 1)
x = 2 ~x == -3
实现一元运算符和中缀运算符的特殊方法一定不能修改操作数 return A()
只有增量赋值表达式可能会修改第一个操作数(self) return self
生成器
itertools.zip_longest(x, y, fillvalue=0.0) # 生成(a, b)形式的元祖,a来自x,b来自y,如果x和y长度不同,使用fillvalue填充较短的那个可迭代对象
__add__和__radd__ 反向特殊方法,为了实现 a + b 和 b + a 都能正常计算,反向方法一般很简单,换个位置委托正向方法就ok,如return self * scalar
bool是int的子类
PEP8建议,把导入标准库的语句放在导入自己编写的模块之前
一般来说,如果中缀运算符的正向方法(如__mul__)只处理与self属于同一类型的操作数,那就无需实现对应的反向方法(如__rmul__),因为按照定义,反向方法是为了处理类型不同的操作数
对序列类型来说,+通常要求两个操作数属于同一类型,而+=的右操作数往往可以是任何可迭代对象
python是门高级语言,易于使用,支持运算符重载可能就是它这些年在科学计算领域得到广泛使用的主要原因
第14章 可迭代的对象、迭代器和生成器
迭代是数据处理的基石
按需一次获取一个数据项,就是迭代器模式
所有生成器都是迭代器,因为生成器完全实现了迭代器接口
python3,range()函数也返回一个类似生成器的对象,如果要返回完整的列表,那么必须明确指明(如list(range(100)))
序列对象之所以是可迭代的,是因为解释器需要迭代对象x时,会自动调用iter(x),它的作用如下
1.检查对象是否实现了__iter__方法,如果实现了就调用它,获取一个迭代器 2.如果没有实现__iter__方法,但是实现了__getitem__方法,Python会创建一个迭代器,尝试按顺序(从索引0开始)获取元素 3.如果尝试失败,Python抛出TypeError异常,通常会提示“C object is not iterable”(C对象不可迭代),其中C是目标对象所属的类
python从可迭代的对象中获取迭代器,通过iter()方法获取
__next__,返回下一个可用的元素,如果没有元素了,抛出StopIteration异常。不过应该避免直接调用特殊方法,使用next()即可
检查对象x是否为迭代器最好的方式是调用isinstance(x, abc.Iterator)
除了调用next()方法,以及捕获StopIteration异常之外,没有办法检查是否还有遗留的元素。此外,也没有办法“还原”迭代器。迭代器的元素用了就取出去了,要想还原,只有重新构建新的一模一样的迭代器
迭代器只需__next__和__iter__两个方法
注意区别,可迭代的对象有个__iter__方法,每次都实力会一个新的迭代器;迭代器也有个__iter__方法,返回迭代器本身。迭代器可以迭代,但是可迭代的对象不是迭代器,因为没有__next__方法
迭代器应该一直可以迭代,所以迭代器的__iter__应该返回自身
可迭代的对象一定不能是自身的迭代器,也就是说必须实现__iter__方法,但不能实现__next__方法。那该你怎么办呢,办法就是生成器函数
只要python函数的定义体中有yield关键字,该函数就是生成器函数,该函数会返回一个生成器对象(生成器是迭代器)
生成器不会以常规的方式“返回”值:生成器函数定义体中的return语句会触发生成器对象抛出StopIteration异常。所以不要说生成器返回值,要说生成值。也就是,生成器函数返回值,这个值是一个生成器对象,生成器生成值
re.finditer是re.findall的惰性版本,返回的不是列表,而是一个生成器
生成器表达式
def f(self): for match in RE_WORD.finditer(self.text): yield match.group() # 等价于 def f(self): return (match.group() for match in RE_WORD.finditer(self.text))
所以把方括号换成圆括号后,不是元组表达式,而是生成器表达式
也有元组表达式,显示加上tuple,tuple(x for x in something)
如果生成器表达式要分成多行写,我倾向于定义生成器函数,以便提高可读性。(同理可证,可读性挺重要的,不应该为了省变量而降低可读性)
注意这种写法
forever = end is None # 如果end为None,forever为True
os.walk是生成器函数,wow
iter()的第二个参数叫哨符,当第一个参数返回这个值时,触发迭代器抛出StopIteration异常
with open('mydata.txt') as fp: for line in iter(fp.readline, '\n'): # '\n'就是哨符 process_line(line)
以iter(func, sentinel)的形式调用时,能使用任何函数构建迭代器
生成器用于生成供迭代的数据
协程是数据的消费者
协程与迭代无关,虽然会使用yield产出值,但这与迭代无关
不能把这2个概念混为一谈
python的迭代器协议定义了2个方法:__next__和__iter__,生成器对象实现了这两个方法,因此从这方面来看,所有生成器都是迭代器
符合python风格的斐波那契数列生成器如下:
def fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b
简单且正确,这正是python之道
第15章 上下文管理器和else块
with语句会设置一个临时的上下文,交给上下文管理器对象控制,并且负责清理上下文
for/else while/else try/else了解一下,不是只有if/else
for 循环运行完,不中途break,运行else块 while 循环运行完,不中途break,运行else块 try 正常运行,没有异常抛出,运行else块 其实这里的else理解成then更好
上下文管理器对象存在的目的是管理with语句,就像迭代器的存在是为了管理for语句一样
with语句的目的是简化try/finally模式
上下文管理器协议包含__enter__和__exit__两个方法
重复导入模块不会消耗很多资源,因为python会缓存导入的模块
@contextmanager装饰器能减少创建上下文管理器的样板代码量,因为不用编写一个完整的类,定义__enter__和__exit__方法,而只需实现一个yield语句的生成器,生成想让__enter__方法返回的值
在使用@contextmanager装饰的生成器中,yield语句的作用是把函数的定义体分成两部分:yield语句前面的所有代码在with块开始时(即解释器调用__enter__方法时)执行,yield语句后面的代码在with块结束时(即调用__exit__方法时)执行
@contextmanager装饰的上下文管理器可以这样理解
@contextmanager def g(): # 上文 yield x # 下文 with g() as y: # x会赋值给y # 对应上文 #对应下文
inplace函数是个上下文管理器,为同一个文件提供了两个句柄,以便同时读写同一个文件
import csv with inplace(csvfilename, 'r', newline='') as (infh, outfh): reader = csv.reader(infh) writer = csv.writer(outfh) for row in reader: row += ['new', 'columns'] writer.writerow(row)
- @contextmanager装饰器优雅且实用,把三个不停的python特性结合到了一起:函数装饰器、生成器和with语句
第16章 协程
协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值
生成器的调用方可以使用.send(...)方法发送数据,发送的数据会成为生成器函数中yield表达式的值,因此,生成器可以作为协程使用
如果函数有yield,那么就是生成器函数,那么调用函数得到的就是一个生成器对象,如<generator object simple_coroutine at 0x100c2be10>
通过inspect.getgeneratorstate()函数获取协程当前状态
GEN_CREATED 等待开始执行
GEN_RUNNING 解释器正在执行
GEN_SUSPENDED 在yield表达式处暂停
GEN_CLOSED 执行结束
协程中, b = yield a,是在yield表达式中结束的,下一次执行还是从这一行代码,然后把yield表达式的值赋值给变量。赋值操作是在下一次执行做的
y = yield x,yield后面跟了变量,意思是下次执行,会把x返回给调用方。执行1:yield表达式接收send传来的值 执行2:赋值+返回x
如果不预激,那么协程没什么用
可以在生成器对象上调用throw和close两个方法,显示地把异常发给协程
yield from x表达式对x对象所做的第一件事是,调用iter(x),从中获取迭代器
yield from的主要功能是打开双向通道,把最外层的调用方法与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码
事件驱动型框架(如Tornado和asyncio)的运作方式:在单个线程中使用一个主循环驱动协程执行并发活动。使用协程做面向事件编程时,协程会不断把控制权让步给主循环,激活并向前运行其他协程,从而执行各个并发活动。这是一种协作式多任务:协程显示自主地把控制权让步给中央调度程序。而多线程实现的是抢占式多任务。调度程序可以在任何时刻暂停线程(即使在执行一个语句的过程中),把控制权让给其他线程
第17章 使用future处理并发
future指一种对象,表示异步执行的操作
在I/O密集型应用中,如果代码写得正确,那么不管使用哪种并发策略(使用线程或asyncio包),吞吐量都比依序执行的代码高很多
正常情况下,遇到换行才会刷新stdout缓冲,所以如果print指定了end为其他,那么要显式sys.stdout.flush()
concurrent.futures模块的主要特色是ThreadPoolExecutor和ProcessPoolExecutor类,这两个类实现的接口能分别在不同的线程或进程中执行可调用的对象
from concurrent import futures with futures.ThreadPoolExecutor(workers) as executor: res = executor.map(download_one, sorted(cc_list))
executor.__exit__方法会调用executor.shutdown(wait=True)方法,它会在所有线程都执行完毕前阻塞线程
编写并发代码时经常这样重构:把依序执行的for循环体改成函数,以便并发调用
客户端代码不应该改变future的状态,并发框架在future表示的延迟计算结束后会改变future的状态,而我们无法控制计算何时结束
GIL Global Interpreter Lock,全局解释器锁。CPython解释器本身就不是线程安全的,因此有GIL,一次只允许使用一个线程执行python字节码。因此,一个python进程通常不能同时使用多个CPU核心
标准库中所有的阻塞性I/O函数都会释放GIL,允许其他线程运行。time.sleep()函数也会释放GIL。因此,尽管有GIL,python线程还是能在I/O密集型应用中发挥作用
ProcessPoolExecutor的价值体现在CPU密集型作业上
future.result()方法会阻塞,直到future运行结束
Executor.submit和futures.as_completed这个组合比executor.map更灵活,因为submit方法能处理不同的可调用对象和参数,而executor.map只能处理参数不同的同一个可调用对象。此外,传给futures.as_completed函数的future集合可以来自多个Executor实例,例如一些由ThreadPoolExecutor实例创建,另一些由ProcessPoolExecutor实例创建
with futures.ThreadPoolExecutor(max_workers=3) as executor: to_do = [] future = executor.submit(download_one, cc) to_do.append(future) ... for future in futures.as_completed(to_do): ...
futures.as_completed函数返回一个迭代器,在future运行结束后产出future,它特别有用的惯用法:构建一个字典,把各个future映射到其他数据(future运行结束后可能有用)上。这样,尽管future生成的结果顺序已经乱了,依然便于使用结果做后续处理
to_do_map = {} for cc in sorted(cc_list): future = executor.submit(download_one, cc, base_url, verbose) to_do_map[future] = cc done_iter = futures.as_completed(to_do_map)
进度条,可以研究一下
import time from tqdm import tqdm for i in tqdm(range(100)): time.sleep(.01)
使用Executor.submit()方法创建future(参数是一个可调用的对象,调用这个方法后会为传入的可调用对象排期,并返回一个future),使用concurrent.futures.as_completed()函数迭代运行结束的future
我被python的句法宠坏了
通常情况下自己不应该创建future,而只能由并发框架(concurrent.futures或asyncio)实例化。标准库中有两个名为Future的类:concurrent.futures.Future和asyncio.Future
future封装待完成的操作,可以放入队列,完成的状态可以查询,得到结果(或抛出异常)后可以获取结果(或异常)
第18章 使用asyncio包处理并发
并发是指一次处理多件事
并行是指一次做多件事
二者不同,但是有联系
一个关于结构,一个关于执行
并发用于指定方案,用来解决可能(但未必)并行的问题
举例就是,1个cpu是不能并行的,只能实现并发来解决并行
itertools.cycle函数会从指定的序列中反复不断地生成元素
对协程来说,无需保留锁,在多个线程之间同步操作,协程自身就会同步,因为在任意时刻只有一个协程运行
想要协程交出控制权时,可以使用yield或yield from把控制权交还调度程序。这就是能够安全地取消协程的原因:按照定义,协程只能在暂停的yield处取消,因此可以处理CancelledError异常,执行清理操作
协程是可以暂停和恢复的函数
可以使用yield from从asyncio.Future对象中产出结果
asyncio.async()函数排定协程的运行时间,使用一个Task对象包装协程,并立即返回。Task对象的repr
<Task pending coro=<spin() running at spinner_asyncio.py:12>>
asyncio.Task对象差不多与theading.Thread对象等效。Task对象用于驱动协程。通过把协程传给asyncio.async()函数或loop.create_task()方法获取
如果想终止任务,可以使用Task.cancel()实例方法,在协程内部抛出CancelledError异常。协程可以在暂停的yield处捕获这个异常,处理终止请求
如果调用.result()方法时future还没运行完毕,那么.result()方法不会阻塞去等待结果,而是抛出asyncio.InvalidStateError异常
asyncio包只支持TCP和UDP,如果想使用HTTP或其他协议,那么要借助第三方包,如aiohttp
yield from foo句法能防止阻塞,是因为当前协程(即包含yield from代码的委派生成器)暂停后,控制权回到事件循环手中,再去驱动其他协程;foo future或协程运行完毕后,把结果返回给暂停的协程,将其恢复
理解asyncio有一个技巧,就是假装没有yield from关键字来阅读代码,只不过协程版从不阻塞
为了降低内存的消耗,通常使用回调来实现异步调用。使用回调时,我们不等待响应,而是注册一个函数,在发生某件事时调用。这样,所有调用都是非阻塞的
对事件循环来说,调用回调与在暂停的协程上调用.send()方法效果差不多
在使用asyncio包的程序中只有一个主线程,而在这个线程中不能有阻塞型调用,因为事件循环也在这个线程中运行
Node.js的发明者把执行硬盘或网络I/O操作的函数定义为阻塞型函数
协程使用yield from做异步调用
获取asyncio.Future对象的结果,最简单的方法是使用yield from,而不是调用future.result()方法
所有文件系统函数都会阻塞,因为这些函数的签名中指明了要有回调
asyncio.get_event_loop() 事件循环。asyncio的事件循环在背后维护着一个ThreadPoolExecutor对象,我们可以调用run_in_executor方法,把可调用的对象发给它执行
如果需要协调异步请求,而不只是发起完全独立的请求,协程>回调
在协程中使用yield from把职责委托给另一个协程,而不阻塞事件循环。但注意yield from的基本原则是只能用于协程和asyncio.Future实例(包括Task实例)
StreamWriter.write是普通的函数而不是协程,我们假定它大多数时候都不会阻塞,因为它把数据写入缓冲。而刷新缓冲并真正执行I/O操作的StreamWriter.drain是协程,StreamReader.readline也是协程
asyncio.start_server函数和loop.create_server方法都是协程,返回的结果都是asyncio.Server对象
只有驱动协程,协程才能做事,而驱动asyncio.coroutine装饰的协程有两种方法,要么使用yield from,要么传给asyncio包中某个参数为协程或future的函数,例如run_until_complete
尽管有些函数必然会阻塞,但是为了让程序持续运行,有两种解决方案可用:使用多个线程,或者异步调用——后者以回调或协程的形式实现
在应用中,我们只需确保没有阻塞的代码,事件循环会在背后处理并发
使用协程解决回调的主要问题:执行分成多步的异步任务时丢失上下文,以及缺少处理错误所需的上下文
若想精通asyncio包,一定要下一番功夫
第19章 动态属性和特性
在python中,数据的属性和处理数据的方法统称属性。其实,方法只是可调用的属性
统一访问原则:不管服务是由存储还是计算实现的,一个模块提供的所有服务都应该通过统一的方式使用
使用点号访问属性时(如obj.attr),python解释器会调用特殊的方法(如__getattr__和__setattr__)计算属性
with可以使用两个上下文管理器
with urlopen(URL) as remote, open(JSON, 'wb') as local: ...
仅当无法使用常规的方式获取属性(即在实例、类或超类中找不到指定的属性),解释器才会调用特殊的__getattr__方法
解决json多层嵌套取值的问题,AttrDict 或addict,作者实现了FrozenJson
feed['Schedule']['speakers'][-1]['name'] # 简化为 feed.Schedule.speakers[-1].name
@classmethod装饰器经常用于备选构造方法,因为不是self,而是cls,可以return cls(obj)
@classmethod def build(cls, obj): if isinstance(obj, abc.Mapping): return cls(obj) elif isinstance(obj, abc.MutableSequence): return [cls.build(item) for item in obj] else: return obj
判断是否为关键字 keyword.iskeyword(x)
判断是否为有效的python标识符 s.isidentifier()
__init__其实是“初始化方法”,真正的构造方法是__new__
对象的__dict__属性中存储着对象的属性(前提是类中没有声明__slots__属性)。更新实例的__dict__属性,把值设为一个映射,能快速地在那个实例中创建一堆属性。这是使用关键字参数传入的属性构建实例的常用简便方式
def __init__(self, **kwargs): self.__dict__.update(kwargs)
Django ORM访问models.ForeignKey字段时,得到的不是键,而是链接的模型对象
特性是用于管理实例属性的类属性
特性经常用于把公开的属性变成使用读值方法和设值方法管理的属性,且在不影响客户端代码的前提下实施业务规则
使用@property装饰器实现只读特性
特性其实是覆盖型描述符
obj.attr这样的表达式不会从obj开始寻找attr,而是从obj.__class__开始,而且,仅当类中没有名为attr的特性时,python才会在obj实例中寻找
__class__ 对象所属类的引用
__dict__ 一个映射,存储对象或类的可写属性
__slots__ 类可以定义这个属性,限制实例能有哪些属性
第20章 属性描述符
- 描述符是对多个属性运用相同存取逻辑的一种方式
- 描述符是实现了特定协议的类,这个协议包括__get__、__set__、__delete__方法。换言之,如果类实现了这些方法中的一个,那么这个类就是描述符
- 理解描述符是精通python的关键
- 描述符的用法是,创建一个实例,作为另一个类的属性
- __set__(self, instance),2个参数的意思
- Django模型的字段就是描述符
- 实现__set__方法的描述符属于覆盖型描述符,因为会覆盖对实例属性的赋值操作
- 没有实现__set__就叫非覆盖型描述符。如果设置了同名的实例属性,描述符会被遮盖,致使描述符无法处理那个实例的那个属性
- 读类属性的操作可以由依附在托管类上定义有__get__方法的描述符处理,但是写类属性的操作不会由依附在托管类上定义有__set__方法的描述符处理
第21章 类元编程
- 元类是深奥的知识,99%的用户都无需关注
- 类是一等对象,任何时候都可以使用函数创建类,而无需使用class关键字
- 除非开发框架,否则不要编写元类
- 在导入时,解释器会从上到下一次性解析完.py模块的源码,然后生成用于执行的字节码。如果本地的__pycache__文件夹中有最新的.pyc文件,解释器会跳过上述步骤,因为已经有运行所需的字节码了
- import语句可以触发任何“运行时”行为
- 导入时,解释器会编译函数的定义体,把函数对象绑定到对应的全局名称上,但不会执行函数的定义体。解释器会执行类的定义体,甚至会执行嵌套类的定义体,偶买噶,这就意味着在导入时,就定义了类的属性和方法,并构建了类对象
- 类装饰器可能对子类没有影响
- 元类是生成机器的机器(用于构建类的类)
- type是大多数内置的类和用户定义的类的元类 == 默认情况下,python中的类是type类的实例
结语
Python是给法定成年人使用的语言
版权申明:本文为博主原创文章,转载请保留原文链接及作者。