函式庫和擴充功能的常見問題
**************************


常見函式問題
============


如何找到可以用来做 X 任务的模块或应用？
---------------------------------------

在 标准库参考 中查找是否有适合的标准库模块。（如果你已经了解标准库的内
容，可以跳过这一步）

对于第三方软件包，请搜索 Python Package Index 或者是尝试 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 編寫並與直譯器鏈接的模組；要獲得這些 list，請輸入：

      import sys
      print(sys.builtin_module_names)


我如何使 Python script 執行在 Unix？
------------------------------------

你需要做两件事：文件必须是可执行的，并且第一行需要以 "#!" 开头，后面跟
上 Python 解释器的路径。

第一点可以用执行 "chmod +x scriptfile" 或是 "chmod 755 scriptfile" 做
到。

第二点有很多种做法，最直接的方式是：

   #!/usr/local/bin/python

在文件第一行，使用你所在平台上的 Python 解释器的路径。

如果你希望脚本不依赖 Python 解释器的具体路径，你也可以使用 **env** 程
序。假设你的 Python 解释器所在目录已经添加到了 "PATH" 环境变量中，几乎
所有的类 Unix 系统都支持下面的写法：

   #!/usr/bin/env python

*不要* 在 CGI 脚本中这样做。CGI 脚本的 "PATH" 环境变量通常会非常精简，
所以你必须使用解释器的完整绝对路径。

有时候，用户的环境变量如果太长，可能会导致 **/usr/bin/env** 执行失败；
又或者甚至根本就不存在 env 程序。在这种情况下，你可以尝试使用下面的
hack 方法（来自 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" 模块从模块的 docstring 中寻找示例并
执行，对比输出是否与 docstring 中给出的是否一致。

"unittest" 模块是一个模仿 Java 和 Smalltalk 测试框架的更棒的测试框架。

为了使测试更容易，你应该在程序中使用良好的模块化设计。程序中的绝大多数
功能都应该用函数或类方法封装 —— 有时这样做会有额外惊喜，程序会运行得更
快（因为局部变量比全局变量访问要快）。除此之外，程序应该避免依赖可变的
局部变量，这会使得测试困难许多。

程序的“全局主逻辑”应该尽量简单：

   if __name__ == "__main__":
       main_logic()

并放置在程序主模块的最后面。

一旦你的程序已经组织为一个函数和类行为的有完整集合，你就应该编写测试函
数来检测这些行为。 可以将自动执行一系列测试的测试集关联到每个模块。 这
听起来似乎需要大量的工作，但是由于 Python 是如此简洁灵活因此它会极其容
易。 你可以通过与“生产代码”同步编写测试函数使编程更为愉快和有趣，因为
这将更容易并更早发现代码问题甚至设计缺陷。

程序主模块之外的其他“辅助模块”中可以增加自测试的入口。

   if __name__ == "__main__":
       self_test()

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


怎样用 docstring 创建文档？
---------------------------

"pydoc" 模块可以用你的 Python 源代码中的文档字符串来创建 HTML。 纯粹通
过文档字符串来创建 API 文档的一种替代方案是 epydoc。 Sphinx 也可以包括
文档字符串的内容。


怎样一次只获取一个按键？
------------------------

在类 Unix 系统中有多种方案。最直接的方法是使用 curses，但是 curses 模
块太大了，难以学习。


執行緒
======


如何使用執行緒編寫程式？
------------------------

一定要使用 "threading" 模块，不要使用 "_thread" 模块。"threading" 模块
对 "_thread" 模块提供的底层线程原语做了更易用的抽象。


我的執行緒似乎都沒有運行：為什麼？
----------------------------------

一旦主线程退出，所有的子线程都会被杀掉。你的主线程运行得太快了，子线程
还没来得及工作。

简单的解决方法是在程序中加一个时间足够长的 sleep，让子线程能够完成运行
。

   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)  # <---------------------------!

但目前（在许多平台上）线程不是并行运行的，而是按顺序依次执行！原因是系
统线程调度器在前一个线程阻塞之前不会启动新线程。

简单的解决方法是在运行函数的开始处加一个时间很短的 sleep。

   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" 模块创建一个 queue 对象，让每一个线程在运行结束
时 append 一个令牌到 queue 对象中，主线程中从 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" 类提供了多种接口。


怎样修改全局变量是线程安全的？
------------------------------

Python VM 内部会使用 *global interpreter lock* （GIL）来确保同一时间只
有一个线程运行。通常 Python 只会在字节码指令之间切换线程；切换的频率可
以通过设置 "sys.setswitchinterval()" 指定。从 Python 程序的角度来看，
每一条字节码指令以及每一条指令对应的 C 代码实现都是原子的。

理论上说，具体的结果要看具体的 PVM 字节码实现对指令的解释。而实际上，
对内建类型（int，list，dict 等）的共享变量的“类原子”操作都是原子的。

举例来说，下面的操作是原子的（L、L1、L2 是列表，D、D1、D2 是字典，x、y
是对象，i，j 是 int 变量）：

   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__()" 方法，这可能会产生一些影响。 对字典和列表进行大量更新尤其
如此。 如有疑问，请使用互斥锁！


不能擺脫全局直譯器鎖嗎？
------------------------

*global interpreter lock* （GIL）通常被视为 Python 在高端多核服务器上
开发时的阻力，因为（几乎）所有 Python 代码只有在获取到 GIL 时才能运行
，所以多线程的 Python 程序只能有效地使用一个 CPU。

在 Python 1.5 时代，Greg Stein 开发了一个完整的补丁包（“free
threadings” 补丁），移除了 GIL，并用粒度更合适的锁来代替。Adam Olsen
最近也在他的 python-safethread 项目里做了类似的实验。不幸的是，由于为
了移除 GIL 而使用了大量细粒度的锁，这两个实验在单线程测试中的性能都有
明显的下降（至少慢 30%）。

但这并意味着你不能在多核机器上很好地使用 Python！你只需将任务划分为多*
进程*，而不是多*线程*。新的 "concurrent.futures" 模块中的
"ProcessPoolExecutor" 类提供了一个简单的方法；如果你想对任务分发做更多
控制，可以使用 "multiprocessing" 模块提供的底层 API。

恰当地使用 C 拓展也很有用；使用 C 拓展处理耗时较久的任务时，拓展可以在
线程执行 C 代码时释放 GIL，让其他线程执行。"zlib" 和 "hashlib" 等标准
库模块已经这样做了。

也有建议说 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 默认是当前的搜索位置。也可以对使用
"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" 模
块。

備註:

  要读写二进制数据的话，需要强制以二进制模式打开文件（这里为 "open()"
  函数传入 ""rb""）。如果（默认）传入 ""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：

   https://pypi.org/project/pyserial/

對於 Unix，請參閱 Mitch Chapman 的 Usenet 貼文：

   https://groups.google.com/groups?selm=34A04430.CF9@ohioee.com


为什么关闭 sys.stdout（stdin，stderr）并不会真正关掉它？
--------------------------------------------------------

Python *文件对象* 是一个对底层 C 文件描述符的高层抽象。

对于在 Python 中通过内建的 "open()" 函数创建的多数文件对象来说，
"f.close()" 从 Python 的角度将其标记为已关闭，并且会关闭底层的 C 文件
描述符。在 "f" 被垃圾回收的时候，析构函数中也会自动处理。

但由于 stdin，stdout 和 stderr 在 C 中的特殊地位，在 Python 中也会对它
们做特殊处理。运行 "sys.stdout.close()" 会将 Python 的文件对象标记为已
关闭，但是 *不会* 关闭与之关联的文件描述符。

要关闭这三者的 C 文件描述符的话，首先你应该确认确实需要关闭它（比如，
这可能会影响到处理 I/O 的拓展）。如果确实需要这么做的话，使用
"os.close()"：

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

或者也可以使用常量 0，1，2 代替。


網路 (Network)/網際網路 (Internet) 程式
=======================================


Python 有哪些 WWW 工具？
------------------------

参阅代码库参考手册中 互联网协议和支持 和 網際網路資料處理 这两章的内容
。Python 有大量模块来帮助你构建服务端和客户端 web 系统。

Paul Boddie 维护了一份可用框架的概览，见
https://wiki.python.org/moin/WebProgramming 。

Cameron Laird 在 https://web.archive.org/web/20210224183619/http://ph
aseit.net/claird/comp.lang.python/web_python 维护了一组有关 Python web
技术的实用网页。


如何模擬 CGI 表單送出 (submission) (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.'

也參考: 如何使用 urllib 套件取得網路資源 內有大量範例。


我應該使用什麼模組來輔助產生 HTML？
-----------------------------------

你可以在 Web 编程 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)


socket 的 connect() 方法怎样避免阻塞？
--------------------------------------

通常会用 "select" 模块处理 socket 异步 I/O。

要防止 TCP 连接发生阻塞，你可以将 socket 设为非阻塞模式。 这样当你执行
"connect()" 时，你将要么立即完成连接（不大可能）要么得到一个包含错误编
号如 ".errno" 的异常。 "errno.EINPROGRESS" 表示连接正在进行中，但尚未
完成。 不同的操作系统将返回不同的值，所以你需要检查一下你的系统会返回
什么值。

你可以使用 "connect_ex()" 方法来避免创建异常。 它将只返回 errno 值。
要进行轮询，你可以稍后再次调用 "connect_ex()" -- "0" 或
"errno.EISCONN" 表示已经连接 -- 或者你也可以将此 socket 传给
"select.select()" 来检查它只否可写。

備註:

  "asyncio" 模組提供了一個通用的單執行緒並發非同步函式庫，可用於編寫非
  阻塞網路程式碼。第三方 Twisted 函式庫是一種流行且功能豐富的替代方案
  。


資料庫
======


Python 中有数据库包的接口吗？
-----------------------------

有的。

标准 Python 还包含了基于磁盘的哈希接口例如 "DBM" 和 "GDBM" 。除此之外
还有 "sqlite3" 模块，该模块提供了一个轻量级的基于磁盘的关系型数据库。

大多数关系型数据库都已经支持。查看 数据库编程 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)" 對常態（高斯）分佈進行採樣 (sample)。

一些更高階的函式會直接對序列進行操作，例如：

* "choice(S)" 會從給定序列中選擇一個隨機元素。

* "shuffle(L)" 會原地 (in-place) 打亂 list，即隨機排列它。

還有一個 "Random" 類別，你可以將它實例化以建立多個獨立的隨機數生成器。
