🍞 argparse 模块

🍞 binascii 模块

🎯 编码和解码十六进制数

将一个十六进制字符串解码成一个字节字符串或者将一个字节字符串编码成一个十六进制字符串。可以使用 binascii 模块。例如:

>>> # Initial byte string
>>> s = b'hello'
>>> # Encode as hex
>>> import binascii
>>> h = binascii.b2a_hex(s)
>>> h
b'68656c6c6f'
>>> # Decode back to bytes
>>> binascii.a2b_hex(h)
b'hello'
>>>

类似的功能同样可以在 base64 模块中找到。例如:

>>> import base64
>>> h = base64.b16encode(s)
>>> h
b'68656C6C6F'
>>> base64.b16decode(h)
b'hello'
>>>

大部分情况下,通过使用上述的函数来转换十六进制是很简单的。 上面两种技术的主要不同在于大小写的处理。 函数 base64.b16decode()base64.b16encode() 只能操作大写形式的十六进制字母, 而 binascii 模块中的函数大小写都能处理。

🍞 base64 模块

🎯 编码解码Base64数据

使用Base64格式解码或编码二进制数据。base64 模块中有两个函数 b64encode() and b64decode() 可以解决这个问题。例如

>>> # Some byte data
>>> s = b'hello'
>>> import base64

>>> # Encode as Base64
>>> a = base64.b64encode(s)
>>> a
b'aGVsbG8='

>>> # Decode from Base64
>>> base64.b64decode(a)
b'hello'
>>>

Base64编码仅仅用于面向字节的数据比如字节字符串和字节数组。 此外,编码处理的输出结果总是一个字节字符串。 如果想混合使用Base64编码的数据和Unicode文本,必须添加一个额外的解码步骤。例如:

>>> a = base64.b64encode(s).decode('ascii')
>>> a
'aGVsbG8='
>>>

当解码Base64的时候,字节字符串和Unicode文本都可以作为参数。 但是,Unicode字符串只能包含ASCII字符。

🍞 bisect 模块

🍞 contextlib 模块

🎯 contextmanager

实现一个新的上下文管理器的最简单的方法就是使用 contexlib 模块中的 @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))

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

在函数 timethis() 中,yield 之前的代码会在上下文管理器中作为 __enter__() 方法执行, 所有在 yield 之后的代码会作为 __exit__() 方法执行。 如果出现了异常,异常会在 yield 语句那里抛出。

🍞 collections 模块

collections 定义了很多抽象基类,当想自定义容器类的时候它们会非常有用。

🎯 deque()

在迭代操作或者其他操作的时候,只保留最后有限几个元素的历史记录。deque(maxlen=N)构造函数会新建一个固定大小的队列。当新的元素加入并且这个队列已满的时候, 最老的元素会自动被移除掉。

在队列两端插入或删除元素时间复杂度都是O(1),区别于列表,在列表的开头插入或删除元素的时间复杂度为O(N)

在写查询元素的代码时,通常会使用包含yield表达式的生成器函数,这样可以将搜索过程代码和使用搜索结果代码解耦。

from collections import deque


def my_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)

# Example use on a file
if __name__ == '__main__':
    with open(r'../../cookbook/somefile.txt') as f:
        for line, prevlines in my_search(f, 'python', 5):
            for pline in prevlines:
                print(pline, end='')
            print(line, end='')
            print('-' * 20)

🎯 defaultdict()

一个字典就是一个键对应一个单值的映射。如果想要一个键映射多个值,就需要将这多个值放到另外的容器中,比如列表或者集合里面。

可以很方便的使用collections模块中的defaultdict来构造这样的字典。

from collections import defaultdict

d = defaultdict(list)
d['a'].append(1)
d['a'].append(2)
d['b'].append(4)

d = defaultdict(set)
d['a'].add(1)
d['a'].add(2)
d['b'].add(4)

需要注意的是, defaultdict 会自动为将要访问的键(就算目前字典中并不存在这样的键)创建映射实体。

🎯 OrderedDict()

创建一个字典,并且在迭代或序列化这个字典的时候能够控制元素的顺序。

为了能控制一个字典中元素的顺序,可以使用collections模块中的OrderedDict类。在迭代操作的时候它会保持元素被插入时的顺序

from collections import OrderedDict

d = OrderedDict()
d['foo'] = 1
d['bar'] = 2
d['spam'] = 3
d['grok'] = 4
# Outputs "foo 1", "bar 2", "spam 3", "grok 4"
for key in d:
    print(key, d[key])

OrderedDict 内部维护着一个根据键插入顺序排序的双向链表。每次当一个新的元素插入进来的时候, 它会被放到链表的尾部。对于一个已经存在的键的重复赋值不会改变键的顺序。

需要注意的是,一个 OrderedDict 的大小是一个普通字典的两倍,因为它内部维护着另外一个链表。 所以如果要构建一个需要大量 OrderedDict 实例的数据结构的时候(比如读取 100,000 行 CSV 数据到一个 OrderedDict 列表中去), 那么就得仔细权衡一下是否使用 OrderedDict 带来的好处要大过额外内存消耗的影响。

🎯 Counter()

找出一个序列中出现次数最多的元素

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)
# 出现频率最高的3个单词
top_three = word_counts.most_common(3)
print(top_three)
# Outputs [('eyes', 8), ('the', 5), ('look', 4)]

🎯 namedtuple()

collections.namedtuple()这个函数实际上是一个返回 Python 中标准元组类型子类的一个工厂方法。

传递一个类型名和需要的字段给它,然后它就会返回一个类,可以初始化这个类,为定义的字段传递值等。

>>> from collections import namedtuple
>>> Subscriber = namedtuple('Subscriber', ['addr', 'joined'])
>>> sub = Subscriber('jonesy@example.com', '2012-10-19')
>>> sub
Subscriber(addr='jonesy@example.com', joined='2012-10-19')
>>> sub.addr
'jonesy@example.com'
>>> sub.joined
'2012-10-19'
>>>

尽管 namedtuple 的实例看起来像一个普通的类实例,但是它跟元组类型是可交换的,支持所有的普通元组操作,比如索引和解压。 比如:

>>> len(sub)
2
>>> addr, joined = sub
>>> addr
'jonesy@example.com'
>>> joined
'2012-10-19'
>>>

命名元组的一个主要用途是将代码从下标操作中解脱出来。 因此,在数据库调用中返回了一个很大的元组列表的情况下,使用命名元组增删就会很方便。

def compute_cost(records):
    total = 0.0
    for rec in records:
        total += rec[1] * rec[2]
    return total

下标操作通常会让代码表意不清晰,并且非常依赖记录的结构。 下面是使用命名元组的版本:

from collections import namedtuple

Stock = namedtuple('Stock', ['name', 'shares', 'price'])
def compute_cost(records):
    total = 0.0
    for rec in records:
        s = Stock(*rec)
        total += s.shares * s.price
    return total

命名元组另一个用途就是作为字典的替代,因为字典存储需要更多的内存空间。 如果需要构建一个非常大的包含字典的数据结构,那么使用命名元组会更加高效。 但是需要注意的是,不像字典那样,一个命名元组是不可更改的。比如:

>>> s = Stock('ACME', 100, 123.45)
>>> s
Stock(name='ACME', shares=100, price=123.45)
>>> s.shares = 75
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>>

如果真的需要改变属性的值,那么可以使用命名元组实例的 _replace() 方法, 它会创建一个全新的命名元组并将对应的字段用新的值取代。比如:

>>> s = s._replace(shares=75)
>>> s
Stock(name='ACME', shares=75, price=123.45)
>>>

🎯 ChainMap()

现在有多个字典或者映射,将它们从逻辑上合并为一个单一的映射后执行某些操作,比如查找值或者检查某些键是否存在。

a = {'x': 1, 'z': 3 }
b = {'y': 2, 'z': 4 }
from collections import ChainMap
c = ChainMap(a,b)
print(c['x']) # Outputs 1 (from a)
print(c['y']) # Outputs 2 (from b)
print(c['z']) # Outputs 3 (from a)

一个 ChainMap 接受多个字典并将它们在逻辑上变为一个字典。 然后,这些字典并不是真的合并在一起了, ChainMap 类只是在内部创建了一个容纳这些字典的列表 并重新定义了一些常见的字典操作来遍历这个列表。大部分字典操作都是可以正常使用。

update() 不同的是:使用 update 如果原字典做了更新,这种改变不会反应到新的合并字典中去。ChainMap 使用原来的字典,它自己不创建新的字典,所以会随之改变。

🍞 datetime 模块

from datetime import datetime, timedelta

# 当前时间
now = datetime.now()
print("当前 datetime:", now, type(now))  
# 2025-04-10 15:30:45.123456 <class 'datetime.datetime'>

# 自定义时间
d = datetime(2024, 12, 31, 23, 59, 59)
print("自定义 datetime:", d, type(d))  
# 2024-12-31 23:59:59 <class 'datetime.datetime'>

# datetime -> 字符串
formatted = d.strftime("%Y/%m/%d %H:%M")
print("格式化:", formatted, type(formatted))  
# '2024/12/31 23:59' <class 'str'>

# 字符串 -> datetime
parsed_dt = datetime.strptime("2025-03-15 08:30", "%Y-%m-%d %H:%M")
print("字符串解析为 datetime:", parsed_dt, type(parsed_dt))  
# 2025-03-15 08:30:00 <class 'datetime.datetime'>

# 获取时间差
delta = now - parsed_dt
print("时间差:", delta, type(delta))  
# datetime.timedelta(days=..., seconds=...) <class 'datetime.timedelta'>

print("差多少天:", delta.days, type(delta.days))  
# 26 <class 'int'>
print("差多少秒:", delta.total_seconds(), type(delta.total_seconds()))  
# 2276400.0 <class 'float'>

# 加减时间
future = now + timedelta(days=7)
print("7天后:", future, type(future))  
# <class 'datetime.datetime'>
past = now - timedelta(hours=3)
print("3小时前:", past, type(past))  
# <class 'datetime.datetime'>

# datetime -> 时间戳
timestamp = now.timestamp()
print("当前 datetime 转时间戳:", timestamp, type(timestamp))  
# 1681440000.0 <class 'float'>

# 时间戳 -> datetime
from_ts = datetime.fromtimestamp(timestamp)
print("时间戳转 datetime:", from_ts, type(from_ts))  
# <class 'datetime.datetime'>

# ISO 格式时间(标准格式)
iso_str = now.isoformat()
print("ISO 格式:", iso_str, type(iso_str))  
# '2025-04-10T15:30:45.123456' <class 'str'>

# 获取当前 UTC 时间
utc_now = datetime.utcnow()
print("当前 UTC 时间:", utc_now, type(utc_now))  
# <class 'datetime.datetime'>

# 替换 datetime 中的部分值
new_time = now.replace(hour=0, minute=0)
print("替换后的时间:", new_time, type(new_time))  
# <class 'datetime.datetime'>
内容 示例输出类型
时间戳 <class 'float'>
时间元组 <class 'time.struct_time'>
格式化字符串时间 <class 'str'>
datetime 对象 <class 'datetime.datetime'>
时间差对象 <class 'datetime.timedelta'>
total_seconds() <class 'float'>
days 属性 <class 'int'>

🍞 decimal 模块

精确计算浮点数(并能容忍一定的性能损耗)。

decimal 模块主要用在涉及到金融的领域。 在这类程序中,哪怕是一点小小的误差在计算过程中蔓延都是不允许的。 因此, decimal 模块为解决这类问题提供了方法。 当Python和数据库打交道的时候也通常会遇到 Decimal 对象,并且,通常也是在处理金融数据的时候。

>>> from decimal import Decimal
>>> a = Decimal('4.2')
>>> b = Decimal('2.1')
>>> a + b
Decimal('6.3')
>>> print(a + b)
6.3
>>> (a + b) == Decimal('6.3')
True

🍞 fractions 模块

fractions 模块可以被用来执行包含分数的数学运算。比如:

>>> from fractions import Fraction
>>> a = Fraction(5, 4)
>>> b = Fraction(7, 16)
>>> print(a + b)
27/16
>>> print(a * b)
35/64

>>> # Getting numerator/denominator
>>> c = a * b
>>> c.numerator
35
>>> c.denominator
64

>>> # Converting to a float
>>> float(c)
0.546875

>>> # Limiting the denominator of a value
>>> print(c.limit_denominator(8))
4/7

>>> # Converting a float to a fraction
>>> x = 3.75
>>> y = Fraction(*x.as_integer_ratio())
>>> y
Fraction(15, 4)
>>>

🍞 functools 模块

🎯 partial()

partial() 函数允许给一个或多个参数设置固定的值,减少接下来被调用时的参数个数。

第一个例子是,假设有一个点的列表来表示(x,y)坐标元组。 可以使用下面的函数来计算两点之间的距离:

points = [ (1, 2), (3, 4), (5, 6), (7, 8) ]

import math
def distance(p1, p2):
    x1, y1 = p1
    x2, y2 = p2
    return math.hypot(x2 - x1, y2 - y1)

现在假设想以某个点为基点,根据点和基点之间的距离来排序所有的这些点。 列表的 sort() 方法接受一个关键字参数来自定义排序逻辑, 但是它只能接受一个单个参数的函数(distance()很明显是不符合条件的)。 现在我们可以通过使用 partial() 来解决这个问题:

>>> pt = (4, 3)
>>> points.sort(key=partial(distance, pt))
>>> points
[(3, 4), (1, 2), (5, 6), (7, 8)]
>>>

很多时候 partial()能实现的效果,lambda 表达式也能实现。比如,之前的几个例子可以使用下面这样的表达式:

points.sort(key=lambda p: distance(pt, p))

🎯 total_ordering(让类支持比较操作)

想让某个类的实例支持标准的比较运算(比如>=,!=,<=,<等),但是又不想去实现那一大丢的特殊方法。

Python类对每个比较操作都需要实现一个特殊方法来支持。 例如为了支持>=操作符,需要定义一个 __ge__() 方法。 尽管定义一个方法没什么问题,但如果要实现所有可能的比较方法那就有点烦人了。

装饰器 functools.total_ordering 就是用来简化这个处理的。 使用它来装饰一个类,只需定义一个 __eq__() 方法, 外加其他方法(__lt__, __le__, __gt__, or __ge__)中的一个即可。 然后装饰器会自动为填充其它比较方法。

作为例子,我们构建一些房子,然后给它们增加一些房间,最后通过房子大小来比较它们:

from functools import total_ordering

class Room:
    def __init__(self, name, length, width):
        self.name = name
        self.length = length
        self.width = width
        self.square_feet = self.length * self.width

@total_ordering
class House:
    def __init__(self, name, style):
        self.name = name
        self.style = style
        self.rooms = list()

    @property
    def living_space_footage(self):
        return sum(r.square_feet for r in self.rooms)

    def add_room(self, room):
        self.rooms.append(room)

    def __str__(self):
        return '{}: {} square foot {}'.format(self.name,
                self.living_space_footage,
                self.style)

    def __eq__(self, other):
        return self.living_space_footage == other.living_space_footage

    def __lt__(self, other):
        return self.living_space_footage < other.living_space_footage

这里我们只是给House类定义了两个方法:__eq__()__lt__() ,它就能支持所有的比较操作:

# Build a few houses, and add rooms to them
h1 = House('h1', 'Cape')
h1.add_room(Room('Master Bedroom', 14, 21))
h1.add_room(Room('Living Room', 18, 20))
h1.add_room(Room('Kitchen', 12, 16))
h1.add_room(Room('Office', 12, 12))
h2 = House('h2', 'Ranch')
h2.add_room(Room('Master Bedroom', 14, 21))
h2.add_room(Room('Living Room', 18, 20))
h2.add_room(Room('Kitchen', 12, 16))
h3 = House('h3', 'Split')
h3.add_room(Room('Master Bedroom', 14, 21))
h3.add_room(Room('Living Room', 18, 20))
h3.add_room(Room('Office', 12, 16))
h3.add_room(Room('Kitchen', 15, 17))
houses = [h1, h2, h3]
print('Is h1 bigger than h2?', h1 > h2) # prints True
print('Is h2 smaller than h3?', h2 < h3) # prints True
print('Is h2 greater than or equal to h1?', h2 >= h1) # Prints False
print('Which one is biggest?', max(houses)) # Prints 'h3: 1101-square-foot Split'
print('Which is smallest?', min(houses)) # Prints 'h2: 846-square-foot Ranch'

其实 total_ordering 装饰器也没那么神秘。 它就是定义了一个从每个比较支持方法到所有需要定义的其他方法的一个映射而已。 比如定义了 __le__() 方法,那么它就被用来构建所有其他的需要定义的那些特殊方法。 实际上就是在类里面像下面这样定义了一些特殊方法:

class House:
    def __eq__(self, other):
        pass
    def __lt__(self, other):
        pass
    # Methods created by @total_ordering
    __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

🍞 heapq 模块

🎯 从一个集合中获得最大或者最小的 N 个元素列表。

heapq 模块有两个函数:nlargest()nsmallest()可以完美解决这个问题。

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

在底层实现里面,首先会先将集合数据进行堆排序后放入一个列表中。

堆数据结构最重要的特征是heap[0]永远是最小的元素。并且剩余的元素可以很容易的通过调用heapq.heappop()方法得到, 该方法会先将第一个元素弹出来,然后用下一个最小的元素来取代被弹出元素(这种操作时间复杂度仅仅是O(log N)N是堆大小)。

>>> nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]
>>> import heapq
>>> heap = list(nums)
>>> heapq.heapify(heap)
>>> heap
[-4, 2, 1, 23, 7, 2, 18, 23, 42, 37, 8]
>>>

当要查找的元素个数相对比较小的时候,函数 nlargest()nsmallest() 是很合适的。 如果仅仅想查找唯一的最小或最大(N=1)的元素的话,那么使用 min()max() 函数会更快些。 类似的,如果 N 的大小和集合大小接近的时候,通常先排序这个集合然后再使用切片操作会更快点 ( sorted(items)[:N] 或者是 sorted(items)[-N:] )。 需要在正确场合使用函数 nlargest()nsmallest() 才能发挥它们的优势 (如果 N 快接近集合大小了,那么使用排序操作会更好些)。

🎯 顺序迭代合并后的排序迭代对象

有一系列排序序列,想将它们合并后得到一个排序序列并在上面迭代遍历。

>>> 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

heapq.merge() 需要所有输入序列必须是排过序的。 特别的,它并不会预先读取所有数据到堆栈中或者预先排序,也不会对输入做任何的排序检测。它仅仅是检查所有序列的开始部分并返回最小的那个,这个过程一直会持续直到所有输入序列中的元素都被遍历完。

🍞 hashlib 模块

🍞 inspect 模块

🎯 🎯 *args**kwargs的强制参数签名。

有一个函数或方法,它使用*args**kwargs作为参数,这样使得它比较通用, 但有时候想检查传递进来的参数是不是某个想要的类型。

对任何涉及到操作函数调用签名的问题,都应该使用 inspect 模块中的签名特性。 我们最主要关注两个类:SignatureParameter 。下面是一个创建函数前面的交互例子:

>>> from inspect import Signature, Parameter
>>> # Make a signature for a func(x, y=42, *, z=None)
>>> parms = [ Parameter('x', Parameter.POSITIONAL_OR_KEYWORD),
...         Parameter('y', Parameter.POSITIONAL_OR_KEYWORD, default=42),
...         Parameter('z', Parameter.KEYWORD_ONLY, default=None) ]
>>> sig = Signature(parms)
>>> print(sig)
(x, y=42, *, z=None)
>>>

一旦有了一个签名对象,就可以使用它的 bind() 方法很容易的将它绑定到 *args**kwargs 上去。 下面是一个简单的演示:

>>> def func(*args, **kwargs):
...     bound_values = sig.bind(*args, **kwargs)
...     for name, value in bound_values.arguments.items():
...         print(name,value)
...
>>> # Try various examples
>>> func(1, 2, z=3)
x 1
y 2
z 3
>>> func(1)
x 1
>>> func(1, z=3)
x 1
z 3
>>> func(y=2, x=1)
x 1
y 2
>>> func(1, 2, 3, 4)
Traceback (most recent call last):
...
    File "/usr/local/lib/python3.3/inspect.py", line 1972, in _bind
        raise TypeError('too many positional arguments')
TypeError: too many positional arguments
>>>
>>> func(y=2)
Traceback (most recent call last):
...
    File "/usr/local/lib/python3.3/inspect.py", line 1961, in _bind
        raise TypeError(msg) from None
TypeError: 'x' parameter lacking default value
>>>
>>> func(1, y=2, x=3)
Traceback (most recent call last):
...
    File "/usr/local/lib/python3.3/inspect.py", line 1985, in _bind
        '{arg!r}'.format(arg=param.name))
TypeError: multiple values for argument 'x'
>>>

可以看出来,通过将签名和传递的参数绑定起来,可以强制函数调用遵循特定的规则,比如必填、默认、重复等等。

下面是一个强制函数签名更具体的例子。在代码中,我们在基类中先定义了一个非常通用的 __init__() 方法, 然后我们强制所有的子类必须提供一个特定的参数签名。

from inspect import Signature, Parameter

def make_sig(*names):
    parms = [Parameter(name, Parameter.POSITIONAL_OR_KEYWORD)
            for name in names]
    return Signature(parms)

class Structure:
    __signature__ = make_sig()
    def __init__(self, *args, **kwargs):
        bound_values = self.__signature__.bind(*args, **kwargs)
        for name, value in bound_values.arguments.items():
            setattr(self, name, value)

# Example use
class Stock(Structure):
    __signature__ = make_sig('name', 'shares', 'price')

class Point(Structure):
    __signature__ = make_sig('x', 'y')


>>> import inspect
>>> print(inspect.signature(Stock))
(name, shares, price)
>>> s1 = Stock('ACME', 100, 490.1)
>>> s2 = Stock('ACME', 100)
Traceback (most recent call last):
...
TypeError: 'price' parameter lacking default value
>>> s3 = Stock('ACME', 100, 490.1, shares=50)
Traceback (most recent call last):
...
TypeError: multiple values for argument 'shares'
>>>

在我们需要构建通用函数库、编写装饰器或实现代理的时候,对于 *args**kwargs 的使用是很普遍的。 但是,这样的函数有一个缺点就是当想要实现自己的参数检验时,代码就会笨拙混乱。这时候我们可以通过一个签名对象来简化它。

在最后的一个方案实例中,我们还可以通过使用自定义元类来创建签名对象。下面演示怎样来实现:

from inspect import Signature, Parameter

def make_sig(*names):
    parms = [Parameter(name, Parameter.POSITIONAL_OR_KEYWORD)
            for name in names]
    return Signature(parms)

class StructureMeta(type):
    def __new__(cls, clsname, bases, clsdict):
        clsdict['__signature__'] = make_sig(*clsdict.get('_fields',[]))
        return super().__new__(cls, clsname, bases, clsdict)

class Structure(metaclass=StructureMeta):
    _fields = []
    def __init__(self, *args, **kwargs):
        bound_values = self.__signature__.bind(*args, **kwargs)
        for name, value in bound_values.arguments.items():
            setattr(self, name, value)

# Example
class Stock(Structure):
    _fields = ['name', 'shares', 'price']

class Point(Structure):
    _fields = ['x', 'y']

当我们自定义签名的时候,将签名存储在特定的属性 __signature__ 中通常是很有用的。 这样的话,在使用 inspect 模块执行内省的代码就能发现签名并将它作为调用约定。

>>> import inspect
>>> print(inspect.signature(Stock))
(name, shares, price)
>>> print(inspect.signature(Point))
(x, y)
>>>

🧩 被包装函数的函数签名如何重新绑定为正确的,如下:

from functools import wraps
import inspect

def optional_debug(func):
    if 'debug' in inspect.getargspec(func).args:
        raise TypeError('debug argument already defined')

    @wraps(func)
    def wrapper(*args, debug=False, **kwargs):
        if debug:
            print('Calling', func.__name__)
        return func(*args, **kwargs)

    sig = inspect.signature(func)
    parms = list(sig.parameters.values())
    parms.append(inspect.Parameter('debug',
                inspect.Parameter.KEYWORD_ONLY,
                default=False))
    wrapper.__signature__ = sig.replace(parameters=parms)
    return wrapper

通过这样的修改,包装后的函数签名就能正确的显示 debug 参数的存在了。例如:

>>> @optional_debug
... def add(x,y):
...     return x+y
...
>>> print(inspect.signature(add))
(x, y, *, debug=False)
>>> add(2,3)
5
>>>

🍞 itertools 模块

🎯 islice()

用于 迭代器和生成器 上的切片操作。

🎯 dropwhile()

用于丢弃一个可迭代对象前面的元素。

🎯 permutations()/combinations()/combinations_with_replacement()

排列组合的迭代(迭代遍历一个集合中元素的所有可能的排列或组合)。

🎯 zip_longest()

同时迭代多个序列,并且对其长度。

🎯 chain()

合并多个集合,创建迭代器。

🎯 groupby()

有一个字典或者实例的序列,然后想根据某个特定的字段比如 date 来分组迭代访问。

itertools.groupby() 函数对于这样的数据分组操作非常实用。

rows = [
    {'address': '5412 N CLARK', 'date': '07/01/2012'},
    {'address': '5148 N CLARK', 'date': '07/04/2012'},
    {'address': '5800 E 58TH', 'date': '07/02/2012'},
    {'address': '2122 N CLARK', 'date': '07/03/2012'},
    {'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'},
    {'address': '1060 W ADDISON', 'date': '07/02/2012'},
    {'address': '4801 N BROADWAY', 'date': '07/01/2012'},
    {'address': '1039 W GRANVILLE', 'date': '07/04/2012'},
]

from operator import itemgetter
from itertools import groupby

# Sort by the desired field first
rows.sort(key=itemgetter('date'))
# Iterate in groups
for date, items in groupby(rows, key=itemgetter('date')):
    print(date)
    for i in items:
        print(' ', i)

运行结果:

07/01/2012
  {'date': '07/01/2012', 'address': '5412 N CLARK'}
  {'date': '07/01/2012', 'address': '4801 N BROADWAY'}
07/02/2012
  {'date': '07/02/2012', 'address': '5800 E 58TH'}
  {'date': '07/02/2012', 'address': '5645 N RAVENSWOOD'}
  {'date': '07/02/2012', 'address': '1060 W ADDISON'}
07/03/2012
  {'date': '07/03/2012', 'address': '2122 N CLARK'}
07/04/2012
  {'date': '07/04/2012', 'address': '5148 N CLARK'}
  {'date': '07/04/2012', 'address': '1039 W GRANVILLE'}

🎯 compress()

它以一个 iterable 对象和一个相对应的 Boolean 选择器序列作为输入参数。 然后输出 iterable 对象中对应选择器为 True 的元素。 当需要用另外一个相关联的序列来过滤某个序列的时候,这个函数是非常有用的。 比如:

addresses = [
    '5412 N CLARK',
    '5148 N CLARK',
    '5800 E 58TH',
    '2122 N CLARK',
    '5645 N RAVENSWOOD',
    '1060 W ADDISON',
    '4801 N BROADWAY',
    '1039 W GRANVILLE',
]
counts = [ 0, 3, 10, 4, 1, 7, 6, 1]

将那些对应 count 值大于5的地址全部输出,可以这样做:

>>> from itertools import compress
>>> more5 = [n > 5 for n in counts]
>>> more5
[False, False, True, False, False, True, True, False]
>>> list(compress(addresses, more5))
['5800 E 58TH', '1060 W ADDISON', '4801 N BROADWAY']
>>>

这里的关键点在于先创建一个 Boolean 序列,指示哪些元素符合条件。 然后 compress() 函数根据这个序列去选择输出对应位置为 True 的元素。

filter() 函数类似, compress() 也是返回的一个迭代器。因此,如果需要得到一个列表, 那么需要使用 list() 来将结果转换为列表类型。

🍞 ipaddress 模块

🎯 通过CIDR地址生成对应的IP地址集

有一个CIDR网络地址比如“123.45.67.89/27”,想将其转换成它所代表的所有IP (比如,“123.45.67.64”, “123.45.67.65”, …, “123.45.67.95”)

可以使用 ipaddress 模块很容易的实现这样的计算。例如:

>>> import ipaddress
>>> net = ipaddress.ip_network('123.45.67.64/27')
>>> net
IPv4Network('123.45.67.64/27')
>>> for a in net:
...     print(a)
...
123.45.67.64
123.45.67.65
123.45.67.66
123.45.67.67
123.45.67.68
...
123.45.67.95
>>>

>>> net6 = ipaddress.ip_network('12:3456:78:90ab:cd:ef01:23:30/125')
>>> net6
IPv6Network('12:3456:78:90ab:cd:ef01:23:30/125')
>>> for a in net6:
...     print(a)
...
12:3456:78:90ab:cd:ef01:23:30
12:3456:78:90ab:cd:ef01:23:31
12:3456:78:90ab:cd:ef01:23:32
12:3456:78:90ab:cd:ef01:23:33
12:3456:78:90ab:cd:ef01:23:34
12:3456:78:90ab:cd:ef01:23:35
12:3456:78:90ab:cd:ef01:23:36
12:3456:78:90ab:cd:ef01:23:37
>>>

Network 也允许像数组一样的索引取值,例如:

>>> net.num_addresses
32
>>> net[0]

IPv4Address('123.45.67.64')
>>> net[1]
IPv4Address('123.45.67.65')
>>> net[-1]
IPv4Address('123.45.67.95')
>>> net[-2]
IPv4Address('123.45.67.94')
>>>

另外,还可以执行网络成员检查之类的操作:

>>> a = ipaddress.ip_address('123.45.67.69')
>>> a in net
True
>>> b = ipaddress.ip_address('123.45.67.123')
>>> b in net
False
>>>

一个IP地址和网络地址能通过一个IP接口来指定,例如:

>>> inet = ipaddress.ip_interface('123.45.67.73/27')
>>> inet.network
IPv4Network('123.45.67.64/27')
>>> inet.ip
IPv4Address('123.45.67.73')
>>>

⚠️ ipaddress 模块有很多类可以表示IP地址、网络和接口。 当需要操作网络地址(比如解析、打印、验证等)的时候会很有用。

要注意的是,ipaddress 模块跟其他一些和网络相关的模块比如 socket 库交集很少。 所以,不能使用 IPv4Address 的实例来代替一个地址字符串,首先得显式的使用 str() 转换它。例如:

>>> a = ipaddress.ip_address('127.0.0.1')
>>> from socket import socket, AF_INET, SOCK_STREAM
>>> s = socket(AF_INET, SOCK_STREAM)
>>> s.connect((a, 8080))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't convert 'IPv4Address' object to str implicitly
>>> s.connect((str(a), 8080))
>>>

🍞 json 模块

🍞 logging 模块

🍞 math 模块

🎯 数学函数

import math
print(f"math.ceil(3.2) = {math.ceil(3.2)}")  # 输出: math.ceil(3.2) = 4 (向上取整)
print(f"math.floor(3.8) = {math.floor(3.8)}")  # 输出: math.floor(3.8) = 3 (向下取整)
print(f"math.trunc(-3.7) = {math.trunc(-3.7)}")  # 输出: math.trunc(-3.7) = -3 (截断小数部分)

🍞 numbers 模块

提供了一个类似的跟整数类型相关的抽象类型集合。

🍞 os 模块

🎯 八进制

>>> import os
>>> os.chmod('script.py', 0755)
    File "<stdin>", line 1
        os.chmod('script.py', 0755)
                            ^
SyntaxError: invalid token
>>>

# 需确保八进制数的前缀是 0o 
>>> os.chmod('script.py', 0o755)
>>>

🎯 文件名的操作

对于任何的文件名的操作,都应该使用 os.path 模块,而不是使用标准字符串操作来构造自己的代码。

>>> import os
>>> path = '/Users/beazley/Data/data.csv'

>>> # Get the last component of the path
>>> os.path.basename(path)
'data.csv'

>>> # Get the directory name
>>> os.path.dirname(path)
'/Users/beazley/Data'

>>> # Join path components together
>>> os.path.join('tmp', 'data', os.path.basename(path))
'tmp/data/data.csv'

>>> # Expand the user's home directory
>>> path = '~/Data/data.csv'
>>> os.path.expanduser(path)
'/Users/beazley/Data/data.csv'

>>> # Split the file extension
>>> os.path.splitext(path)
('~/Data/data', '.csv')
>>>

🎯 测试文件是否存在

使用 os.path 来进行文件测试是很简单的。但是要注意文件权限问题。

>>> import os
>>> os.path.exists('/etc/passwd')
True
>>> os.path.exists('/tmp/spam')
False
>>>

>>> # Is a regular file
>>> os.path.isfile('/etc/passwd')
True

>>> # Is a directory
>>> os.path.isdir('/etc/passwd')
False

>>> # Is a symbolic link
>>> os.path.islink('/usr/local/bin/python3')
True

>>> # Get the file linked to
>>> os.path.realpath('/usr/local/bin/python3')
'/usr/local/bin/python3.3'
>>>

>>> os.path.getsize('/etc/passwd')
3669
>>> os.path.getmtime('/etc/passwd')
1272478234.0
>>> import time
>>> time.ctime(os.path.getmtime('/etc/passwd'))
'Wed Apr 28 13:10:34 2010'
>>>

🎯 打印不合法文件名

程序获取了一个目录中的文件名列表,但是当它试着去打印文件名的时候程序崩溃, 出现了 UnicodeEncodeError 异常和一条奇怪的消息—— surrogates not allowed

当打印未知的文件名时,使用下面的方法可以避免这样的错误:

def bad_filename(filename):
    return repr(filename)[1:-1]

try:
    print(filename)
except UnicodeEncodeError:
    print(bad_filename(filename))

这里讨论的是在编写必须处理文件系统的程序时一个不太常见但又很棘手的问题。 默认情况下,Python 假定所有文件名都已经根据 sys.getfilesystemencoding() 的值编码过了。 但是,有一些文件系统并没有强制要求这样做,因此允许创建文件名没有正确编码的文件。 这种情况不太常见,但是总会有些用户冒险这样做或者是无意之中这样做了( 可能是在一个有缺陷的代码中给 open() 函数传递了一个不合规范的文件名)。

当执行类似 os.listdir() 这样的函数时,这些不合规范的文件名就会让Python陷入困境。 一方面,它不能仅仅只是丢弃这些不合格的名字。而另一方面,它又不能将这些文件名转换为正确的文本字符串。 Python对这个问题的解决方案是从文件名中获取未解码的字节值比如 \xhh并将它映射成Unicode字符 \udchh 表示的所谓的”代理编码”。 下面一个例子演示了当一个不合格目录列表中含有一个文件名为bäd.txt(使用Latin-1而不是UTF-8编码)时的样子:

>>> import os
>>> files = os.listdir('.')
>>> files
['spam.py', 'b\udce4d.txt', 'foo.txt']
>>>

如果有代码需要操作文件名或者将文件名传递给 open() 这样的函数,一切都能正常工作。 只有当想要输出文件名时才会碰到些麻烦(比如打印输出到屏幕或日志文件等)。 特别的,当想打印上面的文件名列表时,程序就会崩溃:

>>> for name in files:
...     print(name)
...
spam.py
Traceback (most recent call last):
    File "<stdin>", line 2, in <module>
UnicodeEncodeError: 'utf-8' codec can't encode character '\udce4' in
position 1: surrogates not allowed
>>>

程序崩溃的原因就是字符 \udce4 是一个非法的Unicode字符。 它其实是一个被称为代理字符对的双字符组合的后半部分。 由于缺少了前半部分,因此它是个非法的Unicode。 所以,唯一能成功输出的方法就是当遇到不合法文件名时采取相应的补救措施。 比如可以将上述代码修改如下:

>>> for name in files:
... try:
...     print(name)
... except UnicodeEncodeError:
...     print(bad_filename(name))
...
spam.py
b\udce4d.txt
foo.txt
>>>

bad_filename() 函数中怎样处置取决于自己。 另外一个选择就是通过某种方式重新编码,示例如下:

def bad_filename(filename):
    temp = filename.encode(sys.getfilesystemencoding(), errors='surrogateescape')
    return temp.decode('latin-1')

注:

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

使用这个版本产生的输出如下:

>>> for name in files:
...     try:
...         print(name)
...     except UnicodeEncodeError:
...         print(bad_filename(name))
...
spam.py
bäd.txt
foo.txt
>>>

🍞 operator 模块

🎯 methodcaller()

通过字符串调用对象方法

有一个字符串形式的方法名称,想通过它调用某个对象的对应方法。最简单的情况,可以使用 getattr() :

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)


p = Point(2, 3)
d = getattr(p, 'distance')(0, 0)  # Calls p.distance(0, 0)

另外一种方法是使用 operator.methodcaller() ,例如:

import operator
operator.methodcaller('distance', 0, 0)(p)

当需要通过相同的参数多次调用某个方法时,使用 operator.methodcaller 就很方便了。 比如需要排序一系列的点,就可以这样做:

points = [
    Point(1, 2),
    Point(3, 0),
    Point(10, -3),
    Point(-5, -7),
    Point(-1, 8),
    Point(3, 2)
]
# Sort by distance from origin (0, 0)
points.sort(key=operator.methodcaller('distance', 0, 0))

调用一个方法实际上是两步独立操作,第一步是查找属性,第二步是函数调用。 因此,为了调用某个方法,可以首先通过 getattr() 来查找到这个属性,然后再去以函数方式调用它即可。

operator.methodcaller()创建一个可调用对象,并同时提供所有必要参数, 然后调用的时候只需要将实例对象传递给它即可,比如:

>>> p = Point(3, 4)
>>> d = operator.methodcaller('distance', 0, 0)
>>> d(p)
5.0
>>>

🎯 itemgetter()

根据某个或某几个字典字段来排序这个列表,通过使用operator模块的itemgetter函数,可以非常容易的排序这样的数据结构:

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_fname = sorted(rows, key=itemgetter('fname'))
rows_by_uid = sorted(rows, key=itemgetter('uid'))

itemgetter()有时候也可以用lambda表达式代替,比如:

rows_by_fname = sorted(rows, key=lambda r: r['fname'])
rows_by_lfname = sorted(rows, key=lambda r: (r['lname'],r['fname']))

使用itemgetter()方式会运行的稍微快点。因此,如果对性能要求比较高的话就使用itemgetter()方式。

>>> min(rows, key=itemgetter('uid'))
{'fname': 'John', 'lname': 'Cleese', 'uid': 1001}
>>> max(rows, key=itemgetter('uid'))
{'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
>>>

🎯 attrgetter()

排序类型相同的对象,但是他们不支持原生的比较操作。

内置的 sorted() 函数有一个关键字参数 key ,可以传入一个 callable 对象给它, 这个 callable 对象对每个传入的对象返回一个值,这个值会被 sorted 用来排序这些对象。 比如,如果在应用程序里面有一个 User 实例序列,并且希望通过他们的 user_id 属性进行排序, 可以提供一个以 User 实例作为输入并输出对应 user_id 值的 callable 对象。

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

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


sorted(users, key=lambda u: u.user_id)      # [User(3), User(23), User(99)]

from operator import attrgetter
sorted(users, key=attrgetter('user_id'))    # [User(3), User(23), User(99)]

🍞 re 模块

🎯 正则

  • 最短匹配模式(非贪婪)
  • 多行匹配(re.compile(r'/\*(.*?)\*/', re.DOTALL) 或者 (?:.|\n) 指定一个非捕获组)

🎯 查找

如果想匹配的是字面字符串,那么通常只需要调用基本字符串方法就行, 比如 str.find() , str.endswith() , str.startswith()

复杂的匹配需要使用正则表达式和 re 模块。

>>> text1 = '11/27/2012'
>>> text2 = 'Nov 27, 2012'
>>>
>>> import re
>>> # Simple matching: \d+ means match one or more digits
>>> if re.match(r'\d+/\d+/\d+', text1):
... print('yes')
... else:
... print('no')
...

如果想使用同一个模式去做多次匹配,应该先将模式字符串预编译为模式对象。比如:

>>> datepat = re.compile(r'\d+/\d+/\d+')
>>> if datepat.match(text1):
... print('yes')
... else:
... print('no')
...

match() 总是从字符串开始去匹配,如果想查找字符串任意部分的模式出现位置, 使用 findall() 方法去代替。

在定义正则式的时候,通常会利用括号去捕获分组。

>>> datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
>>> 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
>>>

findall() 方法会搜索文本并以列表形式返回所有的匹配。 如果想以迭代方式返回匹配,可以使用 finditer() 方法来代替,比如:

>>> for m in datepat.finditer(text):
... print(m.groups())
...
('11', '27', '2012')
('3', '13', '2013')
>>>

🎯 替换

对于简单的字面模式,直接使用 str.replace() 方法即可,对于复杂的模式,使用 re 模块中的 sub() 函数。

>>> text = 'UPPER PYTHON, lower python, Mixed Python'
>>> re.findall('python', text, flags=re.IGNORECASE)
['PYTHON', 'python', 'Python']
>>> re.sub('python', 'snake', text, flags=re.IGNORECASE)
'UPPER snake, lower snake, Mixed snake'
>>>

上面例子有个缺陷,替换字符串并不会自动跟被匹配字符串的大小写保持一致。

def matchcase(word):
    def replace(m):
        text = m.group()
        if text.isupper():
            return word.upper()
        elif text.islower():
            return word.lower()
        elif text[0].isupper():
            return word.capitalize()
        else:
            return word
    return replace

>>> re.sub('python', matchcase('snake'), text, flags=re.IGNORECASE)
'UPPER SNAKE, lower snake, Mixed Snake'
>>>

matchcase('snake') 返回了一个回调函数(参数必须是 match 对象),前面一节提到过, sub() 函数除了接受替换字符串外,还能接受一个回调函数。

对于一般的忽略大小写的匹配操作,简单的传递一个 re.IGNORECASE 标志参数就已经足够了。但是需要注意的是,这个对于某些需要大小写转换的 Unicode 匹配可能还不够。

🎯 在正则式中使用 Unicode

默认情况下re模块已经对一些 Unicode 字符类有了基本的支持。 比如,\\d 已经匹配任意的 unicode 数字字符了。

>>> import re
>>> num = re.compile('\d+')
>>> # ASCII digits
>>> num.match('123')
<_sre.SRE_Match object at 0x1007d9ed0>
>>> # Arabic digits
>>> num.match('\u0661\u0662\u0663')
<_sre.SRE_Match object at 0x101234030>
>>>

混合使用Unicode和正则表达式,最好还是使用增强的第三方库,比如: regex

🎯 字符串令牌解析

有一个字符串,想从左至右将其解析为一个令牌流。

对于复杂的语法,最好是选择某个解析工具比如 PyParsing 或者是 PLY。

🍞 random 模块

random模块有大量的函数用来产生随机数和随机选择元素。 比如,要想从一个序列中随机的抽取一个元素,可以使用 random.choice() :

>>> import random
>>> values = [1, 2, 3, 4, 5, 6]
>>> random.choice(values)
2
>>> random.choice(values)
3
>>> random.choice(values)
1
>>> random.choice(values)
4
>>> random.choice(values)
6
>>>

为了提取出N个不同元素的样本用来做进一步的操作,可以使用 random.sample()

>>> random.sample(values, 2)
[6, 2]
>>> random.sample(values, 2)
[4, 3]
>>> random.sample(values, 3)
[4, 3, 1]
>>> random.sample(values, 3)
[5, 4, 1]
>>>

打乱序列中元素的顺序,可以使用 random.shuffle()

>>> random.shuffle(values)
>>> values
[2, 4, 6, 5, 3, 1]
>>> random.shuffle(values)
>>> values
[3, 5, 2, 1, 6, 4]
>>>

生成随机整数,请使用 random.randint() :

>>> random.randint(0,10)
2
>>> random.randint(0,10)
5
>>> random.randint(0,10)
0
>>> random.randint(0,10)
7
>>> random.randint(0,10)
10
>>> random.randint(0,10)
3
>>>

为了生成0到1范围内均匀分布的浮点数,使用 random.random()

>>> random.random()
0.9406677561675867
>>> random.random()
0.133129581343897
>>> random.random()
0.4144991136919316
>>>

如果要获取N位随机位(二进制)的整数,使用 random.getrandbits() :

>>> random.getrandbits(200)
335837000776573622800628485064121869519521710558559406913275
>>>

🍞 struct 模块

🎯 读写二进制数组数据

读写一个二进制数组的结构化数据到Python元组中。可以使用 struct 模块处理二进制数据。 下面是一段示例代码将一个Python元组列表写入一个二进制文件,并使用 struct 将每个元组编码为一个结构体。

from struct import Struct
def write_records(records, format, f):
    '''
    Write a sequence of tuples to a binary file of structures.
    '''
    record_struct = Struct(format)
    for r in records:
        f.write(record_struct.pack(*r))

# Example
if __name__ == '__main__':
    records = [ (1, 2.3, 4.5),
                (6, 7.8, 9.0),
                (12, 13.4, 56.7) ]
    with open('data.b', 'wb') as f:
        write_records(records, '<idd', f)

有很多种方法来读取这个文件并返回一个元组列表。 首先,如果打算以块的形式增量读取文件,可以这样做:

from struct import Struct

def read_records(format, f):
    record_struct = Struct(format)
    chunks = iter(lambda: f.read(record_struct.size), b'')
    return (record_struct.unpack(chunk) for chunk in chunks)

# Example
if __name__ == '__main__':
    with open('data.b','rb') as f:
        for rec in read_records('<idd', f):
            # Process rec
            ...

如果想将整个文件一次性读取到一个字节字符串中,然后在分片解析。那么可以这样做:

from struct import Struct

def unpack_records(format, data):
    record_struct = Struct(format)
    return (record_struct.unpack_from(data, offset)
            for offset in range(0, len(data), record_struct.size))

# Example
if __name__ == '__main__':
    with open('data.b', 'rb') as f:
        data = f.read()
    for rec in unpack_records('<idd', data):
        # Process rec
        ...

两种情况下的结果都是一个可返回用来创建该文件的原始元组的可迭代对象。

🎯 读取嵌套和可变长二进制数据

需要读取包含嵌套或者可变长记录集合的复杂二进制格式的数据。这些数据可能包含图片、视频、电子地图文件等。

struct 模块可被用来编码/解码几乎所有类型的二进制的数据结构。为了解释清楚这种数据,假设用下面的Python数据结构 来表示一个组成一系列多边形的点的集合:

polys = [
    [ (1.0, 2.5), (3.5, 4.0), (2.5, 1.5) ],
    [ (7.0, 1.2), (5.1, 3.0), (0.5, 7.5), (0.8, 9.0) ],
    [ (3.4, 6.3), (1.2, 0.5), (4.6, 9.2) ],
]

🍞 statistics 模块

🍞 sys 模块

🎯 将字节写入文本文件

将字节数据直接写入文件的缓冲区即可,例如:

>>> import sys
>>> sys.stdout.write(b'Hello\n')
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
TypeError: must be str, not bytes
>>> sys.stdout.buffer.write(b'Hello\n')
Hello
5
>>>

类似的,能够通过读取文本文件的 buffer 属性来读取二进制数据。

I/O系统以层级结构的形式构建而成。 文本文件是通过在一个拥有缓冲的二进制模式文件上增加一个Unicode编码/解码层来创建。 buffer 属性指向对应的底层文件。如果直接访问它的话就会绕过文本编码/解码层。

🍞 tempfile 模块

需要在程序执行时创建一个临时文件或目录,并希望使用完之后可以自动销毁掉。

tempfile 模块中有很多的函数可以完成这任务。 为了创建一个匿名的临时文件,可以使用 tempfile.TemporaryFile

from tempfile import TemporaryFile

with TemporaryFile('w+t') as f:
    # Read/write to the file
    f.write('Hello World\n')
    f.write('Testing\n')

    # Seek back to beginning and read the data
    f.seek(0)
    data = f.read()

# Temporary file is destroyed

🎯 创建临时文件和文件夹

🍞 time 模块

import time

# 当前时间戳(秒)
ts = time.time()
print("当前时间戳:", ts, type(ts))  
# 输出示例: 1681440000.123456 <class 'float'>

# 时间戳 -> 本地时间元组
local = time.localtime(ts)
print("本地时间元组:", local, type(local))  
# time.struct_time(tm_year=..., ...) <class 'time.struct_time'>

# 时间元组 -> 字符串
str_time = time.strftime("%Y-%m-%d %H:%M:%S", local)
print("格式化时间字符串:", str_time, type(str_time))  
# '2025-04-10 15:30:45' <class 'str'>

# 字符串 -> 时间元组
parsed = time.strptime("2025-01-01 12:00:00", "%Y-%m-%d %H:%M:%S")
print("解析后的时间元组:", parsed, type(parsed))  
# time.struct_time(tm_year=2025, ...) <class 'time.struct_time'>

# 时间元组 -> 时间戳
ts_from_struct = time.mktime(parsed)
print("从结构体得到时间戳:", ts_from_struct, type(ts_from_struct))  
# 1735723200.0 <class 'float'>

🍞 time 和 datetime 对比

功能 time 模块 datetime 模块
时间戳操作 time.time() datetime.timestamp()
字符串格式化 strftime / strptime ✅ 同样支持
时间差计算 ❌(需手动计算) datetime - datetime
加减时间 ❌(需要封装) ✅ 使用 timedelta
可读性 & OOP 较底层 更直观、更强大

🍞 weakref 模块(循环引用)

的程序创建了很多循环引用数据结构(比如树、图、观察者模式等),碰到了内存管理难题。

一个简单的循环引用数据结构例子就是一个树形结构,双亲节点有指针指向孩子节点,孩子节点又返回来指向双亲节点。 这种情况下,可以考虑使用 weakref 库中的弱引用。例如:

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

这种是想方式允许parent静默终止。例如:

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

循环引用的数据结构在Python中是一个很棘手的问题,因为正常的垃圾回收机制不能适用于这种情形。 例如考虑如下代码:

# Class just to illustrate when deletion occurs
class Data:
    def __del__(self):
        print('Data.__del__')

# Node class involving a cycle
class Node:
    def __init__(self):
        self.data = Data()
        self.parent = None
        self.children = []

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


>>> a = Data()
>>> del a # Immediately deleted
Data.__del__
>>> a = Node()
>>> del a # Immediately deleted
Data.__del__
>>> a = Node()
>>> a.add_child(Node())
>>> del a # Not deleted (no message)
>>>

可以看到,最后一个的删除时打印语句没有出现。原因是Python的垃圾回收机制是基于简单的引用计数。 当一个对象的引用数变成0的时候才会立即删除掉。而对于循环引用这个条件永远不会成立。 因此,在上面例子中最后部分,父节点和孩子节点互相拥有对方的引用,导致每个对象的引用计数都不可能变成0。

弱引用消除了引用循环的这个问题,本质来讲,弱引用就是一个对象指针,它不会增加它的引用计数。 可以通过 weakref 来创建弱引用。例如:

>>> import weakref
>>> a = Node()
>>> a_ref = weakref.ref(a)
>>> a_ref
<weakref at 0x100581f70; to 'Node' at 0x1005c5410>
>>>

为了访问弱引用所引用的对象,可以像函数一样去调用它即可。如果那个对象还存在就会返回它,否则就返回一个None。 由于原始对象的引用计数没有增加,那么就可以去删除它了。例如;

>>> print(a_ref())
<__main__.Node object at 0x1005c5410>
>>> del a
Data.__del__
>>> print(a_ref())
None
>>>