第11章 模块和包#

好雨知时节,当春乃发生。随风潜入夜,润物细无声。

——杜甫

Python 语言如今能居于各类编程语言排行榜前列,除了它简单易学之外,开放的生态系统也是不容忽视的因素。所谓“开放”,就是每个人都可以根据自己的意愿编写和发布 Python 模块或包;所谓“生态系统”,是指基于 Python 语言的模块和包已经涵盖了能够编写软件的各个领域,不论做哪方面的开发,都能找到可以使用的 Python 模块和包作为“轮子”。虽然“是否重复造轮子”在开发者中争论不休,但作为开始自学的读者,还是应该掌握“造轮子”的基本方法——是不是要亲自动手造,应该“具体问题具体分析”。

11.1 模块#

Python 的模块(Module)就是扩展名为 .py 的文件。是不是觉得太简单了?还有更简单的,其实各位读者此前已经使用过它了。自从第8章8.5.2节开始,不止一次“在文件当前位置进入交互模式”,而后执行形式为 from filename import * 的操作,于是就可以使用文件 filename.py 中所定义的类、函数、变量等对象。这里的 filename.py 文件,就是一个模块,而 from filename import * 就是从该文件中引入所定义的对象,即模块里面的对象。

尽管已经熟悉,还是用一个示例给予完整说明,温故而知新。在 IDE 中编写如下文件:

#coding:utf-8
'''
filename: mymodule.py
'''
class Book:
    __lang = "python"
    def __init__(self, author):
        self.__author = author
    
    def get_name(self):
        return (self.__author, self.__lang)

def foo(x):
    return x * 2

python = Book("laoqi")
python_name = python.get_name()
x = 2
mul_result = foo(x)

保存文件后,要牢记文件所在目录。本书中所使用的代码目录是:/Users/qiwsir/Documents/my_books/codes (请特别注意,如此路径表示形式为 Linux 或 MacOS 操作系统中的,如果是 Windows 操作系统,会与此不同。请读者熟悉自己的操作系统)。

然后进入到 Python 交互模式——不一定非要按照第8章8.5.2节中那样“在当前位置进入交互模式”,可以在任何目录位置,进入到 Python 交互模式。执行如下操作:

>>> import sys
>>> sys.path
['', '/Library/Frameworks/Python.framework/Versions/3.9/lib/python39.zip', '/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9', '/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/lib-dynload', '/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages']

模块 sys 是标准库中的一员(就如同此前用过的 math 模块一样,参阅第3章3.3.2节),执行 sys.path 后显示的内容会因不同的计算机和操作系统而异。返回值以列表的形式显示了搜索模块的路径,即在这些目录中查找程序所使用的模块(和包)。搜索路径列表的第一项 sys.path[0] 是一个空字符串( ' ' ),表示进入交互模式时的目录,即所谓的“当前位置”——所以在第8章8.5.2节以及之后的很多操作中,强调“从当前位置进入交互模式”。后续其他项都是在本地安装 Python 后默认的搜索目录。

% pwd
/Users/qiwsir

上面使用的是 Linux 命令,如果读者使用的是 Windows 操作系统,可以在 CMD 窗口使用 chdir 命令。所显示的结果为当前所在位置,注意此位置与之前保存 mymodule.py 文件的目录不同。然后在此位置进入到交互模式,并用 import 语句在当前环境中引入 mymodule 模块:

% pwd
/Users/qiwsir

% python
Python 3.9.4 (v3.9.4:1f2e3088f3, Apr  4 2021, 12:32:44)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import mymodule
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'mymodule'

Python 解释器会按照 sys.path 列表中的搜索目录顺序,依次查找是否有 mymodule.py 文件,即 mymodule 模块。首先看当前位置 /Users/qiwsir 目录,肯定没有 mymodule.py 文件,然后是后面个各个目录,也当然没有。最后就抛出了 ModuleNotFoundError 异常,虽然已经编写并保存了 mymodule.py 文件,但它所在的目录没有列入 sys.path 中,Python 解释器还是找不到的——这是一个常见的异常,只要出现此异常,就说明是“搜索路径问题”。

如何解决 ModuleNotFoundError 异常?既然 sys.path 是一个列表,就可以通过列表的方法,将文件 mymodule.py 所在的目录加入其中。接着前面继续操作:

>>> import sys
>>> sys.path
['', '/Library/Frameworks/Python.framework/Versions/3.9/lib/python39.zip', '/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9', '/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/lib-dynload', '/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages']

# 追加 mymodule.py 所在目录 
>>> sys.path.append('/Users/qiwsir/Documents/my_books/codes')
>>> sys.path
['', '/Library/Frameworks/Python.framework/Versions/3.9/lib/python39.zip', '/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9', '/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/lib-dynload', '/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages', '/Users/qiwsir/Documents/my_books/codes']

在 Python 搜索目录中增加了 mymodule.py 文件所在的目录,再在当前的交互模式中引入模块 mymodule ——不要写成 import mymodule.py

>>> import mymodule
>>> dir(mymodule)
['Book', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'foo', 'mul_result', 'python', 'python_name', 'x']

如此则达成愿望。但要注意,必须是在执行了 sys.path.append() 之后的当前交互模式中执行上述操作(也可以用列表的 insert() 方法)。否则,sys.path 没有该搜索路径。

正确地引入自己编写的模块之后,执行 dir(mymodule) 可以查看此模块能提供的东西,与 mymodule.py 文件对照:

  • Book 是文件中定义的类;

  • foo 是文件中定义的函数;

  • python 是文件中实例化 Book 类时用的一个变量;

  • python_name 也是文件中的一个变量;

  • xmul_result 都是文件中定义的变量。

接下来,在当前的 Python 交互模式中,就可以使用文件 mymodule.py 中定义的类 Book 和函数 foo()

>>> my_book = mymodule.Book("qiwei")
>>> my_book.get_name()
('qiwei', 'python')
>>> f = mymodule.foo(3)
>>> f
6

其他的变量,如 pythonpython_namemul_resultx ,实则没有什么用,因为它们是在 mymodule.py 文件中调用类 Book 和函数 foo() 时所使用的,对于使用该模块的程序环境而言,它们都是多余的。所以,在 mymodule.py 中就不应该有它们,可以将其删除,只保留类 Book 和函数 foo() ,并保存 mymodule.py 文件。再次引入模块:

>>> import mymodule
>>> dir(mymodule)
['Book', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'foo', 'mul_result', 'python', 'python_name', 'x']

会发现,引入的模块内容并没有因为修改了 mymodule.py 文件而改变,这里必须要使用第8章8.5.2节中所提到的“重载”模块方法——最灵验的方法是退出交互模式,重新进入,并将 mymodule.py 的路径加入到 sys.path 中。

>>> import sys
>>> sys.path.append('/Users/qiwsir/Documents/my_books/codes')
>>> import mymodule
>>> dir(mymodule)
['Book', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'foo']

现在看到的 mymodule 模块不再包含那些变量了。这才达到了创建和引入 mymodule 的最终目的。

其实,之前我们写的程序文件,都不是像前面那样使用该文件中的函数或者类,而是这样做的(参阅第7章7.1.1节的注释(2),从那之后的很多程序都如法炮制):

#coding:utf-8
'''
filename: mymodule.py
'''
class Book:
    __lang = "python"
    def __init__(self, author):
        self.__author = author
    
    def get_name(self):
        return (self.__author, self.__lang)

def foo(x):
    return x * 2

if __name__ == '__main__':
    python = Book("laoqi")
    python_name = python.get_name()
    x = 2
    mul_result = foo(x)

将增加了 if__name__ == '__main__'mymodule.py 文件作为模块引入,会有什么不同?再次重载之后,执行下述操作。

>>> import mymodule
>>> dir(mymodule)
['Book', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'foo']

条件语句下面的那些变量都没有作为模块 mymodule 中的一员,与不在 mymodule.py 中写调用类和函数的情况完全一样。那么, if__name__ == '__main__' 有何神奇之处?

先创建一个名为 foo.py 的文件,并输入如下内容:

#coding:utf-8
'''
filename: foo.py
'''
print(f"foo __name__ is set to {__name__}")

然后执行此文件:

% python foo.py
foo __name__ is set to __main__

foo.py 中使用了一个变量 __name__ ,但其中的用法有点特殊,因为并没有将该变量引用任何对象,像这样的情况如果出现在其他变量中必然会抛出 NameError 异常,同时会提示该变量没有定义。然而,__name__ 是一个特殊的变量,更准确地说,它是当前模块(文件 foo.py 就是一个 Python 模块)的一个属性,当在模块文件的空间执行时(此处即在 foo.py 的空间),__name__ 的值是 __main__

再创建一个名为 bar.py 的文件,并与 foo.py 在同一个目录中。

#coding:utf-8
'''
filename: bar.py
'''
import foo

执行之后,显示结果如下:

% python bar.py
foo __name__ is set to foo

依照前面所述,在 bar.py 文件空间,__name__ 的值应该为 __main__ 。然而,在 bar.py 中实施了 import foo ,则此时 __name__ 的值就变为了 foo

再修改 foo.py 文件,最终代码如下所示:

#coding:utf-8
'''
filename: foo.py
'''
print(f"foo __name__ is set to {__name__}")

def main():
    print('The main() function was executed.')

if __name__ == '__main__':
    main()
else:
    print('Do not execute main() funciton!')

执行此文件,结果是:

% python foo.py
foo __name__ is set to __main__
The main() function was executed.

bar.py 中,引入 foo 模块,再执行 bar.py ,效果如下:

% python bar.py
foo __name__ is set to foo
Do not execute main() funciton!

从执行结果的比较中会发现,当 foo.py 作为模块在另外一个文件中被引入后(不在同一个文件空间),__name__ 的值不再是 __main__ ,if 分支下的代码块不再被执行,因此它们所产生的各种对象都不会代入到 foo 模块中。

所以,可以在 .py 文件中用 if __name__ == '__main__' 调用本文件中的各类对象,当本文件作为模块被其他文件引入是,并不会为模块增加累赘之物。有的资料,会仿照 C 语言等的说法,称“ if __name__ == '__main__' ”是 Python 程序的入口,当然这并不是典型的 Python 开发者的术语。

自学建议

每一个 .py 文件都可以作为一个模块,因此开发实践中,常常会把功能类似的代码组织到一个文件中,或者每个文件实现某类功能(可能是一个,也可能是多个)。这样做的优势在于:

  • 便于代码的维护。

  • 将每个文件当做模块,在最终执行的程序中引入后即可使用,使得代码更整洁、便于阅读。

所以,工程实践中提倡以模块为单元组织代码。

11.2 包#

(Package),顾名思义,应该比模块“大”。

通常,“包”是有一定层次的目录结构,它由一些 .py 文件或者子目录组成,并且,每个目录中要包含名为 __init__.py 的文件。如图11-2-1所示,创建了一个名为 mypack 的目录,在该目录内有一个__init__.py 文件和一个名为 rust.py 的文件,另外有两个子目录 a_packb_pack ——即两个“子包”,在这两个子目录中分别创建了图中所显示的 .py 文件(注意:图示中的 tree 是 Linux 命令,用于显示目录结构。使用 Windows 操作系统的读者不要搬用)。

图11-2-1 mypack 包的目录结构

然后在 rust.pya_basic.py a_python.pyb_java 文件中分别写入如下代码。

#coding:utf-8
'''
filename rust.py
'''
def rust_func():
    return "I learn Rust."
#coding:utf-8
'''
filename a_basic.py
'''
def a_basic_func():
    return "I learn BASIC."
#coding:utf-8
'''
filename a_python.py
'''
def a_python_func():
    return "I learn Python."
#coding:utf-8
'''
filename b_java.py
'''
def a_java_func():
    return "I learn Java."

这样就创建了一个包 mypack ,它里面包括一个模块 rust.py ;还有两个“子包”,其中也包含由 .py 文件构成的模块。

如何使用这个包?下面在交互模式中演示,特别注意进入交互模式的位置,因为这关系到搜索路径的问题。

首先,在 mypack 所在目录中进入到交互模式(如图11-2-2所示,目录 ./codes 里有包 mypack ,即在 ./codes 处进入 Python 交互模式)。

图11-2-2 在当前位置进入交互模式

然后引入包 mypack ,如下操作,并用内置函数 dir() 显示包的内容:

>>> import mypack
>>> dir(mypack)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__']

在结果中没有看到 mypack 包里面的模块,也没有子包。如果非要访问包里面的模块 rust

>>> mypack.rust
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'mypack' has no attribute 'rust'

抛出了 AttributeError 异常。分明已经定义了,为什么找不到?——此时请不要怀疑是 Python 的问题,更不要怀疑是本书写错了。其原因在于我们没有告诉 Python 解释器模块 rust 在哪里——难道它不会自己找吗?不会。Python 不知道贵计算机中有几个 rust.py 文件,也不知道贵开发者想要的是哪一个 rust 模块。所以,在《Python 之禅》中有一句:面对不确定性,拒绝妄加猜测(参阅第1章1.4节)。

>>> import mypack.rust    # (1)
>>> dir(mypack.rust)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'rust_func']
>>> mypack.rust.rust_func()
'I learn Rust.'

注释(1)用 mypack.rust 的方式指明了模块的明确路径,并将该模块引入。类似的,还可以这样做:

>>> from mypack import rust   # (2)
>>> dir(rust)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'rust_func']
>>> rust.rust_func()
'I learn Rust.'

注释(1)和注释(2)本质是一样的方法,均是以 mypack 包为路径,指明所引入的模块位置和名称。

如果用 import mypack 就没有什么价值了吗?至少目前是。为了让 import mypack 能有价值,需要编辑其下的 __init__.py 文件(注意,是 ./mypack 目录下的,不是子目录下的)。

'''
filename: ./mypack/__init__.py
'''
from . import rust    # (3)

在执行 import mypack 的时候,会首先检查其下的 __init__.py 文件是否有应该执行的代码。现在文件中增加注释(3)所示代码,注意书写的时候,不要丢掉 . 符号,它表示当前位置,即从当前位置引入模块 rust 。保存文件之后,重新加载包 mypack (最简单的方法是退出当前交互模式后再键入 python ,或参考第8章8.5.2节的方法):

>>> import mypack
>>> dir(mypack)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'rust']
>>> mypack.rust       # (4)
<module 'mypack.rust' from '/Users/qiwsir/Documents/my_books/codes/mypack/rust.py'>
>>> mypack.rust.rust_func()
'I learn Rust.'

现在查看包 mypack 里的内容,就能显示模块 rust 了,并且以注释(4)的方式能够调用模块。

再进一步,子包 a_packb_pack 如何引入?方法也类似:

>>> from mypack import a_pack            # (5)
>>> dir(a_pack)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__']
>>> from mypack.a_pack import a_basic    # (6)
>>> dir(a_basic)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a_basic_func']
>>> a_basic.a_basic_func()
'I learn BASIC.'

注释(5)又如同在注释(1)之前的 import mypack 那样,看不到子包里的模块;注释(6)则以明确的路径声明了子包模块的位置,与注释(2)含义一样。如果想用注释(5)引入子包,并能看到子包的模块,可以仿照注释(3),在 ./mypack/a_pack/__init__.py 中增加如下代码:

'''
filename: ./mypack/a_pack/__init__.py
'''
from . import a_basic
from . import a_python

在交互模式中重新加载之后,执行如下操作:

>>> from mypack import a_pack
>>> dir(a_pack)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'a_basic', 'a_python']
>>> a_pack.a_python.a_python_func()
'I learn Python.'

前面编辑了 ./mypack/__init__.py 文件,就能在 import mypack 之后,通过 mypack 找到模块 rust ,即 mypack.rust 。那么,如果通过这种方式找到两个子包,该怎么办?继续编辑此文件:

'''
filename: ./mypack/__init__.py
'''
from . import rust
from . import a_pack
from . import b_pack

进入交互模式并重载后执行:

>>> import mypack
>>> dir(mypack)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'a_pack', 'b_pack', 'rust']
>>> dir(mypack.a_pack)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'a_basic', 'a_python']
>>> mypack.a_pack.a_python.a_python_func()
'I learn Python.'

这样,从包 mypack 到子包 a_basic ,再到子包内的模块,就“畅通无阻”了,可以使用多种方式引入(关于子包 b_pack 中的 __init__.py 文件的编辑,请读者自行完成)。

如果读者按照前面的流程进行了操作,再看看此时的目录 ./mypack ,其结构变成了图11-2-3所示:

图11-2-3 含有字节码的目录结构

与图11-2-1相比,多了扩展名为 .pyc 的文件,这是什么?请参阅第2章2.1.3节的内容。

自学建议

包比模块有更复杂的代码组织结构,读者可以到 github.com 等代码托管网站,浏览其他人或机构开源的程序,很多都是以包的方式提供。特别建议读者选择一款不太大、不太复杂的包,对其中的结构进行深入研究,初步理解该程序的代码组织方式。

11.3 标准库举例#

(Library)听起来是一个比包还要“大”的概念了。事实上,这两个概念没有什么区别,“库”可以看作“包”的集合(当然,看作是“模块”的集合也未尝不可)。也有资料认为“库”不是 Python 的概念,是从其他语言中借过来的说法。

“不争论”,重点看它对编程有什么作用。

Python 有一个很重要的库:标准库(Standard Library)。选择一些重要的 Python 模块,将它们视为 Python 语言的重要组成部分,在安装 Python 的同时也将它们安装在本地计算机。这就构成了标准库。

被选入 Python 标准库的模块都是编程中常用的,为通常的开发工作带来了便利。标准库包括但不限于以下内容:

  • 基本支持模块

  • 操作系统接口

  • 网络协议

  • 文件格式

  • 数据转换

  • 线程和进程

  • 数据存储

在 Python 官方文档中,对标准库有非常完整的归类索引和内容介绍,读者可以参考(网址:https://docs.python.org/3/library/)。本节仅选择标准库中的三个模块,主要是以它们为载体,简要介绍如何学习使用标准库中的模块。

11.3.1 sys#

前面已经使用过标准库中的 sys 模块,用于显示 Python 对模块的搜索路径,即 sys.path 。下面再使用模块中的 sys.argv 捕获命令行参数。

从已经编写过的 Python 程序中任选一个文件,比如选择第7章7.1.2节创建的 fibonacci.py 文件,然后执行这个程序文件。在第2章2.1.2节学习过执行此文件的途径有二:一是利用 IDE 提供的命令(通常快捷键是 F5 或 Ctrl+F5),二是在命令行中,执行如下所示的命令(这是本书演示中最常用的方法)。

% python fibonacci.py
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

虽已司空见惯,但神奇会在平淡中产生。在 IDE 中创建一个名为 test.txt 的文件,并在其中写入如下内容:

我最喜欢读老齐写的书

然后,执行:

% python test.txt
Traceback (most recent call last):
  File "/Users/qiwsir/Documents/my_books/codes/test.txt", line 1, in <module>
    我最喜欢读老齐写的书
NameError: name '我最喜欢读老齐写的书' is not defined

看到这里,估计你会用一个非常简洁的“鄙视流行语”评价我了。我真的如你所说吗?继续看:

# 我最喜欢读老齐写的书
print("只有他的书才有那么多神奇")

修改了刚才的 test.txt 文件,在之前的那句话前面增加 # ——即 Python 程序中所用的行注释符号,然后写上 Python 中的一条语句——你是不是觉得应该在“流行语”前面加一个“更”字了?看结果吧!

% python test.txt
只有他的书才有那么多神奇

就这么神奇!只有在这里才能看到这样地神奇。

还要延续。再创建一个 C 语言的程序文件 test2.c ,但是在里面不写 C 语言程序(可以认为我不会),而是写上 Python 程序——是张冠李戴,还是鸠占鹊巢?

#coding:utf-8
'''
filename: test2.c
'''
print('hello, I am in c file.')

看执行效果:

% python test2.c 
hello, I am in c file.

这个程序居然也能跑!

总结上面不管是“平凡的”还是“神奇的”操作,python fibonacci.pypython test.txtpython test2.c ,其中 python 是一条指令,它意味着要将后面文件中的内容送给 Python 解释器来执行,不论是什么类型的文本文件,只要其中的文本是 Python 解释器能够执行的 Python 语句或表达式即可——注意,必须是文本文件,不能是二进制文件(关于文件的类型,参阅第12章12.1.2节),比如编写了一个 test3.docx 的文件,其中内容是:

print(“I am in word.docx”)

然后执行:

 % python test3.docx
/usr/local/bin/python: can't find '__main__' module in '/Users/qiwsir/Documents/my_books/codes/test3.docx'

这就不再“神奇”了,此时可以很痛快地说出“流行语”。

形如 python filename.extension 的指令中的 filename.extension 就类似于第7章中所学过的函数的参数,只不过它是在命令行中,称之为命令行参数(Command line parameter)。指令 python 会把命令行参数交付给 Python 解释器。

指令 python 后面的命令行参数,还可有其他形式,比如:

% python -c "print('hello, I am in command line')"
hello, I am in command line

这里以 -c 后面跟随 Python 语句,则在命令行——没有进入到 Python 交互模式——执行了此语句。如果读者在命令行中执行 python -h 可以看到更多类似参数的说明,或者阅读官方文档(https://docs.python.org/3/using/cmdline.html),对此本书不在这里详解。

下面要研究 Python 程序如何捕捉到命令行参数,因为这种操作在现实应用中时常遇到。例如编写名为 comdlinearg.py 的文件代码:

#coding:utf-8
'''
filename: cmdlinearg.py
'''
import sys

x = sys.argv[1]     # (1)
y = sys.argv[2]     # (2)
print(f"{x} + {y} = {float(x) + float(y)}")

此程序就是实现两个浮点数的和,但是,在程序中没有对变量 xy 明确设置它们各自引用的对象,只是以注释(1)和(2)定义了这两个变量。如果用以往的方式执行此程序:

% python cmdlinearg.py
Traceback (most recent call last):
  File "/Users/qiwsir/Documents/my_books/codes/cmdlinearg.py", line 7, in <module>
    x = sys.argv[1]
IndexError: list index out of range

抛出了 IndexError 异常,由此可知 sys.argv 是一个列表,并且目前它的索引最大值不会大于 1 ,当然就是 0 了,即当前 sys.argv 列表中只有一个成员。为什么?继续看,就会理解。

% python cmdlinearg.py 7 28   
7 + 28 = 35.0

现在指令 python 后面的命令行参数不再是一个,而是三个,从程序运行结果可知,后面两个 728 分别被 sys.argv[1]sys.argv[2] 捕获,或者说,sys.argv 能够将 python 指令的命令行参数捕获并“装入”列表。

不妨在 cmdlinearg.py 中增加如下所示两行:

#coding:utf-8
'''
filename: cmdlinearg.py
'''
import sys

x = sys.argv[1]
y = sys.argv[2]
print(f"{x} + {y} = {float(x) + float(y)}")
print(f'sys.argv = {sys.argv}')        # 新增
print(f'the file is {sys.argv[0]}')    # 新增

执行结果是:

% python cmdlinearg.py 7 28
7 + 28 = 35.0
sys.argv = ['cmdlinearg.py', '7', '28']
the file is cmdlinearg.py

sys.argv 收集了所有的命令行参数,sys.argv[0] 表示第一个参数 cmdlinearg.py

由此可知,如果程序需要从命令行参数中获得运行所需的数据,即可利用 sys.argv 捕获。若读者有意深入了解 sys 模块中其他函数,建议参阅官方文档(https://docs.python.org/3/library/sys.html)。

11.3.2 os#

os 提供了面向操作系统的访问接口,其官方文档(https://docs.python.org/3/library/os.html)的说明比较详细,此处对常用的几个函数给予简要说明,供读者参考。

>>> import os
>>> os.getcwd()
'/Users/qiwsir/Documents/my_books/codes'

os.getcwd() 得到了当前的位置(对于 Python 交互模式,即进入交互模式时所在的位置)。

>>> os.mkdir("./newdir")    # (3)
>>> os.chdir("./newdir")    # (4)
>>> os.getcwd()
'/Users/qiwsir/Documents/my_books/codes/newdir'

注释(3)在当前目录内创建一个名为 newdir 的子目录,注释(4)即从当前位置进入到指定的目录,以 ./newdir 表示相对路径。

至此读者可能想在这个目录中创建新文件了,此处用如下方式创建,并写入相应内容(在第12章12.1.1节还会介绍另外一种创建文件的方法)。

>>> command = "echo 'I learn Python' > python.txt"
>>> os.system(command)
0

变量 command 所引用的是一个 shell 的 echo 命令,将字符串 'I learn Python' 输出到(或者说写入到)当前目录中的 python.txt 文件(此文件不存在,则新建)。

command 作为 os.system() 的参数,即在 Python 环境中执行 shell 命令,然后查看当前目录:

>>> os.listdir(os.getcwd())
['python.txt']

已经有了刚才创建的文件。其内容可以看吗?

>>> os.system("cat python.txt")
I learn Python
0

此处的参数 "cat python.txt" 所包含的是 Linux 中的命令(若读者在 Windows 操作系统中调试,则不能如本示例这样执行)。

以上演示了两次调用 os.system() 函数,在返回值中都有一个默认的值 0 ,它不是程序执行的返回值,它是什么呢?此问题留给读者利用网络资料进行探究。

由此可知,操作系统的任何命令都可以作为 os.system() 的参数,从而能在 Python 程序中执行系统的命令。再如,显示当前目录的内容,如果使用 Linux 命令:

>>> os.system("ls")
python.txt
0

os.listdir(os.getcwd()) 显示内容相同。有了 os 模块,就能轻易地在 Python 程序中实现操作系统的各种指令。

11.3.3 json#

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,以文字为基础,且易于让人阅读。从名称上就可以得知,这是一种受到 JavaScript 启发的数据格式,并得到了多种编程语言的支持。Python 标准库的 json 模块,实现了将对象编码为 JSON 格式的数据(序列化,encoding)和将 JSON 格式的数据转化为 Python 数据对象(反序列化,decoding)的功能。

>>> import json
>>> data = ["python", {"author": "laoqi", "age":"30"}]
>>> data_json = json.dumps(data)
>>> data_json
'["python", {"author": "laoqi", "age": "30"}]'
>>> type(data_json)
<class 'str'>

data 是一个列表(Python 数据),使用 json.dumps() 函数,将其编码为 JSON 数据,即变量 data_json 所引用的对象,在 Python 中它是字符串类型,故也称之为“JSON 字符串”。

这就是所谓的序列化过程。对 JSON 字符串的反序列化过程,也比较简单:

>>> de_data = json.loads(data_json)
>>> de_data == data
True
>>> de_data
['python', {'author': 'laoqi', 'age': '30'}]

使用 json_loads() 函数将 JSON 字符串转化为了 Python 数据对象,且与原始的数据 data 相等。

刚才已经看到,当把 Python 数据序列化为 JSON 格式后,data_jsonstr 类型,这与直接用 str() 函数将 data 进行类型转换有何不同?下面通过操作进行对比。

>>> data_str = str(data)
>>> data_str
"['python', {'author': 'laoqi', 'age': '30'}]"
>>> data_json
'["python", {"author": "laoqi", "age": "30"}]'

目前看来——注意时间状语——模样相同,但是:

>>> data_str == data_json
False
>>> data_str is data_json
False

它们既不相同又不相等,data_str 还不能用 json.loads() 反序列化:

>>> json.loads(data_str)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/decoder.py", line 355, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 2 (char 1)

此外,在使用 json.dumps() 对 Python 数据对象进行序列化的时候,还有其他参数,可以对序列化之后的 “JSON 字符串”的形式进行控制。

>>> d = {"author": "laoqi", 
         "age": 30, 
         'book':{1: "跟老齐学Python系列", 
                 2: "数据准备和特征工程", 
                 3: "机器学习数学基础"}, 
         "city":"soochow"}
>>> dj = json.dumps(d, sort_keys=True, indent=2, ensure_ascii=False)
>>> print(dj)
{
  "age": 30,
  "author": "laoqi",
  "book": {
    "1": "跟老齐学Python系列",
    "2": "数据准备和特征工程",
    "3": "机器学习数学基础"
  },
  "city": "soochow"
}

这里使用了参数 sort_keys = True ,让序列化之后的“JSON字符串”中的数据能够实现排序,如 print(dj) 的结果所示。参数 indent=2 的作用在于“对人友好”,print(dj) 的结果自动实现缩进。在数据 d 中,因为有中文,在 json.dumps() 中使用参数 ensure_ascii=False 能保证字符的原样输出,否则默认为 ensure_ascii=True

自学建议

Python 标准库的模块数量足够多——据不完全统计,目前已经超过了 200 个;所涵盖的领域也足够广——涉及到常见的各个开发领域。因此,形象地称 Python “自带电池”。这对学习者和开发者而言既是好事情,也带来了“富人的烦恼”:这么多钱(模块)怎么花(学)?

古人慨叹“吾生也有涯,而知也无涯”,如今我们也遇到了同样的困境。怎么办?最重要的就是本书一贯倡导的:提升自学能力,会读文档。当然,这不是提倡就能有的能力,读者必须在学习中不断实践。所以,根据开发需要阅读官方文档,就是应对琳琅满目的标准库的不二法门。

11.4 第三方包#

在 Python 的生态系统中,如果仅有官方认定的标准库,还不能说它是一个开放系统。开放系统的重要特征是每个开发者都有权编辑和发布模块(或包),人人能够为这个系统增砖添瓦。因此就有了标准库之外的模块(或包),统称为第三方包

Python 第三方包都会在指定网站 https://pypi.org/ 上发布,图11-4-1为网站首页截图,从中可以看到当前网站的项目数量(读者阅读本书时,此数量会有所不同。在第1章1.4节也提到了 PyPI 网站,编写那部分内容时对网站的截图如第1章1.4节的图1-4-3所示,图中所显示的项目数量与图11-4-1所示不同,这两张截图的时间间隔大约半年左右,由此读者可以体会到 Python 生态体系的快速发展之势)。

image-20210727104427299

图11-4-1 PyPI 首页截图

11.4.1 管理第三方包#

标准库的模块不需要单独安装,第三方包则要在用到时单独安装到本地计算机。本书推荐使用 pip 安装。

pip 是 Python 的包管理工具,一般在安装 Python 的时候,它已经被安装到本地了。可以用下面的方式检查本地是否已经安装(注意,在命令行中执行如下操作):

% pip --version
pip 21.1.3 from /Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/pip (python 3.9)

返回结果中显示了当前所安装的 pip 版本(读者在本地计算机上所安装的可能与此不同)以及所在的位置——第三方包都会安装在 ./site-packages 目录内。

如果本地没有安装 pip 包管理工具,可以选择如下两种方式中的一种进行安装:

  • 方式一:使用标准库的 ensurepip 模块安装。

    % python -m ensurepip 
    

    还可以用这个模块对 pip 升级。

    % python -m ensurepip --upgrade
    
  • 方式二:下载安装脚本文件 get-pip.py (下载地址:https://bootstrap.pypa.io/get-pip.py),然后在终端运行此文件安装:

    % python get-pip.py
    

pip 安装好之后,就可以用它管理本地的第三方包,比如安装、卸载等操作。

在安装某个第三方包之前,特别建议先到 PyPI 官方网站找到该包,了解其基本情况,特别是它能支持的 Python 版本,以及最新版本的发布时间。例如 requests 包(提醒:要非常认真地在搜索结果中观察名称,避免“李鬼”冒充“李逵”),在 PyPI 上显示了如图11-4-2所示的内容。

图11-4-2 requests 包页面部分截图

从图11-4-2所示的截图中,可以得到如下基本信息(读者阅读本书时,此页面的信息会有所不同)

  • 当前最新版本是 2021年7月13日发布的 2.26.0 版——说明此包仍在维护,可以放心使用。

  • 安装方法,可以用 pip install requests (顶部所示),也可以用 python -m pip install requests (截图底部所示)

  • 所支持的 Python 版本是 2.7 或者 3.6 以上——本书演示所用的是 3.9 ,在此支持范围。

  • 点击导航(Navigation)的 Release history ,可以查看所发布的历史版本,如果需要安装某个历史版本,可以在此查找。

  • 点击导航(Navigation)的 Download files ,可以下载当前发布版的分发文件(distribution file) .whl 文件和 .tar.gz 文件。

  • 点击项目链接(Project links)的 Homepage ,可以打开包的官方网站(https://docs.python-requests.org/en/master/),其中包括对包的完整说明和使用文档——有中文链接。

  • 点击项目链接(Project links)的 Source ,可以打开包的源码仓库(https://github.com/psf/requests,一般情况下 PyPI 的源码在 github.com 网站)。这里的源码一般是最新开发版(注意,开发版可以正常使用,只是尚未正式发布,可能会在发布之前对某些项目进行调整)

从安装的角度来看,可以用以下三种安装方法安装 requests 包:

  1. 使用 pip 指令安装,这是最常用的安装方法。

% pip install requests

特别注意——很多初学者容易犯的错误——不要在 Python 交互模式中执行此指令(如下操作所示),这不是 Python 语言的语句。

>>> pip install requests
  File "<stdin>", line 1
    pip install requests
        ^
SyntaxError: invalid syntax

如果你确信本地已经安装了 pip ,但是用上面正确的方式安装,仍然提示找不到 pip ——特别是使用 Windows 操作系统的读者,可能是因为没有将 pip 命令纳入系统环境变量,解决方法之一就是将它加入到环境变量。还有另外一种可以尝试方法:

% python -m pip install requests

这两种方法没有本质的区别。如果 pip 没有在本地计算机的系统环境变量中,使用后者可以让 Python 解释器自动在 sys.path 的路径范围内查找 pip 模块,并执行安装。

用上面的方式安装,其实是要从 PyPI 的服务器上下载有关程序,但有时因为不可抗力,访问该服务器会出现连接超时等某些问题。对此,也是为了服务国内用户,有不少机构做了 PyPI 的国内镜像(或国内源),比如清华大学开源软件镜像站的 PyPI 地址是:https://mirrors.tuna.tsinghua.edu.cn/help/pypi/ (如图11-4-3所示),读者可以根据说明使用该网站的进行安装。

image-20210727142533000

图11-4-3 PyPI 镜像使用方法

有的情况下,因为特殊需要,不一定要安装模块的最新版本,比如要安装 requests 2.25.0 :

% pip install requests == 2.25.0
  1. 使用分发文件安装

在图11-4-2所示页面中点击导航(Navigation)的 Download files ,可以看到图11-4-4所示的分发文件,将它们下载到本地。

image-20210727164535026

图11-4-4 下载分发文件

然后进入下载的文件所在的目录,并执行:

% ls *.whl
requests-2.26.0-py2.py3-none-any.whl
% pip install requests-2.26.0-py2.py3-none-any.whl
Processing ./requests-2.26.0-py2.py3-none-any.whl
Requirement already satisfied: urllib3<1.27,>=1.21.1 in /Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages (from requests==2.26.0) (1.26.5)
Requirement already satisfied: certifi>=2017.4.17 in /Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages (from requests==2.26.0) (2021.5.30)
Requirement already satisfied: charset-normalizer~=2.0.0 in /Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages (from requests==2.26.0) (2.0.3)
Requirement already satisfied: idna<4,>=2.5 in /Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages (from requests==2.26.0) (2.10)
Installing collected packages: requests
Successfully installed requests-2.26.0

如此就成功地安装了。如果下载的是 requests-2.26.0.tar.gz 文件,安装方法亦类似:

% pip install requests-2.26.0.tar.gz
  1. 使用 github 网站的源码安装

首先,本地要已经安装了 git (对 git 的介绍超出本书范畴,请读者自行查找有关资料)。仍然以 requests 模块为例,它在 github.com 网站的仓库地址是:https://github.com/psf/requests.git 。

% pip install git+https://github.com/psf/requests.git

如此即可使用 requests 源码仓库进行安装。另外,还可以将源码克隆(clone)到本地,然后通过执行其中的 setup.py 程序进行安装(参阅11.4.2节)。

在上述各个方法中,最常用的还是第一种。安装好之后,可以执行以下操作,查看安装结果。

% pip show requests
Name: requests
Version: 2.26.0
Summary: Python HTTP for Humans.
Home-page: https://requests.readthedocs.io
Author: Kenneth Reitz
Author-email: me@kennethreitz.org
License: Apache 2.0
Location: /Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages
Requires: urllib3, certifi, charset-normalizer, idna
Required-by: translate

这里显示了 requests 模块的有关信息,特别要关注 Location 一项,显示了此模块安装在本地的目录。

如果已经安装的包或模块需要升级,也可以用 pip 轻松实现,例如对 requests 升级:

% pip install --upgrade requests

由于 pip 本身也在不断地维护发展,所以用它安装第三方包的时候,如果当前所使用的 pip 版本低于最新发布版,会提示对 pip 升级,可以这样完成升级:

% pip install --upgrade pip
Requirement already satisfied: pip in /Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages (21.1.3)
Collecting pip
  Downloading pip-21.2.1-py3-none-any.whl (1.6 MB)
     |████████████████████████████████| 1.6 MB 612 kB/s
Installing collected packages: pip
  Attempting uninstall: pip
    Found existing installation: pip 21.1.3
    Uninstalling pip-21.1.3:
      Successfully uninstalled pip-21.1.3
Successfully installed pip-21.2.1

现在就将 pip 从 21.1.3 升级到了 21.2.1 。

 % pip --version
pip 21.2.1 from /Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/pip (python 3.9)

有的第三方包或者模块需要卸载,其指令如下(以卸载 requests 模块为例):

% pip uninstall requests

会提示是否需要卸载此模块,输入 y 则卸载。如果确定无疑要卸载,还可以用:

% pip uninstall requests -y

以上介绍了用 pip 管理本地第三方包和模块的常用操作,此外,pip 还有其他一些命令,读者可以通过官方文档(https://pip.pypa.io/en/stable/cli/pip_list/)了解,以备不时之需。

当第三方包或模块被安装到本地之后,其使用方法与标准库中的模块使用方法一样,请参阅11.3节,不再赘述。

11.4.2 发布包#

PyPI 网站的第三方包都是开发者发布的,本节就向读者介绍发布包的方法。如果能够有更多人使用自己写的程序,是一件非常爽的事情。

在本地找个适当位置,创建一个目录(例如: laoqipackage ),此目录的结构如下所示(其中的文件均为空文件):

qiwsir@qiwsirs-MacBook-Pro laoqipackage % tree
.
├── README.md
├── javaspeak
│   ├── __init__.py
│   ├── __pycache__
│      ├── __init__.cpython-39.pyc
│      └── javaspeak.cpython-39.pyc
│   └── javaspeak.py
├── langspeak.py
└── setup.py

2 directories, 7 files

然后,按照以下所示,依次在各个文件中写入对应的内容。

#coding:utf-8
'''
filename: langspeak.py
'''
class LangSpeak:
    def speak(self):
        return "Everyone should learn programming language."
#coding:utf-8
'''
filename: javaspeak.py
'''
class JavaSpeak:
    def speak(self):
        return "Java! Java!"

完成以上文件代码之后,重点是 setup.py 文件的编写:

import setuptools
import os
os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))  # (1)
with open("README.md", "r") as fh:  # (2)
    long_description = fh.read()
setuptools.setup(
    name="laoqipackage",  
    version="1.0.0",
    author="laoqi", 
    author_email="laoqi@mail.com",    # 个人邮箱
    description="You can listen the speaking of programming language.",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url="",
    py_modules = ['langspeak',],
    packages=setuptools.find_packages(),
    classifiers=(
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
        ),
    )

setup.py 文件中,主要实现包的有关参数配置。注释(2)中利用内置函数 open() 打开 README.md 文件(这种打开文件的方式,在第12章12.1节会详细讲述),这个文件不是必须的,但通常要有。在此文件中,对包或模块的功能进行必要说明,.md 表示文件是 markdown 类型的文件。

对于 setuptools.setup() 的参数配置,是这个“包”能不能成功的关键,以下几项特别要注意:

  • name:将来发布到 PyPI 上之后显示的名称。注意,不是安装这个包后引入的名称,也不一定与表示本包的顶级目录名称相同。

  • py_modules:在目录结构中,langspeak.pysetup.py 在同一级目录中,这一级的 .py 文件就是模块。如果这个包安装成功之后,可以使用 import langspeak 的方式直接引入模块。py_modules 的值就是声明包中的模块。

  • packages:用于声明包里面的“子包”。在目录结构中可以看到,javaspeak 是子目录,即“子包”,需要在这里进行声明。如果子包数量较少,声明方式可以使用 packages = [ “javaspeak”, ] 的模式。如果数量多了,可以使用 packages = setuptools.fin_packages() 的方式,同时要辅之以注释(1)。

setup.py 文件配置好之后,就可以先在本地尝试一下,能不能作为包来安装。进入到 laoqiproject 目录,执行 setup.py 文件进行安装。

% python setup.py install

在很多提示信息之后,会显示:

Processing dependencies for laoqipackage==1.0.0
Finished processing dependencies for laoqipackage==1.0.0

这就说明我们所创建的包已经安装到了本地,并可以在 Python 程序中使用。用11.4.1节学习过的方法查看这个包的详细信息:

% pip show laoqipackage
Name: laoqipackage
Version: 1.0.0
Summary: You can listen the speaking of programming language.
Home-page: UNKNOWN
Author: laoqi
Author-email: laoqi@mail.com
License: UNKNOWN
Location: /Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/laoqipackage-1.0.0-py3.9.egg
Requires:
Required-by:

不妨将上述显示的内容与前面在 setup.py 中所做的各项设置进行比较,从而理解 setuptools.setup() 中各项的实现效果。

也可以进入到交互模式,引入所安装的包中的模块。

>>> import langspeak
>>> dir(langspeak)
['LangSpeak', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
>>> lang = langspeak.LangSpeak()
>>> lang.speak()
'Everyone should learn programming language.'
>>> from javaspeak import javaspeak
>>> java = javaspeak.JavaSpeak()
>>> java.speak()
'Java! Java!'

以上测试说明,本地安装和使用这个包都没有问题,下面就开始把它发到 PyPI 上。

先将源码上传到 github 的仓库上(其他源码托管网站也可以,这里以 github 为例)。上传之后,还可以将 setup.py 中的 url 值补充完整。

再为所编写的包增加许可(英文:LICENSE)。其实,在 setup.py 的配置中,已经设定了包的许可,即 classifiers 的值中所定义的 “License”。此外,还要在与 setup.py 同一级的目录中创建一个名为 LICENSE 的文件,并且从网站 https://choosealicense.com/ 中将MIT License的内容复制过来,放到此文件中。https://choosealicense.com/ 是专门提供各种开源许可的网站,如果读者使用其它许可,可以到这里来查找。

以上都是准备工作。接下来生成所发布包的文档,具体操作如下。

  • 确认本地已经安装的 setuptoolswheel 是最新版本,如果没有安装或者搞不清楚是否是最新版,就执行下面的命令:

    % pip install --upgrade setuptools wheel
    
  • laoqiproject 目录中(即 setup.py 所在的目录层级)执行如下命令:

    laoqipackage % python setup.py sdist bdist_wheel
    

    执行此命令之后,会自动做一些事情,最终在 ./dist 目录中会看到 .whl.gz 文件(如图11-4-5所示)。

图11-4-5 生成分发文件

终于要发布了。不过,还要装一个专门用来发布包的工具—— twine (https://pypi.org/project/twine/):

% pip install twine

作为练习项目,建议读者到 https://test.pypi.org/ 注册账号,后面的演示中,也是使用这个网站。它区别于正式的 PyPI网站 https://pypi.org 的账号,以后读者如果想向 PyPI 正式发布包,可以到其网站再次注册。

确认当前所在位置,注意观察如下操作(注意,pwd 是 Linux 命令):

laoqipackage % pwd
/Users/qiwsir/Documents/my_books/codes/laoqipackage

前面提到的 ./dist 目录就在当前位置。然后执行(需要输入在网站 https://test.pypi.org 上注册的用户名和密码):

laoqipackage % twine upload --repository-url https://test.pypi.org/legacy/ ./dist/*
Uploading distributions to https://test.pypi.org/legacy/
Enter your username: laoqi
Enter your password:
Uploading laoqipackage-1.0.0-py3-none-any.whl
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 4.88k/4.88k [00:03<00:00, 1.47kB/s]
Uploading laoqipackage-1.0.0-py3.9.egg
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5.71k/5.71k [00:02<00:00, 2.76kB/s]
Uploading laoqipackage-1.0.0.tar.gz
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 4.39k/4.39k [00:01<00:00, 2.86kB/s]

View at:
https://test.pypi.org/project/laoqipackage/1.0.0/

当看到以上执行结果的最后一行,说明包已经成功地发布了,打开网址 https://test.pypi.org/project/laoqipackage/1.0.0/ 查看效果,如图11-4-6所示。

图11-4-6 已经发布的包

将上述流程掌握之后,待读者自己开发了一个有分享价值的包或模块之后,即可发布到 PyPI 的官方网站上。

自学建议

善于思考,敢于尝试和实践,是开发者的优秀品质。如果读者在学习过程中有了“灵光乍现”的思维火花,一定要立刻记录下来,而后到网上搜索,检查自己的想法是否“前无古人”。

在确定了独创性,欣喜之后,还要冷静对待。首先将创意分解为可实施的若干部分,然后用已有知识判断实现该部分的可能性,并且还要利用网络搜索是否有对应的第三方包支持,有则实行“拿来主义”。若有必要,自己编写第三方包发布到 PyPI 网站,填补空白。

11.5 创建虚拟环境#

在实际的项目中,是不是一定要用“最新版”的模块或包呢?不一定。实际的项目要求往往比较复杂,比如有一个比较“古老的”网站项目中使用了 Django 2.2(参阅第12章12.3节),现在又要新建一个网站,要求使用 Django 3 。如此,在本地计算机的开发环境中就出现了同一个包的不同版本冲突,如何解决?

我们希望是每个项目都有相对独立的开发环境,与系统配置、其他项目的配置之间相隔离,从而能在该项目中“为所欲为”。这种相对独立的开发环境就是 Python 中的虚拟环境(Virtual Environment)。

在 Python 标准库中已经提供了创建虚拟环境的模块 venv ,下面就应用此模块演示创建虚拟环境的过程。

虚拟环境,其表现是一个目录,首先要创建此目录。以下演示中,准备将虚拟环境的目录放在 /Users/qiwsir/Documents/my_books/codes 内,并且虚拟环境目录的名称是 myvenv 。然后执行:

 % python -m venv /Users/qiwsir/Documents/my_books/codes/myvenv

为了避免那么长的路径,可以先进入到目录 ../codes 内,再执行:

 % python -m venv myvenv

同样也是在 ../codes 目录内创建了一个名为 myvenv 的子目录,这就是虚拟环境目录。

进入到 myvenv 子目录中:

qiwsir@qiwsirs-MacBook-Pro codes % cd myvenv
qiwsir@qiwsirs-MacBook-Pro myvenv % ls
bin		include		lib		pyvenv.cfg

指令 ls 是 Linux 命令,即查看本目录中的文件和子目录(用 Windows 操作系统的用户不能照抄此命令,改用 dir ,并且显示出的目录名称可能稍有差别),发现这个目录里面非空,而是已经有了基本配置。

在子目录 bin 中( Windows 系统是 Scripts ),会看到如下内容:

qiwsir@qiwsirs-MacBook-Pro myvenv % cd bin
qiwsir@qiwsirs-MacBook-Pro bin % ls
Activate.ps1		activate.csh		easy_install		pip			pip3.9			python3
activate		activate.fish		easy_install-3.9	pip3			python			python3.9

这说明此虚拟环境已经配置了 Python 3.9 ,这是因为在创建虚拟环境的时候,指令 python 即 Python 3.9 。

在子目录 lib 中,读者会发现有 ./python3.9/site-packages 子目录( Windows 系统上是 Lib\site-packages ),以后在本虚拟环境中安装的第三方包都会放在这里。

下面启动虚拟环境(没启动不能用),执行如下操作(目前在 ./myeven 目录中):

qiwsir@qiwsirs-MacBook-Pro myvenv % source ./bin/activate
(myvenv) qiwsir@qiwsirs-MacBook-Pro myvenv %

执行了 source ./bin/activate 指令之后,当前的命令行前面出现了 (myvenv) ,表示已经进入了(或者已经启动了)myvenv 虚拟环境。

(myvenv) qiwsir@qiwsirs-MacBook-Pro myvenv % pip list
Package    Version
---------- -------
pip        20.2.3
setuptools 49.2.1
WARNING: You are using pip version 20.2.3; however, version 21.2.1 is available.
You should consider upgrading via the '/Users/qiwsir/Documents/my_books/codes/myvenv/bin/python -m pip install --upgrade pip' command.

由上述操作发现,当前虚拟环境中除了列出来的两项,尚未安装其它模块,并且此环境中的 pip 版本是 20.2.3 。在11.4.1节,已经将本地计算机系统所安装的 pip 升级到 21.2.1 ,而此处还是 Python 3.9 默认的 pip 版本,由此可见,虚拟环境相对系统环境是隔离的。

(myvenv) qiwsir@qiwsirs-MacBook-Pro myvenv % pip install django
Collecting django
  Downloading Django-3.2.5-py3-none-any.whl (7.9 MB)
     |████████████████████████████████| 7.9 MB 1.1 MB/s
Collecting pytz
  Using cached pytz-2021.1-py2.py3-none-any.whl (510 kB)
Collecting asgiref<4,>=3.3.2
  Downloading asgiref-3.4.1-py3-none-any.whl (25 kB)
Collecting sqlparse>=0.2.2
  Downloading sqlparse-0.4.1-py3-none-any.whl (42 kB)
     |████████████████████████████████| 42 kB 509 kB/s
Installing collected packages: pytz, asgiref, sqlparse, django
Successfully installed asgiref-3.4.1 django-3.2.5 pytz-2021.1 sqlparse-0.4.1

在这个虚拟环境中安装了 Django 3.2.5 ——记住这个安装方法,在第12章12.3节会用到。

如果不在 myvenv 目录里面,是不是就意味着退出了虚拟环境?

(myvenv) qiwsir@qiwsirs-MacBook-Pro myvenv % cd
(myvenv) qiwsir@qiwsirs-MacBook-Pro ~ %

并没有,只要前面还显示 (myvenv) 标记,就意味着仍然在该虚拟环境中,不论处于哪个目录中,即使在上述所示的位置,如果安装第三方包,也被安装到虚拟目录里面。

(myvenv) qiwsir@qiwsirs-MacBook-Pro ~ % pip install Flask

安装了另外一个常用于 web 项目开发的框架 Flask ,然后查看一下在当前虚拟环境中已经具有的第三方包。

(myvenv) qiwsir@qiwsirs-MacBook-Pro ~ % pip list
Package      Version
------------ -------
asgiref      3.4.1
click        8.0.1
Django       3.2.5
Flask        2.0.1
itsdangerous 2.0.1
Jinja2       3.0.1
MarkupSafe   2.0.1
pip          20.2.3
pytz         2021.1
setuptools   49.2.1
sqlparse     0.4.1
Werkzeug     2.0.1

虽然前面仅仅演示安装了两个,由于这两个包都是 web 开发框架,它们还有一些依赖的包,也一同自动安装了。以上显示的就是当前虚拟环境中已经具有包和模块——这点内容远远少于本地计算机系统中所安装的包。

有了一个相对独立的环境后,在此环境内进行各项开发,就避免了不同项目之间的干扰。关于更详细的开发过程,参阅第12章有关章节内容。

当虚拟环境中的工作结束了,就必须退出,基本操作是:

(myvenv) qiwsir@qiwsirs-MacBook-Pro ~ % deactivate
qiwsir@qiwsirs-MacBook-Pro ~ %

现在没有了 (myvenv) 标记,回到了系统环境中。

自学建议

到目前为止,读者已经学完了 Python 的基础知识,这些知识虽然已经能够支持一般的项目开发,但我认为重点不在于此,而在于通过借助本书学习,自学能力得到了培养和提升,才是最大的收获。从此之后,读者面对项目中所遇到的任何新知识,都有有信心和能力快速掌握。