库和扩展 FAQ

通用库问题

如何找到一个模块或应用程序来执行任务 X?

查看库参考,看看是否有相关的标准库模块。(最终你会了解标准库中有什么,并能够跳过这一步。)

对于第三方包,请搜索 Python Package Index 或尝试 Google 或其他网络搜索引擎。搜索“Python”加上一两个与你感兴趣的主题相关的关键词通常会找到有用的东西。

math.py (socket.py, regex.py, etc.) 的源文件在哪里?

如果你找不到模块的源文件,它可能是一个内置的或动态加载的模块,用 C、C++ 或其他编译语言实现。在这种情况下,你可能没有源文件,或者它可能像 mathmodule.c 一样,在某个 C 源目录中(不在 Python 路径上)。

Python 中有 (至少) 三种模块

  1. 用 Python 编写的模块 (.py);

  2. 用 C 编写并动态加载的模块 (.dll, .pyd, .so, .sl, etc);

  3. 用 C 编写并与解释器链接的模块;要获取这些模块的列表,请键入

    import sys
    print(sys.builtin_module_names)
    

如何在 Unix 上使 Python 脚本可执行?

你需要做两件事:脚本文件的模式必须是可执行的,并且第一行必须以 #! 开头,后面跟着 Python 解释器的路径。

第一步是通过执行 chmod +x scriptfilechmod 755 scriptfile 完成的。

第二步可以通过多种方式完成。最直接的方法是写入

#!/usr/local/bin/python

作为文件的第一行,使用 Python 解释器安装在你平台上的路径。

如果你希望脚本独立于 Python 解释器的位置,你可以使用 env 程序。几乎所有的 Unix 变体都支持以下内容,假设 Python 解释器位于用户 PATH 中的目录中

#!/usr/bin/env python

CGI 脚本不要这样做。CGI 脚本的 PATH 变量通常非常少,所以你需要使用解释器的实际绝对路径。

偶尔,用户的环境太满,导致 /usr/bin/env 程序失败;或者根本没有 env 程序。在这种情况下,你可以尝试以下技巧(由 Alex Rezinsky 提供)

#! /bin/sh
""":"
exec python $0 ${1+"$@"}
"""

次要的缺点是这定义了脚本的 __doc__ 字符串。但是,你可以通过添加

__doc__ = """...Whatever..."""

Python 有 curses/termcap 包吗?

对于 Unix 变体:标准 Python 源代码分发在 Modules 子目录中包含一个 curses 模块,但默认情况下不编译。(请注意,这在 Windows 分发版中不可用——Windows 没有 curses 模块。)

curses 模块支持基本的 curses 功能以及 ncurses 和 SYSV curses 的许多附加功能,例如颜色、替代字符集支持、衬垫和鼠标支持。这意味着该模块与仅具有 BSD curses 的操作系统不兼容,但目前似乎没有任何维护的操作系统属于此类别。

Python 中有 C 语言 onexit() 的等价物吗?

atexit 模块提供了一个类似于 C 语言 onexit() 的注册函数。

为什么我的信号处理程序不起作用?

最常见的问题是信号处理程序声明的参数列表不正确。它被调用为

handler(signum, frame)

所以它应该声明两个参数

def handler(signum, frame):
    ...

常见任务

如何测试 Python 程序或组件?

Python 附带两个测试框架。doctest 模块查找模块文档字符串中的示例并运行它们,将输出与文档字符串中给出的预期输出进行比较。

unittest 模块是一个更高级的测试框架,模仿了 Java 和 Smalltalk 测试框架。

为了使测试更容易,你应该在你的程序中使用良好的模块化设计。你的程序应该将几乎所有的功能封装在函数或类方法中——这有时会产生令人惊讶和愉快的副作用,即使程序运行得更快(因为局部变量访问比全局访问更快)。此外,程序应该避免依赖于改变全局变量,因为这使得测试变得更加困难。

你的程序的“全局主逻辑”可能很简单,如下所示

if __name__ == "__main__":
    main_logic()

在程序主模块的底部。

一旦你的程序被组织成一个可处理的函数和类行为集合,你应该编写测试函数来练习这些行为。一个自动化测试序列的测试套件可以与每个模块关联起来。这听起来工作量很大,但由于 Python 如此简洁和灵活,它出奇地容易。你可以通过与“生产代码”并行编写测试函数来使编码更加愉快和有趣,因为这使得更容易发现错误甚至设计缺陷。

不打算作为程序主模块的“支持模块”可能包含模块的自测试。

if __name__ == "__main__":
    self_test()

即使是与复杂外部接口交互的程序,在外部接口不可用时也可以通过使用 Python 中实现的“模拟”接口进行测试。

如何从文档字符串创建文档?

pydoc 模块可以从你的 Python 源代码中的文档字符串创建 HTML。另一个纯粹从文档字符串创建 API 文档的替代方案是 epydocSphinx 也可以包含文档字符串内容。

如何一次获取一个按键?

对于 Unix 变体有几种解决方案。使用 curses 很容易做到这一点,但 curses 是一个相当大的模块,需要学习。

线程

如何使用线程编程?

请务必使用 threading 模块,而不是 _thread 模块。threading 模块在 _thread 模块提供的低级原语之上构建了方便的抽象。

我的所有线程似乎都没有运行:为什么?

一旦主线程退出,所有线程都会被终止。你的主线程运行得太快,导致线程没有时间做任何工作。

一个简单的解决方法是在程序末尾添加一个足够长的睡眠,以便所有线程都能完成工作。

import threading, time

def thread_task(name, n):
    for i in range(n):
        print(name, i)

for i in range(10):
    T = threading.Thread(target=thread_task, args=(str(i), i))
    T.start()

time.sleep(10)  # <---------------------------!

但现在 (在许多平台上) 线程不是并行运行,而是似乎一个接一个地按顺序运行!原因是操作系统线程调度程序在先前的线程被阻塞之前不会启动新线程。

一个简单的解决方法是在运行函数的开头添加一个微小的睡眠

def thread_task(name, n):
    time.sleep(0.001)  # <--------------------!
    for i in range(n):
        print(name, i)

for i in range(10):
    T = threading.Thread(target=thread_task, args=(str(i), i))
    T.start()

time.sleep(10)

与其尝试猜测 time.sleep() 的合适延迟值,不如使用某种信号量机制。一个想法是使用 queue 模块创建一个队列对象,让每个线程在完成时向队列添加一个令牌,并让主线程从队列中读取与线程数相同数量的令牌。

如何将工作分配给一堆工作线程?

最简单的方法是使用 concurrent.futures 模块,特别是 ThreadPoolExecutor 类。

或者,如果你想对调度算法进行精细控制,你可以手动编写自己的逻辑。使用 queue 模块创建一个包含作业列表的队列。Queue 类维护一个对象列表,并具有一个将项添加到队列的 .put(obj) 方法和一个返回它们的 .get() 方法。该类将负责必要的锁定,以确保每个作业只分发一次。

这是一个简单的例子

import threading, queue, time

# The worker thread gets jobs off the queue.  When the queue is empty, it
# assumes there will be no more work and exits.
# (Realistically workers will run until terminated.)
def worker():
    print('Running worker')
    time.sleep(0.1)
    while True:
        try:
            arg = q.get(block=False)
        except queue.Empty:
            print('Worker', threading.current_thread(), end=' ')
            print('queue empty')
            break
        else:
            print('Worker', threading.current_thread(), end=' ')
            print('running with argument', arg)
            time.sleep(0.5)

# Create queue
q = queue.Queue()

# Start a pool of 5 workers
for i in range(5):
    t = threading.Thread(target=worker, name='worker %i' % (i+1))
    t.start()

# Begin adding work to the queue
for i in range(50):
    q.put(i)

# Give threads time to run
print('Main thread sleeping')
time.sleep(5)

运行时,这将产生以下输出

Running worker
Running worker
Running worker
Running worker
Running worker
Main thread sleeping
Worker <Thread(worker 1, started 130283832797456)> running with argument 0
Worker <Thread(worker 2, started 130283824404752)> running with argument 1
Worker <Thread(worker 3, started 130283816012048)> running with argument 2
Worker <Thread(worker 4, started 130283807619344)> running with argument 3
Worker <Thread(worker 5, started 130283799226640)> running with argument 4
Worker <Thread(worker 1, started 130283832797456)> running with argument 5
...

有关更多详细信息,请查阅模块的文档;Queue 类提供了一个功能丰富的接口。

哪些全局值变异是线程安全的?

内部使用全局解释器锁(GIL)来确保一次只有一个线程在 Python VM 中运行。通常,Python 只在字节码指令之间进行线程切换;切换的频率可以通过 sys.setswitchinterval() 设置。因此,从 Python 程序的角度来看,每条字节码指令以及从每条指令到达的所有 C 实现代码都是原子的。

理论上,这意味着精确的核算需要对 PVM 字节码实现有精确的理解。实际上,这意味着对内置数据类型(整数、列表、字典等)的共享变量进行“看起来是原子”的操作确实是原子的。

例如,以下操作都是原子性的(L、L1、L2 是列表,D、D1、D2 是字典,x、y 是对象,i、j 是整数)

L.append(x)
L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()

这些不是

i = i+1
L.append(L[-1])
L[i] = L[j]
D[x] = D[x] + 1

替换其他对象的操作可能会在它们的引用计数达到零时调用这些其他对象的 __del__() 方法,这可能会影响事情。对于字典和列表的批量更新尤其如此。有疑问时,请使用互斥锁!

我们不能摆脱全局解释器锁吗?

全局解释器锁 (GIL) 通常被视为 Python 在高端多处理器服务器机器上部署的障碍,因为多线程 Python 程序实际上只使用一个 CPU,因为它坚持(几乎)所有 Python 代码只能在持有 GIL 的情况下运行。

PEP 703 批准,目前正在进行从 Python 的 CPython 实现中移除 GIL 的工作。最初,它将作为构建解释器时的可选编译器标志来实现,因此将提供带和不带 GIL 的独立构建。从长远来看,一旦完全了解移除 GIL 的性能影响,希望能够确定一个单一的构建。Python 3.13 可能是第一个包含此工作的版本,尽管它在该版本中可能不完全功能。

当前移除 GIL 的工作基于 Sam Gross 移除了 GIL 的 Python 3.9 分支。在此之前,在 Python 1.5 时代,Greg Stein 实际上实现了一套全面的补丁(“自由线程”补丁),该补丁移除了 GIL 并将其替换为细粒度锁定。Adam Olsen 在他的 python-safethread 项目中也做了类似的实验。不幸的是,这两个早期的实验都显示出单线程性能急剧下降(至少慢了 30%),原因是需要大量细粒度锁定来弥补 GIL 的移除。Python 3.9 分支是第一次在可接受的性能影响下移除 GIL 的尝试。

当前 Python 版本中 GIL 的存在并不意味着你不能在多 CPU 机器上很好地利用 Python!你只需要创造性地在多个进程而不是多个线程之间划分工作。新的 concurrent.futures 模块中的 ProcessPoolExecutor 类提供了一种简单的方法来做到这一点;如果你想对任务调度有更多控制,multiprocessing 模块提供了更低级的 API。

明智地使用 C 扩展也有帮助;如果你使用 C 扩展来执行耗时任务,扩展可以在执行线程处于 C 代码中时释放 GIL,并允许其他线程完成一些工作。一些标准库模块,如 zlibhashlib 已经这样做了。

另一种减少 GIL 影响的方法是使 GIL 成为每个解释器状态锁,而不是真正的全局锁。这首次在 Python 3.12 中实现,并在 C API 中可用。预计在 Python 3.13 中将提供一个 Python 接口。目前的主要限制可能是第三方扩展模块,因为这些模块必须在考虑多个解释器的情况下编写才能使用,因此许多较旧的扩展模块将无法使用。

输入和输出

如何删除文件?(以及其他文件问题……)

使用 os.remove(filename)os.unlink(filename);有关文档,请参见 os 模块。这两个函数是相同的;unlink() 只是 Unix 系统调用此函数的名称。

要删除目录,请使用 os.rmdir();使用 os.mkdir() 创建目录。os.makedirs(path) 将在 path 中创建所有不存在的中间目录。os.removedirs(path) 将删除中间目录,只要它们是空的;如果你想删除整个目录树及其内容,请使用 shutil.rmtree()

要重命名文件,请使用 os.rename(old_path, new_path)

要截断文件,请使用 f = open(filename, "rb+") 打开它,然后使用 f.truncate(offset);offset 默认为当前查找位置。对于使用 os.open() 打开的文件,也有 os.ftruncate(fd, offset),其中 fd 是文件描述符(一个小的整数)。

shutil 模块还包含许多用于文件操作的函数,包括 copyfile()copytree()rmtree()

如何复制文件?

shutil 模块包含一个 copyfile() 函数。请注意,在 Windows NTFS 卷上,它不复制 备用数据流,也不复制 macOS HFS+ 卷上的 资源 fork,尽管两者现在都很少使用。它也不复制文件权限和元数据,尽管使用 shutil.copy2() 代替会保留大部分(但不是全部)权限和元数据。

如何读取 (或写入) 二进制数据?

要读取或写入复杂的二进制数据格式,最好使用 struct 模块。它允许你获取包含二进制数据(通常是数字)的字符串并将其转换为 Python 对象;反之亦然。

例如,以下代码从文件中读取两个 2 字节整数和一个 4 字节整数,采用大端格式

import struct

with open(filename, "rb") as f:
    s = f.read(8)
    x, y, z = struct.unpack(">hhl", s)

格式字符串中的 '>' 强制使用大端数据;字母 'h' 读取一个“短整数”(2 字节),'l' 读取一个“长整数”(4 字节)。

对于更规则的数据(例如,一个同构的整数或浮点数列表),你还可以使用 array 模块。

备注

要读取和写入二进制数据,必须以二进制模式打开文件(此处,将 "rb" 传递给 open())。如果你使用 "r" 代替(默认),文件将以文本模式打开,f.read() 将返回 str 对象而不是 bytes 对象。

我似乎无法在用 os.popen() 创建的管道上使用 os.read();为什么?

os.read() 是一个低级函数,它接受一个文件描述符,一个表示已打开文件的小整数。os.popen() 创建一个高级文件对象,与内置的 open() 函数返回的类型相同。因此,要从使用 os.popen() 创建的管道 p 中读取 n 字节,你需要使用 p.read(n)

如何访问串行 (RS232) 端口?

对于 Win32、OSX、Linux、BSD、Jython、IronPython

对于 Unix,请参阅 Mitch Chapman 的 Usenet 帖子

为什么关闭 sys.stdout (stdin, stderr) 并不能真正关闭它?

Python 文件对象 是低级 C 文件描述符的高级抽象层。

对于通过内置的 open() 函数在 Python 中创建的大多数文件对象,f.close() 将 Python 文件对象标记为从 Python 的角度看已关闭,并且还安排关闭底层的 C 文件描述符。当 f 变成垃圾时,这也会在 f 的析构函数中自动发生。

但是 stdin、stdout 和 stderr 在 Python 中被特殊对待,因为 C 也赋予它们特殊的地位。运行 sys.stdout.close() 将 Python 级文件对象标记为已关闭,但不会关闭关联的 C 文件描述符。

要关闭这三个中的一个的底层 C 文件描述符,你应该首先确定这确实是你想要做的事情(例如,你可能会混淆试图进行 I/O 的扩展模块)。如果是这样,请使用 os.close()

os.close(stdin.fileno())
os.close(stdout.fileno())
os.close(stderr.fileno())

或者你可以分别使用数字常量 0、1 和 2。

网络/互联网编程

Python 有哪些 WWW 工具?

请参阅库参考手册中标题为Internet 协议和支持Internet 数据处理的章节。Python 有许多模块可以帮助你构建服务器端和客户端 Web 系统。

Paul Boddie 在 https://wiki.python.org/moin/WebProgramming 维护了一个可用框架的摘要。

我应该使用哪个模块来帮助生成 HTML?

你可以在 Web Programming wiki 页面上找到有用的链接集合。

如何从 Python 脚本发送邮件?

使用标准库模块 smtplib

这是一个使用它的非常简单的交互式邮件发送器。此方法适用于任何支持 SMTP 监听器的主机。

import sys, smtplib

fromaddr = input("From: ")
toaddrs  = input("To: ").split(',')
print("Enter message, end with ^D:")
msg = ''
while True:
    line = sys.stdin.readline()
    if not line:
        break
    msg += line

# The actual mail send
server = smtplib.SMTP('localhost')
server.sendmail(fromaddr, toaddrs, msg)
server.quit()

仅限 Unix 的替代方法使用 sendmail。sendmail 程序的路径在不同系统之间有所不同;有时是 /usr/lib/sendmail,有时是 /usr/sbin/sendmail。sendmail 手册页会帮助你。以下是一些示例代码

import os

SENDMAIL = "/usr/sbin/sendmail"  # sendmail location
p = os.popen("%s -t -i" % SENDMAIL, "w")
p.write("To: receiver@example.com\n")
p.write("Subject: test\n")
p.write("\n")  # blank line separating headers from body
p.write("Some text\n")
p.write("some more text\n")
sts = p.close()
if sts != 0:
    print("Sendmail exit status", sts)

如何避免在套接字的 connect() 方法中阻塞?

select 模块通常用于协助套接字的异步 I/O。

为了防止 TCP 连接阻塞,你可以将套接字设置为非阻塞模式。然后当你执行 connect() 时,你将立即连接(不太可能)或者会得到一个包含错误号作为 .errno 的异常。errno.EINPROGRESS 表示连接正在进行中,但尚未完成。不同的操作系统会返回不同的值,所以你必须检查你的系统上返回的值。

你可以使用 connect_ex() 方法来避免创建异常。它只会返回 errno 值。要轮询,你可以在稍后再次调用 connect_ex() —— 0errno.EISCONN 表示你已连接 —— 或者你可以将此套接字传递给 select.select() 以检查它是否可写。

备注

asyncio 模块提供了一个通用的单线程和并发异步库,可用于编写非阻塞网络代码。第三方 Twisted 库是一个流行且功能丰富的替代方案。

数据库

Python 中有数据库包的接口吗?

有。

标准 Python 还包括对基于磁盘的哈希(例如 DBMGDBM)的接口。此外,还有 sqlite3 模块,它提供了一个轻量级的基于磁盘的关系数据库。

大多数关系数据库都支持。有关详细信息,请参阅 DatabaseProgramming wiki 页面

如何在 Python 中实现持久化对象?

pickle 库模块以非常通用的方式解决了这个问题(尽管你仍然无法存储打开的文件、套接字或窗口之类的东西),而 shelve 库模块使用 pickle 和 (g)dbm 创建包含任意 Python 对象的持久映射。

数学和数值计算

如何在 Python 中生成随机数?

标准模块 random 实现了一个随机数生成器。用法很简单

import random
random.random()

这会返回一个范围在 [0, 1) 的随机浮点数。

此模块中还有许多其他专用生成器,例如

  • randrange(a, b) 从范围 [a, b) 中选择一个整数。

  • uniform(a, b) 从范围 [a, b) 中选择一个浮点数。

  • normalvariate(mean, sdev) 采样正态(高斯)分布。

一些更高级的函数直接对序列进行操作,例如

  • choice(S) 从给定序列中选择一个随机元素。

  • shuffle(L) 在原地打乱列表,即随机排列它。

还有一个 Random 类,你可以实例化它来创建独立的多个随机数生成器。