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

阅读书籍 <<改善 Python 程序的 91 个建议>> 总结内容

什么是 Python 的 packaging/unpackaging 机制 ?

  • 对于表达式 x,y=y,x,其在内存中执行的顺序如下

  • 1 )先计算右边的表达 式 y , x ,因此先在内存中创建元 组 ( y , x ) ,其标示符和值分别 为 y 、 x 及其对应的值 ,其中 y 和 x 是在初始化时已经存在于内存中的对 象

  • 2 )计算表达式左边的值并进行赋值 ,元组被依次分配给左边的标示符 ,通过解压 缩 ( unpacking ),元组第一标识 符 ( 为 y )分配给左边第一个元素 (此时 为 x ) ,元组第二个标识符 ( 为 x )分配给第二个元素 (此时 为 y ) ,从而达到 x 、 y 值交换的目的

Python 如何快速交换两个变量的值?

  • 利用 Python 的 packaging/unpackaging 机制 ?

    1
    a,b=b,a

Python 的注释原则?

  • Python 中有 3 种形式的代码注释 :块注 释 、行注释以及文档注 释 ( docstring )
  • 注释和代码隔开一定的距离 ,同时在块注释之后最好多留几行空白再写代码
1
2
x=x+1        # increace x by 1(更好)
x=x+1# increace x by 1
  • 给外部可访问的函数和方法 (无论是否简单 )添加文档注释 。注释要清楚地描述方法的功 能 ,并对参数 、返回值以及可能发生的异常进行说明 ,使得外部调用它的人员仅仅 看 docs tring 就能正确使用
  • 推荐在文件头中包 含 copyright 申明 、模块描述 等 ,如有必要 ,可以考虑加入作者信息以及变更记录

Python 编码时,如何适当地空行?

  • 在一组代码表达完一个完整的思路之 后 ,应该用空白行进行间隔 。如每个函数之 间 ,导入声明 、变量赋值 等 。通俗点讲就是不要在一段代码中说明几件 事 。推荐在函数定义或者类定义之间空两 行 ,在类定义与第一个方法之间 ,或者需要进行语义分割的地方空一行

  • 尽量保持上下文语义的易理解性 。如当一个函数需要调用另一个函数的时候 ,尽量将它们放在一 起 ,最好调用者在 上 ,被调用者在下

    1
    2
    3
    4
    def A():
    B();
    def B():
    pass
  • 避免过长的代码行 ,每行最好不要超过 8 0 个字 符 。以每屏能够显示完整代码而不需要拖动滚动条为最 佳 ,超过的部分可以用圆括 号 、方括号和花括号等进行行连 接 ,并且保持行连接的元素垂直对齐

编写 Python 函数的原则?

  • 函数设计要尽量短 小 ,嵌套层次不宜过深:函数中需要用 到 if 、 e lif 、 w hile 、 for 等循环语句的地方 ,尽量不要嵌套过深 ,最好能控制在 3 层以 内

  • 函数申明应该做到合理 、简单 、易于使用:除了函数名能够正确反映其大体功能 外 ,参数的设计也应该简洁明了 ,参数个数不宜太多

  • 函数参数设计应该考虑向下兼容

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

Python 编码时,如何处理常量?

  • 无论采用哪一种方式来实现常量 ,都提倡将常量集中到一个文件中 ,因为这样有利于维 护 ,一旦需要修改常量的值 ,可以集中统一进行而不是逐个文件去检查

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class const:
    class ConstError(TypeError): pass
    class ConstCaseError(ConstError): pass
    def _setattr__(self, name, value):
    if self.__dict__.has_key(name):
    raise self.ConstError, "Can't change const.%s" % name
    if not name.isupper():
    raise self.ConstCaseError, \
    'const name "%s" is not all uppercase' % name
    self.__dict__[name] = value
    import sys
    sys.modules[__name__]=_const()
    import const
    const.MY_CONSTANT = 1
    const.MY_SECOND_CONSTANT = 2
    const.MY_THIRD_CONSTANT = 'a'
    const.MY_FORTH_CONSTANT = 'b'
    #使用
    from constant import const
    print const.MY_SECOND_CONSTANT
    print const.MY_THIRD_CONSTANT*2
    print const.MY_FORTH_CONSTANT+'5'

如何认识 Python 中的断言?

  • 断言是有代价 的 ,它会对性能产生一定的影响 ,对于编译型的语言 , 如 C/C++ ,这也许并不那么重 要 ,因为断言只在调试模式下启 用 。 但 Python 并没有严格定义调试和发布模式之间的区 别 ,通常禁用断言的方法是在运行脚本的时候加上 - O 标志 ,这种方式带来的影响是它并不优化字节码 ,而是忽略与断言相关的语句

    1
    python - O asserttest.py
  • 不要滥用:若由于断言引发了异常 ,通常代表程序中存 在 bug

  • 如果 Python 本身的异常能够处理就不要再使用断 言

  • 不要使用断言来检查用戶的输入 。如对于一个数字类 型 ,如果根据用戶的设计该值的范围应该 是 2 〜 1 0 ,较好的做法是使用条件判断 ,并在不符合条件的时候输出错误提示信息

  • 在函数调用后 ,当需要确认返回值是否合理时可以使用断 言

  • 当条件是业务逻辑继续下去的先决条件时可以使用断言

什么是 Python 的 Lazy evaluation 特性?

  • Lazy evaluation 常被译为 “延迟计算” 或 “惰性计算 ” ,指的是仅仅在真正需要执行的时候才计算表达式的 值
  • 对于 or 条件表达式应该将值为真可能性较高的变量写 在 or 的前 面 , 而 and 则应该推 后
1
2
3
4
5
6
7
8
9
10
from time import time
t = time()
abbreviations = ['cf.', 'e.g.', 'ex.', 'etc.', 'fig.', 'i.e.', 'Mr.', 'vs.']
for i in xrange (1000000):
for w in ('Mr.', 'Hat', 'is', 'chasing', 'the', 'black', 'cat', '.'):
if w in abbreviations:
#if w[- 1] == '.' and w in abbreviations: #更快
pass
print "total run time:"
print time()- t
  • 使用 yield 生成数据时

    1
    2
    3
    4
    5
    6
    7
    8
    def fib():
    a, b = 0, 1
    while True:
    yield a
    a, b = b, a+b
    >>> from itertools import islice
    >>> print list(islice(fib(), 5))
    >>> [0, 1, 1, 2, 3]

Python 中如何进行类型检查?

  • 基于内建函数 type:但是由于基于内建类型扩展的用戶自定义类型 , t ype 函数并不能准确返回结果和在古典类 中 ,所有类的实例 的 t ype 值都相等的问题,所以不推荐使用

    1
    2
    3
    4
    5
    6
    7
    8
    >>> a = A()
    >>> class B:
    >>> ... pass
    >>> ...
    >>> b = B()
    >>> type(a) == type(b) #
    >>> 判断两者类型是否相等
    >>> True
  • 推荐使用函数 isinstance 进行检查

    1
    2
    3
    4
    5
    6
    7
    >>> isinstance(2,float)
    >>> False
    >>> isinstance("a",(str,unicode))
    >>> True
    >>> isinstance((2,3),(str,list,tuple))
    >>> ①支持多种类型列表
    >>> True

Python 中如何获取序列的索引和值?

  • 推荐使用 enumerate ()
    1
    2
    3
    li = ['a', 'b', 'c', 'd', 'e']     
    for i,e in enumerate(li):
    print "index:",i,"element:",e

Python 中 = = 和 is 的在字符串上使用的注意事项?

  • is 的作用是用来检查对象的标示符是否一致 的 ,也就是比较两个对象在内存中是否拥有同一块内存空间 ,它并不适合用来判断两个字符串是否相等
  • = = 才是用来检验两个对象的值是否相等 的 ,它实际调用内 部 _ eq_() 方法 ,因此 a == b 相当 于 a.eq(b) ,所以 == 操作符是可以被重载的 , 而 is 不能被重 载

Python 中为什么要有节制使用 from…import 语句?

  • 命名空间冲突:在项目中 ,特别是大型项目中频繁地使 用 fr om a import … 的形式会增加命名空间冲突的概率从而导致出现无法预料的问题 。因此需要有节制地使用 from … import 语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # a.py
    def add():
    print "add in module A"
    # b.py
    def add():
    print "math in module B"
    # main.py
    from a import add
    from b import add
    if __name__ == '__main__':
    math()
  • 循环嵌套导入问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    c1.py:
    from c2 import g
    def x():
    Pass
    c2.py:
    from c1 import x
    def g():
    Pass
    #>>无论运行上面哪一个文件都会抛出ImportError异常

Python 的 i+=1 与 ++i 的区别?

  • ++i 在 Python 中语法上是合法的 ,但并不是我们理解的通常意义上的自增操 作
1
2
3
4
5
6
7
8
>>> +1
>>> 1
>>> ++1
>>> 1
>>> - 2
>>> - 2
>>> - - 2 #负负得正
>>> 2

Python 如何使用 else 简化循环?

1
2
3
4
5
6
7
8
#查找素数
def print_prime2(n):
for i in xrange(2, n):
for j in xrange(2, i):
if i % j == 0:
break
else:
print '%d is a prime number'%i
  • 当循 环 “自然” 终结 (循环条件为假 ) 时 e lse 从句会被执行一次 ,而当循环是由 break 语句中断 时 , e lse 子句就不被执 行

Python 中异常处理的原则?

  • 注意异常的粒 度 ,不推荐在 try 中放入过多的代码:在 t ry 中放入过多的代码带来的问题是如果程序中抛出异 常 ,将会较难定位 , 给 debug 和修复带来不 便 ,因此应尽量只在可能抛出异常的语句块前面放 入 try 语句
  • 谨慎使用单独 的 except 语句处理所有异常 ,最好能定位具体的异常
  • 注意异常捕获的顺 序 ,在合适的层次处理异常:推荐的方法是将继承结构中子类异常在前面的 except 语句中抛 出 ,而父类异常在后面 的 e xcept 语句抛出 。这样做的原因是当 try 块中有异常发生的时候 ,解释器根 据 except 声明的顺序进行匹 配 ,在第一个匹配的地方便立即处理该异 常 。如果将层次较高的异常类在前面进行捕获 ,往往不能精确地定位异常发生的具体位置
  • 使用更为友好的异常信 息 ,遵守异常参数的规 范

Python 中使用 finally 的注意事项?

  • fi n a l l y 语句经常被用来做一些清理工作 ,如打开一个文 件 ,抛出异常后在 fi n a l l y 语句中对文件句柄进行关闭 等

  • 因无法捕获异常导致 finally 被执行: 当 t r y 块中发生异常的时 候 ,如果 在 e x c e p t 语句中找不到对应的异常处 理 ,异常将会被临时保存起 来 , 当 fi n a l l y 执行完毕的时 候 ,临时保存的异常将会再次被抛出 ,但如 果 fi n a l l y 语句中产生了新的异常或者执行 了 r e t u r n 或者 b r e a k 语句 ,那么临时保存的异常将会被丢失 ,从而导致异常屏蔽

  • finally 中使用 return 紊乱:对于第二个调 用 R e t ur n T e s t (2) 为什么也返回 - 1 呢 ?这是因为 a > 0 ,会执 行 e l s e 分支 ,但由于存 在 fi n a l l y 语句 ,在执 行 e l s e 语句 的 r e t u r n a 语句之前会先执行 fi n a l l y 中的语句 ,此时由于 fi n a l l y 语句中有 r e t u r n - 1 ,程序直接返回 了 ,所以永远不会返回 a 对应的值 2

1
2
3
4
5
6
7
8
9
10
11
12
def ReturnTest(a): 
try:
if a <=0:
raise ValueError("data can not be negative")
else:
return a
except ValueError as e:
print e
finally:
print("The End!")
return - 1
print ReturnTest(0) print ReturnTest(2) # - 1,- 1

Python 中使用 None 的注意事项?

  • 常量 None 的特殊性体现在它既不 是 0 、 F alse ,也不是空字符 串 ,它就是一个空值对 象 。其数据类型为 Non eType ,遵循单例模式 ,是唯一的 ,因而不能创建 N one 对象 。所有赋值 为 Non e 的变量都相等 ,并且 None 与任何其他非 N one 的对象比较结果都 为 F alse

Python 中使用 join 和 + 连接字符串的差异?

  • join () 方法的效率要高于 + 操作 符 ,特别是字符串规模较大的时 候 , join () 方法的优势更为明显
  • + 操作符连接字符串会不断申请内存空间
  • 当用 join () 方法连接字符串的时候 ,会首先计算需要申请的总的内存空间 ,然后一次性申请所需内存并将字符序列中的每一个元素复制到内存中 去 ,所以 join 操作的时间复杂度 为 O (n)

Python 中格式化字符串的方法?

  • % 操作符根据转换说明符所规定的格式返回一串格式化后的字符串 ,转换说明符的基本形式 为 : % [转换标记] 宽度 [. 精确 度 ] 转换类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
      #直接格式化字符或者数值
    >>> print "your score is %06.1f" % 9.5
    >>> your score is 0009.5
    >>> #以元组的形式格式化
    >>> import math
    >>> itemname = 'circumference'
    >>> radius = 3
    >>> print "the %s of a circle with radius %f is %0.3f " %(itemname,radius,math.p
    >>> i*radius*2)
    >>> the circumference of a circle with radius 3.000000 is 18.850
    >
    >#以字典的形式格式化
    >
    >>> itemdict = {'itemname':'circumference','radius':3,'value':math.pi*radius*2}
    >>> print "the %(itemname)s of a circle with radius %(radius)f is %(value)0.3f
    >>> " % itemdict
    >>> the circumference of a circle with radius 3.000000 is 18.850
    >
  • .format 方式格式化字符串的基本语法为 : [填充 符] 对 ⻬方式 ] [符号 ][#][0][宽度 ][,][. 精确 度 ][转换类型 ] 。其中填充符可以是除了 “ { ” 和 “ } ” 符号之外的任意符 号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#使用符号位置
>>> "The number {0:,} in hex is: {0:#x},the number {1} in oct is {1:#o}".format(
>>> 4746,45)
>>> 'The number 4,746 in hex is: 0x128a,the number 45 in oct is 0o55'
>>> #使用名称
>>> print "the max number is {max},the min number is {min},the average number is
>>> {average:0.3f}".format(max=189,min=12.6,average=23.5)
>>> the max number is 189,the min number is 12.6,the average number is 23.500
>>> #通过属性
>>> class Customer(object):
>>> ... def __init__(self,name,gender,phone):
>>> ... self.name = name
>>> ... self.gender = gender
>>> ... self.phone = phone
>>> ... def __str__(self):
>>> ... return 'Customer({self.name},{self.gender},{self.phone})'.format
>>> (self=self)
>>> str(Customer("Lisa","Female","67889"))
>>> 'Customer(Lisa,Female,67889)'
>>> #格式化元组的具体项
>>> point = (1,3)
>>> 'X:{0[0]};Y:{0[1]}'.format(point)
>>> 'X:1;Y:3'
  • 优先推荐使用.format 作为连接字符串的首要方法,理由:
    • 理由 一 : format 方式在使用上 较 % 操作符更为灵 活 。使用 f ormat 方式 时 ,参数的顺序与格式化的顺序不必完全相同
    • 理由 二 : format 方式可以方便地作为参数传 递
    • 理由 三 : % 最终会被 .format 方式所代 替
    • 理由 四 : % 方法在某些特殊情况下使用时需要特别小 心
      1
      2
      3
      4
      5
      6
      7
      8
      9
      >> itemname =("mouse","mobilephone","cup")
      >
      >> > print "itemlist are %s" %(itemname) #
      >> > 使用%
      >> > 方法格式化元组
      >> > Traceback (most recent call last):
      >> > File "", line 1, in
      >> > TypeError: not all arguments converted during string formatting
      >

如何区别对待 Python 的可变对象及不可变对象?

  • Python 中一切皆对象 ,每一个对象都有一个唯一的标示 符 ( id () ) 、类型 ( type () )以及 值 。对象根据其值能否修改分为可变对象和不可变对象 ,其中数字 、字符 串 、元组属于不可变对 象 ,字典以及列表 、字节数组属于可变对象
  • 不可变对象不可修改:字符串为不可变对 象 ,任何对字符串中某个字符的修改都会抛出异常
1
2
3
teststr = "I am a pytlon string"
teststr[11]='h'
print teststr
  • 对可变对象进行修改,会影响原值

    1
    2
    3
    4
    5
    6
    7
    >>> list1=['a','b','c']
    >>> list2 = list1
    >>> list1.append('d')
    >>> list1
    >>> ['a', 'b', 'c', 'd']
    >>> list2
    >>> ['a', 'b', 'c', 'd']
  • 在将可变对象作为函数参数时,最好的方法是传入 None 作为默认参数,在创建对象时候动态生成可变对象

Python 中推荐对 []、() 和 {} 进行一致的容器初始化方式?

  • 支持多重嵌套

    1
    2
    3
    4
    5
    >>> nested_list = [['Hello', 'World'], ['Goodbye', 'World']]
    >>> nested_list = [[s.upper() for s in xs] for xs in nested_list]
    >>> print nested_list
    >>> [['HELLO', 'WORLD'], ['GOODBYE', 'WORLD']]
    >
    1
    2
    3
    4
    5
    6
    7

    - 支持多重迭代
    ```python
    >>> [(a,b) for a in ['a','1',1,2] for b in ['1',3,4,'b'] if a != b]
    >>> [('a', '1'), ('a', 3), ('a', 4), ('a', 'b'), ('1', 3), ('1', 4), ('1', 'b'), (1,
    >>> '1'), (1, 3), (1, 4), (1, 'b'), (2, '1'), (2, 3), (2, 4), (2, 'b')]
    >
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    - 解析语句支持函数
    ```python
    >>> def f(v):
    >>> ... if v%2 == 0:
    >>> ... v = v2
    >>> ... else:
    >>> ... v = v+1
    >>> ... return v
    >>> ...
    >>> [f(v) for v in [2,3,4,- 1] if v>0] #表达式可以是函数
    >>> [4, 4, 16]
    >>> [v2 if v%2 ==0 else v+1 for v in [2,3,4,- 1] if v>0]
    >>> [4, 4, 16]
  • 迭代对象可以是任意迭代对象

    1
    2
    3
     fh = open("test.txt", "r")
    result = [i for i in fh if "abc" in i] # 文件句柄可以当做可迭代对象
    print result
  • 除了列表可以使用列表解析的语法之 外 ,其他几种内置的数据结构也支持 ,比如元组 ( tuple )的初始化语法 是 ( e xpr for iter_item in iterable if cond_expr ) ,而集 合 ( s et )的初始化语法 是 {expr for iter_item in iterable if cond_expr} ,甚至字典 ( dic t )也有类似的语 法 {expr1, expr2 for iter_item in iterable if cond_expr} 。它们的使用类似列表解 析

C/C++ 与 Python 的辅助差异?

1
a =5, b = a, b=7;
  • C/C++ 中当执行 b=a 的时 候 ,在内存中申请一块内存并将 a 的值复制到该内存 中 ;当执 行 b =7 之后是将 b 对应的值 从 5 修改 为 7
  • Python 中赋值并不是复制 , b =a 操作使得 b 与 a 引用同一个对 象 。 而 b=7 则是 将 b 指向对象 7

Python 的函数传参是传值还是传引用?

  • Python 的函数传参应该是传对 象 ( c all by object )或者说传对象的引 用 ( call- by- object- reference )
  • 函数参数在传递的过程中将整个对象传入 ,对可变对象的修改在函数外部以及内部都 可 ⻅ ,调用者和被调用者之间共享这个对象 ,而对于不可变对象 ,由于并不能真正被修改 ,因此 ,修改往往是通过生成一个新对象然后赋值来实现的

Python 使用默认参数的风险?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> def appendtest(newitem,lista = []):              #默认参数为空列表
print id(lista)
… lista.append(newitem)
print id(lista)
return lista
>>> appendtest('a',['b',2,4,[1,2]])
39644216
39644216
['b', 2, 4, [1, 2], 'a']
>>> appendtest(1)
39022960
39022960
[1]
>>> appendtest.func_defaults #第一次调用后默认参数变为[1]
([1],)
>>> appendtest('a')
39022960
39022960
[1, 'a']
  • 当解释器执行 de f 的时 候 ,默认参数也会被计 算 ,并存在函数的 .fu nc_defaults 属性 中 。由于 Python 中函数参数传递的是对 象 ,可变对象在调用者和被调用者之间共 享 ,因此当首次调 用 appendtest (1) 的时 候 , [ ] 变为 [ 1] ,而再次调用的时候由于默认参数不会重新计算 , 在 [ 1] 的基础上便变为了 [1, ‘ a ’ ]

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

    1
    2
    3
    4
    5
    6
    >>> def appendtest(newitem,lista = None): #默认参数改为None
    >>> ... if lista is None:
    >>> ... lista = []
    >>> ... lista.append(newitem)
    >>> ... return lista
    >>> ...

如何认识 Python 的变长参数?

  • Python 支持可变 ⻓度的参数列表 ,可以通过在函数定义的时候使用 *args 和 kwargs 这两个特殊语法来实现 ( a rgs 和 k wargs 可以替换成任意你喜欢的变量名 )
  • 使用 *args 来实现可变参数列 表 : * args 用于接受一个包装为元组形式的参数列表来传递非关键字参 数 ,参数个数可以任意
1
2
3
4
5
6
7
8
def SumFun(*args):
result = 0
for x in args[0:]:
result += x
return result
print SumFun(2,4)
print SumFun(1,2,3,4,5)
print SumFun()
  • 使用 kwargs 接受字典形式的关键字参数列表 ,其中字典的键值对分别表示不可变参数的参数名和 值

    1
    2
    3
    4
    5
    def category_table(kwargs):
    for name, value in kwargs.items():
    print '{0} is a kind of {1}'.format(name, value)
    category_table(apple = 'fruit', carrot = 'vegetable',Python = 'programming language')
    category_table(BMW = 'Car')
  • 同时使用普通参数、默认参数和可变参数

    1
    2
    3
    4
    5
    6
    7
    def set_axis(x,y, xlabel="x", ylabel="y", args, *kwargs):
    pass
    set_axis(2,3, "test1", "test2","test3", my_kwarg="test3")
    set_axis(2,3, my_kwarg="test3")
    set_axis(2,3, "test1",my_kwarg="test3")
    set_axis("test1", "test2", xlabel="new_x",ylabel = "new_y", my_kwarg="test3")
    set_axis(2,"test1", "test2",ylabel = "new_y", my_kwarg="test3")

    • 首先满足普通参数 ,然后是默认参 数 。如果剩余的参数个数能够覆盖所有的默认参数 ,则默认参数会使用传递时候的值 ,如标 注 ①处的函数在调用的时候 x label 和 y label 的值分别 为 “test1” 和 “ test2 ” ;
    • 如果剩余参数个数不够 ,则尽最大可能满足默认参数的值 ,标注 ② 中 x label 值为 “t est1” , 而 y label 则使用默认参 数 y
    • 除此之外其余的参数除了键值对以外所有的参数都将作 为 args 的可变参 数 , kwargs 则与键值对对

为什么要慎用 Python 的变长参数?

  • 1 )使用过于灵活:在混合普通参数或者默认参数的情况 下 , 变 ⻓参数意味着这个函数的签名不够清晰 ,存在多种调用方式
  • 2 )如果一个函数的参数列表很 ⻓ ,虽然可以通过使用 *ar gs 和 kwargs 来简化函数的定义 ,但通常这意味着这个函数可以有更好的实现方 式 ,应该被重 构

Python 中 str 和 repr 的区别?

  • 函数 str () 和 repr () 都可以将 Python 中的对象转换为字符串 ,它们的使用以及输出都非常相似