库和扩展常见问题解答

一般库问题

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

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

对于第三方包,请搜索 Python 包索引 或尝试 Google 或其他网络搜索引擎。搜索“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 的操作系统不兼容,但目前似乎没有维护的 OS 属于此类别。

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 时运行。

早在 Python 1.5 时代,Greg Stein 实际上实现了一个全面的补丁集(“自由线程”补丁),它移除了 GIL 并用细粒度锁定替换了它。Adam Olsen 最近在他的python-safethread 项目中进行了类似的实验。不幸的是,这两个实验都表现出单线程性能急剧下降(至少慢了 30%),这是由于为了弥补移除 GIL 而需要进行大量细粒度锁定。

这并不意味着您不能在多 CPU 机器上充分利用 Python!您只需要在多个进程之间而不是多个线程之间巧妙地划分工作即可。新concurrent.futures 模块中的ProcessPoolExecutor 类提供了一种简单的方法;multiprocessing 模块提供了一个更低级的 API,如果您想要更多地控制任务调度,可以使用它。

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

有人建议 GIL 应该是一个每解释器状态的锁,而不是真正的全局锁;这样解释器就无法共享对象。不幸的是,这种情况也不太可能发生。这将是一项巨大的工作,因为许多对象实现目前都有全局状态。例如,小整数和短字符串被缓存;这些缓存必须移动到解释器状态。其他对象类型有自己的空闲列表;这些空闲列表必须移动到解释器状态。等等。

我怀疑这甚至无法在有限的时间内完成,因为第三方扩展也存在同样的问题。第三方扩展的编写速度可能比您将它们转换为将所有全局状态存储在解释器状态中的速度更快。

最后,一旦您拥有多个不共享任何状态的解释器,与在单独的进程中运行每个解释器相比,您获得了什么好处呢?

输入和输出

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

使用 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 默认值为当前的 seek 位置。对于使用 os.open() 打开的文件,还有 os.ftruncate(fd, offset),其中 fd 是文件描述符(一个小整数)。

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

如何复制文件?

shutil 模块包含一个 copyfile() 函数。请注意,在 Windows NTFS 卷上,它不会复制 备用数据流 也不复制 macOS HFS+ 卷上的 资源分支,尽管两者现在很少使用。它也不会复制文件权限和元数据,尽管使用 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 文件描述符上的高级抽象层。

对于您在 Python 中通过内置 open() 函数创建的大多数文件对象,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 Protocols and SupportInternet Data Handling 的章节。Python 有许多模块可以帮助您构建服务器端和客户端 Web 系统。

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

Cameron Laird 在 https://web.archive.org/web/20210224183619/http://phaseit.net/claird/comp.lang.python/web_python 上维护着一组关于 Python Web 技术的有用页面。

如何模拟 CGI 表单提交 (METHOD=POST)?

我想检索 POST 表单后生成的网页。是否有现有的代码可以让我轻松地做到这一点?

是的。这是一个使用 urllib.request 的简单示例

#!/usr/local/bin/python

import urllib.request

# build the query string
qs = "First=Josephine&MI=Q&Last=Public"

# connect and send the server a path
req = urllib.request.urlopen('http://www.some-server.out-there'
                             '/cgi-bin/some-cgi-script', data=qs)
with req:
    msg, hdrs = req.read(), req.info()

请注意,通常对于百分比编码的 POST 操作,查询字符串必须使用 urllib.parse.urlencode() 引用。例如,要发送 name=Guy Steele, Jr.

>>> import urllib.parse
>>> urllib.parse.urlencode({'name': 'Guy Steele, Jr.'})
'name=Guy+Steele%2C+Jr.'

另请参阅

HOWTO Fetch Internet Resources Using The urllib Package 以获取更多示例。

我应该使用哪个模块来帮助生成 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: [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)

如何避免在套接字的 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 类,您可以实例化它来创建独立的多个随机数生成器。