python中创建自己的包(package),其实就只需要里创建一个文件夹就可以了。文件夹里不是必须要有是__init__.py这个文件的,不过如果你的文件夹里存在这个__init__.py文件,那么当你在import这个包时python会立刻执行__init__.py里的内容并且只会执行__init__.py。相反,如果你的文件夹(这个package)里没有这个文件,当import这个package时就不会做任何事情(换句话说你python只会load这个包的名称,但里面接着进行任何其他的操作,你可以查看这个包的默认属性)。因此一般情况下,一个package中是必须要有__init__.py,并且常常会在__init__.py文件中执行import语句,比如import进一些本文件夹里的一些包,或者封装好一些类,一些函数。这样做的好处是,只要你import了这个package,那么你就可以使用__init__.py中load的所有包。

1、创造一个包

我们要做的就是两件事

  1、在工作目录下创建一个文件夹命名为My_package

  2、在文件夹My_package中再创建一个__init__.py文件

__init__.py:

print('包加载成功')

我们可以是在My_package文件夹的同一级目录中加载import一下(注意工作目录要在的同一级目录中才能加载到My_package,后面会介绍如果不在同一级目录该怎么做)

import My_package
包加载成功

可以看到加载包的时候(并且在My_package目录下自动生成了一个__pycache__的文件夹),我们写在__init__.py中的命令行被执行了接下来我们更加丰富一下__init__.py的内容,给这个里面添加两个函数:

__init__.py:

print('包加载成功')

def fun1(a):
    print('执行函数1:',a)
    return

def fun2(a):
    print('执行函数2:',a)

接下来我们导入这个包,并执行里面的函数。

import My_package
My_package.fun1('开始执行')
My_package.fun2('开始执行')
包加载成功
执行函数1: 开始执行
执行函数2: 开始执行

可以看到这些函数都已经被执行了。

当然我们可以使用from My_package import *这种方式来完成上面的内容

from My_package import *
fun1('开始执行')
fun2('开始执行')
包加载成功
执行函数1: 开始执行
执行函数2: 开始执行

可以看到结果是一样的,由此我们已经创建自己的包了

2、为包增加模块

我们常见的包里面都会有很多很多其他的.py程序,并不是只有__init__.py,因为有些内容不需要在导入的时候就执行。下面我们为My_package增加两个模块。model1.py和model2.py。

model1.py

print('这里是model1')

def fun1():
    print('这里是model1的第一个函数')
    return 

def fun2():
    print('这里是model1的第二个函数')
    return

model2.py

print('这里是model2')

def fun1():
    print('这里是model2的第一个函数')
    return 

def fun2():
    print('这里是model2的第二个函数')
    return

创建完这两个模块之后,我们My_package文件夹下就有四个文件了:

  • 文件夹__pycache__
  • 文件__init__.py
  • 文件model1.py
  • 文件model2.py

下面我们用不同的方式来导入包和模块,并执行。

方式一:import My_package.模块名

import My_package.model1
import My_package.model2
包加载成功
这里是model1
这里是model2
My_package.model1.fun1()
My_package.model2.fun2()
这里是model1的第一个函数
这里是model2的第二个函数
My_package.fun1('OK')
执行函数1: OK

  可以看到__init__.py依然被执行了的,而且在导入包的时候,不同的model中的代码也在import的时候执行了。需要注意的是,采用这种import 包名.模块名的方式,会出现一个全局变量My_package来指向这个包(我们可以通过dir(My_package)来查看这个全局变量的属性,我们可以看到'fun1','fun2','model1','model2'都出现了),因此我们可以采用 包名.模块名.函数名 的方式调用不同model的函数。我们还可以通过My_package.函数名的方式来执行__init__.py里的函数(这是因为采用import 包名.模块名的方式来载入,python依然会执行package中的__init__.py,并且生成的对象会被命名为My_package)。

  我们可以通过加as的方法来对这一串很长的包名来重新命名,这就是介绍的第二种方法。

方式二:import My_package.model1 as xxx

import My_package.model1 as new_name1
import My_package.model2 as new_name2
包加载成功
这里是model1
这里是model2
new_name1.fun1() 
new_name2.fun2()
这里是model1的第一个函数
这里是model2的第二个函数

  可以看到我们可以使用new_name1这个变量来代替My_package.model1。这种采用as进行重命名的方式需要注意的是,此时python中将不会出现叫My_package的全局变量来指向这个package了,因此程序会无法找到My_package这个变量。因为python会将最外层的这个model1赋值给new_name1,而不再保存My_package的任何东西,尽管执行了My_package中__init__.py。

方法三:from My_package import model1

from My_package import model1
from My_package import model2
包加载成功
这里是model1
这里是model2
model1.fun1()
model2.fun2()
这里是model1的第一个函数
这里是model2的第二个函数
My_package.fun1('OK')
NameError: name 'My_package' is not defined
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
/tmp/ipykernel_9767/1183774725.py in <module>
      2 from My_package import model2
      3 
----> 4 My_package.fun1('OK')
NameError: name 'My_package' is not defined

根据导入包的方式顾名思义,"从My_package中导入model1.py",这个时候我们可以直接使用model1.函数名()来调用函数了。可以看到跟前一种方式一样此时python中将不会出现叫My_package的全局变量,虽然已经执行了__init__.py,但python没有将My_package赋值给任何变量,因此无法找到My_package。

方法四:from My_package import *

from My_package import *
fun1('OK')
包加载成功
执行函数1: OK
modle1.fun1()
NameError: name 'modle1' is not defined

这种方式加载其实就相当于直接把__init__.py直接加载到工作区,直接可以使用__init__.py中的类或者函数,不用再添加My_package.函数名。不过要注意的是这种导入,只导入了My_package中__init__.py,并没有导入其他的model1,或者model2,因此第三行是会报错的(并且我们可以看到并没有任何model的打印出现,其实这也侧面说明了当只采用import package的方式导入包时,python只会执行__init__.py)。我们又该怎么调用model里的函数呢?用下面的方式就可以了。

from My_package.model1 import *
包加载成功
这里是model1
fun1()
fun2()
这里是model1的第一个函数
这里是model1的第二个函数

其实不推荐大家采用这这种方法,因为如果在不同model中出现了相同的函数名方法,后导入的包将会把前者的函数名给掩盖。比如本文的这两个例子,model1和modle2的函数都叫做fun1,fun2,那如果我们按照这个方式同时导入这两个模块,会出现什么问题呢?

from My_package.model1 import *
from My_package.model2 import *
包加载成功
这里是model1
这里是model2
fun1()
fun2()
这里是model2的第一个函数
这里是model2的第二个函数

大家可以看到前两句执行之后,显示两个包都被导入了,但是我们执行fun1和fun2时编译器识别的是后导入的模块model2的fun1和fun2,而model2的函数被掩盖了,因此不推荐大家采用方法4作为导入包的方法。

3、包的路径问题

刚学python的时候我非常好奇,import的包到底在那里?我pip install xxx下载下来的包又在那里?(当然可以通过路径查找),为什么下载下来的包在不同的地方但是import却能找到这些散落到各地的包?其实这些问题都归结到import是如何导入到包的。其实import包的时候时,python会在一些路径中搜索是否存在名字相符的文件夹(package)或者.py文件。

  大家可以通过运行:

import sys
sys.path
['/home/g4/.vscode/extensions/ms-toolsai.jupyter-2021.3.684299474/pythonFiles/vscode_datascience_helpers/../.does-not-exist',
 '/home/g4/.vscode/extensions/ms-toolsai.jupyter-2021.3.684299474/pythonFiles',
 '/home/g4/.vscode/extensions/ms-toolsai.jupyter-2021.3.684299474/pythonFiles/lib/python',...]
(在ubuntu系统上演示的)

查看的当前的系统环境环境变量(sys.path是一个列表,每个元素是路径),在import的时候python就会在这些路径中去寻找有没有符合的名字,如果没有找到,就会返回我们最常见的"ModuleNotFoundError: No module named 'kae'".

  通过这个问题的讨论其实我们可以知道,如果我们自己编写的包想要被import到,就需要把包的路径写道环境变量中(前文演示的时候工作目录是会自动加入到import的查询范围内的)。我们不用去修改电脑的环境变量,因为刚说道sys.path是一个列表,因此我们可以通过append方法加入我们包所在路径可以了。

  为了演示,我把前面写好的My_package文件夹放到了工作目录的下一级目录。

import  My_package
ModuleNotFoundError: No module named 'My_pacackge'
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
/tmp/ipykernel_31481/1446587236.py in <module>
----> 1 import  My_pacackge
ModuleNotFoundError: No module named 'My_pacackge'

不出意外的,报错了。接下来我们添加My_package文件夹的目录到系统目录中

import sys
sys.path.append('/home/g4/桌面/project')
print('##############')
import My_package
##############
包加载成功

可以看到,包被正常加载了。对于自己写的模块我们可以采用这种方式来对其进行加载。

4、相对路径导入包的问题

  为了更好区分,我们将model2.py中的两个函数改一下名字。

print('这里是model2')

def model2_fun1():
    print('这里是model2的第一个函数')
    return 

def model2_fun1fun2():
    print('这里是model2的第二个函数')
    return

  在同一个package中,如果model1.py中想使用model2.py的函数,那我们可以通过import My_package.model2的方式进行正常导入。不过会有个问题,如果某一天,我把文件夹My_package更名了,那么此时mode1.py中的import My_package.model2将会报错。如何避免这个问题呢,我们就可以采用相对路径的方式导入包。即将import My_package.model2改为import .model2的方式导入。那么无论文件夹My_package如何更名,只要model1.py与model2.py是在同一个package下的,那么这行代码都会正确找到。

  接下来我们要说一下原理:python相对路径导入包的方法其本质是,都是先找到绝对路径再import。他会根据model1的__package__变量去找到model1的package是谁(本例中就是My_package),再将import .model2复原成import My_package.model2,从而完成导入。

  明白这个原理之后,相信大家就会知道,为什么我直接运行model1.py时,其中的import .model2会报错了。因为当你直接运行model1.py的时候,这个文件将会当作main package 加载到内存中,此时它并不属于任何的的package,所以import .model2就不会转化成import My_package.model2,也就无法找到正确的model2,从而报错。

  以上内容如有错误,恳请指正