16. 内建模块

16.1. inspect

inspect 模块用于收集 Python 对象的信息,可以获取类或函数的参数的信息,源码,解析堆栈,对对象进行类型检查等。

我们使用 sample.py 作为测试模块,源码如下:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# -*- coding: utf-8 -*-
"""
Created on Wed Dec 12 17:16:47 2017
@author: Red
"""

sample_str = "sample module"
sample_list = [1, 2, 3]

# This is a function of sample
def sample_func(arg0, args1="name", *args, **kwargs):
    """This is a sample module function."""
    f_var = arg0 + 1
    return f_var

class A():
    """Definition for A class."""

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

    def get_name(self):
        "Returns the name of the instance."
        return self.name

obj_a = A('A Class instance')

class B(A):
    """B class, inherit A class. """

    # This method is not part of A class.
    def cls_func(self):
        """Anything can be done here."""

    def get_name(self):
        "Overrides method from X"
        return 'B(' + self.name + ')'

obj_b = B('B Class instance')

16.1.1. 获取模块信息

getmodulename(path) 方法获取文件名,ismodule(obj) 判断对象是否为模块。

0
1
2
3
4
5
6
7
8
import sample
print(inspect.getmodulename("./sample.py"))
print(inspect.ismodule(sample))
print(inspect.ismodule(1))

>>>
sample
True
False

我们也可以使用 getmembers() 获取更多的模块信息,关于 getmembers() 方法的详细使用请参考下一小节:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
for name, data in inspect.getmembers(sample):
    if name == '__builtins__':
        continue
    if name.startswith('__'):
        print(name, repr(data))

>>>
__cached__ 'Z:\\sdc\\lbooks\\ml\\__pycache__\\sample.cpython-36.pyc'
__doc__ '\nCreated on Wed Dec 12 17:16:47 2017\n@author: Red\n'
__file__ 'Z:\\sdc\\lbooks\\ml\\sample.py'
__loader__ <_frozen_importlib_external.SourceFileLoader object .....>
__name__ 'sample'
__package__ ''
__spec__ ModuleSpec(name='sample', loader=......

16.1.2. getmembers

getmembers(object, predicate=None)
    Return all members of an object as (name, value) pairs sorted by name.
    Optionally, only return members that satisfy a given predicate.

getmembers() 方法非常强大,它可以获取模块,对象成员属性。predicate 用于过滤特定属性的成员。 它返回一个列表,列表中的每个元素是一个形如 (name, value) 的元组。

0
1
2
3
4
print(inspect.getmembers(sample))

>>>
[('A', <class 'sample.A'>), ('B', <class 'sample.B'>), ('__builtins__',
......

由于模块默认继承很多内建属性,它会打印很多信息,内建属性通常以 __ 开头,我们可以进行如下过滤:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
for name,type in inspect.getmembers(sample):
    if name.startswith('__'):
        continue
    print(name, type)

>>>
A <class 'sample.A'>
B <class 'sample.B'>
obj_a <sample.A object at 0x000002B5960E9128>
obj_b <sample.B object at 0x000002B5960E99E8>
sample_func <function sample_func at 0x000002B5960732F0>
sample_list [1, 2, 3]
sample_str sample module

通过 predicate 参数指定 inspect 自带的判定函数,可以获取类,函数等任何特定的信息。

16.1.2.1. 查看模块中的类

0
1
2
3
4
5
for name,type in inspect.getmembers(sample, inspect.isclass):
    print(name, type)

>>>
A <class 'sample.A'>
B <class 'sample.B'>

16.1.2.2. 查看模块中函数

0
1
2
3
4
for name,type in inspect.getmembers(sample, inspect.isfunction):
    print(name, type)

>>>
sample_func <function sample_func at 0x000002B5961F8840>

16.1.2.3. 查看类属性

查看类函数:

0
1
2
3
4
5
for name, type in inspect.getmembers(sample.A, inspect.isfunction):
    print(name, type)

>>>
__init__ <function A.__init__ at 0x000002B5961F8D08>
get_name <function A.get_name at 0x000002B5961F80D0>

16.1.2.4. 查看对象属性

查看对象方法:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
for name, type in inspect.getmembers(sample.obj_a, inspect.ismethod):
    print(name, type)
print()
for name, type in inspect.getmembers(sample.obj_b, inspect.ismethod):
    print(name, type)

>>>
__init__ <bound method A.__init__ of <sample.A object at 0x000002B5961BAA90>>
get_name <bound method A.get_name of <sample.A object at 0x000002B5961BAA90>>

__init__ <bound method A.__init__ of <sample.B object at 0x000002B596117278>>
cls_func <bound method B.cls_func of <sample.B object at 0x000002B596117278>>
get_name <bound method B.get_name of <sample.B object at 0x000002B596117278>>

16.1.3. getdoc 和 getcomments

getdoc(object) 可以获取任一对象的 __doc__ 属性。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
print('A.__doc__:')
print(sample.A.__doc__)
print()
print('getdoc(A):')
print(inspect.getdoc(sample.A))

>>>
A.__doc__:
Definition for A class.

getdoc(A):
Definition for A class.

getcomments() 方法获取模块,函数或者类定义前的注释行,注释必须以 # 开头。

0
1
2
3
4
5
6
print(inspect.getcomments(sample))
print(inspect.getcomments(sample.sample_func))

>>>
# -*- coding: utf-8 -*-

# This is a function of sample

16.1.4. getsource

getsource(object) 可以获模块,函数或者类,类方法的源代码。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
print(inspect.getsource(sample.sample_func))
print(inspect.getsource(sample.B.get_name))

>>>
def sample_func(arg0, arg1="name", *args, **kwargs):
    """This is a sample module function."""
    f_var = arg0 + 1
    return f_var

  def get_name(self):
      "Overrides method from X"
      return 'B(' + self.name + ')'

getsourcelines(object) 返回一个元组,元组第一项为对象源代码行的列表,第二项是第一行源代码的行号。

0
1
2
3
print(inspect.getsourcelines(sample.sample_func))

>>>
(['def sample_func(arg0, *args, **kwargs):\n',...... return f_var\n'], 10)

16.1.5. 函数参数相关

signature() 返回函数的参数列表,常被 IDE 用来做代码提示:

0
1
2
3
4
5
print(inspect.signature(sample.sample_func))
print(inspect.signature(sample.B.get_name))

>>>
(arg0, *args, **kwargs)
(self)

getfullargspec() 将函数参数按不同类型返回。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
arg_spec = inspect.getfullargspec(sample.sample_func)
print('namedkey:', arg_spec[0])
print('*       :', arg_spec[1])
print('**      :', arg_spec[2])
print('defaults:', arg_spec[3])

>>>
namedkey: ['arg0', 'args1']
*       : args
**      : kwargs
defaults: ('name',)

getcallargs() 方法将函数形参与实参绑定,返回一个字典:

0
1
2
3
4
5
6
7
8
def f(a, b=1, *pos, **named):
  pass

print(getcallargs(f, 1, 2, 3) == {'a': 1, 'named': {}, 'b': 2, 'pos': (3,)})
print(getcallargs(f, a=2, x=4) == {'a': 2, 'named': {'x': 4}, 'b': 1, 'pos': ()})

>>>
True
True

16.1.6. getmro

获取继承序列,与类对象的 __mro__ 属性对应:

0
1
2
3
4
5
print(B.__mro__)
print(inspect.getmro(B))

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

16.1.7. 获取调用栈

获取调用栈信息的系列方法均支持 context 参数,默认值为1,可以传入整数值 n 来获取调用栈的上线文的 n 行源码。

16.1.7.1. stack 和 getframeinfo

类似于 C 语言,Python 解释器也使用栈帧(Stack frame)机制来管理函数调用。

stack() 方法获取当前的所有栈帧信息,它是一个 list。getframeinfo() 打印栈帧信息。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def dump_stack(stack):
    for i in stack:
        frame,filename,lineno,funcname,lines,index = i
        print(inspect.getframeinfo(frame))
        print(filename,lineno,funcname,lines,index)

dump_stack(inspect.stack())

>>>
Traceback(filename='tmp.py', lineno=29, function='<module>',
code_context=['dump_stack(inspect.stack())\n'], index=0)
('tmp.py', 29, '<module>', ['dump_stack(inspect.stack())\n'], 0)

可以看到一个栈帧是一个元组,包含文件名,行号,函数名(如果是在函数外调用,则显示模块名),调用 stack() 处的代码和上下文索引 6 个元素。

所谓上下文索引,即调用 stack() 所在语句在源码上下文的编号。如果要获取栈帧信息的更多源码,可以给传入 context 参数,默认为 1。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# before line 2
# before line 1
dump_stack(inspect.stack(3))
# after line 1

>>>

Traceback(filename='tmp.py', lineno=29, function='<module>',
code_context=['dump_stack(inspect.stack(3))\n'], index=0)
('tmp.py', 29, '<module>', ['# before line 1\n', 'dump_stack(inspect.stack(3))\n',
 '# after line 1\n'], 1)

16.1.7.2. trace

trace() 返回异常时的栈帧信息,如果没有异常发生,trace() 返回空列表。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def call():
    try:
        1/0
    except:
        dump_stack(inspect.trace())

call()

>>>

Traceback(filename='tmp.py', lineno=31, function='call',
code_context=['        dump_stack(inspect.trace())\n'], index=0)
('tmp.py', 29, 'call', ['        1/0\n'], 0) # lines 返回触发异常时的代码

这里与 stack() 做一对比,显然 stack() 返回所有栈帧信息,顶层栈帧记录的不是触发异常的代码行,而是调用 stack() 的代码行。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def call():
    try:
        1/0
    except:
        dump_stack(inspect.stack())

call()

>>>
Traceback(filename='tmp.py', lineno=31, function='call',
code_context=['        dump_stack(inspect.stack())\n'], index=0)
('tmp.py', 31, 'call', ['        dump_stack(inspect.stack())\n'], 0)
Traceback(filename='tmp.py', lineno=33, function='<module>',
code_context=['call()\n'], index=0)
('tmp.py', 33, '<module>', ['call()\n'], 0)

16.1.7.3. currentframe

获取当前正在运行的代码行所在的栈帧,也即当前栈帧。

0
1
2
3
4
5
6
7
def dump_frame(frame):
    print(getframeinfo(frame))

dump_frame(inspect.currentframe())

>>>
Traceback(filename='tmp.py', lineno=31, function='<module>',
code_context=['dump_frame(inspect.current)\n'], index=0)

16.1.7.4. getouterframes

getouterframes(frame) 返回从 frame 到栈底的所有栈帧,对于 frame 来说,从它到栈底的帧都被称为外部帧。

0
1
2
3
def current_frame():
    return inspect.currentframe()

stack = inspect.getouterframes(current_frame())

上述代码返回含当前栈帧的所有帧,等同于 stack()。

16.1.7.5. getinnerframes

getinnerframes(traceback) 用于获取一个 traceback 对象中的栈帧。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import sys
try:
    1/0
except:
    prev_cls, prev, tb = sys.exc_info()
    frames = inspect.getinnerframes(tb)
    dump_stack(frames)

>>>
Traceback(filename='tmp.py', lineno=42, function='<module>',
code_context=['    dump_stack(frames)\n'], index=0)
tmp.py 38 <module> ['    1/0\n'] 0

16.2. collections

Python 提供了多种内置数据类型,比如数值型 int、float 和 complex,字符串 str 以及复合数据类型 list、tuple 和 dict。 collections 模块基于这些基本数据类型,封装了其他复杂的数据容器类型。

16.2.1. namedtuple

命名元组(namedtuple)使用 namedtuple() 工厂函数(Factory Function)返回一个命名元组类。 这个类继承自 tuple,用来创建元组类似的对象,对象拥有只读属性,这些属性有对应的名字,可以通过名字访问属性。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from collections import namedtuple

# Point 是一个类,第一个参数定义类名
Point = namedtuple('PointClass', ['x', 'y'])
print(Point.__name__)
# 实例化对象 p
p = Point(1, 2)
print(p)

# 使用索引访问属性
print(p[0], p[0] + p[1])

# 使用属性名访问
print(p.x, p.x + p.y)

>>>
PointClass
PointClass(x=1, y=2)
1 3
1 3

对象拥有只读属性,不可更改属性值。

0
1
2
3
p.x = 5

>>>
AttributeError: can't set attribute

namedtuple() 的第一个参数定义类名,列表参数定义类的属性。 它返回的是一个类,我们可以继承它,来扩展对属性的操作。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Point(namedtuple('PointClass', ['x', 'y'])):
    __slots__ = ()          # 禁止动态添加属性
    @property               # 只读属性,求勾股数
    def hypot(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5
    def __str__(self):
        return 'Point: x=%6.3f  y=%6.3f  hypot=%6.3f' % (self.x, self.y, self.hypot)

for p in Point(3, 4), Point(14, 5/7):
    print(p)

>>>
Point: x= 3.000  y= 4.000  hypot= 5.000
Point: x=14.000  y= 0.714  hypot=14.018

我们要定义一个新类,只是在已有类上添加一些参数,那么定义一个子类就太复杂了,一个简单的方法可以调用类属性 Point._fields,它是一个元组类型。比如扩展 Point 类到三维空间:

0
1
2
3
4
5
6
print(Point._fields)
print(type(Point._fields))
Point3D = namedtuple('Point3D', Point._fields + ('z',))

>>>
('x', 'y')
<class 'tuple'>

命名元组的类方法 _make() 可以接受一个序列对象,方便批量把数据转化为命名元组对象。

0
1
2
3
4
5
6
7
8
Point3D = namedtuple("Point3D", Point._fields + ('z',))
datum = [[1,2,3], [4,5,6], [7,8,9]]
for i in map(Point3D._make, datum):
    print(i)

>>>
Point3D(x=1, y=2, z=3)
Point3D(x=4, y=5, z=6)
Point3D(x=7, y=8, z=9)

16.2.2. Counter

计数器 Counter 用于统计元素的个数,并以字典形式返回,格式为 {元素:元素个数}。

Counter 类继承了 dict ,它的帮助信息中提供了很多示例应用,这里引用如下:

16.2.2.1. 生成计数器

Counter() 类接受如下参数生成一个计数器对象:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
c = Counter()                  # 空计数器
print(c.most_common())

c = Counter('gallahad')        # 可迭代对象
print(c.most_common())

c = Counter({'a': 4, 'b': 2})  # 字典
print(c.most_common())

c = Counter(a=4, b=2)          # 关键字参数指定
print(c.most_common())

>>>
[]
[('a', 3), ('l', 2), ('g', 1), ('h', 1), ('d', 1)]
[('a', 4), ('b', 2)]
[('a', 4), ('b', 2)]

16.2.2.2. 统计字符

0
1
2
3
4
5
6
7
8
from collections import Counter

c = Counter('abcdeabcdabcaba')
print(c.most_common())
print(c.most_common(3))

>>>
[('a', 5), ('b', 4), ('c', 3), ('d', 2), ('e', 1)]
[('a', 5), ('b', 4), ('c', 3)]

示例中统计字符串中各个字符出现的次数,most_common(n=None) 参数 n 指定返回出现最多的字符数,不指定则返回全部。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
c['a']     # 获取单个字符的个数
>>>
5

sorted(c)  # 列出所有不重复的字符
>>>
['a', 'b', 'c', 'd', 'e']

for i in c.elements(): # 访问计数器中的元素
  print(i, end=' ')
>>>
a a a a a b b b b c c c d d e

for i in c.values():  # 访问计数器中的计数值
  print(i, end=' ')
>>>
5 4 3 2 1

c['a'] += 1           # 增加 'a' 的计数值
del c['b']            # 从计数器中删除 'b' 的计数信息

d = c.copy()          # 复制计数器
c.clear()             # 清空计数器
  • elements():返回一个迭代器,元素被重复多少次,在迭代器中就包含多少个此元素,所有元素按字母序排列,个数<= 0 的不罗列。
  • values():返回计数器的统计值,元组类型。

16.2.2.3. 计数器加减

除了以上给出的操作,计时器还可以从其他计数器添加计数信息:

0
1
2
3
4
5
d = Counter('simsalabim')       # 构造计数器 b
c.update(d)                     # 将 b 的计数信息添加到 c 中
c['a']

>>>
9

update() 支持传入可迭代对象和字典,与添加对应,还可以将统计数相减,例如:

0
1
2
3
4
5
6
7
8
c = Counter('which')
c.subtract('witch')
print(c.most_common())

c.subtract('watch')
print(c.most_common())
>>>
[('h', 1), ('w', 0), ('i', 0), ('c', 0), ('t', -1)]
[('h', 0), ('i', 0), ('w', -1), ('c', -1), ('a', -1), ('t', -2)]

subtract() 方法可接受一个可迭代对象或者一个计数器对象。如果没有对应的字符,则计数值为负值。

16.2.3. defaultdict

使用 dict 时,如果 Key 不存在,就会抛出错误 KeyError。如果希望 key 不存在时,返回一个默认值,可以使用 defaultdict。

0
1
2
3
4
5
6
7
ddict = defaultdict(lambda:'Default')
print(ddict['key'])
ddict['key'] = 'val'
print(ddict['key'])

>>>
Default
val

defaultdict 的默认值通过传入的函数返回,这个函数不需要传入参数。下面的示例默认返回一个空列表:

0
1
2
3
4
ddict = defaultdict(list)
print(ddict['key'])

>>>
[]

16.2.4. OrderedDict

dict 的 key 是无序的,OrderedDict 对 dict 进行了扩展,构造一个有序字典。OrderedDict 的 Key 按照插入的顺序排列,后加入的元素追加到字典尾部。

from collections import OrderedDict

0
1
2
3
4
5
6
7
od = OrderedDict()
od['z'] = 1
od['y'] = 2
od['x'] = 3
print(od.keys())

>>>
odict_keys(['z', 'y', 'x'])

16.2.4.1. popitem

OrderedDict 继承了 dict 类,可以像 dict 一样进行插入删除操作。 popitem() 会返回最后一次追加的元素,这种行为就构成了一个字典堆栈。

0
1
2
3
print(od.popitem())

>>>
('x', 3)

OrderedDict 把字典的 popitem() 方法扩展为 popitem(last=True),如果 last 为 False, 则总是从头部删除元素,例如:

0
1
2
3
print(od.popitem(False))

>>>
('z', 1)

16.2.4.2. 移动元素

move_to_end(key, last=True) 方法可以将指定 key 的元素移动到尾部或者头部。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
od = OrderedDict()
od['z'] = 1
od['y'] = 2
od['x'] = 3
print(od.keys())

od.move_to_end('y')        # 'y' 移动到尾部
print(od)
od.move_to_end('y', False) # 'y' 移动到头部
print(od)

>>>
OrderedDict([('z', 1), ('x', 3), ('y', 2)])
OrderedDict([('y', 2), ('z', 1), ('x', 3)])

16.2.4.3. 根据索引 popitem

这里对有序字典进行扩展, 可以通过 popitem(index=None) 删除指定位置的元素:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class IndexOrderedDict(OrderedDict):
    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)

    def popitem(self, index=None):
        if index is None:
            return super().popitem()

        if not isinstance(index, int) or index < 0:
            raise ValueError('Invalid index')

        if index >= len(self.keys()):
            return super().popitem()

        key = list(self.keys())[index]
        value = super().pop(key)
        return key,value

iodict = IndexOrderedDict({'a': 1, 'b' : 2})
iodict['c'] = 3
print(iodict)
item = iodict.popitem(1)
print(item)
print(iodict)

>>>
IndexOrderedDict([('a', 1), ('b', 2), ('c', 3)])
('b', 2)
IndexOrderedDict([('a', 1), ('c', 3)])

16.2.5. deque

使用 list 存储数据时,按索引访问元素很快,但是由于 list 是单向链表,插入和删除元素就很慢了,数据量大的时候,插入和删除效率就会很低。

deque 为了提高插入和删除效率,实现了双向列表,允许两端操作元素,适合用于队列和栈。

16.2.5.1. appendleft 和 popleft

0
1
2
3
4
5
6
7
from collections import deque
dq = deque(['center'])
dq.append('right')
dq.appendleft('left')
print(dq)

>>>
deque(['left', 'center', 'right'])

appendleft() 方法从头部扩展,与此对应 popleft() 方法从头部删除:

0
1
2
3
4
5
print(dq.pop())
print(dq.popleft())

>>>
right
left

16.2.5.2. extendleft

extend() 方法从尾部扩展,extendleft() 从头部扩展。

0
1
2
3
4
5
6
dq = deque(['center'])
dq.extend(['right0', 'right1'])
dq.extendleft(['left0', 'left1'])
print(dq)

>>>
deque(['left1', 'left0', 'center', 'right0', 'right1'])

16.2.5.3. rotate

rotate(n) 方法对元素进行旋转操作,n < 0 向左旋转 n 次,n > 0 向右旋转 n 次,1 次移动一个元素:

0
1
2
3
4
5
6
7
8
dq = deque('abcdef')
dq.rotate(-2)
print(dq)
dq.rotate(2)
print(dq)

>>>
deque(['c', 'd', 'e', 'f', 'a', 'b'])
deque(['a', 'b', 'c', 'd', 'e', 'f'])

16.2.6. ChainMap

ChainMap 将多个字典串联起来,按照参数顺序搜索键,找到即返回。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from collections import ChainMap

first = {'x': 1, 'y': 1}
second = {'y': 2, 'z': 2}

cmap = ChainMap(first, second)
print(cmap)
for i, j in cmap.items():
    print(i, '=', j)

>>>
y = 1
z = 2
x = 1

ChainMap 并不对字典进行拷贝,而是指向原字典。

0
1
2
3
print(id(first['x']), id(cmap['x']))

>>>
1531505776 1531505776

如果修改键值将作用在第一个字典上,即便其他字典存在该键,依然会作用在第一个字典上。

0
1
2
3
4
5
6
7
cmap['a'] = 1
cmap['z'] = 3  # second 存在 'z',依然会添加到 first
print(cmap.maps)
print(first)

>>>
[{'x': 1, 'y': 1, 'a': 1, 'z': 3}, {'y': 2, 'z': 2}]
{'x': 1, 'y': 1, 'a': 1, 'z': 3}

16.2.6.1. maps

maps 属性以元组形式返回所有字典:

0
1
2
3
4
5
6
7
first = {'x': 1, 'y': 1}
second = {'y': 2, 'z': 2}

cmap = ChainMap(first, second)
print(cmap.maps)

>>>
[{'x': 1, 'y': 1}, {'y': 2, 'z': 2}]

16.2.6.2. new_child

new_child() 方法基于已经存在的 ChainMap 对象返回新创建的 ChainMap 对象,可以传入需要新加入的字典,如果不提供参数,则加入空字典 {}。

注意新加入的字典优先级最高。

0
1
2
3
4
5
6
7
three = {'a' : 0}
new_cmap = cmap.new_child(three)
print(new_cmap.maps)
print(id(cmap.maps[0]), id(new_cmap.maps[1]))

>>>
[{'a': 0}, {'x': 1, 'y': 1}, {'y': 2, 'z': 2}]
2101860273536 2101860273536

16.3. struct

struct 模块用来实现 bytes 和其他数据类型的转换,本质上类似 C 语言中的结构体,尝试把多个数据类型打包(格式化)到一个结构体中,这个结构体用 bytes 字节序列表示。

在 C 语言中我们可以通过指针类型转换,轻易的访问任何合法地址的任何一个字节,在 Python 中不支持对地址访问,所以也不能轻易访问到一个对象的任意字节。

Python 尽管提供的了 bytes 类,参考 bytes ,可以把字符串,可迭代整数类型等转换为 bytes 对象,但是却不能将任意一个整数转换为 bytes 对象,更无法对 float 等其他数据类型进行转换。

0
1
2
3
bytes0 = bytes([0x123456])

>>>
ValueError: bytes must be in range(0, 256)

尽管可以使用如下讨巧的方式转换一个整形,但是却不通用。

0
1
2
3
4
5
6
int0 = 0x12345678
hexstr = hex(int0)
bytes0 = bytes.fromhex(hexstr[2:])
print(bytes0)

>>>
b'\x124Vx'

struct 模块可以把任意基本数据类型和 bytes 互相转换,这样就可以使用 python 操作一些二进制文件,比如图片。

16.3.1. pack

pack() 方法通过格式化参数将给定的数据类型转换为 bytes 类型,也即打包成一个 bytes 类型的 struct 对象:

0
1
2
3
4
5
6
7
import struct
sp0 = struct.pack('>I', 0x01020304)
print(type(sp0).__name__)
print(sp0)

>>>
bytes
b'\x01\x02\x03\x04'

第一个参数 ‘>I’ 是格式化字符串,其中 > 表示使用大端字节序,I 表示4字节无符号整数。这里采用大端字节序,转换后的结果高位 0x01 在前。

16.3.2. 格式化字符串

struct 的格式化字符串由两部分组成:字节序和数据类型,字节序是可选项,默认为 @ 。

字节序支持如下几种模式:

  • @:本机字节序,进行数据对齐,填充 0 进行对齐,比如 int 型总是对齐到 sizeof(int)(通常为 4)整数倍的地址。
  • =:本机字节序,不对齐。
  • <:小端字节序,不对齐。
  • > 或 !:大端字节序(网络字节序),不对齐。

可以这样获取本机系统的字节序:

0
1
2
3
4
import sys
print(sys.byteorder)

>>>
little

支持的数据类型缩写参考 struct 支持的数据类型字符

0
1
2
3
4
5
6
7
8
fmts = ['ci', '>ci', '<ci']
for i in fmts:
    sp0 = struct.pack(i, b'*', 0x12131415)
    print('%s\t:' % i, sp0)

>>>
ci      : b'*\x00\x00\x00\x15\x14\x13\x12' # 等价于 @ci
>ci     : b'*\x12\x13\x14\x15'
<ci     : b'*\x15\x14\x13\x12'

可以看出默认的 @ 模式会进行数据的对齐,i 表示 int 类型,由于字符 ‘*’ 占用了 1 个地址,所以填充了 3 个 0 使得后边的 int 对齐到位置 4。

格式化字符串 ‘ci’ 等价于 C 语言中结构体:

0
1
2
3
4
struct ci
{
    char  c;
    int   i;
};

calcsize() 方法可以计算格式化字符对应的对齐后字节大小。

0
1
2
3
4
5
print(struct.calcsize('ci'))
print(struct.calcsize('>ci'))

>>>
8
5

如果要格式化的参数有多个是连续类型的,例如 10 个连续的字符类型,那么无需写成 10个重复的 c,而直接用 10c 表示:

0
1
2
3
4
sp0 = struct.pack('2c2i', b'*', b',', 0x12, 0x13)
print(sp0)

>>>
b'*,\x00\x00\x12\x00\x00\x00\x13\x00\x00\x00'

在默认对齐方式下,我们可以指定尾部对齐到的类型,比如 ‘0l’ 表示对齐到 long int 型。

0
1
2
3
4
sp0 = struct.pack('llh0l', 1, 2, 3)
print(sp0)

>>>
b'\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00'

也可以把多个要转换的对象放在元组里面,通过可变参数传递:

0
1
2
3
4
5
6
values = (b'*', 3.14, 'ab'.encode('utf-8'))
s = struct.Struct('c f s') # 空格用于分隔各个格式化字符,被忽略
sp0 = s.pack(*values)
print(sp0)

>>>
b'*\x00\x00\x00\xc3\xf5H@a'

16.3.3. unpack

unpack() 是 pack() 的逆运算,进行解包处理。结果以元组形式返回。

0
1
2
3
4
5
sp0 = struct.pack('lch0l', 1, b'*', 3)
up0 = struct.unpack('lch0l', sp0)
print(up0)

>>>
(1, b'*', 3)

如果 unpack 与 pack 使用的格式化串不一致,会抛出异常 struct.error。

0
1
2
3
4
sp0 = struct.pack('lch0l', 1, b'*', 3)
up0 = struct.unpack('llh', sp0)

>>>
error: unpack requires a bytes object of length 10

16.3.4. 使用 buffer

pack_into(fmt, buffer, offset, v1, v2, ...)
unpack_from(fmt, buffer, offset=0) -> (v1, v2, ...)

pack_into() 向 buffer[offset] 中打包,unpack_from() 从 buffer[offset] 中解包。注意偏移参数指定了打包写入或者解包读取的位置。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
values0 = (b'abc', 3.1415926)
values1 = (b'zyz', 0x12)

s0 = struct.Struct('3sf')
s1 = struct.Struct('3sI')

buffer = bytearray(s0.size + s1.size)
print(buffer)
s0.pack_into(buffer, 0, *values0)
s1.pack_into(buffer, s0.size, *values1)
print(buffer)
print(s0.unpack_from(buffer, 0))
print(s1.unpack_from(buffer, s0.size))

>>>
bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
bytearray(b'abc\x00\xda\x0fI@xyz\x00\x12\x00\x00\x00')
(b'abc', 3.141592502593994)
(b'xyz', 18)

16.3.5. 读取 bmp 头部

一个用 C 语言定义的结构体 bmp_header 描述了一张 bmp 格式的图片头部,转化为 struct 的格式化字符串为 ‘2sIHHI’。

0
1
2
3
4
5
struct bmp_header{
   char  id[2];                 /* 'BM'  */
   unsigned int size;           /* 文件大小,单位 bytes     */
   unsigned short int reserved1, reserved2;
   unsigned int offset;         /* 图像数据偏移,单位 bytes */
};

我们使用 ‘2sIHHI’ 进行 unpack 操作:

0
1
2
3
4
5
6
with open('test.bmp', 'rb') as f:
    header = f.read(struct.calcsize('<2sIHHI'))
    data = struct.unpack('<2sIHHI', header)
    print(data)

>>>
(b'BM', 1214782, 0, 0, 62)

注意 bmp 文件采用小端字节序存储,解读出来的文件大小为 1214782 bytes,与实际相符。

16.4. pickle

有时候我们需要把 Python 对象存储到文件中,以便下次直接使用,而不用再重新生成它,比如机器学习中训练好的模型。或者我们需要在网络中传递一个 Python 对象以进行协同计算,这就需要对 Python 进行字节序列化。

pickle 可以把大部分 Python 数据类型,例如列表,元组和字典,甚至函数,类和对象(只可本地保存和加载, 参考官方 pickle 协议 )进行序列化。如果要在不同版本的 Python 间共享对象数据,需要注意转换协议的版本,可以在 dump() 和 dumps() 方法中指定,默认版本 3 只适用于 Python3。

16.4.1. 导出到文件

pickle.dump() 方法将对象导出到文件,pickle.load() 加载对象。可以同时序列化多个对象,加载时顺序与写入时顺序一致。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
list0 = [1, 2, 3]
tuple0 = ('a', 'b', 'c')
with open('obj.txt','wb') as fw:
    pickle.dump(list0, fw)
    pickle.dump(tuple0, fw)

with open('obj.txt','rb') as fr:
    rlist = pickle.load(fr)
    rtuple = pickle.load(fr)
    print(rlist, rtuple)

>>>
[1, 2, 3] ('a', 'b', 'c')

16.4.2. 生成字节序列

pickle.dumps() 将对象转化为一个字节序列,pickle.loads() 将序列转换回对象:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
list0 = [1, 2, 3]
p = pickle.dumps(list0)
print(type(p).__name__)
print(p)

list0 = pickle.loads(p)
print(list0)

>>>
bytes
b'\x80\x03]q\x00(K\x01K\x02K\x03e.'
[1, 2, 3]

16.5. hashlib

Hash 算法又称为散列算法。通过它可以把任意长度的输入变换成固定长度的输出,该输出就是哈希值,或者散列值。这种转换是一种压缩映射,散列值的空间通常远小于输入的空间,而对于给定的散列值,没有实用的方法可以计算出一个原始输入,也就是说很难伪造,所以常常把散列值用来做信息摘要(Message Digest)。

散列算法有两个显著特点:

  1. 原始数据的微小变化(比如一个 1bit 翻转)会导致结果产生巨大差距。
  2. 运算过程不可逆,理论上无法从结果还原输入数据。

hashlib 提供了多种常见的摘要算法,如 md5,sha,blake等。

16.5.1. md5

md5 是最常见的摘要算法,速度快,生成结果是固定的 128 bits,通常用一个32位的16进制字符串表示。

digest() 返回 bytes 字节序列类型,hexdigest() 则直接返回 16 进制的字符串。传入参数必须是 bytes 类型。

0
1
2
3
4
5
6
7
8
9
import hashlib

md5 = hashlib.md5()
md5.update('你好 Python'.encode('utf-8'))
print(md5.digest())
print(md5.hexdigest())

>>>
b'\x888]P\x8fT[\x83\x1aCj\x80\xad$\x14\x19'
88385d508f545b831a436a80ad241419

如果需要计算的数据非常大,那么可以循环调用 update() 方法,例如下面的示例:

0
1
2
3
4
5
6
md5 = hashlib.md5()
md5.update('你好 '.encode('utf-8'))
md5.update(b'Python')
print(md5.hexdigest())

>>>
88385d508f545b831a436a80ad241419

计算结果与上面的例子是一样的,所以在进行新的数据散列计算时,需要重新生成 md5 对象。

16.5.2. sha1 和 sha2

sha 是英文 Secure Hash Algorithm 的缩写,简称为安全散列算法。

sha1 和 md5 均由 md4 发展而来,sha1 摘要比 md5 摘要长 32 bits,所以抗攻击性更强。sha224 等是 sha1 的加强版本,其中数字表示生成摘要的 bits,换算为结果的长度为 x/8*2。sha1 命名比较特殊,实际上它就是 sha160,摘要长度为 160/8*2 = 40 个字符。

sha224,sha256,sha384 和 sha512 被称为 sha2 算法。

通常信息摘要的结果越长,冲突碰撞越不易发生,安全性越高,生成的速度也越慢,当然占用的存储空间也越大。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
shas = ['sha1', 'sha224', 'sha256', 'sha384', 'sha512']
for i in shas:
    sha = eval('hashlib.' + i + '()')
    sha.update('你好 Python'.encode('utf-8'))
    print('%s(%d)\t:' % (i, len(sha.hexdigest())), sha.hexdigest())

>>>
sha1(40)        : 8ee264aee7e58f2e680587eacd9535d81e1c07fd
sha224(56)      : 8ecbd4ea2e3037db9952f6e8e1df2e1e142d9756e0e26d2825e60757
sha256(64)      : d87357274f3e88a5b2473719f15db8804ae8b4737f75b4f05c6dc2b0d8c8ae80
sha384(96)      : c8254f627ae7de62c2ab8e94807af30c90ee01169f41e33ec0c90f808535bdd33
                  a8b89d44c6f790b298622f35e078dff
sha512(128)     : 6a3cb3ae283d3491310dde804d196849f2acce6ecaa34156ac9d08e6b2c33e557
                : a319d507dbdbf05e8d3c7ea7c6308a3c60303921f68701b768d2982752dd8f4

16.5.3. sha3

鉴于 sha-1 和 sha-2 潜在的安全性问题,sha3 采用了全新的 Keccak 算法,运算更快,安全性更高。它 sha2 系列算法一样,提供了不同版本。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
shas = ['sha3_224', 'sha3_256', 'sha3_384', 'sha3_512']
for i in shas:
    sha = eval('hashlib.' + i + '()')
    sha.update('你好 Python'.encode('utf-8'))
    print('%s(%d)\t:' % (i, len(sha.hexdigest())), sha.hexdigest())

sha3_224(56)    : b1ab601916f7663941fab552dadb19b73a879bf0a37a5664a80a3f26
sha3_256(64)    : 7e2329a2dc0e4f64ea93e35cb99319c6cf06b2c5ead6d61e1e77a9164be6cae5
sha3_384(96)    : 77172e65b6de373864915a874ba927ecda4fb5cab9e9c8c7dde50d4e3fc4f88a
                  9f6bb9601e268f5cf183eb6f3e94a3ad
sha3_512(128)   : ec23b19dddb658d53d9bb4166c6b1958b2b55293a8d2155bdd05e8644af5ade6
                  cdf7a0d2d58ab9c22c3ccac995b1e9be8d0c71ca60b9b362a38a7d109bb36121

16.5.4. blake2

随着硬件计算能力的提升,对 md5 和 sha1 进行伪造原始数据已不再十分困难,blake2 就是为了迎接这一挑战而出现的。

blake2 系列比常见的 md5,sha1,sha2 和 sha3 运算更快,同时提供不低于 sha3 的安全性。blake2 系列从著名的 ChaCha 算法衍生而来,有两个主要版本 blake2b(blake2)和 blake2s 。

  • blake2b 针对 64 位 CPU 优化设计,可以生成最长 64 字节的摘要。
  • blake2s 针对 32 位 CPU 设计,可以生成最长 32 字节的摘要。
0
1
2
3
4
5
6
7
8
9
blake2 = ['blake2b', 'blake2s']
for i in blake2:
    blake = eval('hashlib.' + i + '()')
    blake.update('你好 Python'.encode('utf-8'))
    print('%s(%d)\t:' % (i, len(blake.hexdigest())), blake.hexdigest())

>>>
blake2b(128)    : 182359731af5836fbcffc536defd712acf4fbcfa4065186b40b13c4945602cf0
                  709c4c44b4ad287be1b3b4e7af907b6e43eff442cd756328344eaeeb4a0eda9d
blake2s(64)     : f3b98aeb02a459a950427a4c22fd6a669dde484eeae0f493c2ea51c909e63065

16.5.5. shake

shake128 和 shake256 算法是支持可变长度的信息摘要算法,可输出不大于 128 或 256 的任意比特数。

Python 中通过 hexdigest(n) 传递参数 n 指定输出的字节数。

0
1
2
3
4
5
6
7
8
shas = ['shake_128', 'shake_256']
for i in shas:
    sha = eval('hashlib.' + i + '()')
    sha.update('你好 Python'.encode('utf-8'))
    print('%s(%d)\t:' % (i, len(sha.hexdigest(10))), sha.hexdigest(10))

>>>
shake_128(20)   : 72877ec8143a659e8002
shake_256(20)   : c4118461f9ebbeb02d3e

16.5.6. 实用场景

实用散列值的几种场景:

  • 信息摘要,用于验证原始文档是否被篡改。
  • 存储口令密码,无论是本地计算机还是服务器都不会明文存储密码,通常可以存储密码的散列值,这样即便是技术人员也无法获取用户口令。
  • 网络传输,明文口令绝不应该出现在网络传输中,通常使用的挑战应答(Challenge-Response)密码验证方法就是通过传输散列值完成。
  • 信息摘要类似于一个文件的指纹,同样可以用于相同文件的查找,或者检查两个文件是否相同,比如网盘数据库,不可能为每一个用户维护相同的文件,相同文件只要保存一份即可。

计算文件的 md5 值,示例:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def file_md5(file):
    import hashlib
    with open(file, 'rb') as f:
        md5 = hashlib.md5()
        while True:
            data = f.read(10240)
            if len(data) > 0:
                md5.update(data)
            else:
                break

    return md5.hexdigest()

16.6. hmac

彩虹表是高效的密码散列值攻击方法,为了应对这一挑战,应该在每次对密码进行散列时,加入一个随机的密码值(也称为盐值),这样每次生成的散列值都是变化的,增大了破解难度。

HMAC(Keyed-hash Message Authentication Code 基于密码的散列消息身份验证码)利用哈希算法,以一个密钥和一个消息为输入,在生成消息摘要时将密码混入消息进行计算。

hmac 模块自身不提供散列算法,它借助 hashlib 中的算法实现 HMAC。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
key = b'1234567890'
hash_list = ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', 'blake2b',
             'blake2s','sha3_224', 'sha3_256', 'sha3_384', 'sha3_512']

for i in hash_list:
    h = hmac.new(key, b'Hello ', digestmod=i)
    h.update(b'Python')
    print(h.hexdigest())

>>>
d9a6f4a6e6c986332e337cff24e153ef
fe6ab8031ce989fd7e9da20f2adf80a609c04a0e
......

16.6.1. 实用场景

hmac 主要用于密码认证,通常步骤如下:

  • 服务器端生成随机的 key 值,传给客户端。
  • 客户端使用 key 将帐号和密码做 HMAC ,生成一串散列值,传给服务器端。
  • 服务端使用 key 和数据库中用户和密码做 HMAC 计算散列值,比对来自客户端的散列值。

这就是挑战应答(Challenge-Response)密码验证方式的基本步骤。

16.7. itertools

itertools 模块提供了一组常用的无限迭代器(生成器)以及一组高效的处理迭代器的函数集。

16.7.1. 无限迭代器

16.7.1.1. count

count(start=0, step=1) --> count object

count 生成一个累加迭代器,用于生成从 start 开始的等差为 step 的数列,默认则从 0 开始,每次加 1。

由于 Python3.0 开始对数值大小不再有限制,所以它是一个无限生成器。

0
1
2
3
4
5
6
7
8
import itertools
uints = itertools.count()
for n in uints:
    if n > 10:
        break
    print(n, end=' ')

>>>
0 1 2 3 4 5 6 7 8 9 10

start 和 step 参数可以为负数和小数,不支持复数。

0
1
2
3
4
5
6
7
8
9
uints = itertools.count(1.1, -0.1)
for n in uints:
    if n < 0:
        break
    print(n, end=' ')

>>>
1.1 1.0 0.9 0.8 0.7000000000000001 0.6000000000000001 0.5000000000000001
0.40000000000000013 0.30000000000000016 0.20000000000000015 0.10000000000000014
1.3877787807814457e-16

可以借用它为列表进行编号,例如:

0
1
2
3
4
5
6
7
list0 = ['a', 'b', 'c']
for i in zip(itertools.count(0), list0):
    print(i)

>>>
(0, 'a')
(1, 'b')
(2, 'c')

16.7.1.2. cycle

cycle(iterable) --> cycle object

cycle() 会把传入的可迭代对象无限重复循环取值:

0
1
2
3
4
for i in itertools.cycle([1,2,3]):
    print(i, end=' ')

>>>
1 2 3 1 2 3......

16.7.1.3. repeat

repeat(object [,times]) -> create an iterator which returns the object
  for the specified number of times.  If not specified, returns the object
  endlessly.

repeat() 创建一个迭代器,重复生成 object,times 指定重复计数,如果未提供 times,将无限返回该对象。

0
1
2
3
4
5
6
for i in itertools.repeat('abc', 3):
    print(i)

>>>
abc
abc
abc

16.7.2. takewhile 和 dropwhile

takewhile 和 dropwhile 可以为迭代器添加条件。

16.7.2.1. takewhile

takewhile(predicate, iterable) --> takewhile object
  Return successive entries from an iterable as long as the
  predicate evaluates to true for each entry.

predicate 是一个断言函数,只要返回 Flase,停止迭代。它返回一个新的迭代器。

0
1
2
3
4
5
6
7
uints = itertools.count(0)
tw = itertools.takewhile(lambda x: x <= 10, uints)

for i in tw:
    print(i, end=' ')

>>>
0 1 2 3 4 5 6 7 8 9 10

16.7.2.2. dropwhile

dropwhile(predicate, iterable) --> dropwhile object
  Drop items from the iterable while predicate(item) is true.
  Afterwards, return every element until the iterable is exhausted.

dropwhile() 与 takewhile() 相仿,当 predicate 断言函数返回 True 时丢弃生成的元素,一旦返回 False,返回迭代器中剩下来的项。它返回一个新的迭代器。

0
1
2
3
4
5
dw = itertools.dropwhile(lambda x: x < 3, [1, 2, 3, 0])
for i in dw:
    print(i, end=' ')

>>>
3 0

16.7.3. chain

chain(*iterables) --> chain object

chain() 可以把一组迭代对象串联起来,形成一个新的迭代器,返回的元素按迭代对象在参数中出现的顺序,依次取出。

0
1
2
3
4
for i in itertools.chain('abc', [1, 2, 3]):
    print(i, end=' ')

>>>
a b c 1 2 3

16.7.4. groupby

groupby(iterable[, keyfunc]) -> create an iterator which returns
    (key, sub-iterator) grouped by each value of key(value).

groupby() 把迭代器中相邻的重复元素归类到一个组,每一个组都是一个迭代器。不相邻元素不会归类到同一个组:

0
1
2
3
4
5
6
7
for key, group in itertools.groupby('aabbcca'):
    print(key, [i for i in group])

>>>
a ['a', 'a']
b ['b', 'b']
c ['c', 'c']
a ['a']

可以为 groupby() 指定一个 keyfunc,只要作用于函数的元素返回的值相等,就被归类在一组,而函数返回值作为该组的 key 。

下面的例子用于从字符串中挑选出数字和非数字:

0
1
2
3
4
5
6
7
for key, group in itertools.groupby('a0b1', lambda x: x.isdigit()):
    print(key, [i for i in group])

>>>
False ['a']
True ['0']
False ['b']
True ['1']

我们把实现稍加改造,就可以把元素分类到多个组中:

0
1
2
3
4
5
6
result = {True : [], False : []}
for key, group in itertools.groupby('a0b1', lambda x: x.isdigit()):
    result[key] += [i for i in group]
print(result)

>>>
{True: ['0', '1'], False: ['a', 'b']}

16.7.5. compress

compress(data, selectors) --> iterator over selected data

compress() 类似 filter() 函数,只是它接受一个选择器,如果选择器的值为 True,非0值,非 ‘n’ 则返回元素,否则被过滤掉。

0
1
2
3
4
5
6
7
selector = [True, False, 1, 0, -1, 'y', 'n']
val_list = [str(i) for i in selector]
print(val_list)
for item in itertools.compress(val_list, selector):
    print(item, end=' ')

True 1 -1 y n
>>>

16.7.6. islice

islice(iterable, stop) --> islice object
islice(iterable, start, stop[, step]) --> islice object

islice() 类似序列对象的切片操作,通过索引来选择元素。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 类似 string[:5]
for i in itertools.islice(itertools.count(), 5):
    print(i, end=' ')
print()

# 类似 string[5:10]
for i in itertools.islice(itertools.count(), 5, 10):
    print(i, end=' ')
print()

# 类似 string[0:100:10]
for i in itertools.islice(itertools.count(), 0, 100, 10):
    print(i, end=' ')
print()

>>>
0 1 2 3 4
5 6 7 8 9
0 10 20 30 40 50 60 70 80 90

16.7.7. 排列组合

16.7.7.1. permutations

permutations(iterable[, r]) --> permutations object
    Return successive r-length permutations of elements in the iterable.

permutations() 返回一个迭代器,迭代器生成可迭代对象中选取 r 个元素的所有排列组合。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
for item in itertools.permutations([1, 2, 3], 2):
    print(item)

for item in itertools.permutations(range(3)):
    print(item)

>>>
(1, 2)
(1, 3)
......
(0, 1, 2)
(0, 2, 1)
(1, 0, 2)
......

16.7.7.2. combinations

combinations(iterable, r) --> combinations object
    Return successive r-length combinations of elements in the iterable.

combinations() 返回一个迭代器,迭代器生成可迭代对象中选取 r 个元素的所有组合,不考虑排列顺序。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for item in itertools.combinations([1, 2, 3], 2):
    print(item)

for item in itertools.combinations(range(3)):
    print(item)

>>>
(1, 2)
(1, 3)
(2, 3)
(0, 1, 2)

combinations_with_replacement() 包含只有元素重复自身形成的组合:

0
1
2
3
4
5
6
7
8
9
for item in itertools.combinations_with_replacement([1, 2, 3], 2):
    print(item)

>>>
(1, 1)
(1, 2)
(1, 3)
(2, 2)
(2, 3)
(3, 3)

16.7.8. 笛卡尔积

product(*iterables, repeat=1) --> product object
    Cartesian product of input iterables.  Equivalent to nested for-loops.

product() 返回多个可迭代对象的所有排列组合,也即笛卡尔积。

0
1
2
3
4
5
6
7
8
gp = itertools.product((1, 2), ('a', 'b'))
for i in gp:
    print(i)

>>>
(1, 'a')
(1, 'b')
(2, 'a')
(2, 'b')

repeat 指定可迭代对象中的每个元素可以重复次数。

0
1
2
3
4
5
6
7
8
9
gp = itertools.product((1, 2), ('a', 'b'), repeat=2)
for i in gp:
    print(i)

>>>
(1, 'a', 1, 'a')
(1, 'a', 1, 'b')
(1, 'a', 2, 'a')
(1, 'a', 2, 'b')
......

16.7.9. starmap

starmap() 创建一个用传入的函数和可迭代对象计算的迭代器。 map() 和 starmap() 的区别在于参数传递方式:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func = lambda x, y: (x, y, x * y)
for i in itertools.starmap(func, [(0, 5), (1, 6)]):
    print('%d * %d = %d' % i)

for i in map(func, [0, 1], [5, 6]):
    print('%d * %d = %d' % i)

>>>
0 * 5 = 0
1 * 6 = 6
0 * 5 = 0
1 * 6 = 6

16.7.10. 迭代器复制 tee

tee(iterable, n=2) --> tuple of n independent iterators.

tee() 从一个可迭代对象创建 n 个独立的迭代器。类似于复制生成了多个迭代器。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 等价操作
import copy
uint0 = itertools.count(0)
uint1 = copy.deepcopy(uint0)
print(next(uint0), next(uint1))

uint2, uint3 = itertools.tee(itertools.count(0), 2)
print(next(uint2), next(uint3))

>>>
0 0
0 0

16.7.11. 累积 accumulate

accumulate(iterable[, func]) --> accumulate object
    Return series of accumulated sums (or other binary function results).

accumulate() 生成的迭代器返回累积求和结果,默认进行求和,可以通过传入不同的函数完成特定操作:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 默认累积求和
ac = itertools.accumulate([1, 2, 3, 4])
print([i for i in ac])

# 累积乘积
func = lambda x, y: x * y
ac = itertools.accumulate([1, 2, 3, 4], func)
print([i for i in ac])

>>>
[1, 3, 6, 10]
[1, 2, 6, 24]

注意和 functools.reduce() 的区别,reduce() 直接返回累积结果,参考 reduce

16.7.12. filterfalse

filterfalse(function or None, sequence) --> filterfalse object
    Return those items of sequence for which function(item) is false.
    If function is None, return the items that are false.

filterfalse() 与 filter() 恰恰相反,它在断言函数返回 False 时把值加入生成器,否则舍弃。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
dw = filter(lambda x: x < 3, [1, 2, 3, 0])
print([i for i in dw])

dw = itertools.filterfalse(lambda x: x < 3, [1, 2, 3, 0])
print([i for i in dw])

# 传入 None 返回值为 False 的元素
dw = itertools.filterfalse(None, [1, 2, 3, 0])
print([i for i in dw])
>>>
[1, 2, 0]
[3]
[0]

16.7.13. zip_longest

zip_longest(iter1 [,iter2 [...]], [fillvalue=None]) --> zip_longest object

zip_longest() 与 zip() 函数很像,参考 zip 合并 ,用于将两个或多个可迭代对象配对。只是当可迭代对象的长度不同,可以指定默认值。

0
1
2
3
4
5
6
for i in zip_longest('123', 'ab', 'xy', fillvalue='default'):
  print(i)

>>>
('1', 'a', 'x')
('2', 'b', 'y')
('3', 'default', 'default')

16.8. operator

Python 内建了 operator 和 functools 模块来支持函数式编程。

operator 将大部分的运算操作符(算术运算,逻辑运算,比较运算,in 关系等等)都定义了等价函数。例如加法运算:

0
1
2
def add(a, b):
    "Same as a + b."
    return a + b

operator 模块提供的这类函数可以取代很多 lambda 匿名函数,让代码更简洁和易懂,下面是一个求阶乘的函数示例:

0
1
2
3
4
5
6
7
def factorial(n):
    from functools import reduce
    return reduce(lambda x, y: x * y, range(1, n + 1))

def factorial(n):
    from functools import reduce
    import operator
    return reduce(operator.mul, range(1, n + 1))

operator 除了定义了运算符等价函数,最重要的是它还定义了一组对象元素和属性的操作函数,它把属性访问转化成一个可调用(Callable)对象。

16.8.1. attrgetter

attrgetter(attr, ...) --> attrgetter object
    Return a callable object that fetches the given attribute(s) from its operand.

attrgetter() 可以把对象属性转换为一个可调用对象,作为参数传递给其他函数,以实现批量处理。它等价于如下匿名函数:

0
lambda obj, n='attrname': getattr(obj, n):

下面的示例批量获取一个类实例的 arg 属性值:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import operator
class AttrObj():
   def __init__(self, arg):
        self.arg = arg

objs = [AttrObj(i) for i in range(5)]
ga = operator.attrgetter('arg')

# 等价于 [getattr(i, 'arg') for i in objs]
vals = [ga(i) for i in objs]
print(vals)

>>>
[0, 1, 2, 3, 4]

排序函数 sorted 参数 key 接受一个可调用对象,使用它的返回值作为排序关键字,示例中使用学生年龄排序:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Student():
    def __init__(self, name, grade, age):
        self.name = name
        self.grade = grade
        self.age = age
    def __repr__(self):
        return repr((self.name, self.grade, self.age))

student_objects = [
        Student('John', 'A', 15),
        Student('Jane', 'B', 12),
        Student('Davie', 'B', 10),
    ]

print(sorted(student_objects, key=operator.attrgetter('age')))

>>>
[('Davie', 'B', 10), ('Jane', 'B', 12), ('John', 'A', 15)]

attrgetter() 还可以传入多个属性,返回一个包含各个属性值的元组:

0
1
2
3
4
ga = operator.attrgetter('age', 'name', 'grade')
print([ga(i) for i in student_objects])

>>>
[(15, 'John', 'A'), (12, 'Jane', 'B'), (10, 'Davie', 'B')]

16.8.2. itemgetter

itemgetter() 把字典的键值访问转换为一个可调用对象。

0
1
2
3
4
5
6
7
8
list0 = [dict(val = -1 * i) for i in range(4)]
print(list0)

ga = operator.itemgetter('val')
print(ga(list0[0]))

>>>
[{'val': 0}, {'val': -1}, {'val': -2}, {'val': -3}]
0

可以指定字典中的特定键值来对字典进行排序:

0
1
2
3
print(sorted(list0, key=ga))

>>>
[{'val': -3}, {'val': -2}, {'val': -1}, {'val': 0}]

itemgetter() 也支持多个参数,同时传入多个键,返回一个元组:

0
1
2
3
4
5
list0 = [{'name':'John', 'age': 15, 'grade' : 'A'},
         {'name':'Jane', 'age': 18, 'grade': 'B'}]
print(sorted(list0, key=operator.itemgetter('name', 'age')))

>>>
[{'name': 'Jane', 'age': 18, 'grade': 'B'}, {'name': 'John', 'age': 15, 'grade': 'A'}]

sorted() 根据元组进行排序,首先按名字排序,对于名字无法区分顺序的再按年龄排序。

16.8.3. methodcaller

methodcaller() 将实例的方法转换为可调用对象,可以把实例作为参数。尽管使用 attrgetter() 也可以间接实现调用,但是没有 methodcaller() 直接和简单。例如:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Student():
    def __init__(self, name, grade, age):
        self.name = name
        self.grade = grade
        self.age = age
    def __repr__(self):
        return repr((self.name, self.grade, self.age))
    def print_name(self):
        print(self.name)

# 采用 attrgetter 方式
student = Student('John', 'A', 15)
ga = operator.attrgetter('print_name')
ga(student)()

# 采用 methodcaller 方式
mh = operator.methodcaller('print_name')
mh(student)

>>>
John
John

16.9. contextlib

contextlib 是一个用于生成上线文管理器(Context Manager)模块,它提供了一些装饰器,可以把一个生成器转化成上下文管理器。

所谓上下文管理器,就是实现了上下文方法(__enter__ 和 __exit__)的对象,采用 with as 语句,可以在执行一些语句前先自动执行准备工作,当语句执行完成后,再自动执行一些收尾工作。参考 __enter__ 和 __exit__

16.9.1. contextmanager

要实现一个自定义的上下文管理器,就需要定义一个实现了__enter__和__exit__两个方法的类,这很麻烦, contextmanager 是一个装饰器,可以把生成器装换成上下文管理器,在 with as 语句中调用。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from contextlib import contextmanager
@contextmanager
def ctx_generator():
    print("__enter__")   # 这里做 __enter__ 动作
    yield 1
    print("__exit__")    # 这里做 __exit__ 动作

print(type(ctx_generator()))
with ctx_generator() as obj:
    print(obj)

>>>
__enter__
1
__exit__

当然我们也可以不返回任何对象,比如锁机制,这时只需要使用 with 语句:

0
1
2
3
4
5
6
7
8
@contextmanager
def locked(lock):
    lock.acquire()
    yield
    lock.release()

with locked(lock):
    ......
# 自动释放锁

16.9.2. closing 类

contextlib 中定义了一个 closing 类,这个类的定义很简单,它把传入的对象转换成一个支持 with as 语句上下文管理器。

0
1
2
3
4
5
6
class closing(AbstractContextManager):
    def __init__(self, thing):
        self.thing = thing
    def __enter__(self):
        return self.thing
    def __exit__(self, *exc_info):
        self.thing.close()

可以看到 closing 类会把传入的对方赋值给 with as 后的变量,并在 with 语句块退出时执行对象的 close() 方法。

0
1
2
3
4
5
6
7
8
9
from contextlib import closing
class CloseCls():
    def close(self):
        print("close")

with closing(CloseCls()):
    pass

>>>
close

16.9.3. 注意事项

contextlib 主要用于用户自定义的类或者自定义的上线文管理器,大部分的 Python 内置模块和第三方模块都已经实现了上线文管理器方法,例如 requests 模块, 首先应该尝试 with 语句。

0
1
with requests.Session() as s:
    ......

即便一个对象没有实现上线文管理器方法,系统也会给出报错提示,然后再借用 contextlib。

0
1
2
3
4
with object:
    pass

>>>
AttributeError: __enter__

16.10. json

JSON(JavaScript Object Notation, JS对象标记法)是一种轻量级的数据交换格式。它原是 JavaScript 用于存储交换对象的格式, 采用完全独立于编程语言的文本格式来存储和表示数据。由于它的简洁和清晰的层次结构,易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率,使得 JSON 成为理想的数据交换格式,被很多语言支持。 相对于 xml 格式,JSON 没有过多的冗余标签,编辑更简洁,更轻量化。

16.10.1. JS对象的 JSON 表示

在 JavaScript 中,任何支持的类型都可以通过 JSON 来表示,例如字符串、数字、对象、数组等。这里简单看下 JSON 是如何表示这些对象的:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// test.js
var num0 = 0
var num1 = 3.14
var str0 = "string"
var bool0 = true
var bool1 = false
var n = null
var array = [1, "abc"]

str = JSON.stringify(num0);
console.log(str)
//......

JSON.stringify() 实现 JS 数据类型向 JSON 格式的转换,由于 JSON 永远是由可视的字符串构成,可以直接打印到终端。以上内容保存在 test.js 中,通过 nodejs test.js 查看输出结果:

0
1
2
3
4
5
6
0
3.14
"string"
[1,"abc"]
true
false
null

与各种编程语言类似,数组以 [] 表示,字符串放在双引号(JSON 不支持单引号)中,其他数字,布尔量和 null 直接输出值。再看下一个对象是怎么表示的:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// test1.js
testObj = new Object();
testObj.str0="string";
testObj.num0=0;
testObj.num1=3.14;
testObj.array=[1, 'abc'];
testObj.bool0=true;
testObj.bool1=false;
testObj.nop=null;
testObj.subobj = new Object();

str = JSON.stringify(testObj, null, 2);
console.log(str)

一个对象由 {} 表示,内容为键值对,每个键都是一个字符串,是对象的属性名,而值可以为其他任意数据类型。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "str0": "string",
  "num0": 0,
  "num1": 3.14,
  "array": [
    1,
    "abc"
  ],
  "bool0": true,
  "bool1": false,
  "nop": null,
  "subobj": {}
}

将 JSON 格式转换为 JS 对象使用 JSON.parse():

0
1
2
str = JSON.stringify(testObj, null, 2);
newObj = JSON.parse(str)
console.log(newObj.str0) // 输出 string

16.10.2. JSON特殊字符

JSON 中的特殊字符有以下几种,使用时需要转义:

  • ” ,字符串由双引号表示,所以字符串中出现 ” 需要转义。
  • \,用于转义,当字符串中出现 \,需要使用 \。
  • 控制字符 \r,\n,\f,\t,\b。
  • \u 加四个16进制数字,Unicode码值,用于表示一些特殊字符,例如 \0,\v等不可见字符。中文字符在默认 utf-8 编码下可以不转换,json 模块提供了一个 ensure_ascii 开关参数。
0
1
// 输出 "\u000b\u0000你好"
console.log(JSON.stringify('\v\0你好'))

为了JSON的通用性,应该保证输出的JSON文件使用 utf-8 编码。

16.10.3. Python类型和JSON转换

json 模块当前支持如下的 Python 类型和 JSON 之间互相转换。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
+-------------------+---------------+
| Python            | JSON          |
+===================+===============+
| dict              | object        |
+-------------------+---------------+
| list, tuple       | array         |
+-------------------+---------------+
| str               | string        |
+-------------------+---------------+
| int, float        | number        |
+-------------------+---------------+
| True              | true          |
+-------------------+---------------+
| False             | false         |
+-------------------+---------------+
| None              | null          |
+-------------------+---------------+

16.10.3.1. dumps 和 dump

dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True,
      cls=None, indent=None, separators=None, default=None, sort_keys=False, **kw)
    Serialize ``obj`` to a JSON formatted ``str``.

json 中的 dumps() 函数序列化 Python 对象,生成 JSON 格式的字符串。

  • indent=n, 用于缩进显示。
  • sort_keys=False, 表示是否对输出进行按键排序。
  • ensure_ascii=True,表示是否保证输出的JSON只包含 ASCII 字符。
 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
dict0 = {'str0': 'string', 'num0' : 0, 'num1' : 3.14,
         'list': [1, 'abc'], 'tuple': (1, 'abc'), 'True': True,
         'False': False, 'nop': None, 'subdict': {}}

jsonstr = json.dumps(dict0, indent=2) # indent 用于格式化输出
print(jsonstr)

>>>
{
  "str0": "string",
  "num0": 0,
  "num1": 3.14,
  "list": [
    1,
    "abc"
  ],
  "tuple": [
    1,
    "abc"
  ],
  "True": true,
  "False": false,
  "nop": null,
  "subdict": {}
}

ensure_ascii 参数默认为 True,它保证输出的字符只有 ASCII 码组成,也即所有非 ASCII 码字符,例如中文会被转义表示,格式为 \u 前缀加字符的 Unicode 码值。

0
1
2
3
4
5
json0 = json.dumps('hello 你好', ensure_ascii=True)
json1 = json.dumps('hello 你好', ensure_ascii=False)
print(json0, json1)

>>>
"hello \u4f60\u597d" "hello 你好"

如果 Python 对象含有 JSON 特殊字符,dumps() 方法将自动转义:

0
1
2
3
4
json0 = json.dumps('"\\\r\n\f\t\b\x00你好', ensure_ascii=True)
print(json0)

>>>
"\"\\\r\n\f\t\b\u0000\u4f60\u597d"

dump() 方法支持的参数与 dumps() 基本一致,它将生成的 JSON 写入文件描述符,无返回:

0
1
2
data = [{'a': 'A', 'b': (2, 4), 'c': 3.0}]
with open('test.json', 'w') as fw:
    json.dump(data, fw)

16.10.3.2. loads 和 load

loads() 和 load() 将 JSON 转化为 Python 对象。

0
1
2
3
4
5
6
7
json_str = '{"key0": 1, "key1": "abc", "key3": [1,2,3]}'
dict0 = json.loads(json_str)
print(type(dict0).__name__)
print(dict0)

>>>
dict
{'key0': 1, 'key1': 'abc', 'key3': [1, 2, 3]}

load() 从文件描述符加载 JSON 并转化为 Python 对象。

0
1
2
3
4
5
with open('test.json', 'r') as fr:
    dict0 = json.load(fr)
    print(type(dict0).__name__)

>>>
list

16.10.4. Python对象和JSON转换

上面已经介绍过,json 模块默认支持的类型如何与 JSON 互相转换。然而使用最多的用于自定义的 class 实例是否无法转换呢?

0
1
2
3
4
5
6
7
8
9
class JSONCls():
    def __init__(self, name, num):
        self.name = name
        self.num = num

obj = JSONCls('json', 0)
print(json.dumps(obj))

>>>
TypeError: Object of type 'JSONCls' is not JSON serializable

默认情况下,dumps()方法无法实现用户自定义对象和JSON的转换。注意到 default 参数,它可以接受一个函数,用于把对象转换为字典:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def JSONObj2dict(obj):
    d = {
        '__class__': obj.__class__.__name__,
        '__module__': obj.__module__,
    }
    d.update(obj.__dict__)
    return d

print(json.dumps(obj, default=JSONObj2dict))

>>>
{"__class__": "JSONCls", "__module__": "__main__", "name": "json", "num": 0}

loads() 参数中的 object_hook 指定反向转换函数可以实现逆转换:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def dict2JSONObj(d):
    if '__class__' in d:
        class_name = d.pop('__class__')
        module_name = d.pop('__module__')
        module = __import__(module_name)
        cls = getattr(module, class_name)

        args = {
            key: value
            for key, value in d.items()
        }
        return cls(**args)
    return d

jsonstr = '{"__class__": "JSONCls", "__module__": "__main__", "name": "json", "num": 0}'

obj = json.loads(jsonstr, object_hook=dict2JSONObj)
print(type(obj), obj.name, obj.num)

>>>
<class '__main__.JSONCls'> json 0

当然,如果我们不保存模块名和类名,也可以导入,只是要保证当前保存的JSON也生成的类实例是要匹配的,并明确使用 JSONCls() 对参数进行实例化:

0
1
2
3
4
5
6
def JSONObj2dict(obj):
    return obj.__dict__

def dict2JSONObj(d):
    args = { key: value
             for key, value in d.items()}
    return JSONCls(**args)

16.11. base64

有时候我们需要把不可见字符变成可见字符来进行传输,比如邮件附件可以是图片等二进制文件,但是传输协议 SMTP 就只能传输纯文本数据,所以必须进行编码。 如果要把一个文件嵌入到另一个文本文件中,也可以使用这种方法。比如 JSON 文件中嵌入另一个 JSON 文件。

Base64 编码是一种非常简单的将任意二进制字节数据编码成可见字符的机制。

Base64 选用了”A-Z、a-z、0-9、+、/” 这 64 个可打印字符作为索引表,索引从 0-63。编码过程如下:

  • 对二进制数据每 3 个 bytes 一组,每组有 3 x 8 = 24 个 bits,再将它划为 4 个小组,每组有 6 个 bits。
  • 6 个 bits 的值的范围就是 0-63,通过查索引表,转换为对应的 4 个可见字符。
  • 如果要编码数据字节数不是 3 的倍数,最后会剩下 1 或 2 个字节,Base64 用 x00 字节在末尾补足后进行编码,再在编码的末尾加上 1 或 2 个 ‘=’ 号,表示补了多少字节的 x00,解码的时候去掉。

所以,Base64 编码会把 3 bytes 的二进制数据编码为 4 bytes 的文本数据,数据大小会比原来增加 1/3。

16.11.1. 编解码

b64encode(s, altchars=None)
    Encode the bytes-like object s using Base64 and return a bytes object.
b64decode(s, altchars=None, validate=False)
    Decode the Base64 encoded bytes-like object or ASCII string s.

b64encode() 接受一个 bytes 类型参数,并返回一个 bytes 对象,所以如果需要转换为字符串,则需要格式化。 b64decode() 与 b64encode() 类似,用于解码。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import base64

encoded = base64.b64encode(b'\x00\x00\x00\xfb\xef\xff')
print(encoded)

str0 = '{}'.format(encoded) # 格式化为字符串
print(str0[2:-1])

print(base64.b64decode(encoded))

>>>
b'AAAA++//'
AAAA++//
b'\x00\x00\x00\xfb\xef\xff'

altchars 参数接受一个 2 字节长度的 bytes 类型,用于替换字符串中的 + 和 /。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
encoded = base64.b64encode(b'\x00\x00\x00\xfb\xef\xff', altchars=b'-_')
print(encoded)

str0 = '{}'.format(encoded)
print(str0[2:-1])

print(base64.b64decode(encoded, altchars=b'-_'))

>>>
b'AAAA--__'
AAAA--__
b'\x00\x00\x00\xfb\xef\xff'

16.11.2. URL 编码

  • URI(Uniform Resource Identifier),统一资源标识符。
  • URL(Uniform Resource Locater),统一资源定位符。
  • URN(Uniform Resource Name),统一资源名称。

URI 由 URL 和 URN 组成,它们三者的关系如下:

0
1
2
3
4
5
6
               URL                                   Anchor
----------------------------                         ----
http://www.fake.com:80/path/hello.html?query=hi&arg=0#id0
       ----------------------------------------------
                        URN
-----------------------------------------------------
                      URI

URL 包含了获取资源协议,主机,端口和路径部分,它指定了一个地址位置。 URN 不含获取资源的协议部分,唯一定义了某个位置上的资源。

可以看到一个常见的 URI 可以包含如下部分:

  • 获取协议(Scheme),例如 http, https, ftp 等。
  • 主机和端口,以冒号分割,如果使用默认 80 端口,可以不提供端口信息。
  • 路径,用 ‘/’ 表示目录层次
  • 查询字符串,以 ? 开始,& 连接多个查询参数。
  • 片段(Anchor),以 # 开始,标记资源中的子资源,也即资源中的某一部分,不发送给服务器,由浏览器处理。

所以我们所说的 URL 编码准确的说是 URI 编码,现实是这两个概念常常混用。

RFC3986文档规定,URL 中允许字符为 [a-zA-Z0-9] 和 “-_.~4” 个特殊字符以及保留字符。

保留字符:URL可以划分成若干个组件,协议、主机、路径等。有一些字符(:/?#[]@)是用作分隔不同组件的。例如:冒号用于分隔协议和主机,/用于分隔主机和路径,?用于分隔路径和查询参数,等等。还有一些字符(!$&’()*+,;=)用于在每个组件中起到分隔作用的,如=用于表示查询参数中的键值对,&符号用于分隔查询多个键值对。当组件中的普通数据包含这些特殊字符时,需要对其进行编码。

RFC3986中指定的保留字符有 ! * ‘ ( ) ; : @ & = + $ , / ? # [ ]。

所有其他字符均需要编码,当保留字符出现在非功能分割字段时也需要编码。

URL 编码采用百分号编码方式:一个百分号 %,后跟两个表示字符 ASCII 码值的16进制数,例如 %20 表示空格。

RFC 推荐字符采用 UTF-8 编码方式,也即一个字符的 UTF-8 编码为 0x111213,那么 URL 编码对应 %11%12%13。

16.11.3. Base64对URL编码

由于 URL 编码不支持标准的 Base64 索引表中的 + 和 / 字符,所以可以使用 - 和 _ 替代它们。

base64 模块内置了 urlsafe_b64encode() 和 urlsafe_b64decode() 方法用于 URL 编码:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
encoded = base64.urlsafe_b64encode(b'\0\0\0\xfb\xef\xff')
print(encoded)

str0 = '{}'.format(encoded)
print(str0[2:-1])

print(base64.urlsafe_b64decode(encoded))

>>>
b'AAAA--__'
AAAA--__
b'\x00\x00\x00\xfb\xef\xff'

由于=字符也可能出现在Base64编码中,但=在URL和Cookie中不是合法字符,所以,很多Base64编码后会把=去掉。 Base64 把3个字节变为4个字节,Base64编码的长度总是 4 的倍数,当发现编码后长度不是 4 的倍数后,补足= 再解码就可以了。

16.11.4. 注意事项

Base64 适用于轻量级编码,比如查询字段,Cookie内容和内嵌文件,例如在 JSON 或 XML 中内嵌一段二进制数据。

Base64 编码不适用于加密,它只是简单的字符映射,很容易被破解。