谈谈Python3如何自如地导入

本文介绍包的导入,总结如何游刃有余地导入Python3模块(即以.py结尾的文件)的大部分问题。
本文不解决:导入模块时Python底层做了什么、循环导入时怎样才安全、如何导入Java或C语言包。

由于水平有限,如有错误还请不吝指正,感谢。这也是我第一次使用MD编辑器写博客。

什么是模块

模块是提供自包含的变量的包(也就是所谓的命名空间)从而将部件组织为系统的一种可行方式。一个模块文件顶层定义的所有变量都变成了被导入的模块对象的属性。

模块对象是什么?模块对象也是一种对象,对象面向对象中的概念。对象我们可一简单理解为一个东西,我们可以把这个东西归为一个类的实例,或将其本身表述成一个类。而模块对象就是模块这个类的一个实例。

以下代码帮助你迅速体会模块对象

import math
print(type(math))
# 输出:<class 'module'>

在一切开始之前,我们创建一个新的Project,其根目录为root,目录结构如图:
项目目录结构
请牢牢记住它的位置哦,后面会经常见面哒,木啊

我们还要明确一个事实:在一次程序运行过程中,在首次导入模块的时候,被导入的模块会被运行一次(再次导入模块不会运行,除非使用importlib模块的reload函数或imp模块的reload函数(后者已弃用)),我们借此减少程序代码的编写量。我们会在被导入的模块中编写

print(__name__)

来代表被导入模块的程序,并从控制台获得其__name__属性值
什么是__name__?1
为了更准确地知道一个模块的位置,我们还会在模块中编写

print(__file__)

什么是__file__?2

import的用法

"import "是Python中导入模块的关键字。导入的最简单写法为import module,导入方式分为两类:绝对导入和相对导入。

导入用法示例

import pygame
import pygame.sprite
import pygame.sprite, pygame.time, pygame.math
# as相当于将导入的对象重命名
import pygame.sprite as sprite
# from导入
from pygame import sprite
from pygame.sprite import Sprite
from pygame.sprite import Sprite as S
from pygame import math as math, time as time
from pygame import math, time as time
# 批量导入——只能用from导入
from pygame import (
	sprite,
    math as math,
    time,
)
# 全部导入——只能用from导入,导入可由模块的__all__属性指定
# __all__不是本文讨论范围,但不清楚也不影响继续阅读
from pygame.locals import *
# 相对导入——只能用from导入
from . import *
from . import B
from ..m import a as sth
from ... import (
	m1 as m,
	m2 as mm,
	m3
)

关于导入语法的更多详细信息(包括__all__的使用),请移步:
Python官方文档 import详细语法
除了以上导入用法之外,标准库还有一个importlib模块和Python3.3以前使用的imp模块,详细了解请移步:
Python官方文档 5.导入系统

绝对导入

绝对导入是相对于相对导入而言的,即从系统中预编码的一系列目录之下从根部到所要导入的模块的完整路径导入模块,

代码说话,下面我们尝试从top中导入root/spam模块

先在项目中两个spam模块中编写position函数
在root/spam模块中:

print(__name__)

在root/P1/spam模块中:

print(__name__)

在top模块中:

import spam

问题是top模块导入了哪个spam呢?
我们运行top,控制台得到输出:

spam

好像还是看不出来spam模块的位置,
我们将root/spam模块修改一下:

print(__name__)
print(__file__)

top模块不变,下面运行top得到输出:

spam
/Users/chengmin/code/PycharmProjects/code/root/spam.py

我是Mac系统,系统根目录为/Users3

我们直观看到,导入的是root之下的spam
下面我们把top模块改一下:

import P1.spam

root/P1/spam内容:

print(__name__)
print(__file__)

运行root/top结果:

P1.spam
/Users/chengmin/code/PycharmProjects/code/root/P1/spam.py

我们可以知道,这样写导入了root/P1/spam。实际上,root.top代码中的小数点表示父子目录的关系,而导入的模块不需要加上后缀名。那么Python怎么知道是以root为根目录,而不是以我操作系统的User为根目录查找呢?难道她还认识root这个单词不成?非也,其实Python查找模块是从预编码的几个路径下寻找的。

预编码的目录

Python预编码的目录不止个,而是很多个。所有这些路径可以用sys模块的path变量获取。
如在top中编写:

import sys
for i in sys.path:
	print(i)

得到输出:

/Users/chengmin/code/PycharmProjects/code/root
/Users/chengmin/code/PycharmProjects/code/root
/Applications/PyCharm.app/Contents/plugins/python/helpers/pycharm_display
/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
/Users/chengmin/code/PycharmProjects/code/root/venv/lib/python3.9/site-packages
/Applications/PyCharm.app/Contents/plugins/python/helpers/pycharm_matplotlib_backend

这是我在Pycharm编辑器中得到的输出,很可能你的输出和我不一样(一样就奇怪了),但普遍搜索的目录种类是相似的。
Python并不是同时从这些目录中查找要导入的模块,不然从哪个目录之下找到模块就是未知数了,因为这可能取决于多进程查找的速度(岂不成赛跑游戏了)!其实这些目录都是有查找优先级的,如果上一个目录下没有找到对应子模块,就进入下一个预编码的目录中寻找。下面我按照查找优先级从高到低来介绍它们4

程序主目录

这也即所谓的工作目录。它是你所运行的顶层脚本文件的目录。
如运行top文件,主目录就是
‘/Users/chengmin/code/PycharmProjects/code/root’
即top模块所在的目录。

PYTHONPATH

这是操作系统级别的路径,可以通过相应操作系统的相关命令配置,如Windows操作系统(以下命令在你电脑为Windows操作系统且有目录C:\code\spam时才能工作):

set PYTHONPATH=C:\code;C:\code\spam

这样就把目录C:\\code和C:\\code\\spam永久地加入到sys.path中了

标准库目录

没错,接下来才是标准哭目录。因此如果你在主目录或PYTHONPATH中配置了诸如math.py的文件,你将导入你配置的文件而非标准库math模块。因此最好不要在主目录顶层覆盖标准库模块

.pth文件内容

可以将其他想要使用的目录添加在.pth文件中,本人只在Pycharm开发环境中尝试过这种方法,工作的很好。如图:
我打开了External Libraries
我打开了External Libraries/site-packages,然后,在其中加入一个文件mypath.pth
增加的bypath.pth文件

在mypath.pth中编写:

/Users/chengmin/Desktop

这是我Mac系统的桌面目录
Windows读者可以在桌面编写fortest.py,然后获得它的路径,把路径最后文件名fortest.py和其前的一个反斜线符号去掉就能获得该文件的目录啦

然后在桌面上编写一个fortest.py:

def f():
	print('you imported me!')

然后在root/top中编写:

import fortest
fortest.f()

运行root/top输出如下:

you imported me!

可见这样能够能够工作。

第三方扩展应用的site-packages主目录

如上图,我打开的site-packages就是这里所指的目录。通过pip的install命令安装的第三方库大部分都能在这里找到源码。
如我在Pycharm的Terminal中编写:

pip install pygame

安装完毕:
安装完毕
在External Libraries/site-packages中找到源代码包:
当前被选中的就是pygame的源代码包
以上就是所有一般的Python查找的根目录。总结一下:
主目录 > PYTHONPATH > 标准库目录 > .pth配置文件 > 第三方库site-packages目录

相对导入

相对导入是一种指示Python从当前目录开始向上寻找的导入方式。
下面我们了解一下相对导入的查找规则

路径查找规则

先让代码说话
root/top.py中:

import P1.P1_top

root/P1/P1_top.py中:

from . import spam

root/P1/spam.py中:

print(__name__)
print(__file__)

运行root/top.py:

P1.spam
/Users/chengmin/code/PycharmProjects/code/root/P1/spam.py

在root/P1/P1_top.py中我们用了相对导入,一个点就表示从本模块同目录之下查找,是相对于root/P1/P1_top.py这个模块自己的导入,和整个工作目录无关。在编写工具的时候使用广泛,不然依赖工作目录的导入将降低可移植性。比如将root/P1/P1_top.py编写成:

import spam

运行root/top.py输出:

spam
/Users/chengmin/code/PycharmProjects/code/root/spam.py

可以发现被导入的是root/spam.py
若运行root/P1/P1_top.py输出:

spam
/Users/chengmin/code/PycharmProjects/code/root/P1/spam.py

可以发现导入的是root/P1/spam.py
这种依赖于工作目录的导入在编写Python工具(也成轮子)中十分忌讳。
若在相对导入中增加一个点,所查找的目录就向上增加一级。这这就是相对导入。

注意事项

1.要运行的脚本不能用相对导入,否则报错:
如在root/top.py中编写:

from . import *

运行root/top.py输出:
报错
2.与运行的脚本同目录或更高层目录不能用相对导入,否则报错
如在root/P1/egg.py中编写:

from . import egg

在root/P1/spam.py中编写:

import egg

在root/P1/P1_top.py中编写:

import spam
#将导入root/P1/spam.py

运行root/P1/P1_top.py输出:
报错
若只改写root/P1/egg.py:

#从更高层目录相对导入
from .. import spam

并在root/spam.py中仅编写:

#尝试被root/P1/egg.py导入

运行root/P1/P1_top.py输出:
报错
说明所导入的模块不能比顶层脚本的目录更高

最佳实践建议

将要运行的脚本放在最顶层(皇帝只能有一个)。若想要测试模块,且模块中包含相对导入,则从顶层脚本通过绝对导入导入该模块,再测试。或者用在顶层加一个test.py文件,再在该模块中测试,测试完了再删掉该文件。

各种导入情境

下面讨论几个常见导入情境的导入方法

各种模块关系的称呼约定

大家要是懒得向上翻的话,这里把文件目录结构给到大家:
目录结构
声明模块之间的关系协议:
1.兄弟姐妹模块
即同一目录之下的模块。如:
root/spam.py和root/top.py互为兄弟姐妹模块;
root/P1/spam.py、root/P1/egg.py和root/P1/P1_top.py互为兄弟姐妹模块。
2.父母模块
即上一级目录之下的模块。如:
root/spam.py是root/P1/egg.py和root/P1/spam.py的父母模块;
root/top.py也是root/P1/egg.py和root/P1/spam.py的父母模块。
3.堂兄弟姐妹模块
即上一级目录之下的某个包的模块。如:
root/P2/egg.py和root/P1/egg.py互为堂兄弟姐妹模块。
4.子女后代模块
即下层目录下的模块。如:
root/P1/egg.py、root/P1/spam.py、root/P1/P1_P1/P1_P1_M1.py都是root/spam.py或root/top.py的子女后代模块
5.远方亲戚模块
即向上分别不断追溯父母模块最终能够在本项目(而非项目之外的操作系统中)中找到同一个模块(称之为祖先模块)的两个模块。如:
root/P1/P1_P1/P1_P1_M1.py和root.P2/P2_P2/spam.py就是远方亲戚模块。
6.操作系统其他位置模块
即向上分别不断追溯父母模块最终能够在本项目之外、本操作系统中找到同一个目录的两个模块。比如之前在桌面上编写的fortest.py和root之下的任何一个模块。

从兄弟姐妹模块导入(导入兄弟姐妹模块)

下例,在root/P1/spam.py中欲导入root/P1/egg.py。

若顶层模块为root/P1/top.py:

此时主目录为root/P1
顶层模块和要导入的模块们都位于同一目录下,但这不是最佳实践。
root/P1/P1_top.py:

import spam

root/P1/egg.py:

print(__name__)
print(__file__)

root/P1/spam.py:

import egg
print(__name__)
print(__file__)

测试root/P1/P1_top输出:

egg
/Users/chengmin/code/PycharmProjects/code/root/P1/egg.py
spam
/Users/chengmin/code/PycharmProjects/code/root/P1/spam.py

注意:这三个模块都不能用相对导入,因为都是位于顶层目录,会报错,除非

顶层脚本为root/top.py

此时主目录为:root
顶层脚本位于所有模块顶层,这是最佳实践。
root/top.py:

#这里涉及导入子女后代模块
import P1.egg

root/P1/spam.py:

from . import egg
'''
或者用
import P1.egg 此时能从主目录root之下找到P1
注意不能用
import egg 因为此时主目录为root,不能直接找到root/egg.py
'''

测试root/top.py输出:

P1.egg
/Users/chengmin/code/PycharmProjects/code/root/P1/egg.py
P1.spam
/Users/chengmin/code/PycharmProjects/code/root/P1/spam.py

从父母模块导入(导入父母模块)

下例,root/P1/P1_P1/P1_P1_M1.py欲导入root/P1/egg.py:

若顶层脚本为root/P1/P1_top.py:

root/P1/P1_top.py:

import P1_P1.P1_P1_M1

root/P1/egg.py:

print(__name__)
print(__file__)

root/P1/P1_P1/P1_P1_M1.py:

import egg

测试root/P1/P1_top.py输出:

egg
/Users/chengmin/code/PycharmProjects/code/root/P1/egg.py

此处也不能用相对引用,因为要导入的模块和顶层模块位于同一目录下。除非

顶层脚本为root/top.py

此时主目录为:root

此时顶层脚本位于所有模块顶层,为最佳实践。

root/P1/egg.py:

print(__name__)
print(__file__)

root/P1/P1_P1/P1_P1_M1.py

from .. import egg
'''
或用
import P1.egg
P1在root之下,故能找到
'''

root/top.py:

import P1.P1_P1.P1_P1_M1

测试root/top.py输出:

P1.egg
/Users/chengmin/code/PycharmProjects/code/root/P1/egg.py

从堂兄弟姐妹模块导入(导入堂兄弟姐妹模块)

下例:欲从root/P2/egg.py导入root/P1/egg.py
注意:若不在Pycharm项目中,在不手动编写pth文件、改变sys.path或设置PYTHONPATH等其他人为干预sys.path的情况下, 顶层模块只能为top.py或spam.py。但若在Pycharm项目中,由于在sys.path中欲配置了root根目录,因此总能用普通的绝对导入导入项目内的模块。

顶层脚本为root/P2/P2_top.py

此时顶层脚本不位于所有其他模块顶部,不是最佳实践。

此时的主目录为root/P2,但Pycharm会加上一个root

root/P2/P2_top.py:

import egg

root/P2/egg.py:

import P1.egg
print(__name__)
print(__file__)

root/P1/egg.py:

print(__name__)
print(__file__)

测试root/P2/P2_top.py输出:

P1.egg
/Users/chengmin/code/PycharmProjects/code/root/P1/egg.py
egg
/Users/chengmin/code/PycharmProjects/code/root/P2/egg.py

若顶层模块为root/top.py

此时顶层模块位于所有模块顶部,为最佳实践。

此时主目录为root

root/top.py:

import P2.egg

root/P2/egg.py:

import P1.egg
print(__name__)
print(__file__)

root/P1/egg.py:

print(__name__)
print(__file__)

测试root/top.py输出:

P1.egg
/Users/chengmin/code/PycharmProjects/code/root/P1/egg.py
P2.egg
/Users/chengmin/code/PycharmProjects/code/root/P2/egg.py

从子女后代模块导入(导入子女模块)

下例:root/P1/egg.py欲导入root/P1/P1_P1/P1_P1_M1.py

顶层模块为root/P1/P1_top.py

此时顶层脚本不位于所有模块顶部,不是最佳实践。

此时主目录为root/P1

root/P1/P1_top.py:

import egg

root/P1/egg.py:

import P1_P1.P1_P1_M1
print(__name__)
print(__file__)

root/P1/P1_P1/P1_P1_M1.py:

print(__name__)
print(__file__)

测试root/P1/P1_top.py输出:

P1_P1.P1_P1_M1
/Users/chengmin/code/PycharmProjects/code/root/P1/P1_P1/P1_P1_M1.py
egg
/Users/chengmin/code/PycharmProjects/code/root/P1/egg.py

若顶层脚本为root/top.py

此时顶层脚本位于所有模块顶部,为最佳实践。

此时主目录为:root

root/P1/egg.py:

import P1.P1_P1.P1_P1_M1 #在前面加上一个P1.,因为主目录变了
print(__name__)
print(__file__)

root/top.py:

import P1.egg

root/P1/P1_P1/P1_P1_M1/py:

print(__name__)
print(__file__)

测试root/top.py输出:

P1.P1_P1.P1_P1_M1
/Users/chengmin/code/PycharmProjects/code/root/P1/P1_P1/P1_P1_M1.py
P1.egg
/Users/chengmin/code/PycharmProjects/code/root/P1/egg.py

从远房亲戚模块导入 (导入远方亲戚模块)

下例,root/P1/P1_P1/P1_P1_M1欲导入root/P2/spam.py
同样,本例若不考虑Pycharm加入的root目录,将只能以root/top.py或root/spam.py为顶层脚本,且本例也不能用相对导入,因为会出现使用的小数点数会导致相对导入超出顶层脚本的顶部。

若顶层脚本为root/P1/P1_top.py

root/P1/P1_top.py:

import P1_P1.P1_P1_M1.py

root/P1/P1_P1/P1_P1_M1.py:

import P2.spam
print(__name__)
print(__file__)

root/P2/spam.py:

print(__name__)
print(__file__)

测试root/P1/P1_top.py输出:

P2.spam
/Users/chengmin/code/PycharmProjects/code/root/P2/spam.py
P1_P1.P1_P1_M1
/Users/chengmin/code/PycharmProjects/code/root/P1/P1_P1/P1_P1_M1.py

若顶层脚本为root/top.py

此时顶层脚本位于所有模块的最顶部,是最佳实践。

此时主目录为root

root/top.py:

import P1.P1_P1.P1_P1_M1

root/P1/P1_P1/P1_P1_M1.py:

import P2.spam
print(__name__)
print(__file__)

root/P2/spam.py:

print(__name__)
print(__file__)

测试root/top.py输出:

P2.spam
/Users/chengmin/code/PycharmProjects/code/root/P2/spam.py
P1.P1_P1.P1_P1_M1
/Users/chengmin/code/PycharmProjects/code/root/P1/P1_P1/P1_P1_M1.py

从操作系统其他任意位置导入(导入操作系统其他位置模块)

从操作系统其他位置导入模块的思路有三条:
设置PYTHONPATH,将该模块目录加入到其中。
编写.pth文件,将该模块目录加入到其中。
在程序运行过程中将该模块目录添加到sys.path中。
此处留给读者自己尝试,刚才也已经试过了编写.pth文件方法。

最后,如果读者不想自己一个一个文件地创建的话,运行我接下来给出的脚本吧(maxOS和Windows兼容)

import os
import platform
system = platform.system()
ab = os.getcwd()

def try_make(path):
    try:
        os.mkdir(path)
    except FileExistsError:
        pass

def darwinmake():
    global ab
    open(ab + '/top.py', 'w')
    open(ab + '/spam.py', 'w')
    try_make(ab + '/P1')
    open(ab + '/P1/__init__.py', 'w')
    open(ab + '/P1/egg.py', 'w')
    open(ab + '/P1/P1_top.py', 'w')
    open(ab + '/P1/spam.py', 'w')
    try_make(ab + '/P1/P1_P1')
    open(ab + '/P1/P1_P1/__init__.py', 'w')
    open(ab + '/P1/P1_P1/P1_P1_M1.py', 'w')
    try_make(ab + '/P2')
    open(ab + '/P2/__init__.py', 'w')
    open(ab + '/P2/egg.py', 'w')
    open(ab + '/P2/P2_top.py', 'w')
    open(ab + '/P2/spam.py', 'w')
    try_make(ab + '/P2/P2_P2')
    open(ab + '/P2/P2_P2/__init__.py', 'w')
    open(ab + '/P2/P2_P2/spam.py', 'w')

def windowsmake():
    global ab
    open(ab + '\\top.py', 'w')
    open(ab + '\\spam.py', 'w')
    try_make(ab + '\\P1')
    open(ab + '\\P1\\__init__.py', 'w')
    open(ab + '\\P1\\egg.py', 'w')
    open(ab + '\\P1\\P1_top.py', 'w')
    open(ab + '\\P1\\spam.py', 'w')
    try_make(ab + '\\P1\\P1_P1')
    open(ab + '\\P1\\P1_P1\\__init__.py', 'w')
    open(ab + '\\P1\\P1_P1\\P1_P1_M1.py', 'w')
    try_make(ab + '\\P2')
    open(ab + '\\P2\\__init__.py', 'w')
    open(ab + '\\P2\\egg.py', 'w')
    open(ab + '\\P2\\P2_top.py', 'w')
    open(ab + '\\P2\\spam.py', 'w')
    try_make(ab + '\\P2\\P2_P2')
    open(ab + '\\P2\\P2_P2\\__init__.py', 'w')
    open(ab + '\\P2\\P2_P2\\spam.py', 'w')

def main():
    global system, ab
    if system == 'Darwin':
        darwinmake()
    elif system == 'Windows':
        windowsmake()
    elif system == 'Linux':
        print('竟然是用Linux的大佬吗,原谅我才疏学浅不会写帮你生成文件的代码')
    else:
        print('你用的是什么神仙操作系统呀喂!')

if __name__ == '__main__':
    main()



  1. 官方中文文档解释: ___name___属性必须被设为模块的完整限定名称。 此名称被用来在导入系统中唯一地标识模块。
    简单点解释:某个模块的__name__属性就是该模块相对于顶层运行脚本的绝对导入的写法的字符串,从属性字面意思来讲,就是该模块相对于顶层脚本的“名字”,顶层脚本自身的__name__值为"main"。 ↩︎

  2. 模块的__file__是模块在操作系统中完整路径表示的字符串,其表示和操作系统有关 ↩︎

  3. Windows系统根目录我没记错的话应该是磁盘名开头,后面跟着冒号,像C:\ ↩︎

  4. 这些介绍并不会与Pycharm中得到的所有输出的路径对号入座,但能够解释一部分输出的路径。 ↩︎

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值