《Effective Python 编写高质量Python代码的59个有效方法》学习笔记5
实现按需生成属性
#通用代码的动态行为,可以通过__getattr__特殊方法来做
#若某个类定义了__getattr__,同时系统在该类对象的实例字典中又找不到待查的属性,那么系统就会调用这个方法
class LazyDB(object):
"""docstring for LazyDB"""
def __init__(self):
self.exists = 5
def __getattr__(self,name):
value = 'Value for %s' % name
setsttr(self,name,value)
return value
#适合实现无结构数据(无模式数据)的按需访问
#__getattribute__,程序每次访问对象时都会调用这个,即使属性字典已有该属性
#这样就可以在程序每次访问属性时检查全局事物状态
#赋值后以惰性的方式退回数据库,可用__setattr__
#它也可以拦截对属性的赋值操作。只要对实例的属性赋值,无论是直接赋值,还是通过内置的setattr函数赋值,都会触发__setattr__方法
'''
通过__getattr__和__setattr__,我们可以用惰性的方式加载保存对象的属性
要理解__getattr__和__getattribute__的区别:前者只会在带访问的属性缺失时触发,而后者则会在每次访问属性时触发
若要在__getattribute__和__setattr__方法中访问实例属性,那么应该直接通过super()(也就是object类的同名方法)来做,以避免无限递归
'''
元类的三个功能
###1.用元类来验证子类
'''
元类可以验证某个类定义得是否正确。
开发者一般吧验证代码放在本类的__init__方法里运行,这是由于程序在构建该类的对象时会调用。
但用元类来验证可以把验证时机再往前提一点
'''
'''
通过元类,我们可以在生成子类对象之前,先验证子类的定义是否合乎规范
python系统把子类的整个class语句体处理完毕之后,就会调用元类的__new__方法
'''
###2.用元类来注册子类
'''
元类可以在程序中自动注册类型。
对于需要反向查找(reverse lookup)的场合很有用。
'''
'''
在构建模块化的python程序时,类的注册是一种很有用的模式
开发者每次从基类中继承子类时,基类的元类都可以自动运行注册代码
通过元类来实现类的注册,可以确保所有子类都不遗漏,避免后续错误
'''
###3.用元类注解类的属性
'''
元类可以在某个刚定义好但是尚未使用的时候,提前修改或注解该类的属性。
这种写法通常会与描述符搭配起来,令这些属性可以更详细地了解自己在外围类中的使用方式。
'''
'''
借助元类,我们可以在某个类完全定义好之前,率先修改该类的属性
描述符与元类能够有效地组合起来,以便对某种行为作出修饰,或在程序运行时探查相关信息
如果把元类与描述符相结合,那就可以在不使用weakred模块的前提下避免内存泄露
'''
用subprocess模块管理子进程
import subprocess
#并发:交错执行程序
#并行:利用多核真正的并行
'''
用python编写并发程序很容易,也可以通过系统调用、子进程和C语言扩展等机制平行的处理一些事。
但要使并发式的python代码以真正平行的方式来运行相当困难。
所以我们要最为恰当的利用python提供的特性
'''
#现在的python最简单最好用的子进程管理模块——内置的subprocess
#用Popen构造器启动进程,然后用communicate方法读取子进程的输出信息并等待终止
proc = subprocess.Popen(['echo','hello from the child!'],stdout=subprocess.PIPE)
out,err = proc.communicate()
procs=[]
#启动多个子进程:
for _ in range(10):
proc = run_sleep(0.1)
procs.append(proc)
'''
用subprocess模块运行子程序,还可以管理输入输出流
python解释器能平行地运行多条子进程,开发者可以充分利用cpu
可以给communicate传入timeout参数,以免子进程死锁或失去响应。
但只在py3.3以后才有timeout。
老版本需要内置的select模块来处理
'''
线程执行阻塞式IO但不要做平行计算
因为受到全局解释器锁(GIL)的限制,所以多条python线程不能再多个cpu核心上面平行地执行字节码
尽管受制于GIL,但是python的多线程功能依然很有用,它可以轻松地模拟出同一时刻执行多项任务的效果
通过python线程,我们可以平行地执行多个系统调用,这使得程序能够在执行阻塞式I/O操作的同时,执行一些运算操作
在线程中使用Lock防止数据竞争
python的确有全局解释锁GIL,但在自己编写自己的程序时,依然要设法防止多个线程争用同一份数据
如果在不加锁的前提下,允许多条线程修改同一个对象,那么程序的数据结构可能会遭到破坏
在python内置的threading模块中,有个名叫Lock的类,它用标准的方式实现了互斥锁
用Queue协调各线程之间的工作
#如果python程序要同时执行许多事务,那么开发者经常需要协调这些事务。
#而在各种协调方式中,较为高效的一种,则是采用函数管线(pipeline) / 类似流水线
#涉及阻塞式I/O操作或子进程的工作任务,尤其适合用此办法处理。
#内置的queue模块
from queue import Queue
queue = Queue()
def consumer():
print('Consumer waiting')
queue.get()
print('Consumer done')
thread = Thread(target=consumer)
thread.start()
print('producer putting')
queue.put(objecy())
thread.join()
print("producer done")
'''
管线是一种优秀的任务处理方式,它可以把处理流程划分为若干阶段,并使用多条python线程来同时执行这些任务
构建并发式的管线时,要注意许多问题,其中包括:如何防止某个阶段陷入持续等待的状态之中、如何停止工作线程、如何防止内存膨胀等
Queue类提供的机制,可以彻底的解决上述问题,它具备阻塞式的队列操作、能够指定缓冲区尺寸,还支持join方法,可构建健壮的管线
'''
用协程并发运行多个函数
'''
线程的三个缺点:
1.为了确保数据安全,必须用特殊的工具协调线程
2.线程需要占用大量内存
3.线程启动时开销比较大
'''
#而协程(coroutine)可以避免上述问题
#原理其实是对生成器的扩展
'''
每当生成器函数执行到yield表达式时,消耗生成器的那段代码,就通过send方法给生成器回传一个值。
生成器收到后会将其视为yield的执行结果。
'''
def my_coroutine():
while True:
received = yield
print('Received:',received)
it = my_coroutine()
next(it)
it.send('1')
it.send('2')
#>>('Received:', '1')
# ('Received:', '2')
'''
协程令程序看上去好像能够同时运行大量函数
对于生成器内的yield表达式,外部代码通过send方法传给生成器的那个值,就是该表达式所要具备的值
协程可以把程序的核心逻辑与程序同外部环境交互时所用的代码分离
'''
用concurrent.futures实现真正的平行计算
把引发CPU性能瓶颈的那部分代码,用C语言扩展模块来改写,即可在尽量发挥python特性的前提下,有效提升程序的执行速度。
但是这样做的工作量比较大,而且可能会引入bug。
multiprocessing模块提供了一些强大的工具。对于某些类型的任务来说,开发者只需要编写少量代码,即可实现平行计算。
*该做法会以子进程的形式,平行的运行多个解释器,从而令python程序能够利用多核心CPU。
若想利用强大的multiprocessing模块,最恰当的做法,就是通过内置的concurrent.futures模块以及其ProcessPoolExecutor类来使用它。
multiprocessing模块所提供的那些高级功能,都特别复杂,所以开发者尽量不要直接使用它们。