## 模块导入、加载

python中一个py文件就是一个模块。一个package或者一个文件夹就是一个包。

当前包中有哪些可用的属性可以通过dir()方法来查看。

### dir()

```python
import json

if __name__ == '__main__':
# 获取当前模块的属性列表
print(dir()) # ['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'json']
```

dir打印的是当前模块的属性列表。可以看到列表中包含一个json,这其实是json模块的一个标识符。打印json可以看到一个module对象。

```python
import json

if __name__ == '__main__':
print(json) # <module 'json' from 'C:\\Users\\amir\\.pyenv\\pyenv-win\\versions\\3.9.6\\lib\\json\\__init__.py'>
```

### import和from...import

> import语句:import后只能写模块,不能写函数或者类之类的
>
> from...import...: from后只能写模块,但是这个import之后可以写类、函数、变量
>
> from...import *: 表示导入所有允许访问的对象,一般的,允许访问的会用 `__all__`来定义

```python
# test2.py

from typing import Union

x = 123


def add(a: Union[int, float], b: Union[int, float]):
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
return a + b
else:
raise Exception("非法参数")


class Student:
name = "123"
age = "456"
```



```python
# test1.py
from test2 import Student, x, add

if __name__ == '__main__':
print(Student)
print(x)
print(add(1, 2.3))
```

```python
# test3.py
__all__ = ["Genius"]


class Genius:
name = "amir"
age = 18


class Idiot:
name = "jianwa"
age = 99
```

```python
# test4.py
from test3 import *

print(dir()) # ['Genius', '__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
```

### import在干什么

import是在加载被导入模块的内容,这个加载过程为解释器执行该模块的代码。

```python
__all__ = ["Genius"]


class Genius:
name = "amir"
age = 18


class Idiot:
name = "jianwa"
age = 99


print("生存还是毁灭?")
```

当其他文件导入这个模块的时候,我们可以看到,最优先的会打印上述文字。import还会将所有的类|函数|变量装载到内存中,生成一个个对象。

### 访问一个对象的几种方式

```python
# test2.py
__all__ = ["html"]

html = """<li>this is a li tag</li>"""
```

```python
# test1.py
import test2

print(test2.html)
print(getattr(test2, "html"))
print(test2.__dict__["html"])
```

## 模块的搜索顺序

```python
import sys

print(*sys.path, sep="\n")
```

可以通过sys.path看到一个路径列表,这就是模块的加载顺序。注意已经加载过的模块不会重复加载(多次import没用),这点可以在sys.modules中查看,已经加载过的被记录了,所以不用重复加载。实际上程序启动的时候,modules中就会主动加载很多模块,比如buildins,os,异常类等,因为python可能会自己调用自己的一些东西。

总体来说,模块的加载顺序是这样的:

- 首先是程序主目录,程序运行的主程序脚本所在的目录
- PYTHONPATH目录,通过环境变量设置的
- 标准库目录,python自带的标准库所在的目录

## 包的导入

导入一个包(package),默认会执行包下面的`__init__.py`,但是下面的子模块、子包不会被执行。可以通过dir或者sys.modules查看。

比如创建一个包demo:

```python
import demo

print(dir())

import sys
# ('demo', <module 'demo' from '/Users/maningyu/workspace/pythonprojects/machine_learning/demo/__init__.py'>)
print(*filter(lambda x: x[0].startswith("d"), sys.modules.items()), sep="\n")
```

可以看到demo也是一个模块对象,指向demo下的init文件。

但是包下面的模块如果没有在init文件中导入,则不会被加载,需要声明导入。

如果是如下类型的导入:

`from demo import m1` demo包下有一个m1.py文件,可以看在sys.modules中看到demo实际上也被加载了,但是无法直接使用demo,因为dir中并没有这个标识符。

> 总之:sys.modules可以看到哪些模块被缓存了,但是dir可以看到哪些模块可以被使用。

假如有一个包,里面有init文件和一些其他的模块文件,怎样在只import包的情况下能使用到其他子模块呢?

可以在包的init文件中通过相对导入的方式导入其他模块:`from . import m1`,这样就能在主模块中使用`demo.m1.xxx`

## 绝对导入和相对导入

### 绝对导入

- import或者from语句中,模块不是以点(.)开头的就是绝对导入。
- 绝对导入首先看模块是否缓存过,如果没有则是按照模块的搜索路径来查找模块

### 相对导入

- 只能用在from语句中,from后以点开头的模块名称就是相对导入。
- <u>**首先代码的顶层目录不要使用相对导入**</u>(顶层为代码main函数存在的位置),原因如下:

![image-20220922204911572](https://note-1302735599.cos.ap-guangzhou.myqcloud.com/ai/image-20220922204911572.png)

启动test2的导入语句为 `from . import test3`,其他都为绝对导入。运行main模块后,报错如下:

```txt
/Users/maningyu/workspace/pythonprojects/machine_learning/venv/bin/python /Users/maningyu/workspace/pythonprojects/machine_learning/main.py
Traceback (most recent call last):
File "/Users/maningyu/workspace/pythonprojects/machine_learning/main.py", line 1, in <module>
import test1
File "/Users/maningyu/workspace/pythonprojects/machine_learning/test1.py", line 1, in <module>
import test2
File "/Users/maningyu/workspace/pythonprojects/machine_learning/test2.py", line 1, in <module>
from . import test3
ImportError: attempted relative import with no known parent package
```

这个时候不需要问为什么,可以从报错中看到"尝试通过相对导入的方式导入,但是没有找到它的父包",推测出一个设计规范,只有非顶层模块才支持相对导入。

- 一个点表示当前目录内
- 两点表示上一级目录
- 三个点表示上上一级

## 访问控制

```python
# test.py
name = "amir"
_age = 10
__gender = 1
__weight__ = 1
```

```python
# main.py
import test

print(test.name)
print(test._age)
print(test.__gender)
print(test.__weight__)
```

可以看到,导入test,其定义的全局变量都可以使用。

但是导入语句换成`from test import *`,发现只能使用非下划线的变量。这就是一种规则,加下划线就是希望只能本模块使用。

```python
from test import *

print(name)
# print(_age)
# print(test.__gender)
# print(test.__weight__)
```

注意如果一个文件中定义了`__all__`,则导入该模块能使用的属性只有all中定义的。

> 不建议使用import * 这样的语句,因为无法控制,可能会存在变量污染等问题。

## 包管理

python的模块源文件发送给别人就可以使用,但是这样不方便。我们可以选择发布到网络中,供别人使用。

Pypi是python公共模块的存储中心。

打包工具有distutils,setuptools和pip,依次构建在前者之上。wheel是基于zip打包的扩展名为.whl的文件。它可以让python库以二进制的方式安装,而不需要本地编译。

打包指南可以参考官方文档:https://docs.python.org/3.7/distributing/index.html

项目结构要求

```tex
packaging_tutorial/
├── LICENSE
├── pyproject.toml
├── README.md
├── src/
│ └── example_package_YOUR_USERNAME_HERE/
│ ├── __init__.py
│ └── example.py
└── tests/
```

具体怎么上传可以参看官方文档。