- 文档:
Sphinx - 加速:
pybind11 - 二进制:
struct - 装饰器:
@ - 操作系统交互:
os - 进程:
subprocess - 循环器:
itertools - 数据库:
sqlite3 - 基础
- 内存
- 网络
- 元编程
文档:Sphinx
我最近使用 Sphinx 把我的博士期间的代码的文档构建了出来。我十分喜欢得到的结果。使用 Sphinx 构建文档的过程同 Jekyll 的使用类似,都是从文本中读取信息,然后在渲染到 HTML 网页中。
使用流程
- 无论使用什么语言,好好写
docstring - 用
sphinx-quickstart生成一堆东西 - 修改
conf.py - 使用
sphinx-apidoc 模块地址 -o rst导出地址生成每个 package 的 rst 文档 - 把
modules加入到index.rst
有用的扩展
在 Sphinx 中,我们需要 增加一些 扩展 Extension 才能让 Sphinx 变得有用。我个人使用的 Extension 如下,
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'sphinx.ext.intersphinx',
'sphinx.ext.mathjax',
]
其中,每个扩展的作用 分别是
autodoc: 配合sphinx-apidoc自动生成所有 package 下「每个模块」的 文档。napoleon: 让 Sphinx 能够 理解 Google 风格的 docstring。intersphinx: 让 Sphinx 能够自动给文档里的 类、函数 添加链接mathjax: 配合:math:和.. math::在文档里渲染 $\LaTeX$ 公式
在网页中渲染代码块
.. code-block::
your code
在网页中渲染公式
把这个 2020 年还 work 的 CDN 添加到 conf.py
mathjax_path = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML"
使用下面代码渲染 单独的 公式
.. math::
\LaTeX code
使用 :math:latex
加入链接到 Python 文档
普通情况下,我们用 sphinx 写 python 的 docstring 时,我们写出下面的代码
def foo(a):
"""
Foo
Args:
a (int): ...
"""
...
这个时候,类型 int 能够配合 intersphinx 产生一个链接到 python 文档的链接。但是字体是普通的斜体。如果我们写出下面的代码
def foo(a):
"""
Foo
Args:
a (:obj: `int`): ...
"""
...
生成的链接就会是非衬线体,好看一点。
加速:pybind11
Python 中速度慢的代码可以在 C++ 中重写,并且被直接引用到 Python 里。这样通常可以让代码的速度提高一个数量级。
使用 pybind11 ,我们可以将 C++ 代码转化成一个 Python 模块。
为什么用 pybindr11
我想要使用 C++ 来加速我的代码里慢的部分。理想状态下,我希望把我手头现有的「所有」Python 代码都写成对应的 C++ 版本,然后在 Python 脚本里使用 C++ 版本的 wrapper。
同时,我希望保留下最初的 Python 版本。比如,如果我有一个叫做 correlation.py 模块,模块里有一些类和函数,我希望编写一个 c_correlation.cpp 的 wrapper,暴露「相同」的 api 给用户。
在 import 的时候,我希望达到这样的效果
try:
import c_correlation as corr
except ImportError:
import correlation as corr
在我的 c_corrlation.cpp 文件里,我最需要完成的是 Pyhton 和 C++ 数据类型的转换。十分幸运的是,pybind11 可以自动地帮助我将 List, Dict 和 numpy.ndarray 转换成相应的 C++ 数据结构。
我认为这个也是大家使用 pybind11 的意义;否则的话我们总是可以直接用 C 语言扩展 CPython 代码。
安装 pybind11 和 eigen
pybinder11 和 eigen 需要自己下载安装。在 2019 年照着教程安装它们并不是很困难的事情。
使用 pybind11
下面列举了三种用法。
最简单的情况,我们将 Python 里的代码复制一份到 C++ 里,再处理这些数据。例如,我们可以写一个 func.cpp 的函数来处理数据,并且写一个对应的头文件。
// func.cpp
int add(int i, int j){
return i + j;
};
// func.hpp
int add(int i, int j);
同时我们写一个 wrap.py 的文件
//wrap.cpp
#include <pybind11/pybind11.h>
#include "funcs.hpp"
namespace py = pybind11;
PYBIND11_MODULE(wrap, m) {
m.doc() = "pybind11 example plugin";
m.def("add", &add, "Add two numbers", "i"_a=1, "j"_a=2); // default initial values
}
之后,我们在目录下执行编译
g++ -O3 -Wall -shared -std=c++11 -fPIC \
`python3 -m pybind11 --includes` \
-I/usr/local/include \
`python3-config --ldflags` \
-o wrap`python3-config --extension-suffix` \
funcs.cpp wrap.cpp
最终就会得到一个名为 wrap.cpython-37m-darwin.so 的文件。我们可以在 python 里直接执行
In [1]: import wrap
In [2]: wrap.add(1, 2)
Out[2]: 3
我们可以写一个 setup.py 文件来简化编译,这样用户就可以用
python setup.py build_ext -i
来编译需要的 wrap 模块。文件 setup.py 的内容如下
import os, sys
from distutils.core import setup, Extension
from distutils import sysconfig
cpp_args = ['-std=c++11', '-stdlib=libc++', '-mmacosx-version-min=10.7']
ext_modules = [
Extension(
'wrap',
['funcs.cpp', 'func_wrap.cpp'],
include_dirs=['pybind11/include'],
language='c++',
extra_compile_args = cpp_args,
),
]
setup(
name='wrap',
version='0.0.1',
author='Yushi Yang',
author_email='yushi.yang@bristol.ac.uk',
description='Example',
ext_modules=ext_modules,
)
最终得到的东西和之前是一样的。
比起 setup.py,在开发的时候更加方便的选择是 cppimport。它让我们在更改 c++ 代码之后,不需重复地使用 python setup.py build_ext -i 来编译。
为了使用它,我们需要更改我们的 wrap.cpp 文件。作为对比,我们创建一个 ez_wrap.cpp
//ez_wrap.cpp
<%
cfg['compiler_args'] = ['-std=c++11', '-stdlib=libc++', '-mmacosx-version-min=10.7']
cfg['sources'] = ['funcs.cpp']
setup_pybind11(cfg)
%>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include "funcs.hpp"
namespace py = pybind11;
PYBIND11_MODULE(ez_wrap, m) { // 注意这里
m.doc() = "pybind11 example plugin";
m.def("add", &add, "Add two numbers", py::arg("i")=1, py::arg("j")=2);
}
代码最开始的标记就是为了 cppimport 服务的。在我们使用 pip install cppimport 之后,我们可以「直接」导入我们的 c++ 模块。
In [1]: import cppimport
In [2]: wrap = cppimport.imp("ez_wrap")
In [3]: wrap.add()
Out[3]: 3
要注意的是,PYBIND11_MODULE(ez_wrap, m) 里第一个变量的名字,需要和文件名 ez_wrap.cpp 的前缀一致,否则会得到下面的警告
ImportError: dynamic module does not define module export function (PyInit_ez_wrap)
使用 numpy
pybind11 可以让 c++ 代码处理 numpy 数组。每一个 numpy 数组都实现了 Pyhton 中的 buffer protocol。简而言之,通过一些特殊的协议,我们可以在内存中开辟一块「连续的」区域来存储我们的数据,并且共享这块数据的指针。这个博客里有一个对 buffer protocol 的简单介绍。
如果我们想把一个 Python 里的 np.ndarray 读取到 c++ 里,我们只需要使用它的 request 放法,得到下面的结构体
struct buffer_info {
void *ptr;
size_t itemsize;
std::string format;
int ndim;
std::vector<size_t> shape;
std::vector<size_t> strides;
};
这里面的 ptr 指向数据块;我们可以用它「直接」访问存储与 numpy 里的数据。
我们需要导入 numpy 对应的头文件来使用相关的功能。
#include <pybind11/numpy.h>
下面的例子是读取一个 numpy 数组并且改变这个数组:将每个数字换成它的一倍。
void arr_db(py::array_t<double> xs){
py::buffer_info info = xs.request();
auto ptr = static_cast<double *>(info.ptr);
int n = 1;
for (auto r : info.shape){
n *= r;
}
for (int i = 0; i < n; i++){
*ptr++ *= 2;
}
}
我们可以在 Python 里导入这个模块
In [4]: cnp = cppimport.imp("cnp")
In [5]: a = np.random.random((2, 2))
In [6]: a
Out[6]:
array([[0.49394052, 0.14228716],
[0.13781544, 0.51710877]])
In [7]: cnp.arr_db(a)
In [8]: a
Out[8]:
array([[0.98788103, 0.28457432],
[0.27563088, 1.03421754]])
下面的例子是读取一个 numpy 数组并且计算每一个数字的平方。这个函数不会改变已有的数组,而是会返回一个新数组。
(这段代码有一点不好:输入数组可以是 n 维的,不过返回的数组是 1 维。)
py::array_t<double> arr_sqr(py::array_t<double> arr){
py::buffer_info buf_1 = arr.request();
auto result = py::array_t<double>(buf_1.size);
py::buffer_info buf_2 = result.request();
double *ptr_1 = (double *) buf_1.ptr,
*ptr_2 = (double *) buf_2.ptr;
int max_index = 1;
for (int d = 0; d < buf_1.ndim; d++) {
max_index *= buf_1.shape[d];
}
for (int i = 0; i < max_index; i++){
*ptr_2 = *ptr_1 * *ptr_1;
ptr_1++;
ptr_2++;
}
return result;
}
我们可以在 Python 里导入这个模块。
In [1]: import cppimport
In [2]: cnp = cppimport.imp("cnp")
In [3]: import numpy as np
In [4]: test = np.random.random((4, 5, 2))
In [5]: np.allclose(np.power(test, 2).ravel(), cnp.arr_sqr(test))
Out[5]: True
使用 eigen
在 C++ 里,我们可以用库 Eigen 来完成线性代数计算。更棒的是,我们可以直接把 Python 里的 numpy 数组直接转化成 Eigen 里对应的数据结构。
在使用 eigen 的时候,我们需要引入 pybind11 的 eigen 模块,以及我们实际使用的 Eigen 库。
下面是利用 eigen 库计算矩阵的逆和行列式的例子。
//eig.cpp
<%
cfg['compiler_args'] = ['-std=c++11', '-stdlib=libc++', '-mmacosx-version-min=10.7']
setup_pybind11(cfg)
%>
#include <pybind11/pybind11.h>
#include <pybind11/eigen.h>
#include <Eigen/LU>
namespace py = pybind11;
// input numpy array output number
double det(Eigen::MatrixXd arr){
return arr.determinant();
}
// input numpy array output numpy array
Eigen::MatrixXd inv(Eigen::MatrixXd arr){
return arr.inverse();
}
// example of using Eigen
PYBIND11_MODULE(eig, m) {
m.doc() = "pybind11 example plugin";
m.def("det", &det);
m.def("inv", &inv);
}
我们可以直接引用这个模块
In [1]: import cppimport
In [2]: import numpy as np
In [3]: eig = cppimport.imp('eig')
In [4]: a = np.random.random((10, 10))
In [5]: np.allclose(np.linalg.inv(a), eig.inv(a))
Out[5]: True
二进制:struct
Python 的 struct 模块允许我们处理二进制数据。
基础用法
使用 struct.pack(fmt, v1, v2, ...) 根据格式字符串 fmt 打包值 v1, v2…并返回一个包含这些值的字节对象。
packed_data = struct.pack('iif', 6, 19, 4.7) # iif → int, int, float
print(packed_data)
b'\x06\x00\x00\x00\x13\x00\x00\x00ff\x96@'
struct.calcsize(fmt) 返回与格式字符串 fmt 对应的结构体的大小。
print(struct.calcsize('c')) # 1
print(struct.calcsize('i')) # 4
print(struct.calcsize('f')) # 4
print(struct.calcsize('cif')) # 12
struct.unpack(fmt, buffer) 根据格式字符串 fmt 解包缓冲区 buffer,返回一个元组(即使它只包含一个项目)。
data = struct.unpack('iif', b'\x06\x00\x00\x00\x13\x00\x00\x00ff\x96@')
print(data)
(6, 19, 4.699999809265137)
字节序 (Byte Order)
在Python的 struct 模块中,字节序描述了如何在内存中表示多字节数据类型,例如2字节的短整数或4字节的浮点数。
以下是struct模块中用于指定字节序的前缀字符:
@: 本地字节序,大小和对齐。=: 本地字节序,但使用标准大小(无对齐)。<: 小端字节序,标准大小,无对齐。>: 大端字节序,标准大小,无对齐。!: 网络字节序(大端),标准大小,无对齐。
字节序的简要说明:
- 本地字节序: 系统默认使用的字节序。
x86和x86_64架构为小端 (little-endian),- 某些旧架构(如 Motorola 的
68k)为大端 (big-endian)。
- 大小和对齐: 数据类型在内存中的对齐方式可能因系统而异。使用本地大小和对齐意味着打包数据的方式会与系统的C编译器对齐数据的方式相同。
附录:大小端
大端字节序 (Big-endian) 和小端字节序 (Little-endian) 是计算机存储多字节数据类型时的两种常见方法。 它们定义了字节的顺序,即最高有效字节和最低有效字节在内存中的位置。
大端字节序
- 最高有效字节(MSB)存储在最低的内存地址。
示例:
对于32位整数 0x12345678,它的存储方式为:
| 内存地址 | 值 |
|---|---|
| 0x00 | 12 |
| 0x01 | 34 |
| 0x02 | 56 |
| 0x03 | 78 |
优点:
- 直观的在纸上从左到右的阅读方式与内存中的表示相符。
- 在处理网络协议和文件格式时,大多数标准都使用大端字节序。
缺点:
- 在某些平台上,读取非对齐的大端数据可能需要额外的处理。
小端字节序
- 最低有效字节(LSB)存储在最低的内存地址。
示例:
对于32位整数 0x12345678,它的存储方式为:
| 内存地址 | 值 |
|---|---|
| 0x00 | 78 |
| 0x01 | 56 |
| 0x02 | 34 |
| 0x03 | 12 |
优点:
- 对于某些运算,如增加、减少等,小端提供了更简单的实现。
- 直接读取数据的低位字节更为方便。
缺点:
- 不如大端字节序直观,尤其是在纸上或屏幕上查看多字节值时。
附录:内存对齐
内存对齐在计算机架构中具有重要意义。利用 Python 的 struct 库,我们可以直观地理解并探索内存对齐如何影响数据的存储和性能。
内存对齐意味着,数据在内存中的地址应当是某个特定数字(例如4或8)的倍数。例如,4字节的整数应当从 0x04、0x08 等地址开始。
Python 的 struct 库为我们提供了打包和解包数据的工具,能够让我们直观地观察到内存对齐问题。例如,下面的数据能够确保
>>> from struct import pack, calcsize
>>> pack('@ic', 0x12131415, b'#')
b'\x15\x14\x13\x12#'
pack('@ci', b'#', 0x12131415) # -> pack('@ci', b'#', 0x12131415)
通过 struct,我们可以看到如何根据特定格式代码(如 ‘@ic’)来存储数据,并了解可能的填充和大小。
为什么对齐很重要?
- 单一内存访问:对齐的数据可以在一次内存操作中被访问。
- 缓存效率:未对齐的数据可能涉及多个缓存行的访问。
- 总线宽度:对齐的数据可以在一个总线周期内完成访问。
- SIMD 指令:这些指令通常要求数据对齐。
- 避免对齐故障:某些处理器不支持未对齐访问。
Python 的 struct 库为我们提供了一个直观的工具来探索和理解内存对齐的重要性。对齐不仅影响性能,还可能影响数据的存储方式。使用 struct,我们可以更好地把握这些细节。
装饰器:@
装饰器是Python中的高级功能,允许我们在不修改原始代码的情况下增加或修改功能。装饰器简化了代码并提高了可读性。
基本装饰器
装饰器通过在函数定义之前使用@符号来应用:
def decorator(F):
def new_F(a, b):
print(f"The input is {a} and {b}")
return F(a, b)
return new_F
@decorator
def dist_sq(a, b):
return a**2 + b**2
参数化的装饰器
有时,我们可能希望为装饰器提供参数来提供更多的灵活性:
def dec_with_par(parameter=""):
def decorator(func):
def new_func(a, b):
print(parameter)
print(f"The input is {a} and {b}")
return func(a, b)
return new_func
return decorator
@dec_with_par("calculate squared sum")
def dist_sq(a, b):
return a**2 + b**2
装饰类
从Python 2.6开始,我们还可以使用装饰器来装饰类:
def decorator(old_class):
class new_class:
def __init__(self):
old_class.__init__(self)
self.version = "new"
return new_class
@decorator
class Bird:
def __init__(self):
self.name = "bird"
crow = Bird()
print(crow.name) # 输出:bird
print(crow.version) # 输出:new
日志记录
当我们想要跟踪函数的行为时,日志记录是一个很有用的工具。使用装饰器,我们可以很容易地为任何函数添加日志记录功能,而不必修改函数本身的代码。
以下是一个简单的示例,展示如何使用装饰器来记录函数的调用信息:
import logging
# 配置日志记录
logging.basicConfig(level=logging.INFO)
def log_decorator(func):
def wrapper(*args, **kwargs):
logging.info(f"Running {func.__name__} with arguments {args} and keyword arguments {kwargs}")
return func(*args, **kwargs)
return wrapper
@log_decorator
def sample_function(a, b=0):
return a + b
在上述代码中,每次调用sample_function时,都会在日志中记录函数的名称以及传递给它的参数。
例如,调用sample_function(1, b=2)会在日志中产生以下条目:
INFO:root:Running sample_function with arguments (1,) and keyword arguments {'b': 2}
这种方法的优点是可以轻松地为任何函数添加日志记录功能,而无需对原始函数进行大量修改。
操作系统交互:os
Python 的 os 模块提供了一系列与 操作系统 交互的功能。
os.path
使用 os.path 模块,可以跨平台地处理文件路径。以下是其常用功能:
import os
path = "/Users/yushi/Desktop/Folder/file.txt"
os.path.basename(path) # 获取文件名: 'file.txt'
os.path.dirname(path) # 获取目录: '/Users/yushi/Desktop/Folder'
os.path.split(path) # 分割目录和文件名: ('/Users/yushi/Desktop/Folder', 'file.txt')
os.path.exists(path) # 路径是否存在: True
os.path.getsize(path) # 文件大小: 12
os.path.getatime(path) # 上次读取时间: 1604155505.98391
os.path.getmtime(path) # 上次修改时间: 1604155338.653522
os.path.isfile(path) # 是否是文件: True
os.path.isdir(path) # 是否是目录: False
进程信息
os 模块中与进程相关的函数包括:
uname():返回操作系统信息。mask():设置创建文件的权限 mask。get()uid, euid, resuid, gid, egid, resgid:权限信息。pid, pgid, ppid, sid:进程信息。
put()euid, egid:更改 euid 和 egid。uid, gid:更改 uid 和 gid,仅 super user 可用。pgid, sid:更改进程组和会话。
getenviron():获取进程的环境变量。setenviron():设置进程的环境变量。
其他方法
以下是一些其他有用的 os 方法:
os.mkdir('folder') # 创建文件夹
os.listdir('.') # 查看当前目录下文件
os.rmdir('folder') # 删除文件夹
os.remove('file') # 删除文件
os.stat('file/folder') # 获取文件/文件夹信息
os.getcwd() # 获取当前目录
os.symlink('p1', 'p2') # 创建软链接,从 p2 指向 p1
进程:subprocess
Python 运行时会创建进程。我们可以fork子进程并执行其他程序。subprocess 包提供这功能,并允许进程间文本通信。
subprocess主要考虑:
- 父进程是否等待子进程。
- 函数返回值。
returncode的处理。
常用功能
执行 shell 命令:
import subprocess
subprocess.call(["ls", "-l"])
捕捉命令错误:
subprocess.check_call(["rm", "NotExist"])
获取命令输出:
subprocess.check_output(["ls", "."])
Popen()
Popen() 是基础创建子进程的函数。与其他函数不同,创建后主进程不自动等待子进程。
child = subprocess.Popen(["Ping", "-c", "3", "www.google.com"])
child.wait() # 阻塞父进程
print("parent process continues")
文本流控制
子进程的输入/输出可以通过 child.stdin, child.stdout, child.stderr 控制。
使用 Popen() 可构成管道,如:
from subprocess import Popen, PIPE
child_1 = Popen(["ls", "-l"], stdout=PIPE)
child_2 = Popen(["wc"], stdin=child_1.stdout, stdout=PIPE)
out = child_2.communicate()
也可用于子进程输入:
c = Popen(["cat"], stdin=PIPE)
c.communicate(b"Hello\n")
循环器:itertools
在 Python 3 中,循环器 iterator 是可以遍历的对象的容器。通过调用 __next__() 方法,循环器将逐个返回其中的对象。当所有对象都被遍历完毕后,将抛出 StopIteration 错误。
Python 的 itertools 模块为我们提供了创建和操作循环器的强大工具。
无穷循环器
from itertools import count, cycle, repeat
count(0, 3) # 从 0 开始,每次增加 3
cycle('abc') # 无尽地重复序列
repeat('x') # 无尽地重复 'x'
函数式工具
from itertools import filterfalse
map(sum, [(1, 1), (2, 2), (3, 3)]) # -> 2, 4, 6
filter(lambda x: x > 2, [1, 2, 3, 4]) # -> 3, 4
filterfalse(lambda x: x > 2, [1, 2, 3, 4]) # -> 1, 2
组合工具
from itertools import chain, product, permutations, combinations, combinations_with_replacement
chain([1, 2, 3], [4, 5, 6]) # -> [1, 2, 3, 4, 5, 6]
product('ab', [1, 2]) # -> [('a', 1), ('a', 2), ('b', 1), ('b', 2)]
permutations('abc', 2) # -> [('a', 'b'), ('a', 'c'), ...]
combinations('abc', 2) # -> [('a', 'b'), ('a', 'c'), ('b', 'c')]
combinations_with_replacement('ab', 2) # -> [('a', 'a'), ('a', 'b'), ('b', 'b')]
分组:groupby
使用 groupby() 函数可以按照指定的 key 函数将元素分组。下面的示例展示了如何使用它:
from itertools import groupby
def rank(x):
if x <= 1: return "C"
elif x <= 3: return "B"
else: return "A"
scores = [1, 1, 2, 3, 3, 4, 5]
for tag, content in groupby(scores, key=rank):
print(tag, list(content))
数据库:sqlite3
关系型数据库使用二维表结构来组织数据,并支持 SQL(结构化查询语言)进行数据查询。表中的每一行是一个记录,每一列是一个字段。表与表之间可以通过主键和外键建立关系。
Python 标准库中的 sqlite3 模块提供了轻量级关系型数据库 SQLite 的接口。
创建数据库
创建category和book表,并建立它们之间的关系。
import sqlite3
conn = sqlite3.connect("test.db")
c = conn.cursor()
c.execute('''CREATE TABLE category(id INT PRIMARY KEY, name TEXT)''')
conn.commit()
conn.close()
| id | name |
|---|---|
插入数据
conn = sqlite3.connect("test.db")
c = conn.cursor()
c.execute("INSERT INTO category VALUES (1, 'science')")
c.execute("INSERT INTO category VALUES (?, ?)", (2, "philosophy"))
categories = [(3, "medicine"), (4, "novel")]
c.executemany("INSERT INTO category VALUES (?, ?)", categories)
conn.commit()
conn.close()
| id | name |
|---|---|
| 1 | science |
| 2 | philosophy |
| 3 | medicine |
| 4 | novel |
选择数据
conn = sqlite3.connect('test.db')
c = conn.cursor()
for name in c.execute('SELECT name FROM category'):
print(name)
| name |
|---|
| science |
| philosophy |
| medicine |
| novel |
更新和删除数据
conn = sqlite3.connect('test.db')
c = conn.cursor()
c.execute('UPDATE category SET name=? WHERE id=?;', ('metaphysics', 3))
c.execute('DELETE FROM category WHERE id=4;')
conn.commit()
conn.close()
| id | name |
|---|---|
| 1 | science |
| 2 | philosophy |
| 3 | metaphysics |
查看所有表和删除表
conn = sqlite3.connect('test.db')
c = conn.cursor()
tables = c.execute('SELECT name FROM sqlite_master WHERE type="table";').fetchall()
print(tables)
c.execute('DROP TABLE category;')
tables = c.execute('SELECT name FROM sqlite_master WHERE type="table";').fetchall()
print(tables)
conn.commit()
conn.close()
输出:
[('category',)]
[]
基础
内置函数
enumerate 可同时获取循环中的下标和元素:
s = 'abcd'
for (i, c) in enumerate(s):
print(f"Index: {i}; Char: {c}")
zip 可在每次循环时从各个序列分别取出一个元素:
la, lb, lc = [1, 2, 3], [4, 5, 6], [7, 8, 9]
for (a, b, c) in zip(la, lb, lc):
print(a, b, c)
filter() 使用一个函数参数来筛选序列中的元素。在 Python 3 中,返回的是循环对象。
import random
scatter = [(random.random(), random.random()) for _ in range(50)]
# 过滤掉 Y < 0.5 的点
sfx, sfy = zip(*filter(lambda x: x[1] > 0.5, scatter))
reduce 使用一个接收两个参数的函数,来累进地应用于序列。
Python 2:
def f(x, y): return x + y
print(reduce(f, [1, 2, 3, 4, 5]))
Python 3:
from functools import reduce
def f(x, y): return x + y
print(reduce(f, [1, 2, 3, 4, 5]))
动态类型
Python 的变量类型是动态的,允许一个变量从一种类型变为另一种类型,与静态语言如 C 和 C++ 不同。Python 的变量实际上是对象的引用。
a = 3:创建整数对象3并使a指向它。a = "3":创建字符串对象”3”,a现在指向它。整数对象3不再被引用。
注:
- Python 会自动销毁没有引用的对象。
- Python 缓存小整数和短字符串。
引用示例:
a = 5
b = a # b, a 指向同一个对象5
a = a + 2 # a 现在指向新对象7
多个引用可以指向同一个对象,但如果一个引用的值改变,这只影响该引用,而不影响其他引用。
列表引用:
L1 = [1, 2, 3]
L2 = L1
L3 = L1
L1 = 1 # L1 的指向改变
L2[0] = 0 # L2 和 L3 所指的列表内容改变
修改 L2 或 L3 所指向列表的内容会影响彼此,因为它们引用的是同一个对象。
参数传递
在 Python,函数处理不可变和可变对象时,表现是不同的。不可变对象(immutable)的处理如下,
def f(x):
x = 100
a = 1
f(a)
print(a) # 输出:1
x 是新引用,指向 a 的对象。操作 x 不影响 a。
函数对可变对象处理,实例如下:
def f(x):
x[0] = 100
b = [1, 2, 3]
f(b)
print(b) # 输出:[100, 2, 3]
传递可变对象可能改变原始对象。注意此问题以避免错误。
闭包
当一个函数引用了其作用域 (scope) 外的变量,且该函数被作为返回值,则这样的函数称为闭包。例如:
def line_conf(a, b):
def line(x):
return a * x + b
return line
line = line_conf(2, 0)
print(line(1)) # 输出:2
闭包内的「环境变量」保存在函数对象的 __closure__ 属性中。
上下文:with
从Python 2.5开始,上下文管理器提供了规定对象使用范围的语法,一旦进入或退出此范围,特定的操作会被触发(例如对象的分配或释放)。
其语法为 with...as...。
例:
with open("File_name") as f:
# 使用f,之后f自动关闭
使用上下文管理器操作文件:
f = open('new.txt', 'w')
print(f.closed) # 输出:False
f.close()
print(f.closed) # 输出:True
with open('new.txt', 'w') as f:
print(f.closed) # 输出:False
print(f.closed) # 输出:True
使用上下文管理器,Python在进入块前调用对象的 __enter__(),在退出时调用 __exit__()。
对于文件对象 f,它定义了 __enter__() 和 __exit__()(可通过 dir(f) 查看)。f 的 __exit__() 中调用了 self.close(),所以不需手动关闭文件。
下列代码创建了一个带上下文管理器的类,并在上下文中使用它。
class FOO():
def __init__(self):
print("initialising ...")
def __enter__(self):
print("entering ...")
def __exit__(self, exc_type, exc_value, traceback):
print("exiting ...")
with Ranter() as r:
pass
执行结果:
initialising ...
entering ...
exiting ...
序列化:pickle
在 Python 中,所有如「变量」或「函数」都是「对象」。Python 运行时,它们存储于内存,但会因计算机关机而消失。幸运的是,我们可以将它们储存至硬盘上。
Python 中的对象是二进制的。我们可以序列化 (serialize) 这些对象并存入文件中。创建对象需要「对象的类」的定义,所以重建对象时也需要这一定义。
内置对象如整数或列表可以直接重建,但对于用户定义的对象,我们需先定义类。
import pickle
class bird():
has_egg = True
crow = bird()
crow_str = pickle.dumps(crow)
# 使用 pickle.dump 保存对象
starling = bird()
with open('starling.pkl', 'wb') as f:
pickle.dump(starling, f)
# 使用 pickle.load 读取对象
with open('starling.pkl', 'rb') as f:
starling = pickle.load(f)
内存
对象的内存使用
Python中的变量都是对象的引用。例如在赋值a = 1,1是一个整数对象,而a则是这个对象的引用。使用Python的id()函数,我们可以查询对象的内存地址:
a = 1
print(id(a)) # 十进制的内存地址
print(hex(id(a))) # 十六进制的内存地址
Python优化了常用的小整数和短字符串等对象,使它们在内存中只存在一份。因此,多个引用指向相同的对象可能会有相同的内存地址:
a, b = 1, 1
print(a is b) # True
x, y = 12345, 12345
print(x is y) # False
引用计数
每个Python对象都维护着一个引用计数,表示有多少引用指向它。使用sys.getrefcount()可以查询对象的引用计数,但它的返回值会比实际引用计数多1(函数调用自身也是一次引用):
from sys import getrefcount
a = []
print(getrefcount(a)) # 2: a引用 + getrefcount的临时引用
b = [a]
print(getrefcount(a)) # 3
c = [a, a, a]
print(getrefcount(a)) # 6
容器中的引用
容器对象(如列表、字典)保存的是其他对象的引用,而不是对象本身。我们还可以自定义类来引用其他对象:
class FromObj:
def __init__(self, to_obj):
self.to_obj = to_obj
b = [1, 2, 3]
a = FromObj(b)
print(id(a.to_obj) == id(b)) # True
print(getrefcount(b)) # 3
在Python的全局作用域,变量其实都是全局字典中的键值对:
a = 1
print(globals()['a']) # 1
引用环
对象可以互相引用,形成一个引用环。即使对象自引用,也构成一个环:
a = []
b = [a]
a.append(b)
# 检查引用环
print(a is b[0]) # True
print(a[0] is b) # True
# 自引用
a = []
a.append(a)
print(a[0] is a) # True
减少引用
使用del关键字可以删除一个引用,从而减少对象的引用计数:
a = []
b = [a]
c = [a, a, a]
print(getrefcount(a)) # 6
del(a)
print(getrefcount(b[0])) # 5
此外,del也可以删除容器中的元素:
a = [1, 2, 3]
del(a[2])
print(a) # [1, 2]
垃圾回收
Python 自动执行「垃圾回收」 (garbage collection),清除不再使用的对象。当对象的引用计数变为 0,该对象就成为垃圾,等待回收。
例如:
a = [1, 2, 3]
del(a)
在 del(a) 后,没有引用指向 [1, 2, 3]。这个对象如果留在内存中就是浪费。Python 会在特定条件下启动垃圾回收,基于「分配对象」和「取消分配对象」的差值。
使用 gc.get_threshold() 可查看该阈值:
import gc
gc.get_threshold() # Output: (700, 10, 10)
分代回收
Python 也使用「分代回收」策略,即分三代(0、1、2)。新建对象为 0 代。经历回收后仍存活的对象会升代。
例如,get_threshold() 的输出 (700, 10, 10) 表示:每 10 次 0 代回收触发 1 次 1 代回收;每 10 次 1 代回收触发 1 次 2 代回收。
可以使用 set_threshold() 调整:
gc.set_threshold(700, 10, 2)
gc.get_threshold() # Output: (700, 10, 2)
回收孤立的引用环
存在的引用环会影响垃圾回收。例如:
a = []
b = [a]
a.append(b)
创建了两个相互引用的列表对象。即使我们 del(a) 和 del(b),它们的引用计数不为 0,导致不被回收。
其它的对象
▼
┌───┐
┌──│ 1 │──┐ ┌────────┐
│ └───┘ │ │ │
▼ ▼ │ ▼
┌───┐ ┌───┐ ┌───┐ ┌───┐
│ 1 │────▶│ 2 │ │ 1 │ │ 1 │
└───┘ └───┘ └───┘ └───┘
│ ▲ │
┌───┐ │ │ │
│ 1 │◀─┘ └────────┘
└───┘ 孤立的引用环
非孤立的引用环
为解决此,Python 采用 gc_ref 机制。每个对象的引用计数被复制到 gc_ref,对于每个对象引用的对象,gc_ref 会减 1。
其它的对象
▼
┌───┐ 1
┌──│ 1 │──┐ ┌────────┐
│ └───┘ │ │ │
▼ ▼ │ 0 ▼ 0
┌───┐ 0 ┌───┐ 0 ┌───┐ ┌───┐
│ 1 │────▶│ 2 │ │ 1 │ │ 1 │
└───┘ └───┘ └───┘ └───┘
│ ▲ │
0 ┌───┐ │ │ │
│ 1 │◀─┘ └────────┘
└───┘ 孤立的引用环
非孤立的引用环
遍历后,非 0 的 gc_ref 对象及其引用被保留,其它被回收。
网络
想要深入了解 Python 服务器框架,例如 Django, Twisted, 和 web.py,你首先需要掌握它们的基石 - socket (套接字)。套接字提供了基于网络的进程间通信能力。掌握套接字可以帮助我们更有效地使用和设计框架。
所有的 Python 框架,从底层开始,都是基于套接字实现的。如果你能从底层的套接字开始,建立一个完整的 Python 服务器,并处理诸如 MVC (Model-View-Control)、多线程等复杂问题。
值得注意的是,套接字接口是由操作系统提供的,并不仅限于 Python。像 Apache 这样的服务器就是用 C 语言实现的,但框架的语言实现是不可以跨语言使用的。
TCP/IP 和 socket
在探讨套接字之前,我们需要先了解网络传输,尤其是 TCP/IP 协议。套接字是基于网络协议的进程间通信方法,有 TCP 和 UDP 两种类型,其中 TCP 最为常用。
TCP 套接字可以看作双向通道,两个进程可以通过它互相发送和接收信息,即使它们位于两台不同的计算机上。为了支持这种通信,我们需要一个协议 - TCP 协议,它规定了通信的各种规则。
每个套接字都包含四个地址信息:两个 IP 地址 (两台计算机的地址) 和两个端口号 (每个进程的标识)。
IP 1
┌─┐┌──────┐┌────┐
│═││░░░░░░││port│◀─────────┐
│═││░░░░░░│└────┘ │
└─┘╧══════╧ ▼
IP 2 IP 3 ┌────┐
┌─┐┌──────┐┌────┐ ┌─┐┌──────┐│port│
│═││░░░░░░││port│ │═││░░░░░░│├────┤
│═││░░░░░░│└────┘ │═││░░░░░░││port│
└─┘╧══════╧ │ └─┘╧══════╧└────┘
│ ▲
│ │
└──────────────────────┘
Socket 服务器
在网络中,计算机可以作为服务器或客户端。服务器会开放特定的端口,等待其他计算机的连接。当其他计算机尝试连接时,服务器开始提供服务。
在 Python 中,我们可以使用 socket 模块来实现底层的套接字编程。服务器端使用 bind() 方法指定地址和端口,使用 listen() 方法监听连接。当客户端使用 connect() 连接时,服务器使用 accept() 接受连接。
以下是一个简单的服务器和客户端的代码示例:
服务器代码:
# 文件: server.py
import socket
HOST = ''
PORT = 8000
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((HOST, PORT))
server_socket.listen(3)
client_conn, addr = server_socket.accept()
request = client_conn.recv(1024)
print(f"the request of client is: {request}, connected by: {addr}")
client_conn.sendall(b'Yes')
client_conn.close()
客户端代码:
# 文件 client.py
import socket
HOST = '127.0.0.1'
PORT = 8000
request = b'can you hear me'
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.sendall(request)
reply = s.recv(1024)
print(f"the reply is {reply}")
s.close()
为了在同一台计算机上运行这个示例,你需要在两个不同的命令行窗口中分别运行服务器和客户端。
HTTP 服务器
当我们谈到网络连接,我们通常想到的是使用 TCP socket 连接两台计算机。但直接使用 socket 可能会导致一些安全和兼容性问题。为了解决这些问题,我们通常使用应用层的协议,如 HTTP 协议,来规定如何使用 socket。
HTTP 是一个基于请求-响应模型的协议。这意味着客户端发送一个请求到服务器,服务器接收并处理这个请求,然后发送一个响应给客户端。
现在,让我们看一个简单的HTTP服务器的实现:
示例
import socket
# 设置地址和端口
HOST = ''
PORT = 8000
# 定义HTTP响应内容
text_content = """\
HTTP/1.1 200 OK
Content-Type: text/html
<head>
<title>Custom Python Server</title>
</head>
<html>
<p>Welcome to our Python Server!</p>
<img src="test.jpg" width="200px"></img>
<br>
<form name="input" action="/" method="post">
Your Response: <input type="text" name="response"><br>
<input type="submit" value="Send!">
</form>
<p>Response received: {value}</p>
</html>
"""
# 读取图片并设置为HTTP响应格式
with open('test.jpg', 'rb') as f:
pic_content = b'HTTP/1.1 200 OK\n'+\
b'Content-Type: image/jpeg\n\n'
pic_content += f.read()
# 配置 socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
# 服务器始终在线
while True:
# 等待连接
s.listen(3)
conn, addr = s.accept()
request = conn.recv(1024).decode('utf-8')
method = request.split(' ')[0]
url = request.split(' ')[1]
# 处理GET请求
if method == 'GET':
if url == '/test.jpg':
content = pic_content
else:
content = text_content.format(value='').encode('utf-8')
print("Connected by: ", addr)
print("Request is: ", request)
conn.sendall(content)
# 处理POST请求
if method == 'POST':
form = request.split('\r\n')
idx = form.index('')
entry = form[idx:]
value = entry[-1].split('=')[-1]
response = text_content.format(value=value).encode('utf-8')
conn.sendall(response)
# 关闭连接
conn.close()
使用 socketserver
虽然上面的方法是有效的,但还有更简单的方法来创建一个HTTP服务器,这就是使用 Python 的 socketserver 库。
import socketserver
# 设置地址和端口
HOST = ''
PORT = 8000
# 定义HTTP响应内容
text_content = """\
HTTP/1.1 200 OK
Content-Type: text/html
<head>
<title>Custom Python Server</title>
</head>
<html>
<p>Welcome to our Python Server!</p>
<img src="test.jpg" width="200px"></img>
<br>
<form name="input" action="/" method="post">
Your Response: <input type="text" name="response"><br>
<input type="submit" value="Send!">
</form>
<p>Response received: {value}</p>
</html>
"""
# 读取图片并设置为HTTP响应格式
with open('test.jpg', 'rb') as f:
pic_content = b'HTTP/1.1 200 OK\n'+\
b'Content-Type: image/jpeg\n\n'
pic_content += f.read()
class MyTCPHandler(socketserver.BaseRequestHandler):
# 定义如何处理每个连接
def handle(self):
request = self.request.recv(1024).decode('utf-8')
print("Connected by", self.client_address)
print("Request is:", request)
method = request.split(' ')[0]
src = request.split(' ')[1]
if method == 'GET':
if src == '/test.jpg':
content = pic_content
else:
content = text_content.encode('utf-8')
self.request.sendall(content)
if method == 'POST':
form = request.split('\r\n')
idx = form.index('')
entry = form[idx:]
value = entry[-1].split('=')[-1]
response = text_content.format(value=value).encode('utf-8')
self.request.sendall(response)
# 开始服务器
with socketserver.TCPServer((HOST, PORT), MyTCPHandler) as server:
server.serve_forever()
使用 socketserver 的好处是它简化了大部分 socket 编程的复杂性。这让我们更容易理解和维护代码。
http.server
SimpleHTTPRequestHandler
HTTP 协议基于 TCP 协议,但增加了更多的规范。这些规范,虽然限制了 TCP 协议的功能,但大大提高了信息封装和提取的方便程度。
对于一个 HTTP 请求 (request) 来说,它包含有两个重要信息:
- 请求方法
- URL。
例子,刚才的 server 分析:
| 请求方法 | URL | 操作 |
|---|---|---|
| GET | / | 发送 text_content |
| GET | /text.png | 发送 pic_content |
| POST | / | 分析 request 主体中包含的 value |
根据请求方法和URL的不同,一个大型的 HTTP 服务器可以应付成千上万种不同的请求。在 Python 中,我们可以使用 http.server 包来规定针对不同请求的操作。其中,SimpleHTTPServer 可以用于处理 GET 方法和 HEAD 方法的请求。它读取 request 中的 URL 地址,找到对应的静态文件,分析文件类型,用 HTTP 协议将文件发送给客户。
我在当前目录下生成 index.html 文件:
<head>
<title>WOW</title>
</head>
<html>
<p>Wow, Python Server</p>
<IMG src="test.jpg"/>
<form name="input" action="/" method="post">
First name:<input type="text" name="firstname"><br>
<input type="submit" value="Submit">
</form>
</html>
使用 http.server 中的类 SimpleHTTPRequestHandler 写服务器:
import socketserver
import http.server as http_server
HOST = ''
PORT = 8000
# Create the server
handler = http_server.SimpleHTTPRequestHandler
server = socketserver.TCPServer((HOST, PORT), handler)
# Start the server
server.serve_forever()
运行结果与之前的一样。
CGIHTTPServer
CGIHTTPRequestHandler 类继承自 SimpleHTTPRequestHandler 类,所以可以用来代替上面的例子,来提供静态文件的服务。此外,CGIHTTPRequestHandler类还可以用来运行CGI脚本。
先看看什么是 CGI (Common Gateway Interface)。CGI 是服务器和应用脚本之间的一套接口标准。它的功能是让服务器程序运行脚本程序,将程序的输出作为 response 发送给客户。总体的效果,是允许服务器动态的生成回复内容,而不必局限于静态文件。
支持 CGI 的服务器程接收到客户的请求,根据请求中的 URL,运行对应的脚本文件。服务器会将 HTTP 请求的信息和 socket 信息传递给脚本文件,并等待脚本的输出。脚本的输出封装成合法的 HTTP 回复,发送给客户。CGI可以充分发挥服务器的可编程性,让服务器变得“更聪明”。
服务器和 CGI 脚本之间的通信要符合 CGI 标准。CGI 的实现方式有很多,比如说使用 Apache 服务器与 Perl 写的CGI脚本,或者 Python 服务器与 shell 写的CGI脚本。
为了使用 CGI,我们需要使用 http.server 包中的 HTTPServer 类来构建服务器。Python服务器的改动很简单。
import http.server
from http.server import BaseHTTPRequestHandler
from http.server import CGIHTTPRequestHandler
HOST = ''
PORT = 8000
# Create the server
handler = BaseHTTPRequestHandler
server = http.server.HTTPServer((HOST, PORT), handler)
server.serve_forever()
CGIHTTPRequestHandler 默认当前目录下的 cgi-bin 和 ht-bin 文件夹中的文件为 CGI 脚本,而存放于其他地方的文件被认为是静态文件。因此,我们需要修改一下 index.html,将其中 form 元素指向的 action 改为 cgi-bin/post.py。
<head>
<title>WOW</title>
</head>
<html>
<p>Wow, Python Server</p>
<IMG src="test.jpg"/>
<form name="input" action="cgi-bin/post.py" method="post">
First name:<input type="text" name="firstname"><br>
<input type="submit" value="Submit">
</form>
</html>
我创建一个cgi-bin的文件夹,并在cgi-bin中放入如下post.py文件,也就是我们的CGI脚本:
#!/usr/bin/env python3 # 这一行是必要的,指定脚本的解释器
import cgi
form = cgi.FieldStorage()
print("Content-Type: text/html")
print() # 这个空行是必要的,作为首部的终止
print("<p>Hello world!</p>")
print("<p>" + repr(form['firstname']) + "</p>")
(post.py需要有执行权限,chmod +x cgi-bin/post.py)
第一行说明了脚本所使用的语言,即 Python。 cgi 包用于提取请求中包含的表格信息。脚本只负责将所有的结果输出到标准输出 (使用print)。CGIHTTPRequestHandler 会收集这些输出,封装成 HTTP 回复,传送给客户端。
对于 POST 方法的请求,它的 URL 需要指向一个 CGI 脚本 (也就是在 cgi-bin 或者 ht-bin 中的文件)。CGIHTTPRequestHandler 继承自 SimpleHTTPRequestHandler,所以也可以处理 GET 方法和 HEAD 方法的请求。如果 URL 指向CGI脚本时,服务器将脚本的运行结果传送到客户端;当此时URL 指向静态文件时,服务器将文件的内容传送到客户端。
元编程
概念
- 元编程 ( metaprogramming ) - 如果写出的程序有能力 了解/改变 自己,这样的编程就叫元编程。
- 元类 ( metaclass ) - Python 里的一种,支持元编程的 技巧
元编程是深奥的面向对象 ( OOP ) 技巧。大部分的 Python 程序不需要使用它。但是你可以通过 metaclass 来实现很多黑魔法。不过请看一下 Python 专家的 真知灼见:
“Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t (the people who actually need them know with certainty that they need them, and don’t need an explanation about why).”
— Tim Peters
不过,即使我们用不上,我们也有理由理解 Python 的元类。
新式类和旧式类
在 Python 2 里,一个 实例 的 __class__ 特性是它的 类;但是这个实例的 type 是 instance。下面的代码给出了例子。
In [1]: class Foo: pass
In [2]: x = Foo()
In [3]: x.__class__
Out[3]: <class __main__.Foo at 0x10c32f600>
In [4]: type(x)
Out[4]: instance # 不是 __main__.Foo
这样的类被称为 旧式类 ( Old-Style Class )。与之对应,在 Python 3 中,我们拥有了 新式类 ( New-Style Class)。对于 新式类 的实例,其 __class__ 与 type 都是这个 类 本身。下面的代码给出了例子。
In [1]: class Foo: pass
In [2]: x = Foo()
In [3]: x.__class__
Out[3]: __main__.Foo
In [4]: type(x)
Out[4]: __main__.Foo # 与 __class__ 一致
元类:类的类
在 Python 中,所有东西都是对象 ( everything is an object )。所以 Python 里的 类 也是对象。这些 类 也有它们的 type。它们的 type 就是 type,type 自己是一个 元类。
In [2]: class Foo: pass
In [3]: type(Foo)
Out[3]: type
所有 Python 自带的类的 type 也是 type
In [4]: for t in [int, float, list, dict]:
...: print(type(t))
...:
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>
type 的 元类 也是 type
In [5]: type(type)
Out[5]: type
下面的图总结了 Python 里的层级结构。
┌───────────┐
│ │
│ ▼
│ ┌─────────────────┐
└──│ type │
└─────────────────┘
▲
│
│
┌─────────────────┐
│ Foo │
│ (or any class) │
└─────────────────┘
▲
│
│
┌─────────────────┐
│ x = Foo() │
│ (the instances) │
└─────────────────┘
作用:动态定义类
我们可以用 type 动态地创建类,语法如下
type(<name>, <base>, <dct>)
其中的三个参数分别是
name: 类的名字bases: 类的父类dct:类的命名空间字典,包含了类的具体定义
下面是一个具体的例子。
In [1]: Foo = type('Foo', (), {}) # 等价于 class Foo: pass
In [2]: x = Foo()
In [3]: x
Out[3]: <__main__.Foo at 0x104349590>
下面是一个包含 父类 和 dct 的例子
In [4]: Foo = type('Foo', (), {})
In [5]: Bar = type('Bar', (Foo,), dict(T=1))
In [6]: x = Bar()
In [7]: x.T
Out[7]: 1
下面是 dct 里有类的 方法 ( method ) 的例子
In [9]: Foo = type(
...: 'Foo',
...: (),
...: {
...: 'T': 1,
...: 'get_T': lambda x : x.T
...: }
...: )
In [10]: x = Foo()
In [11]: x.get_T()
Out[11]: 1
自定义元类
如果我们使用下面的代码创造一个类
class Foo: pass
f = Foo()
当 Python 解释器遇到 Foo() 的时候,下面的东西会发生
Foo的 parent class 的__call__()方法会被使用。因为 Python 3 里的类的 parent class 都是type, 所以这里会使用type的__call__()- 在
type的__call__()里,下面的连个方法会被调用。__new__()__init__()
如果 Foo 没有定义 __new__ 和 __init__,那么这两个方法会从 type 继承。我们可以用 monkey-patching 来修改 __new__ 来改变一个类的行为。下面展示了一个例子。
In [1]: class Foo: pass
In [2]: f = Foo(); f.T
---------------------------------------------------------------------------
AttributeError
In [3]: def new(cls):
...: x = object.__new__(cls)
...: x.T = 1
...: return x
...:
In [4]: Foo.__new__ = new
In [5]: g = Foo() # 重新定义了 __new__ 后,所有的实例都有 .T 特性
In [6]: g.T
Out[6]: 1
修改 type
如果我们修改 type 的 __new__ 我们岂不是可以修改「所有没有 __new__ 的类」的行为?让我们试一试
In [9]: def new(cls):
...: x = type.__new__(cls)
...: x.T = 1
...: return x
...:
In [10]: Foo.__new__ = new
In [11]: type.__new__ = new
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-11-9f1d36c02af0> in <module>
----> 1 type.__new__ = new
TypeError: can't set attributes of built-in/extension type 'type'
Python 强行制止了这种行为。
自定义元类
我们可以通过自定义元类,来达到类似「修改 type」的效果。下面的代码是一个例子。
# 定义元类
In [12]: class Meta(type):
...: def __new__(cls, name, bases, dct):
...: x = super().__new__(cls, name, bases, dct)
...: x.T = 1
...: return x
...:
# “使用”元类
In [13]: class Bar(metaclass=Meta): pass
In [14]: Bar.T
Out[14]: 1
In [15]: b = Bar()
In [16]: b.T
Out[16]: 1
# 所有从 元类 构造的类,都有共同的特性
In [17]: class Qux(metaclass=Meta): pass
In [18]: Qux.T
Out[18]: 1
避免使用元类
如果要「批量制造 具有共同点 的类」,我们可以用别的方法来达到相同的效果。通常我们应该用下面的这些,更简单的方法。
使用继承:
In [1]: class Base: T = 100
In [2]: class X(Base): pass
In [3]: class Y(Base): pass
In [4]: X.T, Y.T
Out[4]: (100, 100)
使用类的装饰器:
In [1]: def decorator(cls):
...: class NewClass(cls):
...: T = 1
...: return NewClass
...:
In [2]: @decorator
...: class X: pass
In [3]: @decorator
...: class Y: pass
In [4]: X.T, Y.T
Out[4]: (1, 1)