《流畅的python》这本书值得推荐,认真读完2篇,对python的理解提高一大截。本书属于python进阶书,不适合入门读物。另外还有2本进阶书《python cookbook》和《Effective Python:编写高质量Python代码的59个有效方法》。下面是本书的读书笔记,其他2本后面陆续补充。
一些零散小知识
特殊函数:
func,称为双下方法,也是特殊方法或者魔术函数等,是专门给python解释器使用的,自己不要直接使用,而是转换为使用相应的正常函数。比如
len()函数对应的是len()
我们自己经常时间的特殊函数是__init__
python会忽略[] {} ()中的换行,所以可以不加\符号来换行
_占位符
for country, _ in traveler_ids:
*符号可迭代元素拆包、替代多余元素
*(1,2,3,4), *可以拆成单个元素
a, *body, c,d = range(5), *用于处理剩下元素
*对序列复制
[1,2,3]*2,复制2份[1,2,3,1,2,3]
None关于是否返回新对象与返回值是否为None编程规范
如果一个函数是在原对象上面修改,而无需返回值,这时可以返回None表示程序执行成功。
dict和set都是无序的,不应该一遍迭代一遍修改,会混乱,因为添加新键会导致已有键顺序错乱。
dict和set可以散列,所以查找速度非常快。list不可以散列查找速度很慢。但是散列对象是以空间换时间,占用存储空间很大。
什么是可散列对象:
1、支持hash()函数,且通过__hash__返回值不变
2、支持通过__eq__判断相等
3、若a==b,那么hash(a)==hash(b)
__doc__属性用于形成对象的帮助文本,比如给IDE环境使用,比如help(obj)显示
对象引用
关于变量的错误理解是存储数据的盒子。
关于变量的正确理解是对象的引用,是附加在对象上的标注。像便利贴
所以定义一个变量进行赋值,更像把变量贴到对象上。
别名:一个对象可以有多个标签
is判断相等性
is判断的实质是id()两个对象的一致,id在对象的生命周期中不会变化。而不仅仅是内容相等。相等的==对象未必是同一个对象
==判断的是对象的值
is判断的是对象的id
is运算速度比==快,is经常用于判断是否为None,x is None
元组是对象的引用,如果元组引用的对象是可变对象,那么元组内容也是可以变化的。比如一个列表组成的元组。
浅复制和深复制:
copy.copy()和copy.deepcopy()分别为浅复制和深复制
浅复制只复制第一层对象引用,深复制会进一步复制深层的引用。尤其针对第二层是可变对象时。
函数参数的引用
python唯一支持的参数传递模式是共享传参。即一个函数的形参获取的是实参引用的副本,函数体内对形参的修改其实也会修改实参数据。
千万不要使用可变类型作为参数的默认值
比如不要这样做:def __init__(self, passenger=[]),这样做后果是在模块加载时就会创建这个空列表,导致所有的不带参数的实例化对象都使用同一个列表引用。
推荐做法 def __init__(self, passenger=None) 然后在函数体内部判断如果为None再创建一个新的空列表
del和垃圾回收
del仅仅删除对象的引用,而不是对象本身,对象本身只有当该对象所有的引用都没有了之后才会被系统当做垃圾进行回收。
wreakref弱引用
不增加引用的计数,经常用在缓存中。
import weakref
a_set={0,1}
wref=weakref.ref(a_set)
a_set={2,3,4} {0,1}的引用清0
wref()就会因为原来引用的{1,2}被回收而导致清空。
WeakValueDictionary和WeakKeyDictionary
两者也主要用于缓存,也是weakref里面的类。当弱引用的值或key被回收后,字典里对应的键与值也会自动被删除。
另外,注意不是所有类型都可以做弱引用,比如dict, list,int,tuple都不可以作为弱引用。但自己定义的对象可以。
下面是一些使用细节:
1、简单的赋值语句不创建副本
2、+=,-=等增量运算,如果左边是不可变类型会创建新对象,如果左边是可变类型会在原来对象上面修改
3、对象的重新赋值相当于重新绑定,原来老的对象如果引用数为0会被回收
函数
函数是对象,函数是function类的实例
高阶函数:将函数作为参数,或者将函数作为结果返回
匿名函数lambda,python对lambda的支持有限制,不能在定义体中使用赋值、while、for等语句。
格式:lambda x : x in list
可调用对象
怎么判断一个对象是否可以用()进行调用,用callable(),如果一个类实现了__call__方法,类的对象就是可调用对象
7种可调用对象:
自定义函数、内置函数、内置方法、方法、类、类的实例、生成器函数
位置参数和关键字参数的传入方法*和**
传入未知个数的位置参数用*args,所有参数会存为一个元组
传入未知个数的关键字参数用**kwargs,所有参数被存为一个字典
怎么将构造好的参数列表作为一个参数传递给函数,func(**args),args可以是一个字典,里面包含了func所需的所有参数,**会把这些参数作为一个参数传入。
func会自动解析,多余的参数会传给**kwargs关键字参数
仅限关键字参数是python3特性,需要放在*args后面,**kwargs前面,比如func(name,*args, address="SH", **kwargs)
这样address只能接收关键字参数,关键字参数如果不设置默认值,那么调用的时候必须传入参数
如果仅想支持仅限关键字参数,而不想支持数量不定参数,可以用*号替代*args
函数注解
def clip(text:str, max_len:'int > 0'=80) -> str:
参数注解放在参数后面:
有默认值时的注解在参数和默认值=号之间
返回值注解是在函数:前
函数注解是给IDE等解释器或者代码检测用,不影响解释器执行。
这些注解都会存储在__annotations__属性中,使用inspect.signature()可以检查
字符码位与编码
码位:是01114111的十进制数字,Unicode标准是用46个十六进制数表示,如U+0041,这个称为码位
编码:将码位转成字节序列。不同编码会将相同码位转成不同的字节序列。相反过程叫解码。
默认编码:python3默认使用utf-8编码源代码,python2默认使用ascii。linux一般用utf-8,window一般默认是cp1252。推荐不要用默认编码,自己显示指定编码方式。
过程:码位 <–> 编码解码 <–> 字节序
编码错误处理方法UnicodeEncodeError
city.encode(‘cp437’, errors=‘ignore’)
ignore遇到无法编码字符忽略,
replace遇到无法编码的替换成?问号
xmlcharrefreplace遇到无法编码的替换成xml实体
还有一种处理错误方式:linux下是surrogateescape,windows是strict
str.encode(‘ascii’, ‘surrogateescape’)这个会用特殊码位替换错误码位
小字节序和大字节序
小字节序设备中,各个码位的最低有效字节在前面。比如字母E的码位是U+0045,在小字节序中45会在前面,00会在后面。
UTF-16编码序列开头会有BOM字符0xff和0xfe,表示使用的是Intel CPU小字节序
UTF-8编码不管机器是那种字节序,生成的字节序都是一样的,所以可以不需要BOM字节,但是也可以加,不是强制。
Unicode字符串的比较
Unicode存在组合字符(变音符号和附加到前一个字符上的记号,打印时作为一个整体,导致字符串比较复杂
unicodedata.normalize()是Unicode规范化函数,将字符串规范化后再比较。第一个参数有下面几种类型
NFC(Normalization Form C) 使用最少的码位构成等价字符串,也是W3C推荐的规范化形式,清洗文本时经常用到
NFD 把组合字符分解成基字符和单独的组合字符
NFKC、NFKD,K指兼容意思,比如1/2,2次方等一个字符的会被转义成1/2三个字符。会对源数据有损失,所以不用于数据存储,而是用于索引和搜索中。
from unicodedata import normalize, name, combining
normalize('NFC',name) 规范化
combining(name)判断是否为组合字符
name('\u0043') 显示unicode字符的名称
str.casefold()
大小写折叠,类似str.lower(),但对特殊字符处理不同。比如微子符u变成小写u,大约有116个特殊字符
对于不区分大小写的比较最好使用这个函数
Unicode字符串的排序
ASCII字符排序用sorted()
非ASIC字符用sorted(name, key=local.strxfrm),但前提是要先设置自己的区域:
import local #这是一个第三方库
local.setlocal(local.LC_COLLATE, 'pt_BR.UTF-8')
PyPI pyuca库:一个专门库用于unicode排序:
import pyuca
coll = pyuca.Collator()
sorted(name, key=coll.sort_key)
另外PyPI库中的regex模块对unicode支持性更好,用于取代内置的re模块
数据结构
列表 list [] 可写、有序、不可散列
元组 tuple () 不可写、有序、可重复
字典 dict {key:value} 可写、无序、可散列,key不可重复
集合 set {} 可写、无序、不重复
容器序列
list, tuple, collcetions.deque
存放的是对象引用,可以存放不同类型的数据
扁平序列
str, bytes, bytearray, memoryview,, array.array
只能容纳一种类型的数据,是一段连续的内存空间
可变序列
list, bytearray, array.arry, collection.deque, memoryview
不可变序列
tuple, str, bytes
dict和set也是容器,但是不属于序列
使用习惯:
列表中元素最好保持同一个类型
元组中的元素可以是不同类型数据
else与with上下文管理
else在3种句型中的运用
for
for循环没有被break终止才允许else
while
while循环没有被break终止才允许else
try
没有抛出异常时才运行。结构是try/except/else/finally。finally是无论是否异常都会执行
with上下文管理
作用:用with替换try/finally模式
上下文管理协议包括2个方法:__enter__和__exit__,分别完成进入和退出上下文时调用
一些应用场景:
文件操作
锁、信号量等操作
自定义上下文类
contextlib库@contextmanager装饰器工作流程
被装饰的函数中需要实现yield生成器
yield作用是把函数体分为2部分
yield前面是解释器调用__enter__时执行
yield后面是解释器调用__exit__时执行
为了保护用户异常行为,最好将yiled放在try/except里面
举例:
@contextlib.contextmanager
def looking_glass():
....
yiled
....
变量作用域(global, nonlocal):
局部变量:python不要求声明变量,如果在函数定义中赋值的变量python认为是局部变量;
全局变量:在函数中赋值时,如果想让python解释器把该变量当成全局变量,需要加global标识符;
自由变量:用于闭包中延伸变量作用域用途。比如下面例子,average函数中的变量count和total声明为自由变量后,可以使用外面make_average函数中变量。
def make_average():
count = 0
total = 0
def average(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return average
闭包
定义:闭包就是能够读取其他函数内部变量的函数。
闭包可以理解成定义在一个函数内部的函数
装饰器
装饰器只是语法糖,为了方便程序开发人员使用而定义的语法。
被装饰的函数会作为装饰器的参数被执行。相当于变装,虽然还是调用原来的函数及同样参数,但是实际执行的不同了。注意不是调用装饰器函数,而是调用被装饰的函数。比如
@deco
def function():
等于funciton = deco(function),次数要继续按原样使用function函数,而不是去直接使用装饰器函数deco
装饰器2大特性:
1、能把被装饰的函数替换成其他函数;俗称打补丁,当补丁打的太多时,建议重构代码,装饰器会导致代码可读性变差;
2、装饰器在模块加载时立即执行。
实际使用装饰器场景:
1、装饰器通常在一个模块中定义,然后应用到其他模块的函数上;
2、大多数装饰器会在内部定义一个函数,也就是闭包函数,然后返回这个函数。
3、装饰器典型行为,把被装饰的函数替换成新函数,二者接受相同的参数,而且通常返回被装饰的函数本该返回的值,同时还会做些额外的操作。
装饰器种类:
内置3个:property, classmethod, staticmethod
property装饰的方法会变成可读对象,不能进行写与执行等操作,只能读取值。
functools模块3个:functools.wraps, functools.lru_cache, functools.singledispatch
functools.wraps,被装饰的函数可以接收位置参数和关键字参数,而且不会改变被装饰函数的__name__属性。普通装饰器不能带关键字参数,只能带位置参数。
用法:
def clock(func):
@functools.wraps(func)
def clocked(*args, **kwargs):
functools.lru_cache,lru是Least Recently Used缩写,实现备忘功能,短暂存储函数执行结果,下次用到时直接返回结果,不需要重新计算,尤其适用于斐波那契数列计算
用法:
@functools.lru_cache() #注意这里的()表示lru_cache可以接受配置参数,比如@functools.lru_cache(maxsize=128, typed=Falsed) type表示不同类型是否分开存放,比如1和1.0
@clock
def fibonacci(n):
这样functools.lru_cache装饰clock,clock装饰fibonacci函数。
functools.singledispatch,单分派泛函数。被装饰的普通函数会变成泛函数,根据第一个参数的类型,以不同的方式执行相同操作的一组函数。是一组函数。
用法:
@singledispatch
def htmlize(obj):
@htmlize.register(str)
def _(text):
@htmlize.register(numbers.Integral)
def _(n):
参数化装饰器
普通装饰器使用@deco
参数化装饰器使用@deco(),括号里面可以带也可以不带参数,如果不带参数表示使用默认参数。
参数化装饰器会多一层封装,比较绕,最外层是装饰器工厂函数,用于处理装饰器参数,返回的是真正装饰器函数。即第二层是真正装饰器,参数是被装饰的函数,
举例:
def clock(fmt=DEFAULT_FMT):
def decorate(func):
def clocked(*__args):
...
return clocked
return decorate
@clock()
snoop(10)
生成器、迭代器、可迭代对象
生成器有2种方法编写,一种是生成器表达式,另一种是yield的生成器函数。
{}都是生成器,可以生成数据。注意列表推导[]是一次性返回所有数据,而()是[]的惰性版本,返回的是迭代器。可用用list(x)一次性把x所有数据都提取出来。
迭代器是从集合中取出元素,所有集合都支持迭代。还有for循环(等同iter(x)),元组拆包等也支持迭代。
平时基本把生成器和迭代器视为同一概念
当迭代对象x时,python解释器会执行iter(x),按下面顺序检查对象x的方法:
1、是否有__iter__方法
2、检查是否有__getitem__方法,可以从index 0迭代
3、两者都没有实现的话,会抛出TypeError异常
下面是判断对象是否可迭代的代码:
方法1:any(“next” in B.dict for B in C.mro) and any(“iter” in B.dict for B in C.mro)
方法2:isinstance(x, collections.Iterable)
next(x)调用的是x.__next__方法
迭代器只要实现__iter__和__next__两个方法就可以
注意其实iter(it,[stop])有2个参数,stop参数意思是当it返回的值等于stop时会停止迭代
yield、yield from和协程,用于控制程序流程
包含yield函数是生成器函数,该函数返回的是一个生成器,每次返回一个元素。
生成器函数是惰性实现,指尽可能延后生成值,这样可以节省内存,有可能也会避免做无用处理。python通常都有对应的惰性方法,比如re.finditer是re.re.findall函数惰性实现。
yield的典型用法:
input = yield output
yield右边的output通过next(iterobject)获取,是迭代函数返回给上层调用者的值
yield左侧的input通过iterobject.send(value)发送,是上层调用者给迭代函数的输入值。也可以通过iterobject.throw(Exception)终止迭代函数的迭代过程。
注意,input和output都可以缺失不写。也可以只有一个yield。如果没有output默认是产出None
注意,第一次执行next()时,迭代函数会执行到yield处,如果不调用next()方法,迭代函数处于未启动状态。
增加了send()方法后,yield从生成器变成了协程。可以认为yield作为协程多了左边的input表达式。如果只是生成器只需要右边的output就可以了。
协程返回值:
协程可以有return来返回值,上层调用者可以通过捕获异常来获取返回值,比如:
try:
send(None)
except StopInteration as exc:
r=exc.value #r里面存放的就是协程return中的返回值
yield from等同于await或async(aync wait异步等待)
yield from工作流程:
调用者 --> gen --> subgen
gen称为委派生成器
subgen必须是一个迭代器,因为gen调用yield from subgen时,第一个就调用iter(subgen)。subgen可以是任何可迭代对象。比如字符串、集合等。
调用者负责执行result=send()发送数据给subgen,subgen通过input = yield output获取input输入,返回output输出直接给调用者。调用者通过result得到subgen的结果。
subgen结束后,会return一个值,这个值是在委派生成器中通过result = yield from subgen来获取的,结果为result。
委派生成器和调用者直接传递这个最终结果可以通过参数方式。比如调用者通过gen(feedback)传参数给gen,gen可以直接修改这个参数给调用者。
yield from 主要功能:
1、打开双向通道,调用者和子生成器可以互传数据
2、可以互传异常,而不用在中间的gen添加大量的异常处理措施
通过inspect.getgeneratorstate()可以获得4种协程状态:
GEN_CREATED 等待执行,next()和send()都可以激活,称为预激
GEN_RUNNING 执行中
GEN_SUSPENDED 在yield暂停
GEN_CLOSED 执行结束
python风格对象(鸭子类型和白鹅类型)
鸭子类型
具有同样行为的类可以归为鸭子类型,类之间没有继承关系
白鹅类型
传统的继承,类与类之间有继承关系
对象表示形式
__repr__ repr()
__str__ str()
__byte__ byte()
__format__ format()
__repr__, __str__, __format__都要返回Unicode字符,即str类型
__byte__返回字节序
classmethod, staticmethod
类方法的第一个参数是类本身,而不是对象实例。最常见用途是定义备选构造方法,作为__init__备选;
@classmethod
def frombytes(cls, octest):
@staticmethod虽然放在类中,但是实际与类没有什么关系。
1、被静态方法装饰的函数,不能访问类或实例中的其他属性,参数也没有self。定义方式fun(arg1,...)
2、可以实例化后调用,也可以不实例化直接用类名调用。比如cls().fun()或者cls.func()都行。
3、用途通常是放在类开始,作为类的工具来使用。
其他风格:
__iter__ iter()
__hash__ hash()
__getitem__ [1]或[1:2]
__setitem__ 设置序列值
__eq__ ==等号比较
python的私有属性和受保护的属性
私有属性:
__private,以2个_短划线开头,结尾可以是1个短划线但不能是2个。
python解释器会将该变量转换为: 类名+变量名,比如 classname__private
这样当子类继承父类后,不会覆盖该属性或方法。
受保护的属性
python没有在规则语义上实现受保护的属性,而是从使用规范上面形成一个统一遵守的规定。约定俗成。
只要以_1个短划线开始的属性或方法,默认外部就不能使用,只有类内部可以使用该属性和方法
__slots__
__dict__存放了对象的所有属性和方法,用字典方式,虽然访问速度很快,但是非常占用内存。当需要创建几百万个对象时,可以用__slots__替代。
python遇到__slots__后,不会构建__dict__,这样就节约内存空间了。
使用方法:
__slots__ = ('name', 'function')
注意事项:
1、__slots__无法继承,每个类都有自己的属性,无法继承
2、对象实例只能拥有__slots__中定义的属性,没有列出的不识别
3、如果要把实例作为弱引用对象,需要将__weakref__加入到__slots__中
覆盖类属性
子类可以使用父类中属性,而不用自己定义。但是如果在子类对象中对父类属性做了赋值操作,那么该值不会覆盖父类中默认的属性值,而是自己另新建一个同名的实例属性。
如果要修改类中默认属性值,需要直接调用类方法进行修改,不能通过子类实例进行修改。
序列
python要实现一个序列不需要继承序列类型,只需要实现__len__和__getitem__这2个方法就可以。
如果序列只为了支持迭代,可以仅实现__getitem__,而不需要实现__len__方法。
这个行为看起来像序列的特性称为鸭子特性。
__getitem__
[]操作会调用该方法,如果是[1]会传入一个整数的index,如果是[1:2]类型会传入一个slice(1,2,None)的切片类型
动态存取属性
当遇到不存在的属性时的搜索顺序为:
到类my_obj.__class__中查找
顺着继承树查找
调用对象的__getattr__方法
仍然没有就报属性错误
如果对对象没有的属性进行赋值,会创建对象属性,而不会执行__getattr__方法。
当对不存在的属性赋值时会调用__setattr__方法。一般建议这2个属性配对出现,一起实现。
类的继承
抽象基类有2个:abc和collections.abc
抽象方法@abc.abstractmethod,只定义了接口,子类继承后需要自己实现。但是父类也可以实现方法,这样子类可以通过super()方法进行调用。
抽象类方法定义方式,装饰叠加:
@classmethod
@abstractmethod
数字的继承关系: Number->Complex->Real->Rational->Integral
isinstance(1, numbers.Integral)
isinstance("abc",collections.abc.Hashable) #判断是否可迭代
isinstance((1,2,3),collections.abc.Callable) 或者 callalbe(obj) #判断是否可调用
虚拟子类
白鹅特性的一个特点是即使不继承,也有办法吧一个类注册为抽象基类的虚拟子类
注册虚拟子类的方式是在虚拟基类上调用register方法。之后,注册的类会成为抽象基类的虚拟子类。
而且isinstance 和 issubclass等函数都能识别,但是注册的类不会从抽象基类中继承任何方法和属性,需要自己实现。
虚拟子类的注册方法:
@someAbstraceClass.register
someAbstraceClass.register(subclass) #老的python 3.3版本这样使用
抽象基类:
定义一个抽象类,经常的继承方法如下:
class some_class(metaclass=abc.ABCMeta) #老的python 3.3之前版本
class some_class(abc.ABC)
接口
如果一个类的作用是定义接口,应该明确把它定义成抽象基类。
内置类子类化注意事项:
不要直接继承内置类,因为内置类通常用C编写的,会忽略方法的覆盖。如果需要继承内置子类,建议使用collections模块中的python实现的类型。
不是C实现的类,不影响。可以继承。
__mro__(Method Resolution Order)
返回的是一个元组,按顺序列出类及其超类,一直到object类。说明python解释器解析方法或属性的搜索路径。
同mro()
__bases__
由类的基类组成的元组
__qualname__
类似__name__,只是qualname返回的是限定名称。
__subclasses__
子类
多重继承
python的多重继承是广度优先,先列出的类先继承,优先被搜索执行。
可以继承1个2个或多个父类,但应该只有第一个类是具体类,其他类应该是抽象基类或混入类。即具体类可以是0个或1个。
具体类可以理解成用户定义的非抽象、混入类。
混入类(mixin class)
不定义新类型,只是打包方法,便于重用。混入类不能实例化,而且继承类时不能只继承混入类,混入类应该是提供某方面的特定行为,只实现少量关系非常紧密的方法。
可以简单理解成混入类是一些关系紧密的方法集合,没有什么实际意义。
如果一个类是混入类,需要明确在名字中加后缀Mixin。
聚合类
用户可以将抽象类和混入类封装在一个类中,叫聚合类。
异常类继承关系
BaseException
SystemExit
KeyboardInterrupt
GeneratorExit
Exception
StopIterator
ArithmeticError
FloatingPointError
OverflowError
ZeroDivisionError
AssertionError
AttributeError
BufferError
EOFError
ImportError
LooukupError
IndexError
KeyError
MemoryError
TypeError
重载
运算符重载就是对一个方法的重新实现,比如中缀运算符+,就需要实现__add__方法。
中缀重载的逻辑:
先查找对应的中缀运算符,比如__add__
如果没有实现,会返回NotImplemented结果,python会获取然后继续执行反向操作
+的反向操作符是__radd__
如果反向操作符也是返回NotImplemented,就会抛出TypeError错误
注意NotImplemented和NotImplementedError错误区别,后者是需要覆盖但是没有覆盖而抛出的错误异常,前者用户不可见,是给python解释器用的
异常:
raise X from Y,将2个异常关联起来,可以理解成把Y异常变成X异常
抽象
抽象基类ABC,Abstrcat Base Class
@abstractmethod
抽象方法,不需要有实体定义,后面继承该类时需要覆盖抽象方法
典型的抽象类型有:
from collection import abc
abc.Mapping #映射类型,比如字典
abc.MutableSequence #序列类型,比如列表
Futurer并发
并发:一次处理多件事,如多线程多进程
并行:一次做多件事,比如多CPU同时做
concurrent.future模块可以理解成是更高的异步编程的抽象封装。底层实现是Threading和multiprocessing模块。
concurrent.future模块主要包含2个类ThreadPoolExecutor和ProcessPoolExecutor
举例:
from concurrent import futures
with futures.ThreadPoolExecutor(MAX_WORKERS) as executor:
res = executor.map(func, objs)
return len(list(res)) #如果线程有异常会在这里raise
res是一个迭代器,返回的是各个线程future的执行结果,而不是future本身。
concurrent.future.Future和asyncio.Future两个future类关系:
1、客户不应该自己执行future类的创建和维护,而应该交给这2个类的并发框架处理
2、两个类都有.done()方法,且不阻塞,返回线程是否结束。实际并不是这样用
3、两个类都有.add_done_callback()方法,参数为可调用的对象,在线程结束后调用
4、两个类都有.result()方法,如果线程已经运行结束,都会立即返回执行结果或异常。但如果线程还在执行中:
concurrent.future.Future会阻塞,可以指定timeout参数,超时返回TimeoutError异常;不支持yield from结构;
asyncio.Future不会阻塞,且不支持timeout,直接返回asyncio.InvalidError,所以最好使用yield from方法
5、使用.as_complted等待返回结果
举例:
from concurrent import futures
with futures.ThreadPoolExecutor(MAX_WORKERS) as executor:
for obj in objs:
future = executor.submit(func, obj) #将任务提交给future并发框架,返回的是future
to_do.append(future)
for future in futures.as_complted(to_do): #异步等待线程执行结束,返回的也是future
res = future.result() #立即获得返回结果
经常用的一个技巧是建立{future:obj}的字典对应关系,方便后面处理。
注意:
进程的优势在于CPU密集型工作,可以利用多个CPU同时工作;线程的优势在IO密集型工作,可以较快切换。
但是协程是更轻量级的存在,它是纯应用层面的程序,不被操作系统内核管理,所以不像线程切换要耗费资源。因为协程没有切换,所以就不需要做信号量保护等操作。
线程无法从外部停止,可以向线程发送消息,让线程解析消息进行停止。
进程:
和线程类似,区别是线程可以开很多个线程,进程数一般不会超过CPU总数(默认值是os.cpu_count())
Executor.map与Executor.submit和futures.as_complted组合关系:
1、map方法的返回结果是按调用顺序返回的,比如10个线程,返回的时间是用时最长那个线程的执行时间;as_complted可以分开等待执行结果;
2、map只能匹配一种执行函数和一种参数形式,submit可以分开提交,每次提交不同的函数和参数个数;as_complted也可以等待多个不同的future队列的组合
asyncio并发
协程:
asyncio模块是处理future、协程、Task的模块,而这3者也经常混用。总之与concorrent.future是线程和进程的封装不同。asyncio描述的是协程。
语法:
打算交给asyncio处理的协程需要加@asyncio.coroutine装饰,被装饰的协程必须有yield from语法
这不是强制,而是强烈建议
注意:
1、协程最底层必须是yield语法,比如不能用time.sleep()方法,而需要用yield from asyncio.sleep(delay)来完成协程的最后产生源。
协程获取Task对象有2个方法:
1、asyncio.async(coro_or_future, * , loop=None)
第一个参数如果是协程,asyncio会调用loop_create_task()创建task对象。
loop是可选,可传入事件循环,如果没有传入,asyncio会使用asyncio.get_event_loop()函数获取循环对象。
2、BaseEventLoop.create_task(coro)
举例:
import asyncio
import aiohttp
loop = asyncio.get_event_loop() #获取事件循环底层实现的引用
to_do = [download_one(cc) for cc in sorted(cc_list)]
wait_coro = asyncio.wait(to_do_list) #非阻塞, wait是一个协程,等待所有to_do_list中协程结束
res_com, res_not_com = loop.run_untile_complete(wait_coro) #事件循环,等待wait_coro结束
loop.close() #关闭事件循环
说明:
1、这里导入aiohttp是保证最终产生的数据使用yield方法
2、res_com和res_not_com返回值表示执行完和未执行完的返回结果。因为asyncio.wait有两个参数tiemout和return_when
3、最上层驱动是由run_untile_complte()完成
asyncio.as_completed()函数:
可以在协程完成后单独返回结果,不需要像asyncio.wait()那样等待所有的都结束返回结果。
因为需要使用yield from等待.as_completed()产出的结果,所有包含该命令的函数必须是协程,不能像.wait()那样放在普通函数中。
举例:
to_do_iter = asyncio.as_completed(to_do) #返回的是future对象
for future in to_do_iter:
res = yield from future #从future非阻塞等待返回结果
驱动仍然使用asyncio.get_event_loop()和loop.run_untile_complete
asyncio.semaphores使用
使用.acquire()计数器递减,使用.release()计数器增加。如果为0或满了会阻塞
semaphone = asyncio.Semaphore(COUNT)
yield from semaphore
Executor对象,防止阻塞事件循环
比如本地存储,可以使用。
工作原理是asyncio的事件循环背后维护着一个ThreadPoolExecutor对象,可以调用run_in_executor方法,把需要阻塞的工作函数交给它执行。
举例:
@asyncio.coroutine
def download_one():
loop = asyncio.get_evnet_loop()
loop.run_in_executor(None, func, para1, para2..) #第一个参数是Executor对象,如果没有就是None,使用默认的ThreadPoolExecutor实例
服务器:
asyncio模块支持服务器工作模式,收到连接请求后会自动启动任务进行处理
举例:
loop = asyncio.get_event_loop()
server_coro = asyncio.start_server(func, address, port, loop=loop) #返回的是一个asyncio.server实例,即套接字
server = loop.run_untile_complete(server_coro) #启动服务
host = server.socket[0].getsocketname()
try:
loop.run_forever(server) #阻塞执行等待服务连接请求,一直到ctrl+c终止
except KeyboardInterrupt:
pass
server.close() #关闭服务
loop.run_untile_complete(server.wait_closed()) #等待关闭完成
loop.close() #终止事件循环
传统的web架构如Django是在HTML渲染方面,不支持异步访问数据库。但是在短小的web框架中,最好使用异步架构。
python 3.5新特性:
举例:
import asyncio
async def func(second): #协程函数定义方式
await asyncio.sleep(second)
async.run(func) #这个是python 3.8新增的,执行一个协程,并自动管理事件循环
这里的run看起来只能驱动一个协程,如果要驱动多个协程或future还是需要上面的自建循环事件列表。
另外,python -m asyncio 可以完成交互式调试
将协程并发做了大量简化
async称为关键字,用于定义协程函数,在协程函数内部可以使用3个保留关键字 await, async for, async with,并且函数体内禁止使用yield from
await完成异步等待, (__await__)
async for完成异步迭代等待,(__aiter__, __anext__),比如
async for TARGET in ITER:
SUITE
else:
SUITE2
async with完成上下文管理,(__aenter__, __aexit__),比如
async with EXPRESSION as TARGET:
SUITE
GIL, Global Interpreter Lock 全局解释器锁
因为python是线程不安全的,所以同一时间只能运行一个线程。
但是,python在遇到I/O阻塞型的内置函数或C语言扩展时,会释放GIL,给其他线程执行机会
元编程:
元编程:在运行时改变程序的行为
抽象基类、描述符、元类:都是用于构建框架的工具,一般不用,用了经常导致过度设计。
动态属性
主要依靠下面方法完成动态属性的设置和读取:
getattr:当访问的对象没有该属性时,会调用__getattr__方法
delattr: del obj删除对象属性
getattribute:同obj.attr或者getattr/hasattr等
setattr:同obj.attr或者setattr
hasattr(obj, attr) 判断对象或类是否有该属性
getattr(obj, attr) 获得对象或类的属性,获得后可以直接使用该属性或方法
setattr(obj, name, value) 设置对象或类的属性及值
@property, @fun.setter, @fun.deleter 完成只读属性设置、写、删除操作
dict, 存储对象或方法的可写属性,同vars(obj),可以直接通过__dict__操作对象或类的属性和方法,而且经过这个途径经常不会受特殊方法的影响
class, 同type(obj)
dir(obj),列出对象或类的大部分关键属性,比__dict__中的少,比如这些不会列出__mro__,bases,name
快速建立属性的一种方法:
class Record:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
使用特性验证属性:将属性转换为方法来验证属性是否有效
@property
def weight(self): #完成属性的只读装饰
@weight.setter
def weight(self,value): #完成属性的设置装饰,可以在这个函数中检查属性值是否有效
property属性:
property除了用于装饰器,它自己还是一个类,完整格式如下
property(fget=None, fset=None, fdel=None, doc=None)
所有4个参数都是可选的,如果不传入参数,那么相应操作就无法执行。
举例:
weight = property(get_weight, set_weight)
这样当对weight设置值时会调用set_weight方法,读取weight时会调用get_weight
这种用法比较笨拙,不如装饰器方式好用。
注意:
obj.attr这样的表述方式,首先不是从obj开始找attr,而是首先从obj.__class__开始找,如果找不到才从obj实例中寻找。
特性其实是覆盖型描述符
当类中用的是特性属性时,对象是无法直接赋值的,但是通过__dict__可以赋值,可是无法生效。原因就是特性是覆盖型描述符,对象会先读取类的__dict__。
这时如果把类中的特性做了覆盖,变成普通方法,对象中的方法就不会被覆盖了。
__doc__属性通过help()获取
在函数定义首行下面用3个引号,可以作为方法的帮助文档返回。
定义工厂函数:
工厂函数的工作本质就是利用property类的覆盖属性,这样可以为每个变量属性控制读与写操作。
举例:
def quantity(storage_name):
def qty_getter(instance):
return instance.__dict__[storage_name]
def qty_setter(instance,value):
instance.__dict__[storage_name] = value
return property(qty_getter,qty_setter)
使用时:
weight = quantity('weight')
price = quantity('price')
后面weight和price变量属性就变成了property覆盖属性的特性
属性描述符
定义:
属性描述符是对多个属性运用相同存取逻辑的一种方式。
描述符是实现了特定协议的类,这个协议包括__get__, set, delete。可以仅实现部分,比如仅实现一个或两个。
property类实现了完整的描述符协议。
属性描述符可以理解成是特性工厂函数的类实现方式。
举例:
class Quantity:
def __init__(self, storage_name):
self.storage_name = storage_name
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.storage_name] = value #这里必须直接使用__dict__赋值。不能使用setattr(),这会循环触发__set__方法
#另外,这里是用instance.__dict__,而不是self.__dict__,即值是存储在托管实例中的。
else:
raise ValueError('value must be > 0')
class LineItem:
weight = Quantity('weight')
price = Quantity('price')
几个定义:
描述符类:实现了描述符协议的类,比如Quantity类
托管类:把描述符实例声明为类属性的类,比如LineItem类
存储属性:托管实例中存储自身托管属性的属性。简单说托管实例的值都存在自己地盘,而不是存在描述符对象中的。
描述符用途:
管理数据属性。
描述符分类:
覆盖型描述符
实现了__set__方法
没有__get__方法的覆盖型描述符
没有get会用类中的实例描述符,不会去描述符对象中查找值
非覆盖型描述符
没有__set__方法,没有set还赋值的话,会覆盖描述符的__get__操作,使get无效
在类中覆盖描述符
不管描述符是不是覆盖型,为类属性赋值都会覆盖描述符。
函数也是描述符,而且是非覆盖型描述符
描述符使用建议:
1、使用特性以保持简单
@property类其实是覆盖型描述符,但只完成只读属性
2、只读描述符必须有__set__方法
如果不实现会出现覆盖问题,此时只需要返回AttributeError异常就行
3、用于验证值是否有效的描述符可以仅仅实现__set__方法
4、仅有__get__方法的描述符可以实现高效缓存
5、普通的方法会被实例属性覆盖
类元编程
定义:
类元编程是指在运行时创建或定制类的技艺。类是一等对象,可以在任何时候用函数新建类,而无需使用class关键字。
元类,是类元编程的高级工具。
警告:
除非开发框架,不然不要编写元类。
使用type动态构造类的方法:
type(cls_name, base_cls, class_attr)
举例:
def __init__(self,*args, **kwargs):
attr=dict(zip(self.__slots__, args)
attr.update(kwargs)
for name, value in attrs.items():
setattr(self, name, value)
def __iter__(self):
for name in self.__slots__:
yield getattr(self, name)
cls_attrs = dict(__slots__ = field_name,
__init__ = __init__,
__iter__ = __iter)
return type(cls_name, (object,), cls_attrs)
type的实例是类。
exec和eval创建类的限制:
创建类的另一个方式是使用exec函数。就是把类用字符串进行格式化,然后传入内置的exec函数来构建类。
在pyton中构造元类最好别用exec或者eval,因为会有不可信源的问题,即安全问题。
导入时和运行时比较
1、导入时解释器会从上到下一次性解析完.py文件源码,如果有语法错误这时候会报,生成.pyc的字节码。如果已经有最新的.pyc字节码这步跳过;
2、import不止是声明,还会触发执行。进程首次导入模块时,会运行导入模块的所有顶层代码,以后再导入会使用缓存和只做名字绑定。
而顶层代码是可以做任何事情的,比如连接数据库等。
3、导入时和运行时界限模糊
4、导入时,解释器会将所有的def语句定义的函数绑定到对应的全局名称上,当然函数体是不会真正执行的。
5、导入时,解释器会执行类的定义体,形成类的属性和方法,并构建了类的对象。类在导入时运行。
5、import module和 from module import class,这两种方法都会导入module,只是命名空间的区别。
元类:
元类是制造类的工厂。
type是所有内置类和自定义类的元类,type也是自己的实例;
object是type的实例,type是object的子类,很绕;
其他元类如ABCMeta,是type的子类,从type继承了构建类的能力
所有类都是type的实例,但是元类还是type的子类,元类可以作为构造类的工厂。
举例:
class MetaAleph(type):
def __init__(cls, name, bases, dic):
def inner_s(self):
print("MetaAleph.__init__")
cls.mechod_z = inner_2
1、元类继承type类
2、元类的init函数参数通常用cls,而不是self
3、元类可以改变继承该元类的类的行为,简称覆盖。比如这里就是把类的mechod_z方法覆盖为元类中的方法
4、元类通常实现__init__方法就可以,如果需要可实现__new__方法
5、元类的参数同type,分别为类名,基类,类属性和方法
__prepare__
元类在构建类的时候,因为使用的是dict类型存储属性和方法,所以导致没有顺序,是随机的。
而__prepare__方法可以实现顺序。
该方法仅在元类中有用,而且需要是类方法@classmethod装饰
工作流程:先调用__prepare__,再调用__new__和__init__