本系列文章为《编写高质量代码——改善Python程序的91个建议》的精华汇总。
sort()
或者 sorted()
Python 中常用的排序函数有 sort()
和 sorted()
两者的函数形式分别如下:
sorted(iterable[, cmp[, key[, reverse]]])
s.sort([cmp[, key[, reverse]]])
sort()
和 sorted()
有3个共同的参数:
cmp
:用户定义的任何比较函数,函数的参数为两个可比较的元素(来自 iterable 或者 list ),函数根据第一个参数与第二个参数的关系依次返回 -1、0 或者 +1(第一个参数小于第二个参数则返回负数)。该参数默认值为 None
。key
是一个带参数的函数,用来为每个元素提取比较值,默认为 None
(即直接比较每个元素)reverse
表示排序结果是否反转两者对比:
sorted()
作用于任何可迭代的对象;而 sort()
一般作用于列表。
sorted()
函数会返回一个排序后的列表,原有列表保持不变;而 sort()
函数会直接修改原有列表,函数返回为 None
。实际应用过程中需要保留原有列表,使用 sorted()
函数较为合适,否则可以选择 sort()
函数,因为 sort()
函数不需要复制原有列表,消耗的内存较少,效率也较高。
无论是 sort()
还是 sorted()
函数,传入参数 key
比传入参数 cmp
效率要高。cmp
传入的函数在整个排序过程中会调用多次,函数开销较大;而 key
针对每个元素仅做一次处理,因此使用 key 比使用 cmp
效率要高。
sorted()
功能非常强大,它可以对不同的数据结构进行排序,从而满足不同需求。
例:
对字典进行排序:
>>> phone_book = {"Linda": "7750", "Bob": "9345", "Carol": "5834"}
>>> from operator import itemgetter
>>> sorted_pb = sorted(phone_book.items(), key=itemgetter(1))
>>> print(sorted_pb)
[('Carol', '5834'), ('Linda', '7750'), ('Bob', '9345')]
多维 List 排序:实际情况下也会碰到需要对多个字段进行排序的情况,这在 DB 里面用 SQL 语句很容易做到,但使用多维列表联合 sorted()
函数也可以轻易达到
>>> import operator
>>> game_result = [["Bob",95,"A"],["Alan",86,"C"],["Mandy",82.5,"A"],["Rob",86,"E"]]
>>> sorted(game_result, key=operator.itemgetter(2, 1))
[['Mandy', 82.5, 'A'], ['Bob', 95, 'A'], ['Alan', 86, 'C'], ['Rob', 86, 'E']]
字典中混合 List 排序:字典中的 key 或者值为列表,对列表中的某一个位置的元素排序
>>> my_dict = {"Li":["M",7],"Zhang":["E",2],"Wang":["P",3],"Du":["C",2],"Ma":["C",9],"Zhe":["H",7]}
>>> import operator
>>> sorted(my_dict.items(), key=lambda item:operator.itemgetter(1)(item[1]))
[('Du', ['C', 2]), ('Zhang', ['E', 2]), ('Wang', ['P', 3]), ('Zhe', ['H', 7]), ('Li', ['M', 7]), ('Ma', ['C', 9])]
List 中混合字典排序:列表中的每一个元素为字典形式,针对字典的多个 key 值进行排序
>>> import operator
>>> game_result = [{"name":"Bob","wins":10,"losses":3,"rating":75},{"name":"David","wins":3,"losses":5,"rating":57},{"name":"Carol","wins":4,"losses":5,"rating":57},{"name":"Patty","wins":9,"losses":3,"rating":71.48}]
>>> sorted(game_result, key=operator.itemgetter("rating","name"))
[{'losses': 5, 'name': 'Carol', 'rating': 57, 'wins': 4}, {'losses': 5, 'name': 'David', 'rating': 57, 'wins': 3}, {'losses': 3, 'name': 'Patty', 'rating': 71.48, 'wins': 9}, {'losses': 3, 'name': 'Bob', 'rating': 75, 'wins': 10}]
copy
操作等。deepcopy()
操作。浅拷贝并不能进行彻底的拷贝,当存在列表、字典等不可变对象的时候,它仅仅拷贝其引用地址。要解决上述问题需要用到深拷贝,深拷贝不仅拷贝引用也拷贝引用所指向的对象,因此深拷贝得到的对象和原对象是相互独立的。
计数统计就是统计某一项出现的次数。可以使用不同数据结构来进行实现:
defaultdict
实现from collections import defaultdict
some_data = ["a", "2", 2, 4, 5, "2", "b", 4, 7, "a", 5, "d", "a", "z"]
count_frq = defaultdict(int)
for item in some_data:
count_frq[item] += 1
print(count_frq)
# defaultdict(, {'a': 3, '2': 2, 2: 1, 4: 2, 5: 2, 'b': 1, 7: 1, 'd': 1, 'z': 1})
但更优雅,更 Pythonic 的解决方法是使用 collections.Counter
:
from collections import Counter
some_data = ["a", "2", 2, 4, 5, "2", "b", 4, 7, "a", 5, "d", "z", "a"]
print(Counter(some_data))
# Counter({'a': 3, '2': 2, 4: 2, 5: 2, 2: 1, 'b': 1, 7: 1, 'd': 1, 'z': 1})
常见的配置文件格式有 XML 和 ini 等,其中在 MS Windows 系统上,ini 文件格式用得尤其多,甚至操作系统的 API 也都提供了相关的接口函数来支持它。类似 ini 的文件格式,在 Linux 等操作系统中也是极常用的,比如 pylint 的配置文件就是这个格式。Python 有个标准库来支持它,也就是 ConfigParser。
ConfigParser 的基本用法通过手册可以掌握,但仍然有几个知识点值得注意。首先就是 getboolean()
这个函数。getboolean()
根据一定的规则将配置项的值转换为布尔值,如以下的配置:
[section1]
option1=0
当调用 getboolean("section1", "option1")
时,将返回 False。
getboolean()
的真值规则: 除了 0 以外,no、false 和 off 都会被转义为 False,而对应的 1、yes、true 和 on 则都被转义为 True,其他值都会导致抛出 ValueError
异常。
还需要注意的是配置项的查找规则。首先,在 ConfigParser 支持的配置文件格式里,有一个 [DEFAULT]
节,当读取的配置项不在指定的节里时,ConfigParser 将会到 [DEFAULT]
节中查找。
除此之外,还有一些机制导致项目对配置项的查找更复杂,这就是 class ConfigParser 构造函数中的 defaults 形参以及其 get(section, option[, raw[, vars]])
中的全名参数 vars
。如果把这些机制全部用上,那么配置项值的查找规则:
get()
方法的 var
参数中,则返回 var
参数中的值尽管应用程序通常能够通过配置文件在不修改代码的情况下改变行为,但提供灵活易用的命令行参数仍然非常有意义,比如:减轻用户的学习成本,通常命令行参数的用法只需要在应用程序名后面加 --help 参数就能获得,而配置文件的配置方法通常需要通读手册才能掌握。
关于命令行处理,现阶段最好用的参数处理标准库是 argparse。
add_argument()
方法用以增加一个参数声明。import argparse
parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
help='an integer for the accumulator')
parser.add_argument('--sum', dest='accumulate', action='store_const',
const=sum, default=max,
help='sum the integers (default: find the max)')
args = parser.parse_args()
print(args.accumulate(args.integers))
parser = argparse.ArgumentParser()
parser.add_argument("bar", type=argparse.FileType("w"))
parser.parse_args(["out.txt"])
parser.add_argument("door", type=int, choices=range(1, 4))
。parser = argparse.ArgumentParser(prog="PROG", add_help=False)
group1 = parser.add_argument_group("group1", "group1 description")
group1.add_argument("foo", help="foo help")
group2 = parser.add_argument_group("group2", "group2 description")
group2.add_argument("--bar", help="bar help")
parser.print_help()
add_mutually_exclusive_group(required=False)
非常实用:它确保组中的参数至少有一个或者只有一个(required=True)。pip
就有 install/uninstall/freeze/list/show
等子命令,这些子命令又接受不同的参数,使用 ArgumentParser.add_subparsers() 就可以实现类似的功能。import argparse
parser = argparse.ArgumentParser(prog="PROG")
subparsers = parser.add_subparsers(help="sub-command help")
parser_a = subparsers.add_parser("a", help="a help")
parser_a.add_argument("--bar", type=int, help="bar help")
parser.parse_args(["a", "--bar", "1"])
exit(status=0, message=None)
和 error(message)
,可以省了 import sys
再调用 sys.exit()
的步骤。序列化,简单地说就是把内存中的数据结构在不丢失其身份和类型信息的情况下转换成对象的文本或二进制表示的过程。对象序列化后的形式经过反序列化过程应该能恢复原有对象。
Python 中有很多支持序列化的模块,如 pickle、json、marshal 和 shelve 等。
pickle 是最通用的序列化模块,它还有个 C 语言的实现 cPickle,相比 pickle 来说具有较好的性能,其速度大概是 pickle 的 1000 倍,因此在大多数应用程序中应该优先使用 cPickle(注:cPickle 除了不能被继承之外,它们两者的使用基本上区别不大)。pickle 中最主要的两个函数对为 dump()
和 load()
,分别用来进行对象的序列化和反序列化。
pickle 良好的特性总结为以下几点:
接口简单,容易使用。使用 dump()
和 load()
便可轻易实现序列化和反序列化。
pickle 的存储格式具有通用性,能够被不同平台的 Python 解析器共享。比如 Linux 下序列化的格式文件可以在 Windows 平台的 Python 解析器上进行反序列化,兼容性较好。
支持的数据类型广泛。如数字、布尔值、字符串,只包含可序列化对象的元组、字典、列表等,非嵌套的函数、类以及通过类的 __dict__
或者 __getstate__()
可以返回序列化对象的实例等。
pickle 模块是可以扩展的。对于实例对象,pickle 在还原对象的时候一般是不调用 __init__()
函数的,如果要调用 __init__()
进行初始化,对于古典类可以在类定义中提供 __getinitargs__()
函数,并返回一个元组,当进行 unpickle 的时候,Python 就会自动调用 __init__()
,并把 __getinitargs__()
中返回的元组作为参数传递给 __init__()
,而对于新式类,可以提供 __getnewargs__()
来提供对象生成时候的参数,在 unpickle 的时候以 Class.__new__(Class, *arg)
的方式创建对象。对于不可序列化的对象,如 sockets、文件句柄、数据库连接等,也可以通过实现 pickle 协议来解决这些巨献,主要是通过特殊方法 __getstate__()
和 __setstate__()
来返回实例在被 pickle 时的状态。
示例:
import cPickle as pickle
class TextReader:
def __init__(self, filename):
self.filename = filename # 文件名称
self.file = open(filename) # 打开文件的句柄
self.postion = self.file.tell() # 文件的位置
def readline(self):
line = self.file.readline()
self.postion = self.file.tell()
if not line:
return None
if line.endswith("\n"):
line = line[:-1]
return "{}: {}".format(self.postion, line)
def __getstate__(self): # 记录文件被 pickle 时候的状态
state = self.__dict__.copy() # 获取被 pickle 时的字典信息
del state["file"]
return state
def __setstate__(self, state): # 设置反序列化后的状态
self.__dict__.update(state)
file = open(self.filename)
self.file = file
reader = TextReader("zen.text")
print(reader.readline())
print(reader.readline())
s = pickle.dumps(reader) # 在 dumps 的时候会默认调用 __getstate__
new_reader = pickle.loads(s) # 在 loads 的时候会默认调用 __setstate__
print(new_reader.readline())
能够自动维护对象间的引用,如果一个对象上存在多个引用,pickle 后不会改变对象间的引用,并且能够自动处理循环和递归引用。
>>> a = ["a", "b"]
>>> b = a # b 引用对象 a
>>> b.append("c")
>>> p = pickle.dumps((a, b))
>>> a1, b1 = pickle.loads(p)
>>> a1
["a", "b", "c"]
>>> b1
["a", "b", "c"]
>>> a1.append("d") # 反序列化对 a1 对象的修改仍然会影响到 b1
>>> b1
["a", "b", "c", "d"]
但 pickle 使用也存在以下一些限制:
sys.setrecursionlimit()
进行扩展。pickle.loads("cos\nsystem\n(S'dir\ntR.")
便可以查看当前目录下所有文件。可以将 dir 替换为其他更具破坏性的命令。如果要进一步提高安全性,用户可以通过继承类 pickle.Unpickler 并重写 find_class()
方法来实现。Python 的标准库 JSON 提供的最常用的方法与 pickle 类似,dump/dumps 用来序列化,load/loads 用来反序列化。需要注意 json 默认不支持非 ASCII-based 的编码,如 load 方法可能在处理中文字符时不能正常显示,则需要通过 encoding 参数指定对应的字符编码。在序列化方面,相比 pickle,JSON 具有以下优势:
Python 中标准模块 json 的性能比 pickle 与 cPickle 稍逊。如果对序列化性能要求非常高的场景,可以使用 cPickle 模块。
GIL 的存在使得 Python 多线程编程暂时无法充分利用多处理器的优势,并不能提高运行速率,但在以下几种情况,如等待外部资源返回,或者为了提高用户体验而建立反应灵活的用户界面,或者多用户应用程序中,多线程仍然是一个比较好的解决方案。
Python 为多线程编程提供了两个非常简单明了的模块:thread 和 threading。
thread 模块提供了多线程底层支持模块,以低级原始的方式来处理和控制线程,使用起来较为复杂;而 threading 模块基于 thread 进行包装,将线程的操作对象化,在语言层面提供了丰富的特性。实际应用中,推荐优先使用 threading 模块而不是 thread 模块。
就线程的同步和互斥来说,threading 模块中不仅有 Lock 指令锁,RLock 可重入指令锁,还支持条件变量 Condition、信号量 Semaphore、BoundedSemaphore 以及 Event 事件等。
threading 模块主线程和子线程交互友好,join()
方法能够阻塞当前上下文环境的线程,直到调用此方法的线程终止或到达指定的 timeout(可选参数)。利用该方法可以方便地控制主线程和子线程以及子线程之间的执行。
实际上很多情况下我们可能希望主线程能够等待所有子线程都完成时才退出,这时使用 threading 模块守护线程,可以通过 setDaemon() 函数来设定线程的 daemon 属性。当 daemon 属性设置为 True 的时候表明主线程的退出可以不用等待子线程完成。默认情况下,daemon 标志为 False,所有的非守护线程结束后主线程才会结束。
import threading
import time
def myfunc(a, delay):
print("I will calculate square of {} after delay for {}".format(a, delay))
time.sleep(delay)
print("calculate begins...")
result = a * a
print(result)
return result
t1 = threading.Thread(target=myfunc, args=(2, 5))
t2 = threading.Thread(target=myfunc, args=(6, 8))
print(t1.isDaemon())
print(t2.isDaemon())
t2.setDaemon(True)
t1.start()
t2.start()
多线程编程不是件容易的事情。线程间的同步和互斥,线程间数据的共享等这些都是涉及线程安全要考虑的问题。
Python 中的 Queue 模块提供了 3 种队列:
Queue.Queue(maxsize)
:先进先出,maxsize 为队列大小,其值为非正数的时候为无限循环队列
Queue.LifoQueue(maxsize)
:后进先出,相当于栈
Queue.PriorityQueue(maxsize)
:优先级队列
这 3 种队列支持以下方法:
Queue.qsize()
:返回队列大小。Queue.empty()
:队列为空的时候返回 True,否则返回 FalseQueue.full()
:当设定了队列大小的情况下,如果队列满则返回 True,否则返回 False。Queue.put(item[, block[, timeout]])
:往队列中添加元素 item,block 设置为 False 的时候,如果队列满则抛出 Full 异常。如果 block 设置为 True,timeout 为 None 的时候则会一直等待直到有空位置,否则会根据 timeout 的设定超时后抛出 Full 异常。Queue.put_nowait(item)
:等于 put(item, False).block
设置为 False 的时候,如果队列空则抛出 Empty 异常。如果 block 设置为 True、timeout 为 None 的时候则会一直等到有元素可用,否则会根据 timeout 的设定超时后抛出 Empty 异常。Queue.get([block[, timeout]])
:从队列中删除元素并返回该元素的值Queue.get_nowait()
:等价于 get(False)
Queue.task_done()
:发送信号表明入列任务已经完成,经常在消费者线程中用到Queue.join()
:阻塞直至队列中所有的元素处理完毕Queue 模块是线程安全的。需要注意的是, Queue 模块中的队列和 collections.deque 所表示的队列并不一样,前者主要用于不同线程之间的通信,它内部实现了线程的锁机制;而后者主要是数据结构上的概念。
多线程下载的例子:
import os
import Queue
import threading
import urllib2
class DownloadThread(threading.Thread):
def __init__(self, queue):
threading.Thread.__init__(self)
self.queue = queue
def run(self):
while True:
url = self.queue.get() # 从队列中取出一个 url 元素
print(self.name + "begin download" + url + "...")
self.download_file(url) # 进行文件下载
self.queue.task_done() # 下载完毕发送信号
print(self.name + " download completed!!!")
def download_file(self, url): # 下载文件
urlhandler = urllib2.urlopen(url)
fname = os.path.basename(url) + ".html" # 文件名称
with open(fname, "wb") as f: # 打开文件
while True:
chunk = urlhandler.read(1024)
if not chunk:
break
f.write(chunk)
if __name__ == "__main__":
urls = ["https://www.createspace.com/3611970","http://wiki.python.org/moni.WebProgramming"]
queue = Queue.Queue()
# create a thread pool and give them a queue
for i in range(5):
t = DownloadThread(queue) # 启动 5 个线程同时进行下载
t.setDaemon(True)
t.start()
# give the queue some data
for url in urls:
queue.put(url)
# wait for the queue to finish
queue.join()