如果把所有的内容都放在一个文件里,随着项目的发展,想要编辑这个文件会变得非常困难,所以引入了 模块 和 包 的概念,便于管理。
假设当前项目的文件夹层次结构如下:
root
├── main.py
├── child
│ ├── __init__.py
│ └── hello.py
├── empty
└── children
├── test
│ ├── __init__.py
│ ├── hello.py
│ └── world.py
└── __init__.py
模块
python中单个*.py
文件就是一个模块,*.py
的文件名就是模块名。import
语句是用来导入模块或者从模块里导入特定的类或函数。
假设当前定位在root/children/test/world.py
,要导入同级目录下的hello.py
模块中的内容
导入模块
假如hello.py
模块中,包含一个Talk
的类,若要在world.py
模块中实例化一个Talk
类,代码如下:
import hello
talk = hello.Talk()
导入模块中的某个类
import hello
后任何在hello
这个模块里的类或者函数,都可以通过hello.类名
这个写法来访问。或者使用如下方式来导入一个类:
from hello import Talk
talk = Talk()
同时导入模块中的多个类
也可以在一行语句里同时导入多个类,如果hello.py
模块中同时还包含一个叫Speek
的类,可以通过如下方式来导入两个类:
from hello import Talk,Speek
talk = Talk()
speek = Speek()
导入模块中的所有内容
还有一种写法可以一次性从模块里导入所有它所拥有的类和函数:
from hello import *
不推荐这么做!使用import *
会把一些意想不到的对象导入到本地的命名空间。假如多个模块中存在同名的类,使用该写法,会分不清引用的类是来自于哪个模块,增加维护的难度。
使用别名区分同名的类
假如world.py
模块中已存在一个叫Talk
的类,我们不想让两个名字冲突,那么可以使用别名的方式来重新命名便于区分:
from hello import Talk as TK
from world import Talk as TA
hello_talk = TK()
world_talk = TA()
包
随着项目发展成越来越多模块的集合,我们需要增加另外一层的抽象。一个包 就是放到一个文件夹里的模块集合。包的名字就是文件夹的名字。通过把一个名为__init__.py
的文件(通常是空的)放到文件夹里使该文件夹成为一个 包 。hello.py
和world.py
两个模块放到了test
包下。当在包之间导入模块或类时,导入模块有两种方式:绝对导入和相对导入。
绝对导入
绝对导入 需要指明这个模块、函数的完整路径。如果需要访问hello.py
模块里的Talk
类,使用如下代码:
import children.test.hello
talk = children.test.hello.Talk()
# 或者
from children.test.hello import Talk
talk = Talk()
# 或者
from children.test import hello
talk = hello.Talk()
# 或者
from children import test
talk = test.hello.Talk()
import
语句使用点号作为分隔符来分隔包或者模块。
对于任何模块,这些语句都可以运行。可以在main.py
里,也可以在world.py
模块里。
相对导入
当目录层级比较复杂的时候,详细指明完整的路径就有点过于多余了,因为知道父模块的名称,就可以使用 相对导入 。
假设当前定位在root/children/test/world.py
,要导入同级目录下的hello.py
模块中的内容,可以使用相对导入:
from .hello import Talk
# 或者
from hello import Talk
hello
前面的点号说明当前包里的hello
模块。当前的包指的是包含目前正在编辑的world.py
文件的这个包,即test
目录。
如果想要引用root/child/hello.py
模块里的Talk
类,可以使用如下代码:
from ...child.hello import Talk
一个点代表当前目录,两个点代表上级目录,再多个点代表再上一级目录,以此类推。
Pycharm使用问题
问题情况说明
在Pycharm中使用相对导入会产生各种问题:
root/children/test/hello.py
内容如下:
class Talk:
def __init__(self):
print("talk")
-
root/children/test/world.py
内容如下:from hello import Talk talk = Talk()
world.py
中hello
和Talk
底部会有红线提示错误,且无法点击跳转到源码,但是可以正常运行输出结果。
-
root/children/test/world.py
内容如下:from .hello import Talk talk = Talk()
world.py
中hello
和Talk
底部没有红线,且能跳转到源码,但是运行报错。ModuleNotFoundError: No module named ‘_main_.hello’; ‘_main_’ is not a package
-
root/children/test/world.py
内容如下:from ..test.hello import Talk talk = Talk()
world.py
中hello
和Talk
底部没有红线,且能跳转到源码,但是运行报错。attempted relative import beyond top-level package
问题解决方案
-
第1种解决方案:
第一种能正常运行,但对于有强迫症的人来说,有红线看着难受可以用如下方法去掉红线,同时可以跳转到源码:
选择相对导入的那个文件夹test
,右键 → \rightarrow →Mark Directory as
→ \rightarrow →Sources Root
后,选中的文件夹会变成蓝色,解决问题1。 -
第2、3种解决方案
第2种可以去掉“.”解决问题,都是表达导入当前包下的模块。2和3问题可以视为同一种问题,都用了相对导入的方式。在world.py
中启动运行报错,暂时没有找到解决方法,但是在项目根目录下main.py
from children.test import world
通过这种方式,能正常输出
hello.py
中的内容,不会报错。 -
推荐的解决方案
使用绝对导入可以解决以上问题,强烈推荐。
问题分析
问题2: 在world.py
中执行时,__name__这个变量值为”_main_”,而相对引用符号“.”就是对应__name__这个变量。当这个模块是在别的模块中被导入使用,此时”.”就是该模块所在的包路径。在world.py
中执行时,因world.py
是main函数,此时”.”变成了”_main_”,因此报错。
world.py
内容如下:
from hello import Talk
print(__name__)
talk = Talk()
在world.py
中运行程序输出
_main_
在main.py
中运行程序输出
children.test.world
问题3:个人猜测Pycharm是将它的项目根目录作为执行路径,当在world.py
中执行时,world.py
所在的目录为根目录,如果使用相对路径,根目录没有父目录,因此报错。
所以推荐使用绝对导入的方式