编写高质量代码改善 Python 程序的 91 个建议

1.理解Pythonic

美胜丑,显胜隐,简胜杂,杂胜乱,平胜陡,疏胜密
简单问题的方法

充分体现 Python 自身特色的代码风格

2.编写Pythonic代码

避免只用大小写区分不同对象

避免使用容易引起混淆的名称

  • 重复使用变量名表示不同类型数据
  • 误用内建名称表示其他含义而在当前命名空间被屏蔽
  • 没有构建新数据类型的情况下使用element list dict等作为变量名
  • 过长的变量名可能有更好的可读性

3.理解Python和C语言的不同之处

Python底层为C语言实现

1.缩进与{}
不要混用tab和空格

2.C语言中单引号代表一个字符,实际对应于编译器所采用的字符集中的一个整数值

双引号表示字符串,默认以"\0"结尾

3.三元操作符

C?X:Y

Python支持的语法为 结果A if 条件 else 结果B

4.switch...case

Python更灵活

4.适当添加注释

块注释、行注释以及文档注释(docstring)

5.通过适当添加空行使代码布局更为优雅

函数定义或者类定义之间空两行,需要语义分割的地方空一行

避免过长的代码行

6.编写函数的几个原则

函数优点:最大化的代码重用和最小化的代码冗余

提高程序的健壮性

增强可读性,减少维护成本

  1. 设计尽量短小,嵌套层次不宜过深,最好控制在3层以内

  2. 声明应当合理、简单、易于使用,参数个数不宜过多

  3. 考虑向下兼容

  4. 一个函数只做一件事,尽量保证函数语句粒度的一致性

  5. 不要在函数中定义可变对象作为默认值

  6. 使用异常替换返回错误

  7. 保证通过单元测试

7.常量集中到一个文件

1.命名风格提醒使用者该变量代表的意义为常量

常量所有字母大写,下划线连接各个单词

2.自定义的类实现常量功能

命名全部为大写+值一旦绑定便不可修改

将存放常量的文件命名为 constant.py ,并在其中定义一系列的常量。

8.利用assert语句发现问题

assert主要为调试程序服务,方便快速地检查异常和不恰当的输入

__debug__的值默认为True,且为只读

断言会有代价,会对性能产生一定影响,断言只在调试模式下启用

禁用断言的方式是在运行脚本的时候加上-0标志,并不优化字节码,而是忽略与断言相关的语句

  • 不要滥用
    断言引发异常,通常代表程序中有bug,应使用在正常逻辑不可到达的地方或者正常情况下总为真的场合
  • Python本身能够处理的异常不使用assert
    如数组越界,类型不匹配,除数为0之类的错误
  • 不要使用断言来检查用户输入,更好的是使用条件判断
  • 函数调用后确认返回值是否合理可以使用断言

9.数据交换值的时候不推荐使用中间变量

不需要借助任何中间变量,且性能更好

一般情况下 Python 表达式的计算顺序是从左到右,但遇到表达式赋值的时候表达式右边的操作数先于左边的操作数计算

10.lazy evaluation

延迟/惰性 计算,真正需要执行的时候才计算表达式的值

优点:

  • 避免不必要的计算,带来性能上的提升
    对于 or 条件表达式应该将值为真可能性较高的变量写在 or 的前面,而 and 则应该推后。

  • 节省空间,使得无限循环的数据结构成为可能
    生成器和迭代器,yield

11.理解枚举替代实现的缺陷

12.不推荐使用type进行类型检查

作为动态性的强类型脚本语言,python中的变量在定义的时候并不会指明具体类型

python解释器 会在运行的时候自动进行类型检查并根据需要进行隐式类型转换,如果变量类型不同又不能进行隐式类型转换则抛出TypeError的异常

基于内建类型扩展的用户自定义类型,type函数不能准确返回结果

古典类中,所有类的实例的type值都相等

如果类型有对应的工厂函数,可使用工厂函数对类型做相应转换,否则使用isinstance函数

13.尽量转换为浮点类型后再做除法

主要是python2中存在问题

浮点数可能是不准确的,浮点数的比较同样最好能够指明精度

14.警惕eval()的安全漏洞

eval(expression[,globals[,locals]])

globals为字典形式,local为任何映射对象,标是全局和局部命名空间

实际应用过程中如果使用对象不是信任源,应该避免使用eval,可以替换成安全性更好的ast.literal_eval替代

15.enumerate()枚举函数获取序列迭代的索引和值

lazy

实现方法:

  • 每次循环中索引变量自增
  • range( ) 和 len() 结合
  • while循环,len()获取循环次数
  • zip(range(len(sequence)),sequence)
  • enumerate(sequence)

要获取迭代过程中字典的 key 和 value,应该使用 iteritems 方法。

16.分清==和is的适用场景

id()函数可查看变量在内存中的具体存储空间

is 对象标示符号 object identity 比较两个对象在内存中是否拥有同一块内存空间

== 是否相等 equal

==操作符可以被重载,is不能被重载

x is y为True
x==y也为True
特殊情况除外
a = float("Nan")
a==a

17.考虑兼容性,尽可能使用Unicode编码

18.构建合理的包层次来管理module

优点:

合理组织代码,便于维护和使用

能有效避免名称空间冲突,名称一样时,模块前缀不同则可区分

import 包名
import 包名.模块名

19.有节制地使用from...import语句

import 语句
from ... import ...

注意:

一般情况下尽量优先使用 import a

有节制地使用from a import b

尽量避免使用from a import *,会污染命名空间

**无节制使用from a import **

命名空间冲突

使用场景:

只需要导入部分属性或方法

模块中属性和方法访问频率较高导致使用模块名.包名进行访问过于繁琐

明确说明需要使用 from A import B,或者此形式导入更为简单和便利

循环嵌套导入问题,直接使用import

20.优先使用absolute import 导入

21.i+=1 不等于 ++i

++i在python中语法合法,但不是自增操作,而是+(+i),即加上一个正数i

22.使用with自动关闭资源

文件操作完成后应当立即关闭,否则不仅占用系统资源,还有可能影响其他程序或进程的操作

with expr1 as e1:    
	with expr2 as e2:

在文件处理时使用 with 的好处在于无论程序以何种方式跳出 with 块,总能保证文件被正确关闭

​ 任何实现了上下文协议的对象都可以称为一个上下文管理器

23.使用else子句简化循环

for和while语句中,else在循环正常结束和循环条件不成立时被执行

try:

except:

else:

finally:

24.遵循异常处理的几点基本原则

  • 注意异常粒度,不在try中放入过多的代码
  • 谨慎使用单独的except语句处理所有异常,最好能定位
    不推荐使用 except Exception 或者 efashxcept StandardError来捕获异常
  • 注意异常捕获的顺序,在合适的层次处理异常
  • 使用更为友好的异常信息

25.避免finally中可能发生的陷阱

  • ​ try中发生异常的的时候,except语句中找不到对应异常,会被临时保存起来。finally执行完毕后,临时保存的异常会再次被抛出。
    但finally中产生了新的异常或者执行了return或break语句会导致异常屏蔽
  • 实际开发中不推荐在finally中使用return语句

26.深入理解None,正确判断对象是否为空

常量None 空值对象

数据类型为NoneType,遵循单例模式,唯一,无法创建None对象

所有赋值为None的变量都相等

None与其他任何非None的对象(包括0,"")比较结果都为False

__nonzero__() 方法:该内部方法用于对自身对象进行空值测试,返回 0/1True/False。如果一个对象没有定义该方法,Python 将获取 __len__() 方法调用的结果来进行判断。__len__() 返回值为 0 则表示为空。如果一个类中既没有定义 __len__() 方法也没有定义 __nonzero__() 方法,该类的实例用 if 判断的结果都为 True

27.连接字符串优先使用join而不是+

python中字符串为不可变对象

join()方法效率高于 “+”操作符

join方法连接字符串会首先计算所需申请总内存空间,然后一次性申请所需内存,将字符序列中的每一个元素复制到内存中,join操作的时间复杂度为O(n)

执行一次 + 操作便会在内存中申请新的内存空间,
并将上一次操作的结果和本次操作的右操作数复制到新申请的内存空间
N 个字符串连接的过程中,会产生 N-1 个中间结果,总共需要申请 并复制N-1 次内存,从而严重影响了执行效率,时间复杂度近似为 O(n^2)

28.格式化字符串尽量使用.format方式而不是%

29.区别对待可变对象和不可变对象

int str tuple 不可变对象

dict list set 可变对象

区分标准:其值能否被修改

6切片操作相当于浅拷贝

30.[] () {} 一致的容器初始化形式

列表推导式 list comprehension

[expr for iter_item in iterable if cond_expr]

  • 支持多重嵌套,迭代
  • 表达式可以为简单/复杂表达式,甚至函数
  • iterable可以是任意可迭代对象

优点:

1.直观清晰,代码简洁

2.解析效率更高(对于大数据处理,列表解析并不是一个最佳选择,过多的内存消耗可能会导致 MemoryError

31.函数传参既不是传值也不是传引用

传对象或者说是传对象的引用

函数参数在传递的过程中将整个对象传入,
对可变对象的修改在函数外部以及内部都可见,调用者和被调用者之间共享这个对象。
而对于不可变对象,由于并不能真正被修改,因此,修改往往是通过生成一个新对象然后赋值来实现的。

32.警惕默认参数潜在问题

不想让默认参数所指向的对象在所有的函数调用中被共享,而是在函数调用的过程中动态生成,可以在定义的时候用 None 对象作为占位符。

33.慎用变长参数

*args 可变参数列表,接受一个包装为元组形式的参数列表来传递非关键字参数,参数个数可以任意。

**kwargs, 接受字典形式的关键字参数列表,其中字典的键值对分别表示不可变参数的参数名和值。

原因:

  • 使用过于灵活,存在多种调用方式
  • 参数列表过长,意味着此函数有更好的实现方式,应该被重构

适用场景:

为函数添加装饰器

参数数目不确定

实现函数的多态或者在继承的情况下子类需要调用父类的方法

34.理解str()和repr()的区别

  • 目标不同,str()面向用户,目的是可读性,返回类型是用户友好型和可读性都很强的字符串类型;repr()面向python解释器,目的是准确性,返回值表示Python解释器内部含义,debug用途
  • 解释器中直接输入默认调用repr()函数。print调用str()
  • repr()返回值一般可以用eval()函数还原对象
  • 一般都应该定义__repr__()方法,__str()__方法则为可选

35.staticmethod classmethod 适用场景

类名.方法名 实例.方法名

类方法在调用的时候没有显式声明cls,类本身作为隐藏参数传入

36.掌握字符串的基本用法

Python中字符串有str和unicode两种,python3已简化

isinstance(s, basestring)

37.按需选择sort()或者sorted()

sorted(iterable,cmp,key,reverse)

s.sort(cmp,key,reverse)

区别:

sorted()作用于任何可迭代对象,sort()一般作用于列表

sorted()会返回一个新的排序列表,sort()会直接作用于原列表,返回None,消耗内存较少,效率较高

传入key比传入参数cmp效率要高,cmp传入函数在整个排序过程中会调用多次,key仅做一次处理

38.copy模块深拷贝对象

赋值不是给容器装数据,而是给数据贴标签。因此

变量A = 变量B,值和地址都相等

浅拷贝是把存放变量的地址的值传给新变量,引用同一地址

深拷贝是开辟了新的内存地址存放要赋值的变量的值

39.使用 Counter 进行计数统计

40.深入掌握ConfigParser

41.使用 argparse 处理命令行参数

42.使用pandas处理大型csv文件

43.一般情况使用 ElementTree 解析 XML

lxml解析XML文档

elementree 的 iterparse 工具能够避免将整个 XML 文件加载到内存,从而解决当读入文件过大内存而消耗过多的问题

44.理解模块pickle优劣

序列化:把内存中的数据结构在不丢失其身份和类型信息的情况下转换成对象的文本或二进制表示的过程

cPickle相对pickle性能更好,速度更快,1000倍。

  • 接口简单,容易使用。使用 dump()load() 便可轻易实现序列化和反序列化。
  • 存储格式通用,能够被不同平台的 Python 解析器共享,兼容性好
  • 支持数据类型广泛,模块可以扩展
  • 自动处理循环和递归引用

限制:

  • pickle不能保证操作的原子性
  • 安全性问题
  • pickle协议是Python特定,不同语言之间的兼容性难以保证

45.序列化的另一个不错选择-JSON

46.使用traceback获取栈信息

traceback.print_exc()

错误类型

错误对应的值

具体的trace信息,包括文件名,具体行号,函数名以及对应源代码

inspect 模块也提供了获取 traceback 对象的接口

inspect.trace()

inspect.stack() 函数查看函数层级调用的栈相关信息

47.使用logging记录日志信息

等级

DEBUG

INFO

WANRING

ERROR

CRITICAL

主要对象:

logger是程序信息输出的接口,分散在代码中,根据设置的日志级别或filter来决定哪些信息需要输出,并分发到关联的handler()

Handler 处理信息输出,将信息输出到控制台、文件或者网络

StreamHandler发送错误信息到流,FileHandler用于向文件输出日志信息

Formatter:决定 log 信息的格式

Filter:用来决定哪些信息需要输出。可以被 handler 和 logger 使用,支持层次关系

logging 支持 logging.config 进行配置,支持 dictConfig 和 fileConfig 两种形式,其中 fileConfig 是基于 configparser() 函数进行解析,必须包含的内容为 [loggers][handlers][formatters]

注意

1.尽量为logging取一个名字而不是采用默认

2.logging的名字建议以模块或者class命名

3.Logging是线程安全的,不支持多进程写入同一日志文件

48.使用threading模块编写多线程程序

GIL线程锁使得Python多线程编程暂时无法充分利用多处理器的优势

thread 模块提供了多线程底层支持模块,以低级原始的方式来处理和控制线程,使用起来较为复杂;
而 threading 模块基于 thread 进行包装,将线程的操作对象化

创建方式:

继承 Thread 类,重写它的 run() 方法(注意不是 start() 方法)

创建一个 thread.Thread 对象,在它的初始化函数(__init__())中将可调用对象作为参数传入

threading模块

使用threading的优点

1。threading的支持更完善和丰富

thread模块只提供一种锁类型

threading不仅有Lock指令锁

RLock可重入指令锁,条件变量condition

信号量Semaphore BoundedSemaphor 以及 event事件

2.threading.join()可阻塞当前上下文环境的线程,可便利地控制主线程和子线程之间的执行

3.thread不支持守护线程。主线程退出时不会提示,所有子线程会被强制结束

49.使用Queue使多线程编程更安全

Queue.Queue(maxsize) maxsize>0时为无线循环队列

Queue.LifoQueue(maxsize):后进先出相当于栈

Queue.PriorityQueue(maxsize):优先级队列

Queue 模块中的队列和 collections.deque 所表示的队列并不一样,前者主要用于不同线程之间的通信,它内部实现了线程的锁机制;而后者主要是数据结构上的概念,因此支持 in 方法。

50.利用模块实现单例模式

作用:

保证系统中一个类只有一个实例并且该实例易于被外界访问,从而方便对实例个数的控制并节约系统资源

51.用mixin模式让程序更加灵活

52.用发布订阅模式实现松耦合

发布订阅模式 publish/subscribe

发布者和订阅者不需要知道对方的存在,需要终极那代理人broker

53.用状态模式美化代码

通过在不同的条件下将实例的方法(即行为)替换掉,就实现了状态模式。但仍然有缺陷:

  • 查询对象的当前状态很麻烦
  • 状态切换时需要对原状态做一些清扫工作,而对新的状态需要做一些初始化工作,因为每个状态需要做的事情不同,全部写在切换状态的代码中必然重复,所以需要一个机制来简化。

54.理解built-in objects

Python中一切皆对象

python2.2版本前,类和类型(type)并不统一

  • object和古典类没有基类,type基类为object
  • 新式类中 type() 的值和 __class__ 的值是一样的,但古典类中实例的 typeinstance,其 type() 的值和 __class__ 的值不一样
  • 在古典类中,所有用户定义的类的类型都为 instance
  • 应当通过元类的类型来确定类的类型:古典类的元类为 types.ClassType,新式类的元类为 type

55.__init__()不是构造方法

a = Class(args)

__new__()方法才会真正创建实例,是类的构造方法

__init__()方法所做的工作是在类的对象创建好后进行变量的初始化

__new__是静态方法,init是实例方法

  • 当需要控制实例创建的时候可使用 __new__() 方法,而控制实例初始化的时候使用 __init__() 方法
  • 一般情况下不需要覆盖 __new__() 方法,但当子类继承自不可变类型,如 strintunicode 或者 tuple 的时候,往往需要覆盖该方法
  • 当需要覆盖 __new__()__init__() 方法的时候这两个方法的参数必须保持一致,如果不一致将导致异常
  • 一般情况下覆盖 __init__() 能满足大部分需求,特殊情况下需要覆盖 __new__() 方法

56.理解名字查找机制

locals()查看局部变量

globals() 查看全局变量

变量名所在的命名空间直接决定了其能访问到的范围

变量解析机制遵循LEGB法则

局部作用域 > 嵌套作用域 > 全局作用域 >内置作用域

局部作用域:函数内外变量名互不冲突

全局作用域:定义在python模块文件中的变量名

嵌套作用域:如果想在嵌套的函数内修改外层函数中定义的变量,即使使用 global 进行申明也不能达到目的,其结果最终是在嵌套的函数所在的命名空间中创建了一个新的变量。

内置作用域:标准库中名为__builtin__的模块实现的

nonlocal关键字用来在函数或其他作用于中使用外层变量

编程语言不提倡全局变量,且这种写法影响业务逻辑

57.为什么需要self参数

self表示实例对象本身,即类的对象在内存中的地址

​ 在方法声明的时候需要定义 self 作为第一个参数,而调用方法的时候却不用传入这个参数

Python哲学是,显式优于隐式

58.理解MRO与多继承

古典类:MRO 深度优先,按照多继承申明的顺序形成继承树结构,自顶向下采用深度优先的搜索顺序

新式 C3MRO 广度优先

菱形继承是在多继承设计的时候尽量避免的问题

59.描述符机制

__dict__类属性,包含了所有属性

实例属性查找,找不到则进类属性找

能否给类增加属性

能,动态的增减对象的属性与方法是Python动态语言的特性

不能,内置类型和用户定义的类型是有区别的,内置类型不能随意增加属性或方法

通过实例访问 obj.x __get__(obj,type(obj))

通过类访问 cls.x __get__(None,type(obj))

60.区别 __getattr__()__getattribute__() 方法

__getattr__()__getattribute__() 都可以用作实例属性的获取和拦截(仅对实例属性(instance varibale)有效,非类属性)

__getattr__()适用于未定义的属性,__getattribute__()适用于所有属性的访问

61.使用更为安全的property

property 实际上是实现了__get__() __set__()方法的类

数据描述符:如果一个对象同时定义了 __get__()__set__() 方法,则称为数据描述符,如果仅定义了 __get__() 方法,则称为非数据描述符

优点:

代码更简洁,可读性更强

更好的管理属性的访问。设置校验、检查赋值的范围以及对某个属性进行二次计算之后再返回给用户或者计算某个依赖于其他属性的属性。

可维护性

控制属性访问权限,提高数据安全性

62.metaclass

  • 元类是关于类的类,是类的模板
  • 元类用来控制如何创建类
  • 元类的实例是类,类的实例是对象

63.Python对象协议

64.使用操作符重载实现中缀语法

65.迭代器协议

内置有__iter__方法的对象,都称为可迭代对象,可迭代的对象:str,list,tuple,dict,set,文件对象

iter()函数返回一个迭代器对象,接受的参数实现了__iter__()方法的容器或迭代器

  • 实现__iter__()方法,返回一个迭代器
  • 实现next()方法,返回当前的元素并指向下一个元素的位置。若当前位置已无元素,则抛出StopIteration异常

迭代器一定是可迭代对象,但可迭代对象不一定是迭代器

迭代器优缺点

1.不依赖索引的迭代取值方式

2.惰性计算,同一时刻在内存中只存在一个值,更节省内存

1.取值方式不够灵活,不能取指定的值

2.无法预测迭代器的长度

66.Python生成器

生成器:按一定算法生成一个序列

迭代器不是生成器,生成器可以在一定程度上看做迭代器

使用yield语句的函数就是生成器函数

当第一次调用 next() 方法时,生成器函数开始执行,执行到 yield 表达式为止。

67.基于生成器的协程及greenlet

协程,又称微线程和纤程等

大部分协程的实现是协作式而非抢占式的,需要用户自己去调度,所以通常无法利用多核,但用来执行协作式多任务非常合适

68.理解GIL的局限性

GIL 被称为全局解释器锁(Global Interpreter Lock)

它的作用是保证任何情况下虚拟机中只会有一个线程被运行,而其他线程都处于等待 GIL 锁被释放的状态。不管是在单核系统还是多核系统中,始终只有一个获得了 GIL 锁的线程在运行,每次遇到 I/O 操作便会进行 GIL 锁的释放。

69.对象的管理与垃圾回收

引用计数

即针对每一个对象维护一个引用计数值来表示该对象当前有多少个引用。引用该对象时,计数+1,否则-1.

缺点:无法解决循环引用的问题

标记清除

第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收

缺点:

清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。

分代回收

分代回收是一种以空间换时间的操作方式,Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代)

70.pypi安装包

71.pip yolk 安装管理包

72.做paster创建包

73.理解单元测试

单元测试用来验证程序单元的正确性,一般由开发人员完成

创建测试计划

编写测试用例,准备测试数据

编写测试脚本

编写被测代码,在代码完成之后执行测试脚本

修正代码缺陷,重新测试直到代码可接受为止

基本原则:

一致性

原子性

单一职责

隔离性 独立的,无条件逻辑依赖

unittest

74.为包编写单元测试

75.利用测试驱动开发提高代码的可测性

76.使用Pylint检查代码风格

77.高效代码审查

78.将包发布的PyPi

79.了解代码优化的基本原则

1.优先保证代码可工作

2.权衡优化的代价,牺牲时间换空间或者空间换时间

3.定义性能指标,集中力量解决首要问题

4.可读性

80.借助性能优化工具

81.利用cProfile定位性能瓶颈

82:使用 memory_profilerobjgraph 剖析内存使用

83.努力降低算法复杂度

O(1) < O(log * n) < O(n) < O(n log n) < O(n^2) < O(c^n) < O(n!) < O(n^n)

算法复杂度分析建立在同一语言实现的基础上

84.循环优化的基本技巧

减少循环内部的计算

将显式循环改为隐式循环

循环中尽量引用局部变量,局部变量比全局变量的查询快

85.使用生成器提高效率

yield语句与return语句相似,解释器执行遇到yield的时候,函数会自动返回yield语句之后的表达式的值

yield 语句在返回的同时会保存所有的局部变量以及现场信息,以便在迭代器调用 next()send() 方法的时候还原,而不是直接交给垃圾回收器(return() 方法返回后这些信息会被垃圾回收器处理

优点:

用户一般不需要自己实现__iter__和next方法,默认返回迭代器

简洁优雅

惰性计算,节省内存空间,提高效率

协同程序更易实现

86.使用不同的数据结构优化性能

list:list对象如果经常有元素数量的巨变,应当考虑使用deque

deque是双端队列,同时具备栈和队列的特性

87.充分利用set优势

集合 set是通过Hash算法实现的无序不重复的元素集

涉及list求交集、并集或者差等问题可以转换为set操作

88.使用multiprocess克服GIL的缺陷

多进程管理包

每个进程空间地址独立,进程间的数据空间也相互独立,数据共享和传递不如线程方便

上一篇:AcWing 91 最短Hamilton路径


下一篇:91. 解码方法