《Python Cookbook》第三版——读书笔记

Python Cookbook

语法特性篇

简介

  这里主要是整理了该书籍中对 Python 的一些语言特性的用例,属于基础知识的补全。

可迭代对象的解压

解压全部元素

  任何可迭代对象都可以通过一个简单的赋值语句解压并赋值给多个变量。唯一的前提就是变量的数量必须跟可迭代对象元素的数量相等。可迭代对象包括:列表、元组、字符串、文件、迭代器、生成器。
  示例:
    1. 列表

>>> name, age, gender = ["Bob", 28, "man"]
>>> name
'Bob'
>>> age
28
>>> gender
'man'

    2. 元组

>>> name, age, gender = ("Bob", 28, "man")
>>> name
'Bob'
>>> age
28
>>> gender
'man'

    3. 字符串

>>> first_char, second_char, third_char = "abc"
>>> first_char
'a'
>>> second_char
'b'
>>> third_char
'c'

解压部分元素

  当不需要全部的可迭代对象元素时,可使用星号表达式解压多个连续的元素,星号表达式解压结果永远是列表类型,即使它没有解压到任何元素。
  示例:
    1. 从密文中取出标志位、原始密文、校验位。密文格式为:标志位+原始密文+校验位

>>> flag_bit, *source_ciphertext, check_bit = "1ane8fbg8itjqomgn0afabg0"
>>> flag_bit
'1'
>>> source_ciphertext
['a', 'n', 'e', '8', 'f', 'b', 'g', '8', 'i', 't', 'j', 'q', 'o', 'm', 'g', 'n', '0', 'a', 'f', 'a', 'b', 'g']
>>> check_bit
'0'

    2. 从分数列表中,排除一个最高分和一个最低分

>>> highest_score, *effective_scores, lowest_score = [100, 90, 80, 70, 60, 0]
>>> highest_score
100
>>> effective_scores
[90, 80, 70, 60]
>>> lowest_score
0

    3. 从一组联系方式数据中获取所有电话号码

>>> name, *phone_number = ["Bob", 123456789, 987654321]
>>> name
'Bob'
>>> phone_number
[123456789, 987654321]
>>> name, *phone_number = ["Alice"]
>>> name
'Alice'
>>> phone_number
[]

字典相关

创建带有默认值的字典

  如果需要一个结构复杂的字典,比如值的类型为 list,这很常见:{“one”, [1, 2, 3], “two”, [4, 5, 6]}。如果使用普通的 dict 声明一个空字典,那么向其中添加新元素时,就需要先给键赋一个空列表,作为初始值,然后才能向值空列表中添加新元素。即:
  示例:
    1. 创建一个值为 list 类型的普通 dict 类型字典,并向其中添加新元素

an_dict = {}
an_dict["one"] = []
an_dict["one"].append(1)
an_dict["one"].append(2)
an_dict["two"] = []
an_dict["two"].append(4)
an_dict["two"].append(5)
# 或者使用 setdefault()
an_dict.setdefault('three', []).append(7) 
an_dict.setdefault('three', []).append(8)

  我们可以使用 collections 模块中的 defaultdict 来构造这样的字典。defaultdict 的一个特征是它会自动初始化每个 key 刚开始对应的值,所以你只需要关注添加元素的操作了。
  示例:
    2. 创建一个值为 list 类型的 defaultdict 类型字典,并向其中添加新元素

from collections import defaultdict


teams = defaultdict(list)
teams["one"].append(1)
teams["one"].append(2)
teams["two"].append(4)
teams["two"].append(5)

创建有序字典

  当需要创建一个能保持元素插入顺序的字典时,可以使用 collections 模块中的 OrderedDict 类。有时迭代、序列化、编码成其他格式的映射时,可能会出现这种场景。
  OrderedDict 内部维护着一个根据键插入顺序排序的双向链表。每次当一个新的元素插入进来的时候,它会被放到链表的尾部。而对于一个已经存在的键的重复赋值不会改变键的顺序。
  需要注意的是,一个 OrderedDict 的大小是一个普通字典的两倍,因为它内部维护着另外一个链表。所以如果要构建一个需要大量 OrderedDict 实例的数据结构时,你就得仔细权衡一下额外的内存消耗。
  示例:
    1. 构建一个 OrderedDict

>>> from collections import OrderedDict
>>> d = OrderedDict()
>>> d['foo'] = 1
>>> d['bar'] = 2
>>> d['spam'] = 3
>>> for key in d:
...     print(key, d[key])
...
foo 1
bar 2
spam 3

字典的集合操作

  字典的 keys()items() 方法也支持集合的交、差操作,分别对应 &、- 操作符,该操作可以用于筛选或过滤字典中的特定元素。而 values() 方法则不支持,某种程度上是因为不能保证所有的值互不相同,这样会导致某些集合操作会出现问题。如果你硬要在值上面执行这些集合操作的话,你可以先将值集合转换成 set,然后再执行集合运算就行了。
  示例:
    1. 字典的集合操作

>>> a = {"a": 1, "b": 2}
>>> b = {"b": 2, "c": 3}
>>> a.items() & b.items()
{('b', 2)}
>>> a.keys() & b.keys()
{'b'}
>>>> a.keys() - b.keys()
{'a'}
>>> a.items() - b.items()
{('a', 1)}

    2. 过滤字典的特殊元素

>>> a = {"a": 1, "b": 2, "c": 3}
>>> {key: a[key] for key in a.keys() - {"b", "c"}}
{'a': 1}

字典的排序操作

  字典是构建复杂对象的常用方式之一,而我们往往在构建了大量复杂对象之后,比如将它们放入一个列表,还需要对该列表进行排序,此时可以使用 operator 模块的 itemgetter() 函数,结合常用的排序类方法实现,如:sorted()min()max()
  operator.itemgetter() 函数有一个对字典中的记录用来查找值的索引参数。可以是一个字典键名称,一个整形值或者任何能够传入一个对象的 __getitem__() 方法的值。如果你传入多个索引参数给 itemgetter() ,它生成的 callable 对象会返回一个包含所有元素值的元组,并且 sorted() 函数会根据这个元组中元素顺序去排序。
  示例:
    1. 对一个字典列表使用特定的 key 进行排序

>>> rows = [ {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003},
... {'fname': 'David', 'lname': 'Beazley', 'uid': 1002},
... {'fname': 'John', 'lname': 'Cleese', 'uid': 1001},
... {'fname': 'Big', 'lname': 'Jones', 'uid': 1004} ]
>>> from operator import itemgetter
>>> # 按单个键排序
>>> rows_by_uid = sorted(rows, key=itemgetter('uid'))
>>> print(rows_by_uid)
[{'fname': 'John', 'lname': 'Cleese', 'uid': 1001}, {'fname': 'David', 'lname': 'Beazley', 'uid': 1002}, {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003}, {'fname': 'Big', 'lname': 'Jones', 'uid': 1004}]
>>> # 按多个键排序
>>> rows_by_lfname = sorted(rows, key=itemgetter('lname','fname'))
>>> print(rows_by_lfname)
[{'fname': 'David', 'lname': 'Beazley', 'uid': 1002}, {'fname': 'John', 'lname': 'Cleese', 'uid': 1001}, {'fname': 'Big', 'lname': 'Jones', 'uid': 1004}, {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003}]
>>> # 使用 lambda 表达式代替
>>> rows_by_fname = sorted(rows, key=lambda r: r['fname'])
>>> print(rows_by_fname)
[{'fname': 'Big', 'lname': 'Jones', 'uid': 1004}, {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003}, {'fname': 'David', 'lname': 'Beazley', 'uid': 1002}, {'fname': 'John', 'lname': 'Cleese', 'uid': 1001}]
>>> # 在 min 和 max 函数中使用
>>> min(rows, key=itemgetter('uid'))
{'fname': 'John', 'lname': 'Cleese', 'uid': 1001}
>>> max(rows, key=itemgetter('uid'))
{'fname': 'Big', 'lname': 'Jones', 'uid': 1004}

字典的合并操作

  有多个字典或者映射,将它们从逻辑上合并为一个单一的映射后执行某些操作有两种办法,一种是直接使用 update() 方法,将多个字典合并为一个;另一种是使用 collections 模块中的 ChainMap 类。
  两者的区别是:update() 方法需要你创建一个完全不同的字典对象或者是破坏现有字典结构。同时,如果原字典做了更新,这种改变不会反应到新的合并字典中去。而 ChainMap 使用原来的字典,它自己不创建新的字典。所以它并不会产生上面所说的结果。
  一个 ChainMap 接受多个字典并将它们在逻辑上变为一个字典。但这些字典并不是真的合并在一起了,ChainMap 类只是在内部创建了一个容纳这些字典的列表并重新定义了一些常见的字典操作来遍历这个列表,大部分字典操作都是可以正常使用的。同时,如果出现重复键,那么第一次出现的映射值会被返回。对于字典的更新或删除操作总是影响的是列表中第一个字典。
  示例:
    1. 合并多个字典

>>> a = {'x': 1, 'z': 3 }
>>> b = {'y': 2, 'z': 4 }
>>> from collections import ChainMap
>>> c = ChainMap(a,b)
>>> c['x']
1
>>> c['z']
3
>>> len(c)
3
>>> c.keys()
KeysView(ChainMap({'x': 1, 'z': 3}, {'y': 2, 'z': 4}))
>>> list(c.keys())
['y', 'z', 'x']
>>> list(c.values())
[2, 3, 1]
>>> c['w'] = 10
>>> a
{'x': 1, 'z': 3, 'w': 10}
>>> del c['x']
>>> a
{'z': 3, 'w': 10}
>>> b
{'y': 2, 'z': 4}
>>> del c['z']
>>> b
{'y': 2, 'z': 4}
>>> a
{'w': 10}

对象的排序操作

  实际上对象的排序和字典的排序性质差不多,只是通过 operator 模块的 attrgetter() 去获取对象的属性值,然后传入 sorted()即可,同样,也可以使用 lambda 表达式代替,但 attrgetter() 会更快些。
  这些排序操作其实都是借助了内置的 sorted() 函数有一个关键字参数 key ,可以传入一个 callable 对象给它,这个 callable 对象对每个传入的对象都返回一个值,这个值会被 sorted 用来排序这些对象。
  示例:
    1. 对一个对象列表根据单一属性或者多个属性进行排序

from operator import attrgetter


class User:
    def __init__(self, user_id):
        self.user_id = user_id

    def __repr__(self):
        return 'User({})'.format(self.user_id)


users = [User(23), User(3), User(99)]
print(users)
# 使用 attrgetter 方式根据单一属性排序
print(sorted(users, key=attrgetter('user_id')))
# 使用 attrgetter 方式根据多个属性排序
print(sorted(users, key=attrgetter('last_name', 'first_name')))
# 也可以使用 lambda 表达式代替,但只支持单一属性
print(sorted(users, key=lambda u: u.user_id))
# min、max 函数也可以使用
min(users, key=attrgetter('user_id'))
max(users, key=attrgetter('user_id'))

命名切片的使用

  我们常常需要对序列进行切片,但却直接使用元素下标进行硬编码,比如:person_info[20:23]。这些下标数字在旁人看来,是难以理解的,没人知道这个切片意味着什么。何不从现在开始使用命名切片?
  内置的 slice() 函数可以创建一个切片对象,它能被用在任何切片允许使用的地方。同时,如果你有一个切片对象 a,你可以分别调用它的 a.starta.stopa.step 属性来获取该切片的起始下标、终止下标、步长信息。
  另外,你还能通过调用切片的 indices(size) 方法将它映射到一个确定大小的序列上,这个方法返回一个三元组 (start, stop, step),所有值都会被合适的缩小以满足边界限制,从而使用的时候避免出现 IndexError 异常。
  示例:
    1. 命名切片的使用:

>>> person_info = "workerAlice2313349088837"
>>> name_slice = slice(6, 11)
>>> age_slice = slice(11, 13)
>>> person_info[name_slice]
'Alice'
>>> person_info[age_slice]
'23'

命名元组的使用

  命名元组的一个主要用途是将你的代码从下标操作中解脱出来。下标操作通常会让代码表意不清晰,并且非常依赖记录的结构,当添加了新的元素时你的代码可能就会出错了。
  命名元组另一个用途就是作为字典的替代,因为字典存储需要更多的内存空间。如果你需要构建一个非常大的包含字典的数据结构,那么使用命名元组会更加高效。但是需要注意的是,如果你要改变属性的值,那么可以使用命名元组实例的 _replace() 方法,它会创建一个全新的命名元组并将对应的字段用新的值取代。
  collections.namedtuple() 函数通过使用一个普通的元组对象创建一个命名元组。你需要传递一个自定义类名和你需要的字段给它,然后它就会返回一个类,你可以初始化这个类,为你定义的字段传递值等。
  示例:
    1. 命名元组的创建、使用,创建一个个人信息元组

>>> from collections import namedtuple
>>> Person = namedtuple('Person', ["name", "age", "phone", "address"])
>>> alice = Person("alice", 18, "18910987583", "wuhan")
>>> alice.name
'alice'
>>> alice.age
18
>>> len(alice)
4
>>> name, age, phone, address = alice
>>> name
'alice'
>>> age
18

  从上面的示例可以看到命名元组和类的使用很相似,但它同时还能使用元组的操作,比如解压和索引。下面展示使用下标操作和命名元组实现同一段逻辑的代码,看看命名元组对代码的清晰度有什么提升:
  示例:
    2. 使用下标操作,为个人信息元组中的电话号码加上国家代码

>>> bob = ("bob", 20, "13589734051", "hangzhou")
>>> new_phone = "+86-" + bob[2]
>>> new_phone
'+86-13589734051'

  3. 使用命名元组,为个人信息元组中的电话号码加上国家代码

>>> from collections import namedtuple
>>> Person = namedtuple('Person', ["name", "age", "phone", "address"])
>>> alice = Person("alice", 18, "18910987583", "wuhan")
>>> new_phone = "+86-" + alice.phone
>>> new_phone
'+86-18910987583'

  可以看到使用下标操作元组时,业务含义很不明确,同时,后续如果元组的结构变化了,比如电话号码调整到了其他下标处,还需要修改代码。而命名元组则避免了这些问题。

列表的统计操作

计数

  当需要对列表进行计数、统计时,记得优先查看 collections.Counter 类,而不是手动实现。它提供了不少常见场景的实现,同时还增加了对数学运算操作的支持,对于制表非常有用。
  示例:
    1. 统计一个列表中出现次数最多的三个元素

>>> words = ['look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes', 'the', 'eyes', 'the', 'eyes', 'the', 'eyes', 'not',
...          'around', 'the', 'eyes', "don't", 'look', 'around', 'the', 'eyes', 'look', 'into', 'my', 'eyes', "you're",
...          'under']
>>> from collections import Counter
>>> word_counts = Counter(words)
>>> top_three = word_counts.most_common(3)
>>> print(top_three)
[('eyes', 8), ('the', 5), ('look', 4)]
>>> # 也可以查看每个元素的频次
>>> word_counts["look"]
4
>>> # 可以对元素频次进行调整
>>> word_counts["look"] += 1
>>> word_counts["look"]
5
>>> # 支持数学运算,以全体元素为对象
>>> morewords = ['why','are','you','not','looking','in','my','eyes']
>>> more_word_counts = Counter(morewords)
>>> word_counts
Counter({'eyes': 8, 'look': 5, 'the': 5, 'into': 3, 'my': 3, 'around': 2, 'not': 1, "don't": 1, "you're": 1, 'under': 1})
>>> more_word_counts
Counter({'why': 1, 'are': 1, 'you': 1, 'not': 1, 'looking': 1, 'in': 1, 'my': 1, 'eyes': 1})
>>> all_word_counts = word_counts + more_word_counts
>>> all_word_counts
Counter({'eyes': 9, 'look': 5, 'the': 5, 'my': 4, 'into': 3, 'not': 2, 'around': 2, "don't": 1, "you're": 1, 'under': 1, 'why': 1, 'are': 1, 'you': 1, 'looking': 1, 'in': 1})

分组

  当需要对字典列表或对象列表进行分组操作时,可使用 itertools.groupby() 方法。要进行分组操作,首先需要按照指定的字段将列表排序,然后调用 itertools.groupby() 函数

正则表达式使用

字符串分割

  string 对象的 split() 方法只适应于非常简单的字符串分割情形,它并不允许有多个分隔符或者是分隔符周围不确定的空格。当你需要更加灵活的切割字符串的时候,最好使用 re.split() 方法,它允许你为分隔符指定多个正则模式。比如,下列示例中,分隔符可以是分号,逗号,句号或者是空格,并且后面紧跟着任意个的空格。只要这个模式被找到,那么匹配的分隔符两边的实体都会被当成是结果中的元素返回。返回结果为一个字段列表,这个跟 str.split() 返回值类型是一样的。
  示例:
    1. 为诗句分词:To see a world in a grain of sand, And a heaven in a wild flower; Hold infinity in the palm of your hand, And eternity in an hour.

>>> import re
>>> line = "To see a world in a grain of sand, And a heaven in a wild flower; Hold infinity in the palm of your hand, And eternity in an hour."
>>> re.split(r'[;,.\s]\s*', line)
['To', 'see', 'a', 'world', 'in', 'a', 'grain', 'of', 'sand', 'And', 'a', 'heaven', 'in', 'a', 'wild', 'flower', 'Hold', 'infinity', 'in', 'the', 'palm', 'of', 'your', 'hand', 'And', 'eternity', 'in', 'an', 'hour', '']

捕获分组

  在定义正则表达式的时候,通常会利用括号去捕获分组,每对括号包含一个组。捕获分组可以使得后面的处理更加简单,因为可以分别将每个组的内容提取出来。
  示例:
    1. 将语句中所有日期格式转为 yyyy-MM-dd 输出:Today is 11/27/2012. PyCon starts 3/13/2013.

>>> import re
>>> datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
>>> m = datepat.match('11/27/2012')
>>> m.group(0)
'11/27/2012'
>>> m.group(1)
'11'
>>> m.group(2)
'27'
>>> m.group(3)
'2012'
>>> m.groups()
('11', '27', '2012')
>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> datepat.findall(text)
[('11', '27', '2012'), ('3', '13', '2013')]
>>> for month, day, year in datepat.findall(text):
...   print('{}-{}-{}'.format(year, month, day))
...
2012-11-27
2013-3-13

字符串替换

  对于简单的字面模式,直接使用 str.replace() 方法即可。对于复杂的模式,请使用 re 模块中的 sub() 函数。它的第一个参数是被匹配的模式,第二个参数是替换模式。反斜杠数字指向前面模式的捕获组号。
  示例:
    1. 将语句中所有日期格式替换为 yyyy-MM-dd 格式:

>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.' 
>>> import re
>>> re.sub(r'(\d+)/(\d+)/(\d+)', r'\3-\1-\2', text) 
'Today is 2012-11-27. PyCon starts 2013-3-13.'
>>> # 也可以结合预编译使用,此时不必传入第一个参数:被匹配的模式
>>> datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
>>> datepat.sub(r'\3-\1-\2', text)
'Today is 2012-11-27. PyCon starts 2013-3-13.'

内置的标记参数

  re.IGNORECASE 或 re.I
    忽略匹配时的大小写
  re.DOTALL
    让正则表达式中的点 (.) 匹配包括换行符在内的任意字符。(原本是不能匹配换行的)

贪婪匹配和非贪婪匹配

  通过在 * 或者 + 这样的操作符后面添加一个 ? 可以强制匹配算法改成寻找最短的可能匹配。
  在正则表达式中 * 操作符是贪婪的,因此通常的匹配操作会查找最长的可能匹配,这样通常会导致很多中间的被开始与结束符包含的文本被忽略掉,并最终被包含在匹配结果字符串中返回。比如常见的 ".*"会匹配被双引号包含的最长的文本,而结果中可能会包含多个双引号引用的话。
  为了修正这个问题,可以在模式中的 * 操作符后面加上 ? 修饰符,比如 ".*?",这样就使得匹配变成非贪婪模式,从而得到最短的匹配,即仅匹配一句话。

在正则式中使用 Unicode

  默认情况下 re 模块已经对一些 Unicode 字符类有了基本的支持。比如,\d 已经匹配任意的 unicode 编码下的数字字符了。如果你想在模式中包含指定的 Unicode 字符,你可以使用 Unicode 字符对应的转义序列。(比如 \uFFF 或者 \UFFFFFFF)
  当执行匹配和搜索操作的时候,最好是先标准化并且清理所有文本为统一的标准化格式。但是同样也应该注意一些特殊情况,比如在忽略大小写匹配和大小写转换时的行为。
  一般混合使用 Unicode 和正则表达式会让你抓狂,或者在实际运行中遇到各种各样超出预期的情况。如果你真的打算这样做,最好考虑下安装第三方正则式库,它们会为 Unicode 的大小写转换和其他大量有趣特性提供全面的支持,包括模糊匹配。

文本处理

Unicode 文本的标准化

  在 Unicode 中,某些字符能够用多个合法的编码表示,特别是某些带音标、特殊符号的情景。比如:Spicy Jalape\u00f1o 和 Spicy Jalapen\u0303o 实际上都是 Spicy Jalapeño。这里的文本”Spicy Jalapeño”使用了两种形式来表示。第一种使用整体字符”ñ” (U+00F1),第二种使用拉丁字母”n”后面跟一个”~”的组合字符 (U+0303)。
  这就导致在需要比较字符串的程序中使用字符的多种表示会产生问题。为了修正这个问题,你可以使用 unicodedata 模块先将文本标准化。normalize() 函数第一个参数指定字符串标准化的方式,NFC 表示字符应该是整体组成 (比如可能的话就使用单一编码),而 NFD 表示字符应该分解为多个组合字符表示。Python 也支持扩展的标准化形式 NFKCNFKD,它们在处理某些字符的时候还对应增加了额外的兼容特性。
  示例:
    1. 尝试比较:Spicy Jalape\u00f1o 和 Spicy Jalapen\u0303o

>>> s1 = 'Spicy Jalape\u00f1o'
>>> s2 = 'Spicy Jalapen\u0303o'
>>> # 使用值等于比较
>>> s1 == s2
False
>>> len(s1)
14
>>> len(s2)
15
>>> # 先标准化再比较
>>> import unicodedata
>>> # NFC:将字符尽可能合并为单一字符进行标准化
>>> t1 = unicodedata.normalize('NFC', s1)
>>> t2 = unicodedata.normalize('NFC', s2)
>>> t1 == t2
True
>>> # NFD:将字符尽可能拆分为多个字符进行标准化
>>> t3 = unicodedata.normalize('NFD', s1)
>>> t4 = unicodedata.normalize('NFD', s2)
>>> t3 == t4
True
>>> print(ascii(t1))
'Spicy Jalape\xf1o'
>>> print(ascii(t3))
'Spicy Jalapen\u0303o'

    1. 尝试标准化该字符:\ufb01

>>> s = '\ufb01'
>>> import unicodedata
>>> s
' '
>>> unicodedata.normalize('NFD', s)
' '
>>> unicodedata.normalize('NFKD', s)
'fi'
>>> unicodedata.normalize('NFKC', s)
'fi'

  标准化对于任何需要以一致的方式处理 Unicode 文本的程序都是非常重要的。当处理来自用户输入的字符串而你很难去控制编码的时候尤其如此。在清理和过滤文本的时候字符的标准化也是很重要的,此时你可能需要使用 combining() 函数,它可以测试一个字符是否为和音字符。在 unicodedata 模块中还有其他函数用于查找字符类别,测试是否为数字字符等等。
假设你想清除掉一些文本上面的变音符的时候 (可能是为了搜索和匹配):
    3. 清除文本上的变音符:Spicy Jalape\u00f1o

>>> s1 = 'Spicy Jalape\u00f1o'
>>> import unicodedata
>>> t1 = unicodedata.normalize('NFD', s1)
>>> ''.join(c for c in t1 if not unicodedata.combining(c)) 
'Spicy Jalapeno'

删除特定字符

  strip() 方法能用于删除开始或结尾的字符。lstrip()rstrip() 分别从左和从右执行删除操作。默认情况下,这些方法会去除空白字符,但是你也可以指定其他字符,如:s.lstrip("-") 会清除字符串左侧所有 - 符,而 t.strip("-=") 会清除字符串首尾的所有 -= 字符,且无视它们之间的顺序。但是需要注意的是这三种去除操作均不会对字符串的中间的文本产生任何影响。如果你想处理中间的空格,那么你需要求助其他技术。比如使用 replace() 方法或者是用正则表达式替换。

首尾匹配

  检查字符串开头或结尾的一个简单方法是使用 str.startswith() 或者是 str.endswith() 方法。通常如果只需匹配单一子串,则直接作为入参传入即可,如:filename.endswith('.txt') filename.startswith('file:')。如果你想检查多种匹配可能,只需要将所有的匹配项放入到一个元组中去,然后传入,如:filename.endswith(('.c', '.h')) filename.startswith(('http:', 'https:', 'ftp:'))。如果你恰巧有一个 list 或者 set 类型的选择项,要确保传递参数前先调用 tuple() 将其转换为元组类型。

字符串对齐

  对于基本的字符串对齐操作,可以使用字符串的 ljust()rjust()center() 方法,它们接收两个参数,最终字符串长度和填充字符,默认情况下使用空格作为填充符,但也能接受一个可选的填充字符。比如:s.ljust(20, "*")
  函数 format() 同样可以用来很容易的对齐字符串。你要做的就是使用 <> 或者 ^ 字符后面紧跟一个指定的宽度。同样默认情况下使用空格作为填充字符,如果想指定一个非空格的填充字符,将它写到对齐字符的前面即可。比如:format(text, '*^20s')。当格式化多个值的时候,这些格式代码也可以被用在 format() 方法中,比如:'{:>10s} {:>10s}'.format('Hello', 'World')format() 函数的一个好处是它不仅适用于字符串。它可以用来格式化任何值,使得它非常的通用。比如,你可以用它来格式化数字:format(1.23456, '^10.2f')

迭代器相关

迭代器切片

  迭代器和生成器不能使用标准的切片操作,因为它们的长度事先我们并不知道(并
且也没有实现索引)。 itertools 模块的 islice() 函数适用于在迭代器和生成器上做切片操作。函数 islice() 返回一个可以生成指定元素的迭代器,它通过遍历并丢弃直到切片开始索引位置的所有元素,然后才开始一个个的返回元素,并直到切片结束索引位置。要着重强调的一点是 islice() 会消耗掉传入的迭代器中的数据。必须考虑到迭代器是不可逆的这个事实。所以如果你需要之后再次访问这个迭代器的话,那你就得先将它里面的数据放入一个列表中。
  示例:
    1. 截取迭代器中第 11 到 16 个元素:

>>> def get_numbers(start_num):
...     while True:
...         yield start_num
...         start_num += 1
...
>>> a = get_numbers(0)
>>> import itertools
>>> for n in itertools.islice(a, 10, 15):
...     print(n)
...
10
11
12
13
14
>>>

跳过迭代器开始部分

  itertools 模块的 dropwhile() 函数可以实现对可迭代对象的开始部分进行跳过。使用时,给它传递一个函数对象和一个可迭代对象。它会返回一个迭代器对象,丢弃原有序列中直到指定函数返回 False 之前的所有元素,然后返回后面所有元素。请注意这里仅仅只是跳过迭代器开始的部分,即使后续出现满足指定函数为 False 的元素,也不会被跳过。
  示例:
    1. 现想要读取一个文件,但跳过该文件前面几行注释

>>> from itertools import dropwhile 
>>> with open('/etc/passwd') as f:
...     for line in dropwhile(lambda line: line.startswith('#'), f): 
...        print(line, end='')
...
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false 
# 这是一条位于文件中部的注释
root:*:0:0:System Administrator:/var/root:/bin/sh
>>>

    2. 现想要读取一个文件,但跳过该文件所有注释

>>> with open('/etc/passwd') as f:
...     lines = (line for line in f if not line.startswith('#')) 
...     for line in lines:
...         print(line, end='')
... 
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false 
root:*:0:0:System Administrator:/var/root:/bin/sh
>>>

迭代对象的排列组合

  itertools 模块提供了三个函数来解决这类问题。其中一个是 itertools.permutations(),它接受一个集合并产生一个元组序列,每个元组由集合中所有元素的一个可能排列组成。也就是说通过打乱集合中元素排列顺序生成一个元组。如果你想得到指定长度的所有排列,你可以传递一个可选的长度参数。另一个是 itertools.combinations() 函数,它可得到输入集合中元素的所有的组合。但对于 combinations() 来讲,元素的顺序已经不重要了。也就是说,组合 ('a','b')('b','a') 其实是一样的(最终只会输出其中一个)。在计算组合的时候,一旦元素被选取就会从候选中剔除掉(比如如果元素 a 已经被选取了,那么接下来就不会再考虑它了)。而函数 itertools.combinations_with_replacement() 允许同一个元素被选择多次。
  示例
    1. 输出一个列表的所有元素的全排列和任意两个元素的全排列

>>> items = ['a', 'b', 'c']
>>> from itertools import permutations 
>>> for p in permutations(items):
...
...
('a', 'b', 'c') 
('a', 'c', 'b') 
('b', 'a', 'c') 
('b', 'c', 'a') 
('c', 'a', 'b') 
('c', 'b', 'a')
>>> for p in permutations(items, 2):
...     print(p)
...
('a', 'b') 
('a', 'c') 
('b', 'a') 
('b', 'c') 
('c', 'a') 
('c', 'b') 
>>>

    2. 输出一个列表按元素去重后的所有元素的排列组合与任意两个元素的排列组合 、

>>> items = ['a', 'b', 'c']
>>> from itertools import combinations 
>>> for c in combinations(items, 3):
...     print(c)
...
('a', 'b', 'c')
>>> for c in combinations(items, 2):
...     print(c)
...
('a', 'b') 
('a', 'c')
('b', 'c')
>>>

    3. 输出一个列表的所有元素的排列组合,元素可任意重复出现

>>> for c in combinations_with_replacement(items, 3):
...     print(c)
...
('a', 'a', 'a')
('a', 'a', 'b')
('a', 'a', 'c')
('a', 'b', 'b')
('a', 'b', 'c')
('a', 'c', 'c')
('b', 'b', 'b')
('b', 'b', 'c')
('b', 'c', 'c')
('c', 'c', 'c')
>>>

序列的索引值迭代

  内置的 enumerate() 函数可以使你在迭代一个序列的同时跟踪正在被处理的元素索引。你也可以传入一个可选参数,作为起始索引值。它返回的是一个 enumerate 对象实例,是一个迭代器,返回连续的包含一个计数和一个值的元组,元组中的值通过在传入序列上调用 next() 返回。enumerate() 对于跟踪某些值在列表中出现的位置是很有用的,比如你想将一个文件中出现的单词映射到它出现的行号上去。
  示例:
    1. 循环输出一个列表的索引和值

>>> my_list = ['a', 'b', 'c']
>>> for idx, val in enumerate(my_list):
...     print(idx, val)
...
0 a 
1 b 
2 c
>>> for idx, val in enumerate(my_list, 1):
...     print(idx, val)
...
1 a 
2 b 
3 c
>>> # 当然你也可以使用单独一个变量来记录索引,只是不那么优雅
>>> lineno = 1
>>> for line in my_list:
...     print(idx, val)
...     lineno += 1
...
1 a 
2 b 
3 c

同时迭代多个序列

  内置的 zip() 函数可以同时迭代多个序列,每次分别从它们之中取一个元素,并将元素放入元组中返回。当遍历完最短序列时,流程结束。当然,itertools 模块的 zip_longest() 函数实现了当遍历完最长序列时,再结束流程,它会额外要求指定一个默认元素值的参数。它们对于处理成对数据是很有效的。
  示例:
    1. 同时遍历两个列表,以最短的列表作为终点

>>> xpts = [1, 2, 3, 4, 5]
>>> ypts = [101, 78, 37] 
>>> for x, y in zip(xpts, ypts):
...     print(x,y)
...
1 101
2 78
3 37

    1. 同时遍历两个列表,以最长的列表作为终点

>>> from itertools import zip_longest
>>> xpts = [1, 2, 3, 4, 5]
>>> ypts = [101, 78, 37] 
>>> for x, y in zip_longest(xpts, ypts, fillvalue=0):
...     print(x,y)
...
1 101
2 78
3 37
4 0
5 0

多个序列的连续迭代

  itertools 模块的 chain() 方法可以将多个不同的序列合并起来迭代,而不必事先将所有序列元素全合并到同一个序列中,这样能节省大量内存。它接受一个可迭代对象列表作为输入,并返回一个迭代器,有效的屏蔽掉在多个容器中迭代细节。
  示例:
    1. 先后输出两个序列的元素

>>> from itertools import chain 
>>> a = [1, 2, 3, 4]
>>> b = ['x', 'y', 'z'] 
>>> for x in chain(a, b): 
...     print(x)
...
1
2
3
4
x
y
z

文件与 IO

内存映射与零拷贝

  memoryview 可以通过零拷贝的方式对已存在的缓冲区执行切片操作,甚至还能修改它的内容。
  示例:
    1. 零拷贝

>>> buf
bytearray(b'Hello World') 
>>> m1 = memoryview(buf) 
>>> m2 = m1[-5:]
>>> m2
<memory at 0x100681390> 
>>> m2[:] = b'WORLD' 
>>> buf
bytearray(b'Hello WORLD') 
>>>

  mmap 模块可以用来内存映射文件。下面是一个工具函数,向你演示了如何打开一个文件并以一种便捷方式内存映射这个文件。默认情况下,mmap() 函数打开的文件同时支持读和写操作。任何的修改内容都会复制回原来的文件中。如果需要只读的访问模式,可以给参数 access 赋值为 mmap.ACCESS_READ

import os 
import mmap

def memory_map(filename, access=mmap.ACCESS_WRITE): 
    size = os.path.getsize(filename)
    fd = os.open(filename, os.O_RDWR) 
    return mmap.mmap(fd, size, access=access)

  需要强调的一点是,内存映射一个文件并不会导致整个文件被读取到内存中。也就是说,文件并没有被复制到内存缓存或数组中。相反,操作系统仅仅为文件内容保留了一段虚拟内存。当你访问文件的不同区域时,这些区域的内容才根据需要被读取并映射到内存区域中,而那些从没被访问到的部分还是留在磁盘上。
  如果多个 Python 解释器内存映射同一个文件,得到的 mmap 对象能够被用来在解释器直接交换数据。也就是说,所有解释器都能同时读写数据,并且其中一个解释器所做的修改会自动呈现在其他解释器中。很明显,这里需要考虑同步的问题。但是这种方法有时候可以用来在管道或套接字间传递数据。

忽略文件名编码

  默认情况下,所有的文件名都会根据 sys.getfilesystemencoding() 返回的文本编码来编码或解码。如果因为某种原因你想忽略这种编码,可以使用一个原始字节字符串来指定一个文件名即可。当你给文件相关函数如 open()os.listdir() 传递字节字符串时,文件名的处理方式会稍有不同。

>>> # 先准备一个名字包含 unicode 编码(变音符)的文件
>>> with open('jalape\xf1o.txt', 'w') as f:
...     f.write('Spicy!')
...
6
>>> # 输出编码后的文件名。注意路径为 str
>>> import os
>>> os.listdir('.') 
['jalapeño.txt']

>>> # 输出未经任何编码的原始文件名。注意路径为 byte
>>> os.listdir(b'.')
[b'jalapen\xcc\x83o.txt']

>>> # 尝试用原始文件名操作文件
>>> with open(b'jalapen\xcc\x83o.txt') as f:
...     print(f.read())
...
Spicy! 
>>>

  通常来讲,你不需要担心文件名的编码和解码,普通的文件名操作应该就没问题了。但是,有些操作系统允许用户通过偶然或恶意方式去创建名字不符合默认编码的文件,或者收集到各个国家本地化后的文件。这些文件名可能会神秘地中断那些需要处理大量文件的 Python 程序。读取目录并通过原始未解码方式处理文件名可以有效的避免这样的问题,尽管这样会带来一定的编程难度。

输出非法文件名

  Python 假定所有文件名都已经根据 sys.getfilesystemencoding() 的值编码过了。但是,有一些文件系统并没有强制要求这样做,因此允许创建文件名没有正确编码的文件。如果你需要操作这些非法文件名或者将非法文件名传递给 open() 这样的函数,一切都能正常工作。而一旦当你想要输出文件名时,会碰到些麻烦 (比如打印输出到屏幕或日志文件等),可能会报错,如:UnicodeEncodeError: 'utf-8' codec can't encode character '\uxxxx' in position 1: surrogates not allowed
  当打印未知的文件名时,使用下面的方法可以避免这样的错误:

# 第一种方法:
try:
    print(filename)
except UnicodeEncodeError: 
    print(return repr(filename)[1:-1])
# 第二种方法:重新编码
filename = filename.encode(sys.getfilesystemencoding(), errors='surrogateescape').decode('latin-1')

  surrogateescape 是 Python 在绝大部分面向 OS 的 API 中所使用的错误处理器,它能以一种优雅的方式处理由操作系统提供的数据的编码问题。在解码出错时会将出错字节存储到一个很少被使用到的 Unicode 编码范围内。在编码时将那些隐藏值又还原回原先解码失败的字节序列。它不仅对于 OS API 非常有用,也能很容易的处理其他情况下的编码错误。

将文件描述符包装成文件对象

  一个文件描述符和一个打开的普通文件是不一样的。文件描述符仅仅是一个由操作系统指定的整数,用来指代某个系统的 I/O 通道。而文件描述符可以通过使用 open() 函数来将其包装为一个 Python 的文件对象。仅仅只需要使用这个整数值的文件描述符作为第一个参数来代替文件名即可,和打开普通文件一样。默认情况下,当高层的文件对象被关闭或者破坏的时候,底层的文件描述符也会被关闭。当然也可以传递一个可选参数,colsefd=False 使得高层文件对象关闭时,底层文件描述符不会关闭。

# 打开一个底层文件描述符
import os
fd = os.open('somefile.txt', os.O_WRONLY | os.O_CREAT) 
# 将文件描述符转为文件
f = open(fd, 'wt')
f.write('hello world\n') 
f.close()

  需要重点强调的一点是,上面的例子只适用于基于 Unix 的系统。如果想将一个类文件接口作用在一个套接字并希望代码可以跨平台,请使用套接字对象的 makefile() 方法。但是如果不考虑可移植性的话,那上面的解决方案会比使用 makefile() 性能更好一点。并且,不是所有的文件模式都被支持,并且某些类型的文件描述符可能会有副作用(特别是涉及到错误处理、文件结尾条件等等的时候)。在不同的操作系统上这种行为也不一样。因此使用该技术需要多加测试。

创建临时文件和文件夹

  tempfile 模块中有很多的函数可以完成这任务。为了创建一个匿名的临时文件,可以使用 tempfile.TemporaryFile()。它可以在程序执行时创建一个临时文件或目录,并使用完之后可以自动销毁。TemporaryFile() 的第一个参数是文件模式,通常来讲文本模式使用 w+t ,二进制模式使用 w+b 。这个模式同时支持读和写操作,在这里是很有用的,因为当你关闭文件去改变模式的时候,文件实际上已经不存在了。TemporaryFile() 另外还支持跟内置的 open() 函数一样的参数。如果不想在文件关闭时自动删除,可以传入可选参数:delete=False。在大多数 Unix 系统上,通过 TemporaryFile() 创建的文件都是匿名的,甚至连目录都没有。如果你想打破这个限制,可以使用 NamedTemporaryFile() 来代替。而使用 tempfile.TemporaryDirectory() 则可以创建一个临时目录。
  示例:
    1. 使用 tempfile.TemporaryFile()

from tempfile import TemporaryFile 

with TemporaryFile('w+t') as f:
    # 操作临时文件
    f.write('Hello World\n') 
    f.write('Testing\n')
    # 将文件游标归零,以从头开始读文件
    f.seek(0)
    data = f.read()

# 临时文件被销毁

串口通信

  当程序需要操控一些硬件设备(比如一个机器人或传感器)时,总会涉及到串口通信。此时最好的选择是使用 pySerial 包。这个包的使用非常简单,先安装 pySerial,使用类似下面这样的代码就能很容易的打开一个串行端口:

import serial
ser = serial.Serial('/dev/tty.usbmodem641', baudrate=9600, bytesize=8, parity='N', stopbits=1)
ser.write(b'G1 X50 Y50') 
resp = ser.readline()

  记住所有涉及到串口的 I/O 都是二进制模式的。因此,确保你的代码使用的是字节而不是文本(或有时候执行文本的编码/解码操作)。另外当你需要创建二进制编码的指令或数据包的时候,struct 模块也是非常有用的。

序列化 python 对象

  对于序列化最普遍的做法就是使用 pickle 模块,它可以将一个 Python 对象序列化为一个字节流,以便将它保存到一个文件、存储到数据库或者通过网络传输它。和 json 模块一样,它的 dumps()loads() 方法会将对象(反)序列化为字符串,dump()load() 则将对象直接(反)序列化到文件中。
  但千万不要对不信任的数据使用 pickle.load()pickle 在加载时有一个副作用就是它会自动加载相应模块并构造实例对象。但是这会导致 Python 能执行随意指定的系统命令。因此,一定要保证 pickle 只在相互之间可以认证对方的解析器的内部使用。
  有些类型的对象是不能被序列化的。这些通常是那些依赖外部系统状态的对象,比如打开的文件、网络连接、线程、进程、栈帧等。用户自定义类可以通过提供 __getstate__()__setstate__() 方法来绕过这些限制。如果定义了这两个方法,
pickle 在序列化时就会调用 __getstate__() 获取序列化的对象,__setstate__() 在反序列化时被调用。
  由于 pickle 是 Python 特有的并且附着在源码上,所有如果需要长期存储数据的时候不应该选用它。例如,如果源码变动了,你所有的存储数据可能会被破坏并且变得不可读取。坦白来讲,对于在数据库和存档文件中存储数据时,你最好使用更加标准的数据编码格式如 XML,CSV 或 JSON。这些编码格式更标准,可以被不同的语言支持,并且也能很好的适应源码变更。

数据处理

读写 CSV 数据

  对于大多数的 CSV 格式的数据读写问题,都可以使用 csv 库,同时 Pandas 包含了一个非常方便的函数叫 pandas.read_csv(),它可以加载 CSV 数据到一个 DataFrame 对象中去,然后利用这个对象你就可以生成各种形式的统计、过滤数据以及执行其他高级操作了。
  使用 csv.reader() 方法通过下标访问每行数据,可以使用 csv.DictReader() 函数来读取以通过列名去访问每一行的数据。默认情况下 csv 库会使用 , 来作为每列的分隔符,也可以使用可选参数 delimiter 来指定。
  示例:
    1. 读取 CSV 数据

import csv

# 通过下标访问每行的列
with open('temp.csv') as f: 
    f_csv = csv.reader(f) 
    headers = next(f_csv) 
    for row in f_csv:
        print(row[0])

# 通过列名直接访问每行的列
with open('temp.csv') as f: 
    f_csv = csv.DictReader(f) 
    for row in f_csv:
        print(row['column_name'])

  而写入 CSV 也有两种方式,一种是使用 writer() 方法,先写入列名,再写入多行数据。另一种则是使用 DictWriter() 方法,先写入列名,再写入字典,字典的键会被作为列名,字典的值会被作为数据。
    2. 写入 CSV 数据

headers = ['Symbol','Price','Date','Time','Change','Volume'] 
rows = [
           ('AA', 39.48, '6/11/2007', '9:36am', -0.18, 181800),
           ('AIG', 71.38, '6/11/2007', '9:36am', -0.15, 195500), 
           ('AXP', 62.58, '6/11/2007', '9:36am', -0.46, 935000),
       ]

with open('temp.csv','w') as f: 
    f_csv = csv.writer(f)
    f_csv.writerow(headers) 
    f_csv.writerows(rows)

headers = ['Symbol', 'Price', 'Date', 'Time', 'Change', 'Volume'] 
rows = [
           {'Symbol':'AA', 'Price':39.48, 'Date':'6/11/2007', 'Time':'9:36am', 'Change':-0.18, 'Volume':181800}, 
           {'Symbol':'AIG', 'Price': 71.38, 'Date':'6/11/2007', 'Time':'9:36am', 'Change':-0.15, 'Volume': 195500}, 
           {'Symbol':'AXP', 'Price': 62.58, 'Date':'6/11/2007', 'Time':'9:36am', 'Change':-0.46, 'Volume': 935000}, 
       ]
with open('temp.csv','w') as f:
    f_csv = csv.DictWriter(f, headers) 
    f_csv.writeheader()
    f_csv.writerows(rows)

Json 与对象的转化

  对象实例通常并不是 JSON 可序列化的,如果想序列化对象实例,可以提供一个函数,它的输入是一个实例,返回一个可序列化的字典。反序列化也一样。随后通过 json 库的 dumps()/loads() 方法的可选参数 default/object_hook 来指定使用它们。
  示例:
    1. 对象的序列化和反序列化工具方法

# 序列化工具
def serialize_instance(obj):
    d = { '__classname__' : type(obj).__name__ } 
    d.update(vars(obj))
    return d

# 反序列化工具
# 类名与已知类的映射
classes = {
    'TestClass' : TestClass
}

def unserialize_object(d):
    clsname = d.pop('__classname__', None) 
    if clsname:
        cls = classes[clsname]
        obj = cls.__new__(cls)
        for key, value in d.items():
            setattr(obj, key, value) 
        return obj
    else: 
        return d

# 使用方式:
p = TestClass(2,3)
s = json.dumps(p, default=serialize_instance)
a = json.loads(s, object_hook=unserialize_object)

XML 文档的创建、修改

  xml.etree.ElementTree 库通常用来处理 XML 文档。当创建 XML 的时候,你被限制只能构造字符串类型的值。并且推荐使用 Element 实例创建而不是字符串手动拼接,因为使用字符串拼接构造一个很大的文档并不容易。而 Element 实例可以不用考虑解析 XML 文本的情况下通过多种方式被处理。也就是说,你可以在一个高级数据结构上完成你所有的操作,并在最后以字符串的形式将其输出。
  修改一个 XML 文档也是很容易的,但必须牢记的是所有的修改都针对父节点元素,将它作为一个列表来处理。例如,如果删除某个元素,通过调用父节点的 remove() 方法从它的直接父节点中删除。如果插入或增加新的元素,同样要使用父节点元素的 insert()append() 方法。也能对元素使用索引和切片操作,比如 element[i]element[i:j]
  最后如果解析 XML 文档时涉及到了命名空间,最好使用 lxml 库来代替 ElementTreelxml 对利用 DTD 验证文档、更好的 XPath 支持和一些其他高级 XML 特性等都提供了更好的支持。
  示例:
    1. 创建 XML 文档

from xml.etree.ElementTree import Element, tostring


def dict_to_xml(root_tag, d):
    """
        将字典转换为 XML 文档。传入根元素标签名及一个字典
    """
    root_elem = Element(root_tag)
    for key, val in d.items(): 
        child = Element(key) 
        child.text = str(val) 
        root_elem.append(child)
    return root_elem


if __name__ == '__main__':
    s = { 'name': 'GOOG', 'shares': 100, 'price':490.1 }
    e = dict_to_xml('stock', s)
    # 为元素设置属性值
    e.set("id", "1")
    tostring(e)

函数参数传递

传入带默认值的参数

  python 可以通过类似 def a_test_function(args1="a") 的语法为函数入参指定默认值。需要注意的是:

  1. 如果你并不想提供一个默认值,而是想仅仅测试下某个默认参数是不是有传递进来,可以像下面这样写(传递一个 None 值和不传值两种情况是有差别的):
>>> _no_value = object()
>>> def spam(a, b=_no_value): 
>>>     if b is _no_value:
>>>         print('b 参数没有传递')
>>> 
>>> spam(1)
b 参数没有传递
>>> spam(1, 2) # b = 2
>>> spam(1, None) # b = None 
>>>
  1. 默认参数的值仅仅在函数定义的时候赋值一次。即使试图将一个变量置为某个入参的默认值,它的实际取值也只是该变量第一次被赋予的值。例如
>>> x = 42
>>> def spam(a, b=x):
...     print(a, b)
...
>>> spam(1)
1 42
>>> x = 23
>>> # 修改变量,不影响参数默认值
>>> spam(1)
1 42
>>>
  1. 默认参数的值应该是不可变的对象,比如 NoneTrueFalse、数字或字符串。特别的,千万不要这样写:def spam(a, b=[])。如果这么写,当默认值在其他地方被修改后,会影响到下次调用这个函数时的默认值。
>>> def spam(a, b=[]):
...     print(b) 
...     return b
...
>>> x = spam(1) 
>>> x
[]
>>> x.append(99) 
>>> x
[99]
>>> # 未传递 b 参数,取 b 的默认值,结果发现不是 []
>>> spam(1)
[99]
>>>

预先为函数参数赋值

  functools.partial() 函数允许你给一个或多个参数设置固定的值,减少接下来被调用时的参数个数。它常用于解决同时使用一些不兼容的代码时遇到的问题,或被用来微调其他库函数所使用函数的参数。
  示例:
    1. 利用 partial 减少函数调用时的参数

>>> def spam(a, b, c, d): 
>>>     print(a, b, c, d)
...
>>> from functools import partial 
>>> s1 = partial(spam, 1) # a = 1 
>>> s1(2, 3, 4)
1234
>>> s1(4, 5, 6)
1456
>>> s2 = partial(spam, d=42) # d = 42 
>>> s2(1, 2, 3)
1 2 3 42
>>> s2(4, 5, 5)
4 5 5 42
>>> s3 = partial(spam, 1, 2, d=42) # a = 1, b = 2, d = 42 
>>> s3(3)
1 2 3 42 
>>> s3(4)
1 2 4 42 
>>> s3(5)
1 2 5 42
>>>

    2. 使用 multiprocessing 来异步计算一个结果值,然后这个值被传递给一个接受一个 result 值和一个可选 logging 参数的回调函数:

def output_result(result, log=None): 
    if log is not None:
        log.debug('Got: %r', result) 

# 一个简单方法
def add(x, y): 
    return x + y

if __name__ == '__main__': 
    import logging
    from multiprocessing import Pool
    from functools import partial

    logging.basicConfig(level=logging.DEBUG) 
    log = logging.getLogger('test')

    p = Pool()
    p.apply_async(add, (3, 4), callback=partial(output_result, log=log)) 
    p.close()
    p.join()

将单方法的类转换为函数

  大多数情况下,可以使用闭包来将单个方法的类转换成函数。何谓单方法的类?除 __init__() 方法外只定义了一个方法的类。如下:

class TestCache:
    def __init__(self, cache): 
        self.cache = cache

    def remove(self, a):
        return self.cache.pop(a)

# 用法:
test_cache = TestCache([1,2,3,4])
test_cache.remove(1)

将其简化为一个函数:

def testcache(cache): 
    def remove(a):
        return self.cache.pop(a) 
    return remove

# 用法
test_cache = testcache([1,2,3,4])
test_cache.remove(1)

  大部分情况下,一个单方法类的出现原因是需要存储某些额外的状态来给方法使用。比如,定义 TestCache 类的唯一目的就是先在某个地方存储 cache,以便将来可以在 remove() 方法中使用。此时使用一个内部函数或者闭包的方案通常会更优雅一些。简单来讲,一个闭包就是一个函数,只不过在函数内部带上了一个额外的变量环境。闭包关键特点就是它会记住自己被定义时的环境。因此,在上面的解决方案中,remove() 函数记住了 cache 参数的值,并在接下来的调用中使用它。

类与对象

让类支持上下文管理协议

  为了让一个对象兼容 with 语句,你需要实现 __enter__()__exit__() 方法。编写上下文管理器的主要原理是你的代码会放到 with 语句块中执行。当出现 with 语句的时候,对象的 __enter__() 方法被触发,它返回的值(如果有的话)会被赋值给 as 声明的变量。然后,with 语句块里面的代码开始执行。最后,__exit__() 方法被触发进行清理工作。不管 with 代码块中发生什么,上面的控制流都会执行完,就算代码块中发生了异常也是一样的。事实上,__exit__() 方法的第三个参数包含了异常类型、异常值和追溯信息。__exit__() 方法能自己决定怎样利用这个异常信息,或者忽略它并返回一个 None 值。如果 __exit__() 返回 True ,那么异常会被清空,就好像什么都没发生一样,with 语句后面的程序继续在正常执行。
  在需要管理一些资源比如文件、网络连接和锁的编程环境中,使用上下文管理器是很普遍的。这些资源的一个主要特征是它们必须被手动的关闭或释放来确保程序的正确运行。例如,如果你请求了一个锁,那么你必须确保之后释放了它,否则就可能产生死锁。通过实现 __enter__()__exit__() 方法并使用 with 语句可以很容易的避免这些问题,因为 __exit__() 方法可以让你无需担心这些了。
  示例:
    1. 编写一个支持上下文管理协议的网络连接类

from socket import socket, AF_INET, SOCK_STREAM 

class LazyConnection:
    def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
        self.address = address
        self.family = family
        self.type = type
        self.sock = None

    def __enter__(self):
        if self.sock is not None:
            raise RuntimeError('Already connected') 
        self.sock = socket(self.family, self.type) 
        self.sock.connect(self.address)
        return self.sock

    def __exit__(self, exc_ty, exc_val, tb): 
        self.sock.close()
        self.sock = None

  还有一个细节问题就是是否允许你的类支持多个 with 语句来嵌套使用。很显然,上面的定义中一次只能允许一个连接,如果正在使用一个的时候又重复使用 with 语句,就会产生一个异常了。不过你可以像下面这样修改下上面的实现来解决这个问题:
    2. 使类支持嵌套使用 with 语句调用

from socket import socket, AF_INET, SOCK_STREAM 

class LazyConnection:
    def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
        self.address = address
        self.family = family
        self.type = type
        self.connections = []

    def __enter__(self):
        sock = socket(self.family, self.type) 
        sock.connect(self.address)
        self.connections.append(sock) 
        return sock

    def __exit__(self, exc_ty, exc_val, tb): 
        self.connections.pop().close()

  在第二个版本中,LazyConnection 类可以被看做是某个连接工厂。在内部,一个列表被用来构造一个栈。每次 __enter__() 方法执行的时候,它复制创建一个新的连接并将其加入到栈里面。__exit__() 方法简单的从栈中弹出最后一个连接并关闭它。实际上 with 的调用链就类似于栈。
  当然,如果只是想实现一个简单的自包含上下文管理器,可以借助 contexlib 模块的 @contextmanager 装饰器。在使用了该装饰器的方法中,yield 之前的代码会在上下文管理器中作为 __enter__() 方法执行,所有在 yield 之后的代码会作为 __exit__() 方法执行。如果出现了异常,异常会在 yield 语句那里抛出。
    3. 使用 @contextmanager 装饰器实现简单的上下文管理器

import time
from contextlib import contextmanager 

@contextmanager
def timethis(label): 
    start = time.time() 
    try:
        yield 
    finally:
        end = time.time()
        print('{}: {}'.format(label, end - start)) 


# 用例
with timethis('counting'):
    n = 10000000 
    while n > 0:
        n -= 1

创建大量简单对象时节省内存

  对于主要是用来当成简单的数据结构的类而言,你可以通过给类添加 __slots__ 属性来极大的减少实例所占的内存。当定义 __slots__ 后,Python 就会为实例使用一种更加紧凑的内部表示。实例通过一个很小的固定大小的数组来构建,而不是为每个实例定义一个字典,这跟元组或列表很类似。在 __slots__ 中列出的属性名在内部被映射到这个数组的指定下标上。使用 slots 的限制是不能再给实例添加新的属性了,只能使用在 __slots__ 中定义的那些属性名。
  尽管 slots 看上去是一个很有用的特性,但也要谨慎使用。 Python 的很多特性都依赖于普通的基于字典的实现,定义了 slots 后的类不再支持一些普通类特性了,比如多继承。大多数情况下,应该只在那些经常被使用到的用作数据结构的类上定义 slots
  示例:
    1. 为类添加 __slots__ 属性

class Date:
    __slots__ = ['year', 'month', 'day'] 
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

在类中封装属性或方法

  Python 不依赖语言特性去封装数据,而是通过遵循一定的属性和方法命名规约来达到这个效果。首先:任何以单下划线 _ 开头的名字都应该是内部实现,适用于属性或方法。其次:使用双下划线开始会导致访问名称变成 _类名原名称 的形式。比如类 A 中的 __private_method() 方法会被重命名为 _A___private_method()。这种重命名使得以 __ 开头的属性或方法无法通过继承被覆盖,因为子类 B 中的 __private_method() 也会被重命名为 _B___private_method()
  编码时应该让非公共名称以单下划线开头。但是,如果代码会涉及到子类,并且有些内部属性应该在子类中被隐藏起来,那么才考虑使用双下划线开头。

创建可管理的属性

  给某个实例的属性增加除访问与修改之外的其他处理逻辑,比如类型检查或合法性验证时,一种简单方法是将它定义为一个 property。property 还是一种定义动态计算属性值的方法。这种类型的属性的值并不会被实际的存储,而是在需要的时候计算出来。
  示例代码中有三个相关联的方法,这三个方法的名字都必须一样。第一个方法是一个 getter 函数,它使得 first_name 成为一个属性。其他两个方法给 first_name 属性添加了 setterdeleter 函数。需要强调的是只有在 first_name 属性被创建后, 后面的两个装饰器 @first_name.setter@first_name.deleter 才能被定义。property 的一个关键特征是它看上去跟普通的属性没什么两样,但是访问它的时候会自动触发 gettersetterdeleter 方法,以此触发验证操作。

class Person:
    def __init__(self, first_name): 
        self.first_name = first_name

    # Getter function
    @property
    def first_name(self): 
        return self._first_name

    # Setter function
    @first_name.setter
    def first_name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string') 
        self._first_name = value

    # Deleter function (optional)
    @first_name.deleter 
    def first_name(self):
        raise AttributeError("Can't delete attribute")

调用父类的方法

  为了调用父类(超类)的一个方法,可以使用 super() 函数。但错误地使用 super() 函数也会导致一些问题。
  有些代码可能会通过父类类名调用父类方法,尽管对于大部分代码而言这么做没什么问题,但是在涉及到多继承的代码中就有可能导致很奇怪的问题发生。比如:

class Base:
    def __init__(self): 
        print('Base.__init__')


class A(Base):
    def __init__(self): 
        Base.__init__(self) 
        print('A.__init__')


class B(Base):
    def __init__(self): 
        Base.__init__(self) 
        print('B.__init__')


class C(A,B):
    def __init__(self):
        A.__init__(self)
        B.__init__(self) 
    print('C.__init__')


  实例化 C 类时,会发现 Base 类的 __init__() 被调用了两次,这可能会导致意外的覆盖。

>>> c = C() 
Base.__init__
A.__init__ 
Base.__init__
B.__init__
C.__init__
>>> 

  换成使用 super(),就正常了:

class Base:
    def __init__(self): 
        print('Base.__init__')


class A(Base):
    def __init__(self): 
        super().__init__() 
        print('A.__init__')


class B(Base):
    def __init__(self): 
        super().__init__() 
        print('B.__init__')


class C(A,B):
    def __init__(self):
        super().__init__() # Only one call to super() here 
        print('C.__init__')


  运行发现 Base 类的 __init__ ()只调用一次了:

>>> c = C() 
Base.__init__
B.__init__ 
A.__init__
C.__init__
>>>

  Python 是如何实现继承的?对于每一个类,Python 会计算出一个方法解析顺序 (MRO) 列表。这个 MRO 列表就是一个简单的所有基类的线性顺序表。

>>> C.__mro__
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Base'>, <class 'object'>)
>>>

  为了实现继承,Python 会在 MRO 列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。而子类的 MRO 列表实际上就是合并所有父类的 MRO 列表并遵循如下三条准则:

  1. 子类会先于父类被检查(从左到右)
  2. 多个父类会根据它们在列表中的顺序被检查(从左到右)
  3. 如果对下一个类存在两个合法的选择,选择第一个父类(从左到右)

  当你使用 super() 函数时,Python 会在 MRO 列表上继续搜索下一个类。只要每个重定义的方法统一使用 super() 并只调用它一次,那么控制流最终会遍历完整个 MRO 列表,每个方法也只会被调用一次。这也是为什么在第二个例子中你不会调用两次 Base.__init__() 的原因。而在第一个例子中,因为直接使用类名调用方法,所以无视了 MRO 规则。
  但 super() 去调用 MRO 列表中的下一个类时,并不会严格检查下一个类是否为子类的直接父类,而是直接调用,下面的例子显示,甚至可以在一个没有直接父类的类中使用 super()

class A:
    def spam(self): 
        print('A.spam') 
        super().spam()


class B:
    def spam(self): 
        print('B.spam') 


class C(A, B):
    pass


实例化 C,并调用 c.spam(),按照 MRO 规则,因为 A 类在 B 类之前,且 A 类具备 spam() 方法,则直接使用 A 类的 spam() 方法,此时整个调用过程已经和 B 类没有关系了。但 A 类的 spam() 方法中,又会调用 super().spam(),而 A 类只有一个父类 objectobject 类里没有 spam() 方法,理应报错。结果反而不报错,还触发了 B 类的 spam() 方法。查看下面的运行示例:

>>> # 直接调用 A 类的 spam(),发现该方法有问题,因为多出了 super().spam(),而 A 没有继承除 object 外的父类
>>> a = A() 
>>> a.spam() 
A.spam
Traceback (most recent call last):
    File "<stdin>", line 1, in <module> 
    File "<stdin>", line 4, in spam
AttributeError: 'super' object has no attribute 'spam' 
>>> # 但通过调用 C 类的 spam(),发现能成功调用从 A 类继承的 spam()
>>> c = C() 
>>> c.spam()
A.spam
B.spam
>>>

  由于 super() 可能会调用不是你想要的方法,你应该遵循一些通用原则。首先,确保在继承体系中所有相同名字的方法拥有可兼容的参数签名(比如相同的参数个数和参数名称)。这样可以确保 super() 调用一个非直接父类方法时不会出错。其次, 最好确保最顶层的类提供了这个方法的实现,这样的话在 MRO 上面的查找链肯定可以找到某个确定的方法。

描述器

  当程序中有很多重复代码的时候,可以尝试使用描述器来提供这些功能。描述器可实现大部分 Python 类特性中的底层魔法,包括 @classmethod@staticmethod@property,甚至是 __slots__ 特性。一个描述器就是一个实现了三个核心的属性访问操作 (get, set, delete) 的类,分别为 __get__()__set__() __delete__() 这三个特殊的方法。这些方法接受一个实例作为输入,之后相应的操作实例底层的字典。同时为了使用一个描述器,需将这个描述器的实例作为类属性放到一个类的定义中,也就是说,它只能在类级别被定义,而不能为每个实例单独定义。因此,无法用于 __init__() 方法。
  示例:
    1. 描述器的定义和使用

class Integer:
    """
        定义一个整型描述器。检查整数传值时的类型
    """
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, cls):
        if instance is None: 
            return self
        else:
            return instance.__dict__[self.name] 

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise TypeError('Expected an int') 
        instance.__dict__[self.name] = value

    def __delete__(self, instance):
        del instance.__dict__[self.name]


class Point:
    x = Integer('x')
    y = Integer('y')

    def __init__(self, x, y):
        self.x = x 
        self.y = y


  此时所有对描述器属性 (比如 x 或 y) 的访问会被 __get__()__set__()__delete__() 方法捕获到。比如:

>>> p = Point(2, 3)
>>> p.x # Calls Point.x.__get__(p,Point) 
2
>>> p.y = 5
>>> p.x = 2.3
Traceback (most recent call last):
    File "<stdin>", line 1, in <module> 
    File "descrip.py", line 12, in __set__
        raise TypeError('Expected an int')
TypeError: Expected an int 
>>>

混入类

  当你有很多有用的方法,想使用它们来扩展其他类的功能,但是这些类并没有任何继承的关系,因此你不能简单的将这些方法放入一个基类,然后被其他类继承时,可以使用混入类。混入类不能直接被实例化使用。其次,混入类没有自己的状态信息,也就是说它们并没有定义 __init__() 方法,并且没有实例属性,所以需要在混入类中明确定义 __slots__ = ()
  示例:
    1. 扩展字典对象,给它添加日志、唯一性设置、类型检查等等功能。

class LoggedMappingMixin: 
    """
        日志扩展混入类,为 get/set/del 方法添加日志
    """
    __slots__ = ()
    def __getitem__(self, key):
        print('Getting ' + str(key)) 
        return super().__getitem__(key)

    def __setitem__(self, key, value):
        print('Setting {} = {!r}'.format(key, value)) 
        return super().__setitem__(key, value)

    def __delitem__(self, key): 
        print('Deleting ' + str(key)) 
        return super().__delitem__(key)


class SetOnceMappingMixin: 
    '''
        唯一性检查混入类,相同的键只允许插入一次
    '''
    __slots__ = ()
    def __setitem__(self, key, value): 
        if key in self:
            raise KeyError(str(key) + ' already set') 
        return super().__setitem__(key, value)


class StringKeysMappingMixin: 
    '''
        键的类型检查混入类,只允许 str 类型的键插入
    '''
    __slots__ = ()
    def __setitem__(self, key, value): 
        if not isinstance(key, str):
            raise TypeError('keys must be strings') 
        return super().__setitem__(key, value)


class StrongDict(LoggedMappingMixin, SetOnceMappingMixin, StringKeysMappingMixin, dict):
    pass


if __name__ == '__main__':
    strong_dict = StrongDict()
    strong_dict["a"] = 96
    strong_dict["a"]
    del(strong_dict["a"])

    strong_dict["b"] = 96
    try:
        strong_dict["b"] = 96
    except Exception as e:
        print(e)
    try:
        strong_dict[1] = 1
    except Exception as e:
        print(e)

  注意混入类并没有继承其他除 object 之外的类,但在方法中依然使用了 super() 方法,前文也提过 super() 的特性,它只是简单地迭代调用 MRO 列表中的不同父类的指定方法而已,不会做额外的检查。因此这里使用它遍历调用了所有父类的同名方法,看看执行结果,可以发现日志、唯一性检查、键类型检查都正常:

>>> ./test.py
Setting a = 96
Getting a
Deleting a
Setting b = 96
Setting b = 96
'b already set'
Setting 1 = 1
keys must be strings

    2. 使用类装饰器改造上面的例子

def LoggedMapping(cls):
    cls_getitem = cls.__getitem__
    cls_setitem = cls.__setitem__
    cls_delitem = cls.__delitem__

    def __getitem__(self, key): 
        print('Getting ' + str(key)) 
        return cls_getitem(self, key)

    def __setitem__(self, key, value):
        print('Setting {} = {!r}'.format(key, value)) 
        return cls_setitem(self, key, value)

    def __delitem__(self, key): 
        print('Deleting ' + str(key)) 
        return cls_delitem(self, key)

    cls.__getitem__ = __getitem__
    cls.__setitem__ = __setitem__
    cls.__delitem__ = __delitem__ 
    return cls


@LoggedMapping
class LoggedDict(dict):
    pass


状态机和状态对象(状态模式)

  在很多程序中,有些对象会根据状态的不同来执行不同的操作,但是我们又不想在代码中出现太多的条件判断语句,就可以使用状态模式。将每种状态定义为一个类,每个状态类都只实现有限的方法,状态之间的切换实际上是类实例之间的切换,不同状态下才能执行的特有逻辑则变为不同类实例之间的特有方法。
  比如有这样一个 Connection 类,它包含两个状态:OpenClosed,只有在 Open 状态下才能执行 read()write()close() 方法,只有在 Close 状态下才能执行 open() 方法。下面是它的普通实现,包含大量的状态判断,这样写代码太复杂,其次是执行效率变低,因为一些常见的操作每次执行前都需要执行检查:

class Connection:
    def __init__(self):
        self.state = 'CLOSED' 

    def read(self):
        if self.state == 'CLOSED':
            raise RuntimeError('Not open') 
        print('reading')

    def write(self, data):
        if self.state == 'CLOSED':
            raise RuntimeError('Not open') 
        print('writing')

    def open(self):
        if self.state == 'OPEN':
            raise RuntimeError('Already open') 
        self.state = 'OPEN'

    def close(self):
        if self.state == 'CLOSED':
            raise RuntimeError('Already closed') 
        self.state = 'CLOSED'

  使用状态模式改造,每个状态对象都只有静态方法,并没有存储任何的实例属性数据。实际上,所有状态信息都只存储在 Connection 实例中。:

class ConnectionState:
    """
        状态类基类
    """
    @staticmethod 
    def read(conn):
        raise NotImplementedError() 

    @staticmethod
    def write(conn, data):
        raise NotImplementedError() 

    @staticmethod
    def open(conn):
        raise NotImplementedError() 

    @staticmethod
    def close(conn):
        raise NotImplementedError()


class ClosedConnectionState(ConnectionState):
    """
        关闭状态类,仅允许执行 open 方法,同时切换到 Open 状态
    """
    @staticmethod 
    def read(conn):
        raise RuntimeError('Not open') 

    @staticmethod
    def write(conn, data):
        raise RuntimeError('Not open') 

    @staticmethod
    def open(conn):
        conn.new_state(OpenConnectionState) 

    @staticmethod
    def close(conn):
        raise RuntimeError('Already closed') 


class OpenConnectionState(ConnectionState):
    """
        开启状态类,允许执行 read、write、close 方法,close 方法调用同时切换到 Close 状态
    """
    @staticmethod
    def read(conn): 
        print('reading')

    @staticmethod
    def write(conn, data): 
        print('writing')

    @staticmethod 
    def open(conn):
        raise RuntimeError('Already open')

    @staticmethod 
    def close(conn):
        conn.new_state(ClosedConnectionState)


class Connection:
    def __init__(self):
        self.new_state(ClosedConnectionState) 

    def new_state(self, newstate):
        self._state = newstate

    def read(self):
        return self._state.read(self) 

    def write(self, data):
        return self._state.write(self, data) 

    def open(self):
        return self._state.open(self) 

    def close(self):
        return self._state.close(self)

使用字符串调用方法

  可以使用 getattr() 方法通过字符串形式的方法名称,调用某个对象的对应方法。传入实例和方法名,它会查找该实例中的指定方法,并将自己替换为该方法,同时后面紧跟方法所需参数即可。还可以使用 operator 库的 methodcaller() 方法,它的特点是,将方法名、参数放到自身的入参中,将实例后置。这使得在多个实例迭代调用时很好用。
  使用 getattr() 调用一个方法实际上是两部独立操作,第一步是查找属性,第二步是函数调用。operator.methodcaller() 则是直接创建一个可调用对象,同时提供所有必要参数,然后调用的时候只需要将实例对象传递给它即可。
  示例:
    1. 使用 getattr() 方法调用实例方法计算点 (2,3) 与点 (0,0) 之间的距离

import math

class Point:
    def __init__(self, x, y):
        self.x = x 
        self.y = y

    def __repr__(self):
        return 'Point({!r:},{!r:})'.format(self.x, self.y) 

    def distance(self, x, y):
        return math.hypot(self.x - x, self.y - y)


if __name__ == '__main__':
    p = Point(2, 3)
    d = getattr(p, 'distance')(0, 0)

    1. 使用 operator.methodcaller 方法调用实例方法计算点 (2,3) 与点 (0,0)、一组点与点 (0,0) 之间的距离

import math
import operator

class Point:
    def __init__(self, x, y):
        self.x = x 
        self.y = y

    def __repr__(self):
        return 'Point({!r:},{!r:})'.format(self.x, self.y) 

    def distance(self, x, y):
        return math.hypot(self.x - x, self.y - y)


if __name__ == '__main__':
    # 使用示例
    p = Point(2, 3)
    operator.methodcaller('distance', 0, 0)(p)
    # 多次调用示例
    points = [
        Point(1, 2),
        Point(3, 0),
        Point(10, -3),
        Point(-5, -7),
        Point(-1, 8),
        Point(3, 2) 
    ]
    points.sort(key=operator.methodcaller('distance', 0, 0))
    print(points)
    print([operator.methodcaller('distance', 0, 0)(i) for i in points])

  使用字符串调用方法的场景通常是要处理由大量不同类型的对象组成的复杂数据结构,同时每一个对象都需要进行不同的处理。比如进行基本数学计算时,需要不断在加、减、乘、除中切换调用:

class Number:
    def __init__(self, value):
        self.value = value


class NumberOperator:
    def __init__(self, left, right):
        self.left = left 
        self.right = right


class Add(NumberOperator):
    def add(self):
        return self.left + self.right


class Sub(NumberOperator):
    def sub(self):
        return self.left) - self.right) 


class Mul(NumberOperator):
    def mul(self):
        return self.left) * self.right)


class Div(NumberOperator):
    def div(self):
        return self.left) / self.right) 


if __name__ == '__main__':
    t1 = Sub(Number(3), Number(4)).sub()
    t2 = Mul(Number(2), t1).mul()
    t3 = Div(t2, Number(5)).div()
    t4 = Add(Number(1), t3).add()

  这样做的问题是对于每个表达式,每次都要重新定义一遍,对于每个操作,也都需要显式调用方法才行。下面基于访问模式实现:

class Number:
    def __init__(self, value):
        self.value = value


class NumberOperator:
    def __init__(self, left, right):
        self.left = left 
        self.right = right


class Add(NumberOperator):
    pass


class Sub(NumberOperator):
    pass


class Mul(NumberOperator):
    pass


class Div(NumberOperator):
    pass


class NumberOperatorVisitor:
    def visit(self, number_operator):
        methname = 'visit_' + type(number_operator).__name__ 
        meth = getattr(self, methname, None)
        if meth is None:
            meth = self.generic_visit 
        return meth(number_operator)

    def generic_visit(self, number_operator):
        raise RuntimeError('No {} method'.format('visit_' + type(number_operator).__name_))

class Evaluator(NumberOperatorVisitor): 
    def visit_Number(self, number):
        return number.value

    def visit_Add(self, number_operator):
        return self.visit(number_operator.left) + self.visit(number_operator.right) 

    def visit_Sub(self, number_operator):
        return self.visit(number_operator.left) - self.visit(number_operator.right) 

    def visit_Mul(self, number_operator):
        return self.visit(number_operator.left) * self.visit(number_operator.right) 

    def visit_Div(self, number_operator):
        return self.visit(number_operator.left) / self.visit(number_operator.right) 


if __name__ == '__main__':
    t1 = Sub(Number(3), Number(4))
    t2 = Mul(Number(2), t1) 
    t3 = Div(t2, Number(5)) 
    t4 = Add(Number(1), t3)

    e = Evaluator() 
    e.visit(t4)

循环引用的数据结构内存管理

  最简单的循环引用数据结构是树,树的父节点有指向子节点的指针,而子节点又有反过来指向父节点的指针。而 Python 的垃圾回收机制是基于简单的引用计数。当一个对象的引用数变成 0 的时候才会立即删除掉。而对于循环引用这个条件永远不会成立。因此,在树中,父节点和子节点互相拥有对方的引用,导致每个对象的引用计数都不可能变成 0。
  当然 Python 有另外的垃圾回收器来专门针对循环引用,但是你永远不知道它什么时候会触发,甚至即使手动调用它,也不能保证它在什么时候才会真正执行。想要调用它只需:

>>> import gc
>>> gc.collect()

  处理循环引用场景时,可以考虑使用 weakref 库中的弱引用。弱引用消除了引用循环的这个问题,本质来讲,弱引用就是一个对象指针,它不会增加目标对象的引用计数。为了访问弱引用所引用的对象,你可以像函数一样去调用它即可。如果那个对象还存在就会返回它,否则就返回一个 None。由于弱引用下原始对象的引用计数没有增加,那么当该对象所有的强引用为 0 时,Python 就可以去删除它了。弱引用使用:

>>> import weakref 
>>> a = Node()
>>> a_ref = weakref.ref(a) 
>>> a_ref

  因此我们可以将树的父节点指向子节点时,使用强引用,而子节点指向父节点时,使用弱引用。并在程序中,保存根(父)节点的强引用,这样一旦树的根(父)节点的强引用被释放,其下所有子节点都将随之释放。例如:

import weakref 

class Node:
    def __init__(self, value):
        self.value = value
        self._parent = None
        self.children = []

    def __repr__(self):
        return 'Node({!r:})'.format(self.value)

    # property that manages the parent as a weak-reference
    @property
    def parent(self):
        return None if self._parent is None else self._parent() 

    @parent.setter
    def parent(self, node):
        self._parent = weakref.ref(node) 

    def add_child(self, child):
        self.children.append(child) 
        child.parent = self

这样一旦我们释放根节点,整个树都会被回收,而不担心内存泄漏等问题:

>>> root = Node('parent') 
>>> c1 = Node('child') 
>>> root.add_child(c1) 
>>> print(c1.parent) 
Node('parent')
>>> del root
>>> print(c1.parent) 
None
>>>

让类支持比较操作

  想让某个类的实例支持标准的比较运算(比如 >=!=<=< 等),但烦人的是,你就需要实现一大堆用于比较的特殊方法:__eq__()__lt__()__le__()__gt__()__ge__()
  装饰器 functools.total_ordering 就是用来简化这个过程的。使用它来装饰一个类,你只需定义一个 __eq__() 方法,外加其他方法 (__lt__, __le__, __gt__, or __ge__) 中的一个即可,然后装饰器会自动为你填充其它比较方法。其实它就是定义了一个从每个比较支持方法到所有需要定义的其他方法的一个映射而已。比如你定义了 __lt__() 方法,那么它就被用来和 __eq__() 方法配合构建所有其他的需要定义的那些特殊方法。类似于:

from functools import total_ordering

@total_ordering
class House:
    def __eq__(self, other):
        pass
    def __lt__(self, other):
        pass

    __le__ = lambda self, other: self < other or self == other
    __gt__ = lambda self, other: not (self < other or self == other) 
    __ge__ = lambda self, other: not (self < other)
    __ne__ = lambda self, other: not self == other

元编程

装饰器

  编程原则中有一个“开闭原则”,是说编写代码时要尽量对修改关闭,对扩展开放。那么当你想在函数上扩展额外的操作时,可以使用装饰器。她和 AOP 很相似。一个装饰器就是一个函数,它接受一个函数作为参数并返回一个新的函数,需要强调的是装饰器并不会修改原始函数的参数签名以及返回值,并且任何时候你定义装饰器的时候,都应该使用 functools 库中的 @wraps 装饰器来注解底层包装函数,避免被装饰函数丢失它的元信息。
  @wraps 还有一个重要特征是它能让你通过被包装后的函数属性 __wrapped__ 直接访问原函数。但如果有多个包装器,那么访问 _wrapped__ 属性的行为是不可预知的,它可能无法正常返回原始未包装函数。
  示例:
    1. 定义一个装饰器,为方法扩展输出当前时间日志功能

import time
from functools import wraps 

def timethis(func):
    '''
        定义一个报时装饰器
    '''
    @wraps(func)
    def wrapper(*args, **kwargs): 
        start = time.time()
        result = func(*args, **kwargs) 
        end = time.time()
        print(func.__name__, end-start) 
        return result
    return wrapper

# 第一种使用方式
@timethis
def countdown(n):
    while n > 0: 
    n -= 1

# 第二种使用方式
def countdown1(n):
    while n > 0: 
    n -= 1
countdown1 = timethis(countdown1)

  在上面的 wrapper() 函数中,装饰器内部定义了一个使用 *args**kwargs 来接受任意参数的函数。在这个函数里面调用了原始函数并将其结果返回。
  给类或静态方法提供装饰器就是这么简单,不过要确保装饰器在 @classmethod@staticmethod 之前(装饰器距离被装饰对象越近,则在装饰器链中越靠前)。因为 @classmethod@staticmethod 实际上并不会创建可直接调用的对象,而是创建特殊的描述器对象,所以当其他装饰器中将它们当做函数来使用时就会出错。

# 装饰器顺序。注意将 @classmethod 或 @staticmethod 总放在最后
class Spam:
    @staticmethod
    @timethis 
    def static_method(n): 
        print(n)
        while n > 0: 
            n -= 1

  类装饰器通常可以作为其他高级技术比如混入或元类的一种非常简洁的替代方案。比如想要通过反省或者重写类定义的某部分来修改它的行为,但是你又不希望使用继承或元类的方式时,就可以使用类装饰器。
    2. 定义一个装饰器,扩展类,重写方法,为其添加日志

def log_getattribute(cls):
    # 获取原始方法
    orig_getattribute = cls.__getattribute__ 
    # 定义新的实现
    def new_getattribute(self, name): 
        print('getting:', name)
        return orig_getattribute(self, name) 
    # 将新实现赋予类的原方法
    cls.__getattribute__ = new_getattribute 
return cls

# 使用用例
@log_getattribute 
class A:
    def __init__(self,x):
        self.x = x 

    def spam(self):
    pass

支持入参的装饰器

  而如果想要装饰器支持传入参数,则需要嵌套使用。核心思想很简单:最外层的函数 logged() 接受参数并将它们作用在内部的装饰器函数上面。内层的函数 decorate() 接受一个函数作为参数,然后在函数上面放置一个装饰器。这里的关键点是内层函数的装饰器是可以使用传递给外层 logged() 的参数的。

from functools import wraps 
import logging

def logged(level, name=None, message=None):
    """
        向一个方法中添加日志,并支持日志级别、日志对象名、日志内容。
        默认的日志名为函数 module 名,日志内容为函数名
    """  
    def decorate(func):
        logname = name if name else func.__module__
        log = logging.getLogger(logname)
        logmsg = message if message else func.__name__
        @wraps(func)
        def wrapper(*args, **kwargs): 
            log.log(level, logmsg) 
            return func(*args, **kwargs)
        return wrapper 
return decorate

# 用例
@logged(logging.DEBUG) 
def add(x, y):
    return x + y

支持可选入参的装饰器

  但是上述的装饰器是在调用时必须要传递参数的,如果想要让装饰器支持可选参数,甚至不传入任何参数,则要进行一些改动。(实际上这里的问题是编码一致性的问题,当使用装饰器时,大部分程序员习惯了要么不给它们传递任何参数,要么给它们传递确切参数。就比如:@logged(level=logging.CRITICAL, name='example')@logged(),但是,后一种写法并不符合习惯,有时程序员忘记加上后面的括号会导致错误。当装饰器不必传递参数时,我们更习惯写作:@logged)要实现参数可选,装饰器要利用 functools.partial 返回一个接受一个函数入参并装饰它的函数,它会返回一个未完全初始化的自身,即除了被包装函数外其他参数都已经被初始化。上面的装饰器可改为:

from functools import wraps, partial 
import logging

def logged(func=None, *, level=logging.DEBUG, name="logged_decorator", message=""):
    if func is None:
        return partial(logged, level=level, name=name, message=message)

    logname = name if name else func.__module__
    log = logging.getLogger(logname)
    logmsg = message if message else func.__name__

    @wraps(func)
    def wrapper(*args, **kwargs): 
        log.log(level, logmsg) 
        return func(*args, **kwargs)

    return wrapper 

# 无参数用例
@logged
def add(x, y): 
    return x + y

# 带参数用例
@logged(level=logging.CRITICAL, name='example') 
def spam():
    print('Spam!')

类中定义装饰器

  当某个装饰器与某个类具备高内聚的特性时,我们通常不必将它单独写为一个文件或抽取到公共流程中,只需要在要使用它的类中定义就可以。与方法类似,在类中定义一个装饰器有两种方式:作为一个实例方法或者类方法。并且,作为实例方法的装饰器必须通过类的实例对象调用,而作为类方法的装饰器必须通过类名调用。

from functools import wraps 

class A:
    # 将装饰器定义为实例方法
    def decorator1(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs): 
            print('Decorator 1')
            return func(*args, **kwargs) 
        return wrapper

    # 将装饰器定义为类方法
    @classmethod
    def decorator2(cls, func): 
        @wraps(func)
        def wrapper(*args, **kwargs): 
            print('Decorator 2')
            return func(*args, **kwargs) 
        return wrapper


# 实例方法装饰器使用用例
a = A()
@a.decorator1 
def spam():
    pass

# 类方法装饰器使用用例
@A.decorator2 
def grok():
    pass

  在类中定义装饰器时要注意 selfcls 的正确使用。 尽管最外层的装饰器函数比如 decorator1()decorator2() 需要提供一个 selfcls 参数,但内部被创建的 wrapper() 函数并不需要包含这个 self 参数。不过要访问装饰器中这个实例的某些部分时也可以使用。另外,在涉及到继承时,比如子类要使用父类中定义的装饰器,则父类的装饰器必须要被定义成类方法并且在子类中显式的使用父类名去调用它。这是因为在方法定义时,子类实例还没有被创建,相应的父类实例当然也没有被创建,所以无法调用父类中实例方法形式的装饰器。

将装饰器定义为类

  要将装饰器定义成一个类,需要确保它实现了 __call__()__get__() 方法。装饰器类可以在类中或类外使用。

import types
from functools import wraps 

class Profiled:
    def __init__(self, func): 
        wraps(func)(self) 
        self.ncalls = 0

    def __call__(self, *args, **kwargs):
        self.ncalls += 1
        return self.__wrapped__(*args, **kwargs) 

    def __get__(self, instance, cls):
        if instance is None: 
            return self
        else:
            return types.MethodType(self, instance)

在局部变量域中使用 exec

  exec() 的正确使用是比较难的,大多数情况下当你要考虑使用 exec() 的时候,还有另外更好的解决方案(比如装饰器、闭包、元类等等)。默认情况下,exec() 会在调用者局部和全局范围内执行代码。然而,在函数里面,传递给 exec() 的局部范围是拷贝实际局部变量组成的一个字典。因此,如果 exec() 如果执行了修改操作,这种修改后的结果对实际局部变量值是没有影响的。
  示例:
    1. 全局范围使用及局部范围使用 exec()

>>> # 全局范围内执行 exec
>>> a = 13
>>> exec('b = a + 1') 
>>> print(b)
14
>>> # 局部范围内执行 exec
>>> def test():
...     a = 13
...     exec('b = a + 1') 
...     print(b)
...
>>> test()
Traceback (most recent call last):
    File "<stdin>", line 1, in <module> 
    File "<stdin>", line 4, in test
NameError: global name 'b' is not defined 
>>>

  可以看到,最后抛出了一个 NameError 异常,就跟在 exec() 语句从没执行过一样。要想在后面的计算中使用到 exec() 执行结果的话就会有问题了。为了修正这样的错误,你需要在调用 exec() 之前使用 locals() 函数来得到一个局部变量字典。之后你就能从局部字典中获取修改过后的变量值了。当然原始的局部变量值依旧不会变。
    2. 使用 locals() 收集局部变量,并在后续访问

>>> def test():
...     a = 13
...     loc = locals() 
...     exec('b = a + 1')
...     b = loc['b'] 
...     print(b)
...
>>> test() 
14
>>>

  在使用 locals() 的时候,你需要注意操作顺序。每次它被调用的时候,locals() 会获取局部变量值中的值并覆盖字典中相应的变量。
    3. 两次调用 locals()

>>> def test3():
...    x = 0
...    loc = locals() 
...    print(loc) 
...    # 虽然执行了 exec,但局部变量 x 的值其实并没有变,但 loc 中保存的 x 值变了
...    exec('x += 1') 
...    print(loc) 
...    # 再次调用 local 方法,使用局部变量初始化变量字典
...    locals() 
...    print(loc)
...
>>> test3() 
{'x': 0}
{'loc': {...}, 'x': 1} 
{'loc': {...}, 'x': 0} 
>>>

模块与包

控制模块被全部导入的内容

  尽管强烈反对使用from module import *,但是在定义了大量变量名的模块中,这种方式还是被频繁使用。默认情况下, 这种方式会导入模块中所有不以下划线开头的变量。然而你可以使你的模块在面对这种导入语句时,表现得更加“克制”一些:如果模块定义了 __all__ 列表类型变量,那么只有被列举出的东西会被导出。当然如果你将 __all__ 定义成一个空列表,那也就不会导入任何东西。如果 __all__ 包含未定义的名字, 在导入时引起 AttributeError
  示例:
    1. __all__ 变量使用

# somemodule.py 
def spam():
    pass 

def grok():
    pass 

blah = 42
# 只会导入 spam、grok
__all__ = ['spam', 'grok']

重新加载模块

  使用 imp.reload() 来重新加载先前加载的模块。它在开发和调试过程中很有用,比如修改代码后可以不用重启整个程序而只需要使用它来重新加载修改后的模块即可,但在生产环境中的代码使用会不安全。reload() 擦除了模块底层字典的内容,并通过重新执行模块的源代码来刷新它,模块对象本身的身份保持不变。因此,该操作在程序中所有已导入该模块的地方更新了该模块。但它不会更新像 from module import name 这样使用 import 语句导入的模块。

运行目录或压缩包

  Python 的单个脚本的运行都是通过某个主函数入口开始的,比如:if __name__ == '__main__':。而当多个脚本组合起来形成一个小项目时,可以为它添加 __main__.py 文件,放于顶层目录下。如果 __main__.py 存在,就可以简单地在顶级目录运行 Python 解释器去启动项目。解释器将执行 __main__.py 文件作为主程序。如果代码打包成 zip 文件,这种技术同样也适用。

网络编程

创建 TCP 服务器

  创建一个 TCP 服务器的一个简单方法是使用 socketserver 库。创建 TCP 服务可以粗分为两步:定义服务处理逻辑、打开服务器。首先你可以继承现成的 RequestHandle 类并实现 handle() 方法,用来定义为客户端提供服务的代码。也可以用 request 属性获取客户端 socketclient_addres 则是客户端地址。然后,就可以实例化一个 TCPServer 对象,指定服务地址、端口及服务处理方法了。最后,调用实例的 serve_forever() 方法开启服务。
  示例:
    1. 一个简单的应答服务器

from socketserver import BaseRequestHandler, TCPServer 

class EchoHandler(BaseRequestHandler):
    def handle(self):
        print('客户端连接地址:', self.client_address) 
        while True:
            msg = self.request.recv(8192) 
            if not msg:
                break
            self.request.send(msg)


if __name__ == '__main__':
    serv = TCPServer(('', 20000), EchoHandler) 
    serv.serve_forever()

  默认情况下这种服务器是单线程的,一次只能为一个客户端连接服务。如果想处理多个客户端,可以初始化一个 ForkingTCPServer 或者是 ThreadingTCPServer 对象而不是简单的 TCPServer。但使用 fork 或线程服务器有个潜在问题就是它们会为每个客户端连接创建一个新的进程或线程。由于客户端连接数是没有限制的,因此一个恶意的方式可以同时发送大量的连接让此类服务器崩溃。我们可以创建一个预先分配大小的工作线程池或进程池,然后创建普通的非线程服务器,然后在线程池中使用 serve_forever() 方法来启动它们。
    2. 多线程服务器实现

if __name__ == '__main__': 
    from threading import Thread 
    NWORKERS = 16
    serv = TCPServer(('', 20000), EchoHandler) 
    for n in range(NWORKERS):
        t = Thread(target=serv.serve_forever) 
        t.daemon = True
        t.start()
    serv.serve_forever()

创建 UDP 服务器

  UDP 服务器也可以使用 socketserver 库创建,与 TCP 服务器创建流程一样,先继承预置的 RequestHandler 实现它的 handler() 方法,然后实例化 UDPServer,指定服务地址、端口号及定制化的 handler,最后调用 serve_forever() 开启服务即可。
  另外对于 UDP 数据报的传送,应该使用 socketsendto()recvfrom() 方法。同样 UDPServer 类是单线程的,也就是说一次只能为一个客户端连接服务。实际使用中,这个无论是对于 UDP 还是 TCP 都不是什么大问题。如果你想要并发操作,可以实例化一个 ForkingUDPServerThreadingUDPServer 对象,或者使用线程池存储一批 UDPServer

from socketserver import BaseRequestHandler, UDPServer 
import time

class TimeHandler(BaseRequestHandler): 
    def handle(self):
        print('客户端连接地址:', self.client_address) 
        # 这个类的 request 属性是一个包含了数据报文和底层 socket 对象的元组
        msg, sock = self.request 
        resp = time.ctime()
        sock.sendto(resp.encode('ascii'), self.client_address)


if __name__ == '__main__':
    serv = UDPServer(('', 20000), TimeHandler) 
    serv.serve_forever()

通过 XML-RPC 实现简单的远程调用

  实现一个远程方法调用的最简单方式是使用 XML-RPC,借助 XML-RPC 可以很容易的构造一个简单的远程调用服务。你所需要做的仅仅是创建一个服务器实例,通过它的方法 register_function() 来注册函数,然后使用方法 serve_forever() 启动它。但 XML-RPC 暴露出来的函数只能适用于部分数据类型,比如字符串、整形、列表和字典。XML-RPC 的一个缺点是它的性能,它会将所有数据都序列化为 XML 格式,所以它会比其他的方式运行得慢一些。并且 SimpleXMLRPCServer 的实现是单线程的,所以它不适合于大型程序,
  示例:
    1. 实现一个简单的存储映射关系的服务

from xmlrpc.server import SimpleXMLRPCServer 
class KeyValueServer:
    _rpc_methods_ = ['get', 'set', 'delete', 'exists', 'keys'] 
    def __init__(self, address):
        self._data = {}
        self._serv = SimpleXMLRPCServer(address, allow_none=True) 
        for name in self._rpc_methods_:
            self._serv.register_function(getattr(self, name))

    def get(self, name):
        return self._data[name] 

    def set(self, name, value):
        self._data[name] = value 

    def delete(self, name):
        del self._data[name]

    def exists(self, name): 
        return name in self._data

    def keys(self):
        return list(self._data) 

    def serve_forever(self):
        self._serv.serve_forever() 


# 启动服务
if __name__ == '__main__':
    kvserv = KeyValueServer(('', 15000)) 
    kvserv.serve_forever()

  尝试连接:

>>> from xmlrpc.client import ServerProxy
>>> s = ServerProxy('http://localhost:15000', allow_none=True) 
>>> s.set('foo', 'bar')
>>> s.set('spam', [1, 2, 3]) 
>>> s.keys()
['spam', 'foo'] 
>>> s.get('foo') 
'bar'
>>> s.get('spam') 
[1, 2, 3]
>>> s.delete('spam')
>>>

多进程通信

  通过使用 multiprocessing.connection 模块可以很容易地实现不同解释器之间的通信。目前有很多用来实现各种消息传输的包和函数库,比如 ZeroMQCelery 等。你还有另外一种选择就是自己在底层 socket 基础之上来实现一个消息传输层。但是 multiprocessing.connection 是最简单的方案。另外,该模块最适合用来建立长连接,比如两个解释器之间启动后就开始建立连接并在处理某个问题过程中会一直保持连接状态。
  如果需要通信的解释器运行在同一台机器上面,那么 Unix 域套接字或者是 Windows 命名管道。要想使用 Unix 域套接字来创建一个连接,只需简单的将地址改写为一个文件名即可:

s = Listener('/tmp/myconn', authkey=b'peekaboo'

要想使用 Windows 命名管道来创建连接,只需像下面这样使用一个文件名:

s = Listener(r'\\.\pipe\myconn', authkey=b'peekaboo')

  示例:
    1. 一个简单的应答服务器

from multiprocessing.connection import Listener 
import traceback

def echo_client(conn):
    try:
        while True:
            msg = conn.recv() 
            conn.send(msg)
    except EOFError:
        print('Connection closed')

def echo_server(address, authkey):
    serv = Listener(address, authkey=authkey) 
    while True:
        try:
            client = serv.accept() 
            echo_client(client)
        except Exception: 
            traceback.print_exc()

echo_server(('', 25000), authkey=b'peekaboo')

  尝试使用:

>>> from multiprocessing.connection import Client
>>> c = Client(('localhost', 25000), authkey=b'peekaboo') 
>>> c.send('hello')
>>> c.recv() 
'hello'
>>> c.send([1, 2, 3, 4, 5]) 
>>> c.recv()
[1, 2, 3, 4, 5] 
>>>

并发编程

线程启动、停止、状态查询

  threading 库可以在单独的线程中执行任何的在 Python 中可以调用的对象。我们可以创建一个 Thread 对象并将你要执行的对象以 target 参数的形式提供给该对象。一个线程对象创建好后,并不会立即执行,除非调用它的 start() 方法。Python 中的线程会在一个单独的系统级线程中执行,这些线程将由操作系统来全权管理。线程一旦启动,将独立执行直到目标函数返回。另外可以使用 join() 将一个线程加入到当前线程,并等待它终止。

import time

def countdown(n):
    while n > 0: 
        print('循环次数:', n) 
        n -= 1
        time.sleep(5)

# 创建并启动(前台)线程
from threading import Thread
t = Thread(target=countdown, args=(10,)) 
t.start()
# 检查运行状态
if t.is_alive(): 
    print('运行中')
else:
    print('已结束')
# 加入到主线程,等待它终止
t.join()

  Python 解释器会一直运行直到所有前台线程都终止。对于需要长时间运行的线程或者需要一直运行的后台任务,应当使用后台线程。后台线程无法等待,不过会在主线程终止时自动销毁。

import time

def countdown(n):
    while n > 0: 
        print('循环次数:', n) 
        n -= 1
        time.sleep(5)

# 创建并启动后台线程
from threading import Thread
t = Thread(target=countdown, args=(10,), daemon=True) 
t.start()

  multiprocessing 模块则能使用多进程执行代码,用法与 Thread 类似。

import multiprocessing 
c = CountdownTask(5)
p = multiprocessing.Process(target=c.run) 
p.start()

线程间状态传递

  线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,就涉及到了线程同步问题。此时可以借助 threading 库中 的 Event 对象。
  Event 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,event 对象中的信号标志被设置为假。如果有线程等待一个 event 对象,而这个 event 对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个 event 对象的信号标志设置为真,它将唤醒所有等待这个 event 对象的线程。如果一个线程等待一个已经被设置为真的 event 对象,那么它将忽略这个事件,继续执行。
  示例:
    1. Event 使用示例

from threading import Thread, Event 
import time

def countdown(n, started_evt):
    print('countdown starting') 
    started_evt.set()
    while n > 0: 
        print('T-minus', n) 
        n -= 1
        time.sleep(5)

# 此时事件状态为 False
started_evt = Event()
# 启动线程
print('Launching countdown')
t = Thread(target=countdown, args=(10,started_evt)) 
t.start()
# 等待事件状态为 True
started_evt.wait()
print('countdown is running')

线程池

  concurrent.futures 函数库 ThreadPoolExecutor 类可以方便地创建指定大小的线程池。线程池必须确保有指定大小,避免出现线程无限创建的情况。ThreadPoolExecutor 类的 submit() 方法接受第一个参数为需要使用多线程执行的方法名,后面的参数则是要传给方法的所有入参。最后,可以通过 result() 方法获取被调用函数的返回值。

from concurrent.futures import ThreadPoolExecutor
import urllib.request


def fetch_url(url):
    u = urllib.request.urlopen(url) 
    data = u.read()
    return data


pool = ThreadPoolExecutor(10)
a = pool.submit(fetch_url, 'http://www.baidu.com')
b = pool.submit(fetch_url, 'http://www.google.com')
x = a.result()
y = b.result()

线程间通信

  从一个线程向另一个线程发送数据最安全的方式可能就是使用 queue 库中的队列,创建一个被多个线程共享的 queue 对象,这些线程通过使用 put()get() 操作 来向队列中添加或者删除元素。Queue 对象已经包含了必要的锁,所以可以通过它在多个线程间多安全地共享数据。当使用队列时,协调生产者和消费者的关闭问题可能会有一些麻烦。可以在队列中放置一个特殊的值。当消费者读到这个值的时候,终止执行。
  示例:
    1. 使用 queue 实现线程间通信

from queue import Queue
from threading import Thread 

_sentinel = object()
# 生产者
def producer(out_q):
    while running:
        out_q.put(data)
    out_q.put(_sentinel

# 消费者
def consumer(in_q):
    while True:
        # Get some data
        data = in_q.get()
        # Check for termination 
        if data is _sentinel:
            in_q.put(_sentinel) 
        break

# 创建一个队列并交给两个线程
q = Queue()
t1 = Thread(target=consumer, args=(q,)) 
t2 = Thread(target=producer, args=(q,)) 
t1.start()
t2.start()

简单的并行编程

  并行和并发可不一样,并行是指同一时间,有多个进程在执行,充分利用了多核心性能。而并发则是在一段时间内,有多个进程执行过,实际上还是只运用了单核心。concurrent.futures 库提供了一个 ProcessPoolExecutor 类,可以实现并行执行。其原理是,一个 ProcessPoolExecutor 创建 N 个独立的 Python 解释器,N 默认是系统上面可用 CPU 的个数。也可以通过提供可选参数给 ProcessPoolExecutor(N) 来修改处理器数量。处理池会一直运行到所有提交的工作被处理完成。被提交到池中的工作必须被定义为一个函数。有两种方法去提交:pool.map() 自动提交并返回结果或 pool.submit() 手动提交并通过 result() 方法手动获取结果或通过 add_done_callback() 方法设置一个回调函数。它的典型用法如下:

from concurrent.futures import ProcessPoolExecutor 

with ProcessPoolExecutor() as pool:
    result = pool.map(func, args)
    do something...

  threading 库中的 Lock 类,提供了简单的锁支持。它和 with 语句块一起使用可以保证互斥执行,就是每次只有一个线程可以执行 with 语句包含的代码块。with 语句会在这个代码块执行前自动获取锁,在执行结束(无论是否异常)后自动释放锁。threading 库的 RLock 类还提供了可重入锁的支持。当锁被持有时,只有一个线程可以使用被锁的部分,并且已经持有这个锁的方法在调用同样使用这个锁的方法时,无需再次获取锁。
  示例:
    1. Lock 类演示

import threading 
class SharedCounter:
    def __init__(self, initial_value = 0): 
        self._value = initial_value
        self._value_lock = threading.Lock() 

    def incr(self,delta=1):
        with self._value_lock: 
            self._value += delta

    def decr(self,delta=1):
        with self._value_lock: 
            self._value -= delta

防止死锁

  在多线程程序中,死锁问题很大一部分是由于线程同时获取多个锁造成的。一个线程获取了第一个锁,然后在获取第二个锁的时候发生阻塞,那么这个线程就可能阻塞其他线程的执行,从而导致整个程序死锁。解决死锁问题的一种方案是为程序中的每一个锁分配一个唯一的 id,然后只允许按照升序规则来使用多个锁。这样做的思路是:单纯地按照 id 递增的顺序加锁不会产生循环依赖,而循环依赖是死锁的一个必要条件,从而避免程序进入死锁状态。另一种方案则是是引入看门狗计数器。当线程正常运行的时候会每隔一段时间重置计数器,在没有发生死锁的情况下,一切都正常进行。一旦发生死锁,由于无法重置计数器导致定时器超时,这时程序会通过重启自身恢复到正常状态。

与系统相关的操作

脚本接收入参

  argparse 模块可被用来解析命令行选项,在创建 ArgumentParser 实例后,就可以使用 add_argument() 方法声明想要支持的参数。在每个 add_argument() 中,可以用 dest 参数指定入参解析结果被赋予的属性名;help 参数可以指定帮助信息;action 参数指定入参对应的处理逻辑,通常为 store,将接收一个值并将其作为字符串值存储(store_true:根据参数是否存在来设置一个布尔值存储。append:允 许某个参数值重复出现多次,并将它们追加到一个列表中);choices 参数接受一个值,但是会将其和指定的可选值做比较,以检测其合法性。required 参数接收一个布尔值,以决定入参是否必填。default 参数则设置了参数未传入时的默认值。最后,可以执行 parse_args() 方法处理 sys.argv 的值并返回 一个结果实例。每个参数值会被设置成该实例中 add_argument() 方法的 dest 参数指定的属性值。

import argparse

# 实例化一个参数解析器对象,并写好描述
parser = argparse.ArgumentParser(description='Search some files')
# 添加匿名参数,值赋给 filenames 变量,该参数支持多个值
parser.add_argument(dest='filenames',metavar='filename', nargs='*')
# 添加 p 参数,别名 pat,值赋给 pattern 变量,必填,将所有值收集到列表中,定义帮助信息
parser.add_argument('-p', '--pat', dest='patterns', metavar='pattern', required=True, action='append', help='text pattern to search for')
# 添加 v 参数,值赋给 verbose 变量,参数出现则值设为 True,定义帮助信息
parser.add_argument('-v', dest='verbose', action='store_true', help='verbose mode')
# 添加 o 参数,值赋给 outfile 变量,参数值作为字符串存储,定义帮助信息
parser.add_argument('-o', dest='outfile', action='store', help='output file')
# 添加 speed 参数,值赋给 speed 变量,参数值作为字符串存储,同时限制可选值为 slow/fast,默认为 slow,定义帮助信息
parser.add_argument('--speed', dest='speed', action='store', choices={'slow','fast'}, default='slow', help='search speed')

args = parser.parse_args()
# Output the collected arguments
print(args.filenames)
print(args.patterns)
print(args.verbose)
print(args.outfile)
print(args.speed)

脚本接收密码

  getpass 模块使得用户在输出密码时,弹出密码输入提示,并且不会在用户终端回显密码。

import getpass

user = getpass.getuser() 
passwd = getpass.getpass()
if custom_login_auth(user, passwd):
    print('登录成功')
else:
    print('登录失败')

执行外部命令

  使用 subprocess.check_output() 函数是执行外部命令并获取其返回值的最简单方式,它能执行一个指定的命令并将执行结果以一个字节字符串的形式返回。如果需要文本形式返回,加一个 decode() 解码即可。但如果被执行的命令以非零码返回,就会抛出异常。stderr 参数可以收集标准输出和错误输出,timeout 参数可以指定命令执行超时时间限制,shell 参数则可以指定该命令是否需要在 SHELL 环境中执行。注意:通常来讲,命令的执行不需要使用到底层 SHELL 环境,此时传入一个字符串列表作为命令,每个元素之间用空格拼接形成完整命令。而如果需要使用底层 SHELL 环境,传入的命令则限定为字符串类型。

try:
    out_bytes = subprocess.check_output(['cmd','arg1','arg2'], timeout=5).decode("utf-8") 
    out_bytes = subprocess.check_output('cmd arg1 arg2', timeout=5, shell=True, stderr=subprocess.STDOUT).decode("utf-8") 
except subprocess.CalledProcessError as e:
    out_bytes = e.output
    code      = e.returncode

  如果需要对子进程做更复杂的交互,比如给它发送输入,则需要使用 subprocess.Popen 类。

import subprocess 
text = b''' 
echo 'command1'
echo 'command2'
echo 'command3'
'''
# 为一个命令启动子进程管道
p = subprocess.Popen(['wc'], stdout = subprocess.PIPE,  stdin = subprocess.PIPE)
# 向管道发送字符串,并接收命令执行后的输出
stdout, stderr = p.communicate(text)
# 输出解码
out = stdout.decode('utf-8') 
err = stderr.decode('utf-8')

执行系统命令

  shutil 模块有许多函数支持系统命令,比如:
复制文件和目录:

import shutil
# 将指定文件复制到指定路径
shutil.copy(src, dst)
# 将指定文件复制到指定路径,并保留原始文件的元数据
shutil.copy2(src, dst)
# 将指定目录及子目录复制到指定路径
shutil.copytree(src, dst)
# 将指定目录及子目录复制到指定路径,并忽略某些特定模式文件名的文件
shutil.copytree(src, dst, ignore=shutil.ignore_patterns('*~', '*.pyc'))
# 将指定文件移动到指定路径
shutil.move(src, dst)
# 解压压缩包
shutil.unpack_archive('Python-3.3.0.tgz')
# 压缩文件,压缩后的文件名为 py33,压缩格式为 zip
shutil.make_archive('py33','zip','Python-3.3.0')

读取配置文件

  configparser 模块能被用来读取、修改、写入配置文件(配置文件中的名字是不区分大小写)。比如有以下配置文件:

; config.ini
; Sample configuration file 

[installation]
library=%(prefix)s/lib 
include=%(prefix)s/include 
bin=%(prefix)s/bin 
prefix=/usr/local

# Setting related to debug configuration
[debug]
log_errors=true 
show_warnings=False

[server] 
port: 8080 
nworkers: 32
pid-file=/tmp/spam.pid 
root=/www/root 

  读取、写入该文件:

>>> from configparser import ConfigParser 
>>> cfg = ConfigParser()
>>> cfg.read('config.ini') 
['config.ini']
>>> # 查看配置文件的分组
>>> cfg.sections()
['installation', 'debug', 'server'] 
>>> # 查看配置文件 installation 分组中的 library 属性,获取字符串
>>> cfg.get('installation','library') 
'/usr/local/lib'
>>> # 查看配置文件 debug 分组中的 log_errors 属性,获取布尔值
>>> cfg.getboolean('debug','log_errors') 
True
>>> # 查看配置文件 server 分组中的 port 属性,获取整型
>>> cfg.getint('server','port') 
8080
>>> # 修改配置并将其写回到文件中
>>> cfg.set('server','port','9000')
>>> import sys
>>> cfg.write(sys.stdout)

测试与调试

扩展、替换测试对象

  unittest.mock.patch() 函数可以用来实现指定对象的扩展与替换,以达到测试目的。它能作为装饰器、上下文管理器、普通方法来使用。
  示例:
    1. 扩展 example.func() 函数

from unittest.mock import patch
import example

# 作为装饰器使用(可以叠加使用)
@patch('example.func')
@patch('example.func2')
@patch('example.func3')
def test1(x, mock_func):
    # 使用目标函数,扩展跟多逻辑
    example.func(x)
    mock_func.assert_called_with(x)

# 作为上下文管理器使用(可以叠加使用)
with patch('example.func') as mock_func, \
     patch('example.func2') as mock_func2, \
     patch('example.func3') as mock_func3:
    example.func(x)
    mock_func.assert_called_with(x)

# 作为普通方法使用
p = patch('example.func')
# 开始扩展标记
mock_func = p.start()
example.func(x)
mock_func.assert_called_with(x)
# 结束扩展标记
p.stop()

  patch() 接受一个已存在对象的全路径名,将其替换为一个新的值。原来的值会在装饰器函数或上下文管理器完成后自动恢复回来。默认情况下,所有值会被 MagicMock 实例替代。
  2. 将 example.var 的值替换为 2

import unittest
from unittest.mock import patch
import io
import example

from urllib.request import urlopen
import csv

class Example:
    @classmethod
    def dowprices(cls):
        u = urlopen('http://finance.yahoo.com/d/quotes.csv?s=@^DJI&f=sl1')
        lines = (line.decode('utf-8') for line in u)
        rows = (row for row in csv.reader(lines) if len(row) == 2)
        prices = { name:float(price) for name, price in rows }
        return prices

class Tests(unittest.TestCase):
    vars = "a"
    print(example.var)
    # 替换变量
    with patch('example.var', 2):
        print(example.var)

    sample_data = io.BytesIO(b'"IBM",91.1, "AA",13.25, "MSFT",27.72')

    # 替换函数,直接将函数返回值设为某个预置值
    @patch('Example.urlopen', return_value=sample_data)
    def test_dowprices(self, mock_urlopen):
        p = Example.dowprices()
        self.assertTrue(mock_urlopen.called)
        self.assertEqual(p, {'IBM': 91.1, 'AA': 13.25, 'MSFT' : 27.72})

异常测试

  assertRaise() 方法可以用来测试函数在某种情况的入参下是否会抛出指定异常。assertRaisesRegex() 方法可同时测试在某种情况的入参下是否会抛出指定异常还能通过正则式匹配异常的字符串表示。assertEqual() 方法则用于检测当前异常是否为指定异常。它们既能作为普通方法使用,也能通过上下文管理器使用。

import unittest
import errno

# 一个样例方法
def parse_int(s):
    return int(s)

class TestConversion(unittest.TestCase):
    def test_bad_int(self):
        self.assertRaises(ValueError, parse_int, 'N/A')

    def test_file_not_found(self):
        try:
            f = open('/file/not/found')
        except IOError as e:
            self.assertEqual(e.errno, errno.ENOENT)
        else:
            self.fail('IOError not raised')

    def test_bad_int(self):
        self.assertRaisesRegex(ValueError, 'invalid literal .*', parse_int, 'N/A')

    def test_bad_int(self):
        with self.assertRaisesRegex(ValueError, 'invalid literal .*'):
            r = parse_int('N/A')

指定测试策略

  unittest 模块有装饰器可用来控制对指定测试方法的处理,例如:unittest.skip——直接跳过、unittest.skipIf——条件跳过、unittest.skipUnless——条件执行、unittest.expectedFailure——预期内的异常。

import unittest
import os
import platform

class Tests(unittest.TestCase):
    def test_0(self):
        self.assertTrue(True)

    @unittest.skip('skipped test')
    def test_1(self):
        self.fail('should have failed!')

    @unittest.skipIf(os.name=='posix', 'Not supported on Unix')
    def test_2(self):
        import winreg

    @unittest.skipUnless(platform.system() == 'Darwin', 'Mac specific test')
    def test_3(self):
        self.assertTrue(True)

    @unittest.expectedFailure
    def test_4(self):
        self.assertEqual(2+2, 5)

自定义异常

  自定义异常类应该总是继承自内置的 Exception 类,或者是继承自那些本身就是从 Exception 继承而来的类。尽管所有类同时也继承自 BaseException,但也不应该使用这个基类来定义新的异常。BaseException 是为系统退出异常而保留的,比如 KeyboardInterruptSystemExit 以及其他那些会给应用发送信号而退出的异常。
另外如果自定义异常重写了 __init__() 方法,确保使用所有参数调用 Exception.__init__()Exception 默认行为是接受所有传递的参数并将它们以元组形式存储在 .args 属性中. 很多其他函数库和部分 Python 库默认所有异常都必须有 .args 属性。

class CustomError(Exception):
    def __init__(self, message, status):
        super().__init__(message, status)
        self.message = message
        self.status = status

异常调用链

  当捕获到某个异常,又想抛出另一种异常时,如果想要保留异常调用链,则可以使用 raise from 语句代替 raise,它可以同时保留前后两个异常的信息。如果想忽视异常调用链,则使用 raise from None。在设计代码时,在另外一个 except 代码块中使用 raise 语句的时候要特别小心。大多数情况下,这种 raise 语句都应该被改成 raise from 语句。

def example():
    try:
        int('N/A')
    except ValueError as e:
        raise RuntimeError('A parsing error occurred') from e
    try:
        int('N/A')
    except ValueError:
        raise RuntimeError('A parsing error occurred') from None

  如果在 except 语句中单独使用 raise 语句的话,它会重新抛出已捕获的异常。这种操作常见于报错后的日志记录、清理等然后又想将其传递出去。

def example():
    try:
        int('N/A')
    except ValueError:
        print("Didn't work")
        raise

输出警告消息

  有时需要提示用户某些信息,但是又不必将其上升为异常级别,就可以使用 warning.warn()。它接收两个入参,一个警告消息和一个警告类,警告类有如下几种:UserWarningDeprecationWarningSyntaxWarningRuntimeWarningResourceWarningFuture-Warning。默认情况下,并不是所有警告消息都会出现。Python 脚本执行时指定 -W 选项能控制警告消息的输出。-W all 会输出所有警告消息,-W ignore 忽略掉所有警告,-W error 将警告转换成异常。还可以使用 warnings.simplefilter() 函数控制输出。always 参数会让所有警告消息出现,ignore 忽略调所有的警告,error 将警告转换成异常。

if logfile is not None:
    warnings.warn('logfile argument deprecated', DeprecationWarning)

程序崩溃后测试

  如果程序因为某个异常而崩溃,运行 python3 -i someprogram.py 可执行简单的调试。-i 项可让程序结束后打开一个交互式 shell,并输出一些运行时环境信息。

# sample.py
def func(n):
    return n + 10

func('Hello')

  运行 python3 -i sample.py 会有类似如下的输出。

bash % python3 -i sample.py
Traceback (most recent call last):
    File "sample.py", line 6, in <module>
        func('Hello')
    File "sample.py", line 4, in func
        return n + 10
    TypeError: Can't convert 'int' object to str implicitly

  Python 还提供了交互式命令行调试工具 pdb。它能直接在命令行中执行常用的调试指令。先了解一下有哪些 pdb 调试指令:

命令缩写描述
steps步进函数
nextn步过函数
returnr跳出函数
listl查看当前行
continuec继续执行直到下个断点
breakb在当前行设置断点
break linenob lineno在指定行数设置断点
printp输出变量的值
argsa查看传入的参数
回车重新执行上一条指令

  可以通过 python -m pdb some.py 命令对指定脚本启动调试器。在脚本中使用 pdb.set_trace() 方法可以使脚本正常启动执行到该行时也能中止并启动 pdb 调试器等待调试。这样在遇到大型程序或者某些特定的控制流程时,可以只在运行到指定位置时开始调试,而不必重头开始。

C:\Users\a\Desktop>python -m pdb example.py
> c:\users\a\desktop\example.py(2)<module>()
-> def func(n):
(Pdb) s
> c:\users\a\desktop\example.py(5)<module>()
-> func('Hello')
(Pdb) s
--Call--
> c:\users\a\desktop\example.py(2)func()
-> def func(n):
(Pdb) s
> c:\users\a\desktop\example.py(3)func()
-> return n + 10
(Pdb) p n
'Hello'
(Pdb) l
  1     # example.py
  2     def func(n):
  3  ->     return n + 10
  4
  5     func('Hello')
[EOF]
(Pdb) a
n = 'Hello'
(Pdb) r
--Return--
> c:\users\a\desktop\example.py(3)func()->None
-> return n + 10
(Pdb) c
Traceback (most recent call last):
  File "D:\Python39\lib\pdb.py", line 1705, in main
    pdb._runscript(mainpyfile)
  File "D:\Python39\lib\pdb.py", line 1573, in _runscript
    self.run(statement)
  File "D:\Python39\lib\bdb.py", line 580, in run
    exec(cmd, globals, locals)
  File "<string>", line 1, in <module>
  File "c:\users\a\desktop\example.py", line 5, in <module>
    func('Hello')
  File "c:\users\a\desktop\example.py", line 3, in func
    return n + 10
TypeError: can only concatenate str (not "int") to str
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> c:\users\a\desktop\example.py(3)func()->None
-> return n + 10
(Pdb) q
Post mortem debugger finished. The C:\Users\a\Desktop\example.py will be restarted
> c:\users\a\desktop\example.py(2)<module>()
-> def func(n):
(Pdb) q

优秀思路篇

使用 deque 队列保留最新历史记录

  collections.deque 类可以被用在任何你只需要一个简单队列数据结构的场合。如果你不设置最大队列大小(maxlen=N),那么就会得到一个无限大小队列,你可以在队列的两端执行添加(append()/appendleft())和弹出(pop()/popleft())元素的操作。使用 deque(maxlen=N) 构造函数会新建一个固定大小的队列。当新的元素加入并且这个队列已满的时候,最老的元素会自动被移除掉。
  尽管你也可以手动在一个列表上实现这一的操作(比如增加、删除等等)。但是这
里的队列方案会更加优雅并且运行得更快些。
  示例:
    1. 搜索整个文件,并返回目标字符串所在行及最后前 N 行

from collections import deque


def search(lines, pattern, history=5):
    previous_lines = deque(maxlen=history)
    for line in lines:
        if pattern in line:
            yield line, previous_lines
        previous_lines.append(line)


if __name__ == '__main__':
    with open(r'./example.txt') as f:
        for line, prevlines in search(f, 'seventh', 5):
            for pline in prevlines:
                print(pline, end="")
            print("命中行:", line, end="")

使用 heapq 查找列表中最大、最小的 N 个元素

  heapq 模块有两个函数:nlargest()nsmallest() 可以完美解决这个问题。
  示例:
    1. 查询数组中最大和最小的 3 个数字

>>> import heapq
>>> nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]
>>> print(heapq.nlargest(3, nums))
[42, 37, 23]
>>> print(heapq.nsmallest(3, nums))
[-4, 1, 2]

  两个函数都能接受一个关键字参数,用于更复杂的数据结构,比较常见的是查询一个对象列表中,某个属性为最值的对象。
  示例:
    2. 查询品牌列表中,价格最贵和最便宜的三个品牌

>>> portfolio = [
... {'name': 'IBM', 'shares': 100, 'price': 91.1},
... {'name': 'AAPL', 'shares': 50, 'price': 543.22},
... {'name': 'FB', 'shares': 200, 'price': 21.09},
... {'name': 'HPQ', 'shares': 35, 'price': 31.75},
... {'name': 'YHOO', 'shares': 45, 'price': 16.35},
... {'name': 'ACME', 'shares': 75, 'price': 115.65}
... ]
>>> cheap = heapq.nsmallest(3, portfolio, key=lambda s: s['price'])
>>> expensive = heapq.nlargest(3, portfolio, key=lambda s: s['price'])
>>> cheap
[{'name': 'YHOO', 'shares': 45, 'price': 16.35}, {'name': 'FB', 'shares': 200, 'price': 21.09}, {'name': 'HPQ', 'shares': 35, 'price': 31.75}]
>>> expensive
[{'name': 'AAPL', 'shares': 50, 'price': 543.22}, {'name': 'ACME', 'shares': 75, 'price': 115.65}, {'name': 'IBM', 'shares': 100, 'price': 91.1}]

  当要查找的元素个数相对比较小的时候,函数 nlargest()nsmallest() 是很合适的。如果你仅仅想查找唯一的最小或最大(N=1)的元素的话,那么使用 min()max() 函数会更快些。
  反之,如果 N 的大小和集合大小接近的时候,通常先排序这个集合,然后再使用切片操作会更快点(sorted(items)[:N] 或者是 sorted(items)[-N:])。

使用 heapq 实现优先级队列

  优先级队列:随机插入一些元素,每次 pop 操作会返回优先级最高的那个元素。
  heapq.heappush()heapq.heappop() 分别在队列 _queue 上插入和删除第一个元素,并且队列 _queue 保证第一个元素拥有最高优先级。另外,由于 push 和 pop 操作时间复杂度为 O(logN),其中 N 是堆的大小,因此就算是 N 很大的时候它们运行速度也依旧很快。
  在下面的示例代码中,队列包含了一个 (-priority, index, item) 的元组。优先级为负数的目的是使得元素按照优先级从高到低排序。这个跟普通的按优先级从低到高排序的堆排序恰巧相反。
  index 变量的作用是保证同等优先级元素的正确排序。通过保存一个不断增加的 index 下标变量,可以确保元素按照它们插入的顺序排序。而且,index 变量也在相同优先级元素比较的时候起到重要作用,Python 在做元组比较时候,如果前面的元素比较已经可以确定结果了,后面的元素比较操作就不会发生了。
  示例:

import heapq


class PriorityQueue:
    def __init__(self):
        self._queue = []
        self._index = 0

    def push(self, item, priority):
        heapq.heappush(self._queue, (-priority, self._index, item))
        self._index += 1

    def pop(self):
        return heapq.heappop(self._queue)[-1]

  如果你使用元组 (priority, item),即没有 index 变量 ,只要两个元素的优先级不同就能正常比较。但如果两个元素优先级一样且 item 是不可比较的对象的话,那么比较操作就会抛出 TypeError,因为 heapq 在比较了元组 (priority, item) 中的 priority 后,因 priority 相同,会尝试比较下一个元素,即 item,而 item 又是不可比较的。因此通过引入另外的 index 变量组成三元组 (priority, index, item) ,就能很好的避免 TypeError,因为不可能有两个元素有相同的 index 值。

使用 heapq 合并多个有序序列

  heapq.merge() 函数可以合并一系列排序序列得到一个仍然有序的序列并在上面迭代遍历。heapq.merge() 可迭代特性意味着它不会立马读取所有序列。可以在非常长的序列中使用它,而不会有太大的开销。要强调的是 heapq.merge() 需要所有输入序列必须是排过序的。特别的,它并不会预先读取所有数据到堆栈中或者预先排序,也不会对输入做任何的排序检测。它仅仅是检查所有序列的开始部分并返回最小的那个,这个过程一直会持续直到所有输入序列中的元素都被遍历完。

>>> import heapq
>>> a = [1, 4, 7, 10] 
>>> b = [2, 5, 6, 11]
>>> for c in heapq.merge(a, b):
...     print(c)
...
1
2
4
5
6
7
10
11

使用 zip() 在字典的值之间做比较

  zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。在字典上执行数学计算时,默认是作用于字典的键。因此可使用 zip() 函数将字典的键值反转来实现对字典的值做计算。即使有字典重复的值,也不会出现覆盖问题,因为 zip() 返回元祖列表。
  示例:
    1. 找出品牌字典数据中,价格最低、最高的品牌

>>> prices = {
... 'ACME': 45.23,
... 'AAPL': 612.78,
... 'IBM': 205.55,
... 'HPQ': 37.20,
... 'FB': 10.75
... }
>>> min_price = min(zip(prices.values(), prices.keys()))
>>> min_price
(10.75, 'FB')
>>> prices = { 'AAA' : 45.23, 'ZZZ': 45.23 }
>>> min(zip(prices.values(), prices.keys()))
(45.23, 'AAA')
>>> max(zip(prices.values(), prices.keys()))
(45.23, 'ZZZ')

消除复杂类型列表的重复值且不改变元素顺序

  如果你仅仅就是想消除重复元素,通常可以简单的构造一个集合来实现,但该方式不能保持元素顺序,并且仅仅适用于列表元素为 hashable 类型的。如果你想基于单个字段、属性或者某个更大的数据结构来消除重复元素,最好通过以下方法实现,这里的 key 参数指定了一个函数,将列表元素转换成 hashable 类型:

def dedupe(items, key=None):
    seen = set()
    for item in items:
        val = item if key is None else key(item)
        if val not in seen:
            yield item
            seen.add(val)


a = [{'x': 1, 'y': 2}, {'x': 1, 'y': 3}, {'x': 1, 'y': 2}, {'x': 2, 'y': 4}]
b = list(dedupe(a, key=lambda d: (d['x'], d['y'])))
print(b)

将多层嵌套序列展开为非嵌套的简单序列

  可以写一个包含 yield from 语句的递归生成器来轻松解决这个问题。比如:

from collections import Iterable
def flatten(items, ignore_types=(str, bytes)):
    for x in items:
        if isinstance(x, Iterable) and not isinstance(x, ignore_types): 
           yield from flatten(x)
        else: 
            yield x

items = [1, 2, [3, 4, [5, 6], 7], 8] 
# Produces 1 2 3 4 5 6 7 8
for x in flatten(items): 
print(x)

  在上面代码中,额外的参数 ignore_types 和检测语句 isinstance(x, ignore_types) 用来将字
符串和字节排除在可迭代对象外,防止将它们再展开成单个的字符。这样的话字符串数组就能最终返回我们所期望的结果了。

优化点

大量拼接字符串

  当我们使用加号 + 操作符去连接大量的字符串的时候是非常低效率的,因为加号连接会引起内存复制以及垃圾回收操作。永远都不应像下面这样写字符串连接代码:

s = ''
for p in parts:
	s += p

  这种写法会比使用 join() 方法运行的要慢一些,因为每一次执行 += 操作的时候会创建一个新的字符串对象。你最好是先收集所有的字符串片段然后再将它们连接起来。一个相对比较聪明的技巧是利用生成器表达式转换数据为字符串的同时合并字符串,比如:

parts= ['ACME', 50, 91.1]
','.join(str(p) for p in parts)

大量日期转换操作

  我们通常会使用 datetime 模块的 strptime()strftime() 方法来做日期与字符串之间的转化操作,但实际上 strptime() 的性能很差,因为它是使用纯 Python 实现,并且必须处理所有的系统本地设置。如果你要在代码中需要解析大量的日期并且已经知道了日期字符串的确切格式,可以自己实现一套解析方案来获取更好的性能。比如,如果你已经知道所以日期格式是 YYYY-MM-DD ,你可以像下面这样实现一个解析函数:

from datetime import datetime 
def parse_ymd(s):
year_s, mon_s, day_s = s.split('-')
return datetime(int(year_s), int(mon_s), int(day_s))

使用迭代器代替 while 无限循环

  iter 函数一个鲜为人知的特性是它接受一个可选的 callable 对象和一个标记(结
尾)值作为输入参数。当以这种方式使用的时候,它会创建一个迭代器,这个迭代器会不断调用 callable 对象直到返回值和标记值相等为止。
  这种特殊的方法对于一些特定的会被重复调用的函数很有效果,比如涉及到 I/O 调用的函数。举例来讲,如果你想从套接字或文件中以数据块的方式读取数据,通常你得要不断重复的执行 read()recv() ,并在后面紧跟一个文件结尾测试来决定是否终止。

CHUNKSIZE = 8192 
# 原始方法
def reader(s):
    while True:
        data = s.recv(CHUNKSIZE) 
        if data == b'':
            break
        process_data(data)

# 优化后
def reader2(s):
    for chunk in iter(lambda: s.recv(CHUNKSIZE), b''):
        pass
    process_data(data)

增量解析大 XML 文件

  如果想要使用尽可能少的内存从一个超大的 XML 文档中提取数据,依旧可以采用迭代器和生成器。下面的工具方法可以只使用很少的内存就能增量式的处理一个大型 XML 文件。它依赖 ElementTree 模块中的两个核心功能:
  1. iterparse() 方法允许对 XML 文档进行增量操作。使用时,你需要提供文件名和一个包含下面一种或多种类型的事件列表:start , end, start-nsend-ns 。由 iterparse() 创建的迭代器会产生形如 (event, elem) 的元组,其中 event 是上述事件列表中的某一个,而 elem 是相应的 XML 元素。start 事件在某个元素第一次被创建并且还没有被插入其他数据(如子元素)时被创建。而 end 事件在某个元素已经完成时被创建,start-nsend-ns 事件被用来处理 XML 文档命名空间的声明。
  2. 在 yield 之后的下面这个语句才是使得程序占用极少内存的 ElementTree 的核心特性:elem_stack[-2].remove(elem),这个语句使得之前由 yield 产生的元素从它的父节点中删除掉。假设已经没有其它的地方引用这个元素了,那么这个元素就被销毁并回收内存。
  这种方案的主要缺陷就是它的运行性能了。测试结果显示,直接读取整个文档到内存并解析的运行速度差不多是增量式处理方式的 2 倍。但是前者使用了超过后者 60 倍的内存。因此,如果你更关心内存使用量的话,那么增量式的版本完胜。
  XML 文件内容如下

<response>
    <rows>
        <row>
            <creation_date>2012-11-18T00:00:00</creation_date>
            <status>Completed</status>
            <completion_date>2012-11-18T00:00:00</completion_date>
            <service_request_number>12-01906549</service_request_number>
            <type_of_service_request>Pot Hole in Street</type_of_service_request>
            <current_activity>Final Outcome</current_activity>
            <most_recent_action>CDOT Street Cut ... Outcome</most_recent_action>
            <street_address>4714 S TALMAN AVE</street_address>
            <zip>60632</zip>
            <x_coordinate>1159494.68618856</x_coordinate>
            <y_coordinate>1873313.83503384</y_coordinate>
            <ward>14</ward>
            <police_district>9</police_district>
            <community_area>58</community_area>
            <latitude>41.808090232127896</latitude>
            <longitude>-87.69053684711305</longitude>
            <location latitude="41.808090232127896" longitude="-87.69053684711305" />
        </row>
        <row>
            <creation_date>2012-11-18T00:00:00</creation_date>
            <status>Completed</status>
            <completion_date>2012-11-18T00:00:00</completion_date>
            <service_request_number>12-01906695</service_request_number>
            <type_of_service_request>Pot Hole in Street</type_of_service_request>
            <current_activity>Final Outcome</current_activity>
            <most_recent_action>CDOT Street Cut ... Outcome</most_recent_action>
            <street_address>3510 W NORTH AVE</street_address>
            <zip>60647</zip>
            <x_coordinate>1152732.14127696</x_coordinate>
            <y_coordinate>1910409.38979075</y_coordinate>
            <ward>26</ward>
            <police_district>14</police_district>
            <community_area>23</community_area>
            <latitude>41.91002084292946</latitude>
            <longitude>-87.71435952353961</longitude>
            <location latitude="41.91002084292946" longitude="-87.71435952353961" />
        </row>
    </rows>
</response>

  工具方法如下:

from xml.etree.ElementTree import iterparse 


def parse_and_remove(filename, path):
    # 获取想要定位的元素路径
    path_parts = path.split('/')
    doc = iterparse(filename, ('start', 'end')) 
	# 初始化 tag 栈和 elem 栈。tag 栈缓存元素路径,elem 栈缓存元素
    tag_stack = [] 
    elem_stack = []
    # 增量处理 xml 文件
    for event, elem in doc:
        # 发现元素起始标签事件,入栈
        if event == 'start':
            tag_stack.append(elem.tag) 
            elem_stack.append(elem)
        # 发现元素结束标签事件,准备处理(返回目标、移除、出栈)
        elif event == 'end':
            # 如果元素标签路径为目标路径,则 yield 返回该元素,同时将该元素从父元素下移除
            if tag_stack == path_parts: 
                yield elem
                # 核心:找到该元素的父元素,并将该元素从父元素下移除
                elem_stack[-2].remove(elem) 
            try:
                # 该元素已结束,清理 tag 栈和 elem 栈
                tag_stack.pop() 
                elem_stack.pop()
            except IndexError:
                pass


if __name__ == '__main__':
    data = parse_and_remove('test.xml', 'response/rows/row')
    for pothole in data:
        print(pothole.findtext('zip'))

避免重复的属性方法

  当类中需要重复的定义一些执行相同逻辑的属性方法时,如进行类型检查,该如何优化?
  有这样如下类,它的属性由属性方法包装,同时还包含了类型检查代码:

class Person:
    def __init__(self, name ,age): 
        self.name = name
        self.age = age 

    @property
    def name(self): 
        return self._name

    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError('name 属性只接受 str 类型的值') 
        self._name = value

    @property
    def age(self): 
        return self._age

    @age.setter
    def age(self, value):
    if not isinstance(value, int):
        raise TypeError('age 属性只接受 int 类型的值') 
    self._age = value

  一个可行的方法是创建一个函数用来定义属性并返回它。

from functools import partial

def typed_property(name, expected_type): 
    storage_name = '_' + name

    @property
    def prop(self):
        return getattr(self, storage_name) 

    @prop.setter
    def prop(self, value):
        if not isinstance(value, expected_type):
            raise TypeError('{} must be a {}'.format(name, expected_type)) 
        setattr(self, storage_name, value)
    return prop 


# 用例:普通使用
class Person1:
    name = typed_property('name', str) 
    age = typed_property('age', int)

    def __init__(self, name, age): 
        self.name = name
        self.age = age


# 用例:通过 partial 使用
String = partial(typed_property, expected_type=str) 
Integer = partial(typed_property, expected_type=int)


class Person2:
    name = String('name') 
    age = Integer('age')

    def __init__(self, name, age): 
        self.name = name
        self.age = age



  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值