库和扩展常见问题解答

常规库问题

如何查找执行任务 X 的模块或应用程序?

查看 库参考 以查看是否有相关的标准库模块。(最终您将了解标准库中的内容,并且能够跳过此步骤。)

对于第三方软件包,请搜索 Python 包索引 或尝试 Google 或其他 Web 搜索引擎。搜索“Python”加上一个或两个您感兴趣的主题的关键字通常会找到一些有用的东西。

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

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

Python 中至少有三种模块

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

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

  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 模块提供了一个 register 函数,该函数类似于 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)  # <---------------------------!

但是现在(在许多平台上),线程不是并行运行,而是看起来顺序运行,一次一个!原因是操作系统线程调度器在之前的线程被阻塞之前不会启动新线程。

一个简单的解决方法是在run函数的开头添加一个很小的休眠。

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 获得批准后,目前正在进行从 CPython Python 实现中删除 GIL 的工作。最初,它将在构建解释器时作为可选的编译器标志实现,因此将提供带有和不带有 GIL 的单独构建。长期来看,希望在充分了解删除 GIL 的性能影响后,确定单个构建。Python 3.13 可能是第一个包含这项工作的版本,尽管它在这个版本中可能并不完全可用。

当前删除 GIL 的工作基于 Sam Gross 的 删除了 GIL 的 Python 3.9 分支。在此之前,在 Python 1.5 的时代,Greg Stein 实际上实现了一个全面的补丁集(“自由线程”补丁),该补丁集删除了 GIL 并将其替换为细粒度锁定。Adam Olsen 在他的 python-safethread 项目中进行了类似的实验。不幸的是,由于为补偿删除 GIL 所需的大量细粒度锁定,这两个早期实验都表现出单线程性能的急剧下降(至少慢 30%)。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 是文件描述符(一个小整数)。

要读取或写入复杂的二进制数据格式,最好使用

例如,以下代码从文件中读取两个 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 字节)。

对于更规则的数据(例如,整数或浮点数的同构列表),你也可以使用

注意

要读取和写入二进制数据,必须以二进制模式打开文件(此处,将 "rb" 传递给

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

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

Python

对于你通过内置的

但是,由于 C 也赋予了 stdin、stdout 和 stderr 特殊状态,Python 会对它们进行特殊处理。运行 sys.stdout.close() 会将 Python 级文件对象标记为已关闭,但不会关闭关联的 C 文件描述符。

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

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

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

请参阅《库参考手册》中标题为

Paul Boddie 在

你可以在

使用标准库模块

这是一个非常简单的交互式邮件发送器,它使用该模块。此方法适用于任何支持 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: [email protected]\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)

要防止 TCP 连接阻塞,可以将套接字设置为非阻塞模式。然后,当你执行

你可以使用

注意

是的。

标准 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 类,你可以实例化它来创建独立的多个随机数生成器。