第九章 模块和包
模块化编程
导入模块的语法
使用import导入模块,主要有两种用法:
- import 模块名1[as 别名1],模块名2[as 别名2],... : 导入整个模块
- from 模块名 import 成员名1[as 别名1], 成员名2[as 别名2],...,:导入模块中指定成员
上面两种import语句的区别主要有两点:
- 第一种import语句导入模块内的所有成员(包括变量、函数、类等);第二种import语句只导入模块内的指定成员(除非使用 from 模块名 import *)
- 当使用第一种import语句导入模块中的成员时,必须添加模块名或模块别名作为前缀;当使用第二种import语句导入模块中的成员时,无须使用任何前缀,直接使用成员名或成员别名即可
使用import导入模块介绍
# 导入sys整个模块
import sys
# 使用sys模块名作为前缀来访问模块中的成员
print(sys.argv[0])
输出结果:
C:Userszz.spyder-py3 emp.py
为模块指定别名,示例:
# 导入sys整个模块,并指定别名为s
import sys as s
# 使用s模块别名作为前缀来访问模块中的成员
print(s.argv[0])
当指定了别名,就必须使用别名作为前缀,如果仍旧使用模块名,则会报错:
# 导入sys整个模块,并指定别名为s
import sys as s
# 使用s模块别名作为前缀来访问模块中的成员
print(s.argv[0])
print("------------")
# 仍旧使用模块名作为前缀,则会报错
print(sys.argv[0])
输出结果:
C:Userszz.spyder-py3 emp.py
------------
Traceback (most recent call last):
File "C:Userszz.spyder-py3 emp.py", line 8, in <module>
print(sys.argv[0])
NameError: name 'sys' is not defined
一次导入多个模块,多个模块之间用逗号隔开,示例:
# 导入sys、os两个模块
import sys,os
# 使用模块名作为前缀来访问模块中的成员
print(sys.argv[0])
# os模块的sep变量代表平台上的路径分隔符
print(os.sep)
输出结果:
C:Userszz.spyder-py3 emp.py
为多个模块指定别名,示例:
# 导入sys、os两个模块,并为sys指定别名s,为os指定别名o
import sys as s,os as o
# 使用模块别名作为前缀来访问模块中的成员
print(s.argv[0])
print(o.sep)
使用from ... import 导入模块内指定成员的用法介绍
# 导入sys模块的argv成员
from sys import argv
# 使用导入成员的语法,直接使用成员名访问,不能添加前缀
print(argv[0])
如果为成员名添加模块名前缀,则会报错,示例:
# 导入sys模块的argv成员
from sys import argv
# 使用导入成员的语法,直接使用成员名访问
print(argv[0])
print("--------------------")
# 如果为成员名添加模块名前缀,则会报错
print(sys.argv[0])
输出结果:
C:Userszz.spyder-py3 emp.py
--------------------
Traceback (most recent call last):
File "C:Userszz.spyder-py3 emp.py", line 9, in <module>
print(sys.argv[0])
NameError: name 'sys' is not defined
为成员名指定别名,示例:
# 导入sys模块的argv成员,并为其指定别名v
from sys import argv as v
# 使用导入成员(并指定别名)的语法,直接使用成员的别名访问
print(v[0])
# 如果指定了别名,就必须使用别名,若仍使用成员名,则会报错
print(argv[0])
输出结果:
C:Userszz.spyder-py3 emp.py
Traceback (most recent call last):
File "C:Userszz.spyder-py3 emp.py", line 7, in <module>
print(argv[0])
NameError: name 'argv' is not defined
导入多个模块,示例:
# 导入sys模块的argv,winver成员
from sys import argv, winver
# 使用导入成员的语法,直接使用成员名访问
print(argv[0])
print(winver)
为多个模块指定别名,示例:
# 导入sys模块的argv,winver成员,并为其指定别名v、wv
from sys import argv as v, winver as wv
# 使用导入成员(并指定别名)的语法,直接使用成员的别名访问
print(v[0])
print(wv)
输出结果:
C:Userszz.spyder-py3 emp.py
3.7
小结:
- 一旦为模块名或成员名指定了别名,则在程序中必须使用别名,若仍旧使用模块名作为前缀则会报错、若仍旧使用成员名也会报错
- 使用成员名访问时,不能添加模块名作为前缀,若强制添加模块名作为前缀则会报错
使用from ... import语法时也可一次导入指定模块内的所有成员,程序即可使用成员名来使用该模块内的所有成员。(不推荐使用这种语法)示例:
# 导入sys模块的所有成员
from sys import *
# 使用导入成员的语法,直接使用成员的别名访问
print(argv[0])
print(winver)
输出结果:
C:Userszz.spyder-py3 emp.py
3.7
定义模块
模块到底是什么?模块就是python程序,任何python程序都可作为模块导入。对于任何程序,只要导入了模块,即可使用该模块内的所有成员。
module1.py代码:
'''
这是我们编写的第一个模块,该模块包含以下内容:
my_book:字符串变量
say_hi:简单的函数
User:代表用户的类
'''
print('这是module 1')
my_book = '疯狂Python讲义'
def say_hi(user):
print('%s,您好,欢迎学习Python' % user)
class User:
def __init__(self, name):
self.name = name
def walk(self):
print('%s正在慢慢地走路' % self.name)
def __repr__(self):
return 'User[name=%s]' % self.name
使用模块的好处在于:如果将程序需要使用的程序单元(比如modulel.py定义的say_hi()函数、User类)定义在模块中,后面不管哪个程序,只要导入该模块,该程序即可使用该模块所包含的程序单元,这样就可以提供很好的复用一一导入模块,使用模块,从而避免每个程序都需要重新定义这些程序单元。
模块文件的文件名就是它的模块名,比如module1.py的模块名就是module1。
为模块编写说明文档
在实际开发中也应该为模块编写说明文档,否则,其他开发者都不知道该模块有什么作用,以及包含哪些功能
为模块编写说明文档很简单,只要在模块开始处定义一个字符串直接量即可,可通过模块的__doc__属性访问文档。module1.py的第一行代码之前添加如下内容,这段内容将作为该模块的说明文档。
'''
这是我们编写的第一个模块,该模块包含以下内容:
my_book:字符串变量
say_hi:简单的函数
User:代表用户的类
'''
为模块编写测试代码
当模块编写完成之后,可能还需要为模块编写一些测试代码,用于测试模块中的每一个测试单元是否都能正常运行。
希望实现的效果:如果直接使用python命令运行该模块(相当于测试),程序应该执行该模块的测试函数;如果是其他程序导入该模块,程序不应该执行该模块的测试函数。此时可借助于所有模块内置的__name__变量进行区分,如果直接使用Python命令来运行一个模块,name__变量的值为__main;如果该模块导入被导入其他程序中,__name__变量的值就是模块名。因此,如果希望测试函数只有在使用python命令直接运行时才执行,则可在调用测试函数
时增加判断:只有当__name__属性为__main__时才调用测试函数。为模块增加如下代码即可
'''
这是我们编写的第一个模块,该模块包含以下内容:
my_book:字符串变量
say_hi:简单的函数
User:代表用户的类
'''
print('这是module 1')
my_book = '疯狂Python讲义'
def say_hi(user):
print('%s,您好,欢迎学习Python' % user)
class User:
def __init__(self, name):
self.name = name
def walk(self):
print('%s正在慢慢地走路' % self.name)
def __repr__(self):
return 'User[name=%s]' % self.name
# ===以下部分是测试代码===
def test_my_book ():
print(my_book)
def test_say_hi():
say_hi('孙悟空')
say_hi(User('Charlie'))
def test_User():
u = User('白骨精')
u.walk()
print(u)
# 当__name__为'__main__'(直接使用python运行该模块)时执行如下代码
if __name__ == '__main__':
test_my_book()
test_say_hi()
test_User()
使用python module1.py命令来运行该模块,输出结果:
这是module 1
疯狂Python讲义
孙悟空,您好,欢迎学习Python
User[name=Charlie],您好,欢迎学习Python
白骨精正在慢慢地走路
User[name=白骨精]
加载模块
在编译一个python模块之后,如果直接用import或from ... import来导入该模块,python通常并不能加载该模块。道理很简单:python怎么知道到哪里去找这个模块呢?
为了让python能找到我们编写(或第三方提供)的模块,可以使用两种方式来告诉它:
- 使用环境变量
- 将模块放在默认的模块加载路径之下
使用环境变量
Python将会根据PYTHONPATH环境变量的值来确定到哪里去加载模块。PYTHONPATH环境变量的值是多个路径的集合,这样Python就会依次搜索PYTHONPATH环境变量所指定的多个路径,试图从中找到程序想要加载的模块。
在windows平台上设置环境变量
一般建议设置“用户变量”即可,因为用户变量只对当前用户有效,而系统变量对所有用户有效。为了减少自己所做的修改对其他人的影响,故设置用户变量。对于当前用户而言,设置用户变量和系统变量的效果大致相同,不过系统变量的路径排在用户变量的路径之前。
在“变量名”文本框内输入PYTHONPATH,表明将要建立名为PYTHONPATH的环境变量:在“变量值”文本框内输入 .;d:python_module,这就是该环境变量的值,该值其实包含了两条路径(分号为分隔符),第一条路径为一个点(.),这个点代表当前路径,表明当运行Python程序时,Python总能从当前路径加载模块:第二条路径为d:python_module,表明当运行Python程序时,Python总能从d:python_module加载模块。
在成功设置了上面的环境变量之后,接下来只要把前面定义的模块(Python程序)放在与当前所运行Python程序相同的路径中(或放在d:python_module路径下),该模块就能被成功加载了。
提示:设置完环境变量后,需要重启 spyder 编辑器,让spyder重新加载环境变量的值。
# 导入module1,并指定其别名为md
import module1 as md
print(md.my_book)
md.say_hi('Charlie')
user = md.User('孙悟空')
print(user)
user.walk()
输出结果:
这是module 1
疯狂Python讲义
Charlie,您好,欢迎学习Python
User[name=孙悟空]
孙悟空正在慢慢地走路
默认的模块加载路径
python默认的模块加载路径由sys.path变量代表,可通过在交互式解释器中输入如下命令来查看:
import sys,pprint
pprint.pprint(sys.path)
['C:\Users\zz',
'D:\Python\python_module',
'D:\6.CommonTools\anaconda2\envs\python37\python37.zip',
'D:\6.CommonTools\anaconda2\envs\python37\DLLs',
'D:\6.CommonTools\anaconda2\envs\python37\lib',
'D:\6.CommonTools\anaconda2\envs\python37',
'',
'D:\6.CommonTools\anaconda2\envs\python37\lib\site-packages',
'D:\6.CommonTools\anaconda2\envs\python37\lib\site-packages\win32',
'D:\6.CommonTools\anaconda2\envs\python37\lib\site-packages\win32\lib',
'D:\6.CommonTools\anaconda2\envs\python37\lib\site-packages\Pythonwin',
'D:\6.CommonTools\anaconda2\envs\python37\lib\site-packages\IPython\extensions',
'C:\Users\zz\.ipython']
如果要打印的内容很多,使用pprint可以显示更友好的打印结果。
通常来说,我们应该将python的扩展模块添加在 libsite-packages 路径下,它专门用于存放python的扩展模块和包。
下面编写一个python模块文件(print_shape.py),并将该文件复制到 libsite-packages 目录下面,就相当于为python扩展了一个 print_shape 模块,这样任何python程序都可使用该模块,示例:
# coding: utf-8
'''
简单的模块,该模块包含以下内容
my_list:保存列表的变量
print_triangle: 使用星号打印三角形的函数
'''
my_list = ['Python', 'Kotlin', 'Swift']
def print_triangle(n):
'''使用星号打印一个三角形'''
if n <= 0:
raise ValueError('n必须大于0')
for i in range(n):
print(' ' * (n - i - 1), end='')
print('*' * (2 * i + 1), end='')
print('')
# ====以下是测试代码====
def test_print_triangle():
print_triangle(3)
print_triangle(4)
print_triangle(7)
if __name__ == '__main__': test_print_triangle()
交互式解释器中测试该模块:
(base) C:Userszz>conda activate python37
(python37) C:Userszz>python
Python 3.7.9 (default, Aug 31 2020, 17:10:11) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import print_shape
>>> print(print_shape.__doc__)
简单的模块,该模块包含以下内容
my_list:保存列表的变量
print_triangle: 使用星号打印三角形的函数
>>> print_shape.print_triangle.__doc__
'使用星号打印一个三角形'
>>> print_shape.my_list[1]
'Kotlin'
>>> print_shape.print_triangle(5)
*
***
*****
*******
*********
>>>
从上面可以看到,程序通过模块名前缀访问my_list变量,输出了该变量的第二个元素。
导入模块的本质
定义一个新的模块(fk_module.py),该模块比较简单,示例:
'一个简单的测试模块: fkmodule'
print("this is fk_module")
name = 'fkit'
def hello():
print("Hello, Python")
接下来,在相同路径下定义如下程序来使用该模块,示例:
import fk_module
print("================")
# 打印fk_module的类型
print(type(fk_module))
print(fk_module)
print(fk_module.name)
print(fk_module.hello)
输出结果:
this is fk_module
================
<class 'module'>
<module 'fk_module' from 'C:\Users\zz\.spyder-py3\fk_module.py'>
fkit
<function hello at 0x000001577127FC18>
使用“import fk_module”导入模块的本质就是:将fk_module.py中的全部代码加载到内存井执行,然后将整个模块内容赋值给与模块同名的变量(fk_module),该变量的类型是module,而在该模块中定义的所有程序单元都相当于该module对象的成员。
再试试使用from ... import 语句来执行导入,示例:
from fk_module import name, hello
print("================")
print(name)
print(hello)
# 打印fk_module
print(fk_module)
输出结果:
this is fk_module
================
fkit
<function hello at 0x00000157712CE678>
Traceback (most recent call last):
File "C:Userszz.spyder-py3 emp.py", line 7, in <module>
print(fk_module)
NameError: name 'fk_module' is not defined
使用 "from fk_module import name, hello" 导入模块中成员的本质是:将fk_module.py中的全部代码加载到内存并执行,然后只导入指定变量、函数等成员单元,并不会将整个模块导入,因此上面程序在输出fk_module时将看到错误提示:name 'fk_module' is not defined
在导入模块后,可以在模块文件所在目录下看到一个名为"pycache"的文件夹,打开该文件夹,可以看到Python为每个模块都生成一个*.cpython-36.pyc文件,比如Python为fk_module模块生成一个fk_module.cpython-36.pyc文件,该文件其实是Python为模块编译生成的字节码,用于提升该模块的运行效率。
模块的__all__变量
在默认情况下,如果使用“from 模块名 import *”这样的语句来导入模块,程序会导入该模块中所有不以下画线开头的程序单元,这是很容易想到的结果。
有时候模块中虽然包含很多成员,但并不希望每个成员都被暴露出来供外界使用,此时可借助于模块的__all__变量,将变量的值设置成一个列表,只有该列表中的程序单元才会被暴露出来。
定义一个包含__all__变量的模块(all_module.py)
'测试__all__变量的模块'
def hello():
print("Hello, Python")
def world():
print("Pyhton World is funny")
def test():
print('--test--')
# 定义__all__变量,指定默认只导入hello和world两个程序单元
__all__ = ['hello', 'world']
示范模块中__all__变量的用处
# 导入all_module模块内所有成员
from all_module import *
hello()
world()
test() # 会提示找不到test()函数
输出结果:
Hello, Python
Pyhton World is funny
Traceback (most recent call last):
File "C:Userszz.spyder-py3 emp.py", line 5, in <module>
test() # 会提示找不到test()函数
NameError: name 'test' is not defined
上面程序显示,只导入了__all__变量所列的程序单元,函数test()默认没有被导入。
__all__变量的意义在于为模块定义了一个开放的公共接口。通常来说,只有__all__变量列出的程序单元,才是希望该模块被外界使用的程序单元。因此,为模块设置__all__变量还是比较有用的。比如一个实际的大模块可能包含了大量其他程序不需要使用的变量、函数和类,那么通过__all__变量即可把它们自动过滤掉,这还是非常酷的。
如果确实希望程序使用模块内__all__列表之外的程序单元,有两种解决方法:
- 第一种是使用“import 模块名”来导入模块。在通过这种方式导入模块之后,总可以通过模块名前缀(如果为模块指定了别名,则使用模块的别名作为前缀)来调用模块内的成员。
- 第二种是使用"from 模块名 import 程序单元”来导入指定程序单元。在这种方式下,即使想导入的程序单元没有位于all列表中,也依然可以导入。
使用包
对于一个需要实际应用的模块而言,往往会具有很多程序单元,包括变量、函数和类等,如果将整个模块的所有内容都定义在同一个Python源文件中,这个文件将会变得非常庞大,显然并不利于模块化开发。
什么是包
为了更好地管理多个模块源文件,python提供了包的概念。那么什么是包呢?
- 从物理上看,包就是一个文件夹,在该文件夹下包含了一个__init__.py文件,该文件夹可用于包含多个模块源文件
- 从逻辑上看,包的本质依然是模块
包的作用是包含多个模块,但包的本质依然是模块,包可用于包含包。
定义包
定义包很简单,主要有两步:
- 创建一个文件夹,该文件夹的名字就是该包的包名
- 在该文件夹内添加一个 init.py文件即可
下面定义一个非常简单的包。先创建一个 first_package 文件夹,在该文件夹中添加一个 init.py 文件,该文件内容如下:
'''
这是学习包的第一个示例
'''
print('this is first_package')
该文件开始部分的字符串是该包的说明文档。通过如下程序使用该包,示例:
# 导入first_package包(模块)
import first_package
print('==========')
print(first_package.__doc__)
print(type(first_package))
print(first_package)
输出结果:
this is first_package
==========
这是学习包的第一个示例
<class 'module'>
<module 'first_package' from 'C:\Users\zz\.spyder-py3\first_package\__init__.py'>
再次强调,包的本质就是模块,因此导入包和导入模块的语法完全相同。
从上面的输出结果可以看出,在导入first_package包时,程序执行了该包所对应的文件夹下的__init__.py;从倒数第二行输出可以看到,包的本质就是模块;从最后一行输出可以看到,使用"import first_package"导入包的本质就是加载并执行该包下的__init__.py文件,然后将整个文件内容赋值给与包同名的变量,该变量的类型是module。
与模块类似的是,包被导入之后,会在包目录下生成一个__pycache__一文件夹,并在该文件夹内为包生成一个__init__.cpython-37.pyc文件。
由于导入包就相当于导入该包下的__init__.py文件,因此我们完全可以在__init__.py文件中定义变量、函数、类等程序单元,但实际上往往并不会这么做。想一想原因是什么?包的主要作用是包含多个模块,因此__init__.py文件的主要作用就是导入该包内的其他模块。下面再定义一个更加复杂的包,在该包下将会包含多个模块,并使用__init__.py文件来加载这些模块。
新建一个 fk_package 包,包含三个模块文件:
- arithmetic_chart.py
def print_multiple_chart(n):
'打印乘法口角表的函数'
for i in range(n):
for j in range(i + 1):
print('%d * %d = %2d' % ((j + 1) , (i + 1) , (j + 1)* (i + 1)), end=' ')
print('')
- billing.py
class Item:
'定义代表商品的Item类'
def __init__(self, price):
self.price = price
def __repr__(self):
return 'Item[price=%g]' % self.price
- print_shape.py
def print_blank_triangle(n):
'使用星号打印一个空心的三角形'
if n <= 0:
raise ValueError('n必须大于0')
for i in range(n):
print(' ' * (n - i - 1), end='')
print('*', end='')
if i != n - 1:
print(' ' * (2 * i - 1), end='')
else:
print('*' * (2 * i - 1), end='')
if i != 0:
print('*')
else:
print('')
init.py 文件暂时为空,不用编写任何内容。
这意味着:fk_package 包(也是模块)总共包含这三个模块。在这种情况下,这三个模块就相当于fk_package 包的成员。
导入包内成员
# 导入fk_package包,实际上就是导入包下__init__.py文件
import fk_package
# 导入fk_package包下的print_shape模块,
# 实际上就是导入fk_package目录下的print_shape.py
import fk_package.print_shape
# 实际上就是导入fk_package包(模块)导入print_shape模块
from fk_package import billing
# 导入fk_package包下的arithmetic_chart模块,
# 实际上就是导入fk_package目录下的arithmetic_chart.py
import fk_package.arithmetic_chart
fk_package.print_shape.print_blank_triangle(5)
im = billing.Item(4.5)
print(im)
fk_package.arithmetic_chart.print_multiple_chart(5)
输出结果:
*
* *
* *
* *
*********
Item[price=4.5]
1 * 1 = 1
1 * 2 = 2 2 * 2 = 4
1 * 3 = 3 2 * 3 = 6 3 * 3 = 9
1 * 4 = 4 2 * 4 = 8 3 * 4 = 12 4 * 4 = 16
1 * 5 = 5 2 * 5 = 10 3 * 5 = 15 4 * 5 = 20 5 * 5 = 25
# 从当前包导入print_shape模块
from . import print_shape
# 从.print_shape导入所有程序单元到fk_package中
from .print_shape import *
# 从当前包导入billing模块
from . import billing
# 从.billing导入所有程序单元到fk_package中
from .billing import *
# 从当前包导入arithmetic_chart模块
from . import arithmetic_chart
# 从.arithmetic_chart导入所有程序单元到fk_package中
from .arithmetic_chart import *
# 导入fk_package包,实际上就是导入包下__init__.py文件
import fk_package
# 直接使用fk_package前缀即可调用它所包含的模块内的程序单元。
fk_package.print_blank_triangle(5)
im = fk_package.Item(4.5)
print(im)
fk_package.print_multiple_chart(5)
输出结果:
*
* *
* *
* *
*********
Item[price=4.5]
1 * 1 = 1
1 * 2 = 2 2 * 2 = 4
1 * 3 = 3 2 * 3 = 6 3 * 3 = 9
1 * 4 = 4 2 * 4 = 8 3 * 4 = 12 4 * 4 = 16
1 * 5 = 5 2 * 5 = 10 3 * 5 = 15 4 * 5 = 20 5 * 5 = 25
上面粗体字代码是导入fk_package包,导入该包的本质就是导入该包下的__init__.py文件。而__init__.py文件又执行了导入,它们会把三个模块内的程序单元导入fk_package包中,因此程序的下面代码可使用fk_package.前缀来访问三个模块内的程序单元。
查看模块内容
在导入模块之后,开发者往往需要了解模块包含哪些功能,比如包含哪些变量、哪些函数、哪些类等,还希望能查看模块中各成员的帮助信息,掌握这些信息才能正常地使用该模块。
模块包含什么
为了查看模块包含什么,可以通过如下两种方式:
- 使用dir()函数 (返回模块或类所包含的全部程序单元,包括变量、函数、类和方法等)
- 使用模块本身提供的__all__变量
import string
dir(string)
Out[24]:
['Formatter',
'Template',
'_ChainMap',
'_TemplateMetaclass',
'__all__',
'__builtins__',
'__cached__',
'__doc__',
'__file__',
'__loader__',
'__name__',
'__package__',
'__spec__',
'_re',
'_string',
'ascii_letters',
'ascii_lowercase',
'ascii_uppercase',
'capwords',
'digits',
'hexdigits',
'octdigits',
'printable',
'punctuation',
'whitespace']
很明显,该模块内有大量以下划线开头的程序单元,其实这些程序单元并不希望被其他程序使用,因此列出这些程序单元意义不大,可以使用列表推导式过滤掉,示例:
[e for e in dir(string) if not e.startswith('_')]
Out[25]:
['Formatter',
'Template',
'ascii_letters',
'ascii_lowercase',
'ascii_uppercase',
'capwords',
'digits',
'hexdigits',
'octdigits',
'printable',
'punctuation',
'whitespace']
也可通过该模块的__all__变量来查看模块内的程序单元(有些模块不提供__all__变量,只能使用上面列表推导式来查看),如下
tring.__all__
Out[26]:
['ascii_letters',
'ascii_lowercase',
'ascii_uppercase',
'capwords',
'digits',
'hexdigits',
'octdigits',
'printable',
'punctuation',
'whitespace',
'Formatter',
'Template']
使用__doc__属性查看文档
使用help()函数之所以能查看到程序单元的帮助信息,其实完全是因为该程序单元本身有文档信息,也就是有__doc__属性。换句话说,使用help()函数查看的其实就是程序单元的__doc__属性值。
提示:Python库的参考文档:https://docs.python.org/3/library/index.html
使用__file__属性查看模块的源文件路径
通过模块的__file__属性即可查看到指定模块的源文件路径。开发者完全可以根据路径打开查看该模块的全部源代码。
需要说明的是,并不是所有模块都是使用Python语言编写的,有些与底层交互的模块可能是用C语言编写的,而且是C程序编译之后的效果,因此这种模块可能没有__file__属性。
原文来源于我的语雀,我的微信公众号:细细研磨