本文续[第1部分]()。元类和属性第29条:用纯属性替换get和set方法(1)编写新类时,应使用简单的公共属性定义其接口,而不是手动实现set和get方法(2)如果访问object某个属性需要表现出特殊的行为,那么使用@property来定义这个行为比如下面的例子:成绩必须在0-100之间classHomework:def__init__(self):self.__grade=0@propertydefgrade(self):returnself.__grade@grade.setterdefgrade(self,value):如果不是(0<=value<=100):raiseValueError('Grademustbebetween0and100')self.__grade=value(3)@property方法要遵循最小惊奇原则,不能有奇怪的副作用(4)@property方法需要执行快、慢或复杂的工作,应该放在一个正常的地方方法(5)@property最大的缺点就是和property相关的方法只能是在子类中共享,其他不相关的类不能复用相同的实现代码。第30条:考虑使用@property代替property来重构作者的意思是的:当我们需要迁移属性时(即对属性的需求发生变化时),我们只需要在这个类中添加新的功能,而原来的调用不需要更改代码。正在不断改进接口的过程中是一个重要的缓冲方案(1)@property可以为已有的实例属性添加新的功能(2)@properpy可以用来逐步完善数据模型(3)如果@property是使用过于频繁,应该考虑彻底重构类,修改相关调用代码。第31条:使用描述符重写需要重用的@property方法。首先,描述描述符。首先,看下面的例子:classGrade:def__init(self):self.__value=0def__get__(self,instance,instance_type):returnself.__valuedef__set__(self,instance,value):ifnot(0<=值<=100):提高值Error('成绩必须在0到100之间')self.__value=valueclassExam:math_grade=Grade()chinese_grade=Grade()science_grade=Grade()ifname=="__main__":exam=Exam()exam.math_grade=99exam1=Exam()exam1.math_grade=75print('exam.math_grade:',exam.math_grade,'iswrong')print('exam1.math_grade:',exam1.math_grade,'isright')输出:是发现当对两个Exam实例进行math_grade操作时,会导致错误的结果。这样做的原因是math_grade属性是Exam类的一个实例。解决这个问题,看下面的代码0)def__set__(self,instance,value):如果不是(0<=value<=100):raiseValueError('Grademustbebetween0and100')self.__value[instance]=valueclassExam:math_grade=Grade()chinese_grade=Grade()science_grade=Grade()ifname=="__main__":exam=Exam()exam.math_grade=99exam1=Exam()exam1.math_grade=75print('exam.math_grade:',exam。垫h_grade,'iswrong')print('exam1.math_grade:',exam1.math_grade,'isright')输出:上面的实现很简单,可以正常工作,但是还是有一个问题,就是会内存泄漏。在程序的生命周期中,对于每一个传递给__set__方法的Exam实例,__values字典都会保存一个对该实例的引用,这会导致该实例的引用计数失败降为0,从而使其无法实现让垃圾收集器使用python内置的weakref模块回收它,解决了上面的问题。classGrade:def__init(self):self.__value=weakref.WeakKeyDictionary()(1)如果想复用@property方法及其验证机制,可以自己定义描述符(2)WeakKeyDictionary可以保证描述符class不会Leakmemory(3)在使用描述符协议实现属性获取和设置操作时,不用担心__getattribute__方法的具体操作细节。第三十二条:使用__getattr__、__getattribute__和__setattr__实现按需生成属性如果一个类定义了__getattr__,系统在该类对象的实例字典中找不到要查询的属性,就会调用该方法。惰性访问的概念:在第一次执行__getattr__时执行一些操作,加载相关属性,后面访问该属性时,只需要从已有的结果中获取即可。每次程序访问对象的属性时,Python系统都会调用__getattribute__,即使该属性已经存在于属性字典中,这样就会触发__getattribute__方法(1)通过__getattr__和__setattr__,我们可以加载和保存惰性方式获取对象的属性(2)理解__getattr__和__getattribute__的区别:前者只会在要访问的属性缺失时触发,后者会在每次访问属性时触发(3)如果要在__getattribute__和__setattr__方法中访问实例属性,应该直接使用super()来做,避免无限递归Item33:使用元类验证子类元类最简单的用途之一是验证某个是否类定义是正确的。在构建一个复杂的类系统时,我们可能需要保证类的风格保持一致,确保某些方法被重写,或者类属性之间有一些严格的关系。下面的例子判断class属性是否包含name属性:验证类的定义是否正确classMeta(type):def__new__(meta,name,bases,class_dict):print('class_dict:',class_dict)if不是class_dict。get('name',None):#判断class属性是否包含name属性raiseAttributeError('musthasnameattribute')returntype.__new__(meta,name,bases,class_dict)classA(metaclass=Meta):def__init__(self):self.chinese_grade=90self.math_grade=99ifname=='__main__':a=A()输出:(1)通过元类,我们可以在生成子类对象之前验证子类是否定义符合规范(2)python系统处理完子类的整个class语句体后,会调用其元类的__new__方法。第34条:用元类注册子类元类的另一个用途是在程序中自动注册类型。对于需要反向查找(reverselookup)的场合,这个注册操作非常有用。请参见以下示例:序列化和反序列化对象Meta中的类:',cls)register[cls.__name__]=clsreturnclsclassSerializable(metaclass=Meta):def__init__(self,*args):self.args=argsdefserialize(self):returnjson.dumps({'class':self.__class__.__name__,'args':self.args})defdeserilize(self,json_data):json_dict=json.loads(json_data)classname=json_dict['class']args=json_dict['args']returnregisterclassnameclassPoint2D(Serializable):def__init__(self,x,y):super().__init__(x,y)self.x=xself.y=ydefadd(self):returnself.x+self.yifname=="__main__":p=Point2D(2,5)data=p.serialize()打印('serialize_data:',data)new_point2d=p.deserilize(data)print('new_point2d:',new_point2d)print(new_point2d.add())输出:(1)通过元类实现类注册,保证所有子类不会被泄露,以免后续出错。第三十五条:使用元类来注解类的属性(1)借助元类,我们可以在一个类没有完全定义之前先修改它的属性(2)可以将描述符和元类有效地结合起来,以修改某些行为,或者程序运行时探索相关资料(3)如果结合元类和描述符,可以使用weakref在模块的前提下避免内存泄漏并发和并行的关键区别在于是否可以提速。如果是并行的话,整个任务的执行时间会减半。如果是并发,那么即使多条路径可以看似并行的方式执行,仍然不会提高总任务的执行速度。用Python语言编写并发程序相对容易。通过系统调用、子进程、C语言扩展等机制,Python也可以用来并行处理一些事务。然而,让并发的python代码以真正并行的方式运行是相当困难的第36条:使用subprocess模块??管理子进程经过多年的发展,Python演化出了多种运行子进程的方式,包括popen、popen2、os.exec*等。然而,到目前为止,对于Python来说,最好的和最简单的子流程管理模块应该是内置的子流程模块。第37条:你可以使用线程来执行阻塞I/O,但不要将它用于并行计算(1)因为它受到全局解释锁(GIL)的限制,所以多个Python线程不能在多个上并行执行字节码CPU核数(二)虽然受制于GIL,但是python的多线程功能还是很有用的,可以很方便的模拟多个线程同时执行这个任务的效果(三)通过python线程,我们可以执行并行的多个系统调用,这使得程序可以在执行阻塞I/O操作的同时执行一些算术运算。第38条:在线程中使用Lock来防止数据竞争=offset第三十九条:使用Queue来协调线程间的工作,作者举了一个图片处理系统的例子:需求:系统不断地从数码相机中获取照片,调整大小,添加到网络相册中。实现:使用三级流水线实现,需要4个自定义deque消息队列。在第一阶段,获取新照片。在第二阶段,将下载的照片传递给缩放功能。第三阶段,上传放大后的照片。功能问题:虽然程序可以正常运行,但是各个阶段的工作功能会有所不同,这使得前一阶段可能会拖慢后一阶段的进度,从而导致整个流水线卡顿,后一阶段会循环进入它在语句中反复查询输入队列,以获取新的任务,但是任务还没有到达,就会饿死后期,白白浪费CPU时间。内置队列模块的Queue类可以解决上述问题。问题,因为它的get方法将继续阻塞,直到添加新数据为止:item=self.get()try:ifitemisself.SENTINEL:returnyielditemfinally:self.task_done()classStoppabelWoker(threading.Thread):def__init__(self,func,in_queue,out_queue):self。func=funcself.in_queue=in_queueself.out_queue=out_queuedefrun(self):foriteminself.in_queue:result=self.func(item)self.out_queue.put(result)(1)pipeline是一个优秀的Task处理方法,可以把处理流程分成几个阶段,使用多个python线程同时执行这些任务(2)在构建并发管道的时候,要注意很多问题,包括:如何防止某个阶段掉下来进入持续等待状态,如何停止工作线程,如何防止内存膨胀等。(3)Queue类提供的机制可以通过cedilla解决上述问题。它具有阻塞队列操作,可以指定缓冲区的大小,并且还支持join方法,这允许开发人员构建健壮的管道。第40项:考虑使用协程并发运行多个函数(1)协程提供了一种高效的方式让程序看起来可以同时运行大量的函数(2)对于生成器内部的yield表达式,外部代码通过传递给的值send方法的generator是表达式需要有的值(3)协程是一个强大的工具,可以将程序的核心逻辑与程序与外部环境交互时使用的代码隔离开来第41条:考虑使用concurrent.futures实现真正的并行计算内置模块第42条:用functools.wrap定义函数装饰器为了保持函数的接口,修改后的函数必须保留原函数的某些标准Python属性,例如__name__和__module__,这时候我们需要使用functools.wraps来保证修改后的函数有正确的行为Item43:考虑使用contextlib和withstatements重写可重用的try/finally代码(1)是用with语句重写try/finally块中的逻辑,提高重用度,让代码更整洁importthreadinglock=threading.Lock()lock.acquier()try:print("lockisheld")finally:lock.release()可以直接使用如下语法:importthreadinglock=threading.Lock()withlock:print("lockisheld")(2)内置-在contextlib模块中提供了一个名为contextmanager的装饰器,开发者只需要使用它修改自己的函数,使函数支持with语句fromcontextlibimportcontextmanager@contextmanagerdeffile_open(path):'''文件打开测试'''try:fp=open(path,"wb")yieldfpexceptOSError:print("Wehadanerror!")finally:print("Closingfile")fp.close()ifname=="__main__":withfile_open("contextlibtest.txt")asfp:fp.写(“特stingcontextmanagers".encode("utf-8"))(3)contextmanager可以通过yield语句返回一个值给with语句,这个值会赋值给as关键字指定的变量第44条:使用copyreg实现可靠的pickle操作(1)内置的pickle模块只适用于相互信任的程序之间对相关对象进行序列化和反序列化操作(2)如果使用比较复杂,pickle模块的功能可能不同的是,如果出现问题,我们可以使用内置的copyreg模块结合pickle,为旧数据添加缺失的属性值,进行类版本管理,为序列化数据提供固定的引入路径。第45条:你应该使用datetime模块处理本地时间,而不是time模块(1)不要使用time模块在不同时区之间进行转换(2)如果你想可靠地执行不同时区之间的转换操作,y你应该使用内置的datetime模块与开发者社区提供的pytz模块一起使用(3)开发者总是应该首先将时间表示为UTC格式,然后对其进行各种转换操作,最后转换回本地时间。Item46:使用内置的算法和数据结构(1)双向队列collections.deque(2)有序字典dollions.OrderDict(3)带默认值的有序字典collections.defaultdict(4)堆队列(优先队列)heapq.heap(5)bisect模块中的bisect_left函数提供了高效的二分查找算法(6)Iterator相关工具itertools模块第47条:精度重要时应该使用Decimal(1)decimal模块中的Decimal类提供定点数值运算默认28位小数,也可以根据开发者要求的精度进行四舍五入。第48条:学习安装由Python开发者社区构建的模块。协同开发第49条:为每个函数、类、模块编写docstrings第50条:使用包来安排模块,提供可靠的API包和目录下的文件都会成为包的子模块。在包的目录下,还可以包含其他包。(2)在名为__all__的特殊属性中列出外界可见的名称。为包提供一套清晰的API第51条:为自编译模块定义根异常,使调用者与API隔离。这意味着单独使用一个模块提供各种异常API第52条:以适当的方式打破循环依赖(1)调整引入的顺序(2)先导入,再配置,最后运行只在模块中定义函数、类和常量,而不是真正运行那些函数导入时(3)动态引入:在函数或方法内部使用导入语句第53项:使用虚拟环境隔离项目并重建其依赖部署第54项:考虑使用模块级代码配置不同的部署环境(1)内容可以根据外部条件确定模块,例如通过sys和os模块查询宿主操作系统的特性,并以此来定义本模块中的相关结构第55条:通过reprString输出调试信息第56条:通过unittest测试所有代码这个后面会单独写一篇博客详细讲解unittest单元测试模块文章57:考虑使用pdb实现交互式调试第58条:先分析性能,再优化(1)在优化python程序之前,必须先分析其性能,因为python程序的性能瓶颈通常很难直接观察到(2)在做性能分析时,应该使用cProfile模块而不是profile模块,因为前者可以给出更准确的性能分析数据。第59条:使用tracemalloc掌握内存使用和泄漏在Python的默认实现中,即Cpython,内存管理是通过引用计数来处理的。另外,Cpython还有一个内置的循环检测器,它可以让垃圾收集器清除那些自引用的对象(1)使用内置的gc模块来查询和列出垃圾收集器当前已知的每一个对象,这比较笨拙(2)python3.4提供了一个内置模块tracemalloc,可以打印出Python系统在执行每一次内存分配操作时所拥有的完整堆栈信息。这篇文章就到这里了。感谢您如此耐心的阅读!本文由博客群发、多发等运营工具平台OpenWrite发布。
