摘选改善Python程序的91个建议
1、理解Pythonic概念
Pythonic
Tim Peters 的 《The Zen of Python》相信学过 Python 的都耳熟能详,在交互式环境中输入import this可以查看,其实有意思的是这段 Python 之禅的源码:
d = {} for c in (65, 97): for i in range(26): d[chr(i+c)] = chr((i+13) % 26 + c) print "".join([d.get(c, c) for c in s])
书中还举了一个快排的例子:
def quicksort(array): less = [] greater = [] if len(array) <= 1: return array pivot =array.pop() for x in array: if x <= pivot: less.append(x) else: greater.append(x) return quicksort(less) + [pivot] + quicksort(greater)
8、利用assert语句来发现问题
>>> y = 2 >>> assert x == y, "not equals" Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError: not equals >>> x = 1 >>> y = 2 # 以上代码相当于 >>> if __debug__ and not x == y: ... raise AssertionError("not equals") ... Traceback (most recent call last): File "<stdin>", line 2, in <module> AssertionError: not equals
运行时加入-O参数可以禁用断言。
x, y = 1, 2
x, y = y, x
原理:右值创建元组,左值接收元组的对应元素。
10、充分利用Lazy evaluation 的特性
def fib(): a, b = 0, 1 while True: yield a a, b = b, a + b
11、理解枚举替代实现的缺陷
利用Python的动态特性可以实现枚举:
# 方式一 class Seasons: Spring, Summer, Autumn, Winter = range(4) # 方式二 def enum(*posarg, **keysarg): return type("Enum", (object,), dict(zip(posarg, range(len(posarg))), **keysarg)) Seasons = enum("Spring", "Summer", "Autumn", Winter=1) Seasons.Spring # 方式三 >>> from collections import namedtuple >>> Seasons = namedtuple('Seasons', 'Spring Summer Autumn Winter')._make(range(4)) >>> Seasons.Spring 0 # 但通过以上方式实现枚举都有不合理的地方 >>> Seasons._replace(Spring=2) │ Seasons(Spring=2, Summer=1, Autumn=2, Winter=3) # Python3.4 中加入了枚举,仅在父类没有任何枚举成员的时候才允许继承
12、不推荐使用type来进行类型检查、而使用isinstance()
14、警惕eval()的安全漏洞
# 合理正确地使用 >>> eval("1+1==2") True >>> eval('"a"+"b"') 'ab' # 坏心眼的geek >>> eval('__import__("os").system("dir")') Desktop Documents Downloads examples.desktop Music Pictures Public __pycache__ Templates Videos 0 >>> eval('__import__("os").system("del * /Q")') # 嘿嘿嘿
如果确实需要使用eval,建议使用安全性更好的ast.literal_eval。
Python 提供三种方式来引入外部模块:import语句、from...import语句以及__import__函数,其中__import__函数显式地将模块的名称作为字符串传递并赋值给命名空间的变量。 使用import需要注意以下几点: 优先使用import a的形式 有节制地使用from a import A 尽量避免使用from a import * 为什么呢?我们来看看 Python 的 import 机制,Python 在初始化运行环境的时候会预先加载一批内建模块到内存中,同时将相关信息存放在sys.modules中,我们可以通过sys.modules.items()查看预加载的模块信息,当加载一个模块时,解释器实际上完成了如下动作: 在sys.modules中搜索该模块是否存在,如果存在就导入到当前局部命名空间,如果不存在就为其创建一个字典对象,插入到sys.modules中 加载前确认是否需要对模块对应的文件进行编译,如果需要则先进行编译 执行动态加载,在当前命名空间中执行编译后的字节码,并将其中所有的对象放入模块对应的字典中
>>> dir() ['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__'] >>> import test testing module import >>> dir() ['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'test'] >>> import sys >>> 'test' in sys.modules.keys() True >>> id(test) 140367239464744 >>> id(sys.modules['test']) 140367239464744 >>> dir(test) ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b'] >>> sys.modules['test'].__dict__.keys() dict_keys(['__file__', '__builtins__', '__doc__', '__loader__', '__package__', '__spec__', '__name__', 'b', 'a', '__cached__'])
从上可以看出,对于用户自定义的模块,import 机制会创建一个新的 module 将其加入当前的局部命名空间中,同时在 sys.modules 也加入该模块的信息,但本质上是在引用同一个对象,通过test.py所在的目录会多一个字节码文件。
20、优先使用absolute import 来导入模块
23、使用else子句简化循环(处理异常)
Python 的 else 子句提供了隐含的对循环是否由 break 语句引发循环结束的判断
>>> def print_prime(n): ... for i in range(2, n): ... for j in range(2, i): ... if i % j == 0: ... break ... else: ... print('{} is a prime number'.format(i)) ... >>> print_prime(7) 2 is a prime number 3 is a prime number 5 is a prime number
可以看出,else 子句在循环正常结束和循环条件不成立时被执行,由 break 语句中断时不执行,同样,我们可以利用这颗语法糖作用在 while 和 try...except 中。
31、记住函数传参既不是传值也不是引用
正确的说法是传对象(call by object)或传对象的引用(call-by-object-reference),函数参数在传递过程中将整个对象传入,对可变对象的修改在函数外部以及内部都可见,对不可变对象的”修改“往往是通过生成一个新对象然是赋值实现的。
>>> from collections import Counter >>> some_data = {'a', '2', 2, 3, 5, 'c', '7', 4, 5, 'd', 'b'} >>> Counter(some_data) Counter({'7',: 1, 2: 1, 3: 1, 4: 1, 5: 1, '2': 1, 'b': 1, 'a': 1, 'd': 1, 'c': 1})
Counter 类属于字典类的子类,是一个容器对象,用来统计散列对象,支持+、-、&、|,其中&和|分别返回两个 Counter 对象各元素的最小值和最大值。
# 初始化 Counter('success') Counter(s=3, c=2, e=1, u=1) Counter({'s': 3, 'c': 2, 'u': 1, 'e': 1}) # 常用方法 list(Counter(some_data).elements()) # 获取 key 值 Counter(some_data).most_common(2) # 前 N 个出现频率最高的元素以及对应的次数 (Counter(some_data))['y'] # 访问不存在的元素返回 0 c = Counter('success') c.update('successfully') # 更新统计值 c.subtract('successfully') # 统计数相减,允许为0或为负
41、使用 argparse 处理命令行参数
import argparse parse = argparse.ArgumentParser() parse.add_argument('-o', '--output') parse.add_argument('-v', dest='verbose', action='store_true') args = parser.parse_args()
42、使用pandas处理大型CSV文件 教程
reader(csvfile[, dialect='excel'][, fmtparam]) # 读取一个 csv 文件,返回一个 reader 对象 csv.writer(csvfile, dialect='excel', **fmtparams) # 写入 csv 文件 csv.DictWriter(csvfile, fieldnames, restval='', extrasaction='raise', dialect='excel')
43、一般情况下使用 ElementTree 解析 XML 教程
count = 0 for event, elem in ET.iterparse('test.xml'): if event == 'end': if elem.tag == 'userid': count += 1 elem.clear() print(count)
45、使用 traceback 获取栈信息
当发生异常,开发人员往往需要看到现场信息,trackback 模块可以满足这个需求
traceback.print_exc() # 打印错误类型、值和具体的trace信息 traceback.print_exception(type, value, traceback[, limit[, file]]) # 前三个参数的值可以从sys.exc_info() raceback.print_exc([limit[, file]]) # 同上,不需要传入那么多参数 traceback.format_exc([limit]) # 同 print_exc(),返回的是字符串 traceback.extract_stack([file, [, limit]]) # 从当前栈中提取 trace 信息
traceback 模块获取异常相关的数据是通过sys.exc_info()得到的,该函数返回异常类型type、异常value、调用和堆栈信息traceback组成的元组。
同时 inspect 模块也提供了获取 traceback 对象的接口。
50、利用模块实现单例模式
51、用 mixin 模式让程序更加灵活
模板方法模式就是在一个方法中定义一个算法的骨架,并将一些实现步骤延迟到子类中。模板方法可以使子类在不改变算法结构的情况下,重新定义算法中的某些步骤。看个例子:
class People(object): def make_tea(self): teapot = self.get_teapot() teapot.put_in_tea() teapot.put_in_water() return teapot
显然get_teapot()方法并不需要预先定义,也就是说我们的基类不需要预先申明抽象方法,子类只需要继承 People 类并实现get_teapot(),这给调试代码带来了便利。但我们又想到如果一个子类 StreetPeople 描述的是正走在街上的人,那这个类将不会实现get_teapot(),一调用make_tea()就会产生找不到get_teapot()的 AttributeError,所以此时程序员应该立马想到,随着需求的增多,越来越多的 People 子类会选择不喝茶而喝咖啡,或者是抽雪茄之类的,按照以上的思路,我们的代码只会变得越发难以维护。
所以我们希望能够动态生成不同的实例:
class UseSimpleTeapot(object): def get_teapot(self): return SimpleTeapot() class UseKungfuTeapot(object): def get_teapot(self): return KungfuTeapot() class OfficePeople(People, UseSimpleTeapot): pass class HomePeople(People, UseSimpleTeapot): pass class Boss(People, UseKungfuTeapot): pass def simple_tea_people(): people = People() people.__base__ += (UseSimpleTeapot,) return people def coffee_people(): people = People() people.__base__ += (UseCoffeepot,) def tea_and_coffee_people(): people = People() people.__base__ += (UseSimpleTeapot, UserCoffeepot,) return people def boss(): people = People() people.__base__ += (KungfuTeapot, UseCoffeepot, ) return people
以上代码的原理在于每个类都有一个__bases__属性,它是一个元组,用来存放所有的基类,作为动态语言,Python 中的基类可以在运行中可以动态改变。所以当我们向其中增加新的基类时,这个类就拥有了新的方法,这就是混入mixin。
import mixins # 把员工需求定义在 Mixin 中放在 mixins 模块 def staff(): people = People() bases = [] for i in config.checked(): bases.append(getattr(maxins, i)) people.__base__ += tuple(bases) return people
52、用发布订阅模式实现松耦合
发布订阅模式是一种编程模式,消息的发送者不会发送其消息给特定的接收者,而是将发布的消息分为不同的类别直接发布,并不关注订阅者是谁。而订阅者可以对一个或多个类别感兴趣,且只接收感兴趣的消息,并且不关注是哪个发布者发布的消息。要实现这个模式,就需要一个中间代理人 Broker,它维护着发布者和订阅者的关系,订阅者把感兴趣的主题告诉它,而发布者的信息也通过它路由到各个订阅者处。
from collections import defaultdict route_table = defaultdict(list) def sub(topic, callback): if callback in route_table[topic]: return route_table[topic].append(callback) def pub(topic, *args, **kw): for func in route_table[topic]: func(*args, **kw)
将以上代码放在 Broker.py 的模块,省去了各种参数检测、优先处理、取消订阅的需求,只向我们展示发布订阅模式的基础实现:
import Broker def greeting(name): print('Hello, {}'.format(name)) Broker.sub('greet', greeting) Broker.pub('greet', 'LaiYonghao')
注意学习 blinker 和 python-message 两个模块。