本文主要探讨模块和包两个概念,了解这两个概念,有助于我们更好地使用python进行模块化编程,通过模块化编程,我们能把大的工程拆分成小的子任务和子模块,在比较大的项目中,进行模块化编程的好处有以下几点:
- 简化编程,不必把重点放在整个项目上了;
- 可维护性好,即使出了问题也便于排查;
- 复用性好,直接使用编写好的模块去实现功能,当需要重复实现时,再次调用即可,不必再重新编写了;
- 范围性好,这个的意思就是每个模块都有单独的命名空间,避免发生一些例如变量命名上的冲突
在编程中多多使用函数,模块和包能够提升模块化编程水平。
基础概念
什么是命名?
每个变量都拥有一个名字,这个就是命名,给变量命名。变量命名也是让很多程序员头疼的一件事情,怎么样能起一些有意义,又高大上的名字。在Python中,一切皆对象,我们甚至可以给一个函数一个命名,命名就可以理解为所有对象的一个引用的名称。
什么是命名空间?
名空间就是用来保证命名之间不发生冲突的规则,分为:
- 局部命名空间:在一个程序中为每个函数创建的命名空间,当函数执行完毕就失效
- 全局命名空间:在程序中为所有模块创建的命名空间,当程序执行完毕失效
- 内置命名空间:也就是默认的一些命名,退出程序失效
Python模块介绍
如何创建一个模块?
在python中,有三种方式去创建模块:
- 自己写一个python文件;
- 用C语言实现,然后在运行时动态加载,比如常用的正则表达式模块
re
; - 内置模块,我们直接引用就可以了;
-
我们主要关注第一种方式,也就是说,
python
给我们提供了十分简单的方法去创建一个模块,我们只需要写一个python
文件即可,也就是说写一个.py
为后缀的文件,不必用额外的语法。举个例子,我们现在写了一个文件,命名为
mod.py
,如下:mod.py
-
s = "If Comrade Napoleon says it, it must be right." a = [100, 200, 300] def foo(arg): print(f'arg = {arg}') class Foo: pass
可以看到,我们在这个python文件里面创建了一下几个对象:
- s,一个字符串
- a,一个列表
- foo,一个函数
- Foo,一个类
-
假定这个文件路径正确,我们就可以把
mod.py
作为一个模块,在别的文件中引用,举例:>>> import mod >>> print(mod.s) If Comrade Napoleon says it, it must be right. >>> mod.a [100, 200, 300] >>> mod.foo(['quux', 'corge', 'grault']) arg = ['quux', 'corge', 'grault'] >>> x = mod.Foo() >>> x <mod.Foo object at 0x03C181F0>
模块搜索路径
继续上面的例子,当执行这句
import mod
的时候,解释器都干了什么?
解释器主要是在如下几个路径中搜索mod.py
文件:
- 首先在当前执行的文件所在的文件夹中查看是否存在,如果当前文件夹不包含
mod.py
文件,那么这个搜索就失败了; - 在系统设置的python环境变量下的一些文件夹里面;
- 一些在python安装时指定的文件夹下面;
具体我们可以使用:
>>> import sys
>>> sys.path
['', 'C:\\Users\\john\\Documents\\Python\\doc', 'C:\\Python36\\Lib\\idlelib',
'C:\\Python36\\python36.zip', 'C:\\Python36\\DLLs', 'C:\\Python36\\lib',
'C:\\Python36', 'C:\\Python36\\lib\\site-packages']
来查看在路径搜索时,都有哪些路径,当然搜索的结果因为安装,版本和操作系统的原因都不会相同,每个人的结果可能都不会相同。
因此,当我们import
一些模块出现问题的时候,我们可以查看这个模块文件是不是在搜索路径中。
当我们想要把自己写的文件作为一个模块的时候,但是又不在搜索路径中,我们可以自己添加进去,比如:
>>> sys.path.append(r'C:\Users\john')
>>> sys.path
['', 'C:\\Users\\john\\Documents\\Python\\doc', 'C:\\Python36\\Lib\\idlelib',
'C:\\Python36\\python36.zip', 'C:\\Python36\\DLLs', 'C:\\Python36\\lib',
'C:\\Python36', 'C:\\Python36\\lib\\site-packages', 'C:\\Users\\john']
>>> import mod
因为sys.path
返回的是路径的列表,我们直接使用append()
方法把想要引用的模块路径添加进去即可。
让Python文件既可以被当作模块引用,也能被当作脚本执行
s = "If Comrade Napoleon says it, it must be right."
a = [100, 200, 300]
def foo(arg):
print(f'arg = {arg}')
class Foo:
pass
if (__name__ == '__main__'):
print('Executing as standalone script')
print(s)
print(a)
foo('quux')
x = Foo()
print(x)
加上了if (__name__ == '__main__'):
,这样python解释器就知道这个文件时单独在执行,还是被作为模块引用。在单独执行时,有输出,而在作为模块引用时,没有输出。
模块推荐导入顺序
在文件中需要导入内置模块、第三方模块或自定义模块时,建议的导入顺序是:标准库模块--第三方模块--自定义模块。
什么是Python的包(package)?
简单来说,包就是多个模块的集合。当项目较大,模块较多时,我们就可以把模块放在包中,便于管理。
我们在包中经常能见到__init__.py
文件,如下图:
__init__.py文件到底是什么?
对于一个python项目,里面的每一个文件夹都可以认为是一个package,而每一个.py文件被认为是一个module。如果你用的IDE是PyCharm,那么当你新建一个Python Package的时候,PyCharm都会自动为你新建一个__init__.py文件。这个__init__.py文件可以看作这个package的初始化文件,具体用途且看下文。
__init__.py文件在做什么?
文件目录如下
"""
.
├── demo.py
├── package
| ├── __init__.py
| ├── module.py
"""
demo.py文件如下
# demo.py
import package
__init__.py文件如下
# __init__.py
print("__init.py file is called!")
那么当我执行demo.py文件时,输出结果如下
$ python demo.py
__init.py file is called!
这说明__init__.py中的代码被执行了。如果把demo.py中的 import package 换成import package.module也是一样的结果。这说明,当我们从一个package里面调用东西的时候,该__init__.py文件内的代码会被首先执行。
__init__.py文件有什么用?
简化import语法
假设在module.py文件中有一个函数a_function()如下
# module.py
def a_function():
print("Test function is called!")
如果我现在想从demo.py中调用它,没有__init__.py文件的话,只能这么写(方法一)
# demo.py
from package.module import a_function
a_function()
但是我可以在__init__.py中进行如下定义
# __init__.py
from package.module import a_function
这样定义完了以后在demo.py就可以如下调用了(方法二)
# demo.py
from package import a_function
a_function()
好像也没简洁到哪儿去?试想,如果你在paSckage里面有几十个module,那么当你想调用这几十个module里面的几十上百个函数的时候,你就需要在demo.py文件中写几十行import语句,这样无疑是不简洁的。而采用方法二的办法,你就可以把这些import语句统统放进__init__.py文件。但这不是__init__.py最重要的用途,最重要的是下面两点。
批量导入和规范化导入以及__all__
如果你在module.py中定义了很多函数,你想在demo.py中调用,怎么办呢?如module.py中有两个函数
# module.py
def a_function_1():
print("Test function 1 is called!")
def a_function_2():
print("Test function 2 is called!")
先在__init__.py批量导入
# __init__.py
from package.module import *
再在demo.py中批量导入
# demo.py
from package import *
a_function_1()
这样就可以实现package中诸多函数的批量导入了。注意批量导入遵从覆盖原则,即如果有多个类和方法名字相同,那么后导入的会覆盖先导入的。
但是如果你只允许a_function_1被外界调用呢,就可以把__init__.py改成
# __init__.py
from package.module import a_function_1
这样在demo.py文件中,调用a_function_2就会报错。
# demo.py
from package import *
a_function_2() # 这里就会报错
更优雅一点可以调用__all__属性,在__init__.py中定义可以被外界调用的类和方法,如
# __init__.py
__all__ = ['a_function_1'] # 这样,在demo.py只能调用a_function_1方法
from package.module import *
这里举的例子都是函数/方法,对于类来说,是一样的。