Skip to content

Commit

Permalink
错别字
Browse files Browse the repository at this point in the history
  • Loading branch information
satori1995 committed Jan 6, 2025
1 parent 4b44a6c commit 173c3ef
Showing 1 changed file with 27 additions and 26 deletions.
53 changes: 27 additions & 26 deletions src/84.import 机制是怎么实现的?.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
## 楔子

通过上一篇的 import 黑盒探测,我们已经对 import 机制有了一个非常清晰的认识,Python 的 import 机制基本上可以切分为三个不同的功能
通过上一篇的 import 黑盒探测,我们已经对 import 机制有了一个非常清晰的认识,Python 的 import 机制基本上可以分为三个不同的功能

- Python运行时的全局模块池的维护和搜索
- Python 运行时的全局模块池的维护和搜索
- 解析与搜索模块路径的树状结构;
- 对不同文件格式的模块执行动态加载机制;

尽管 import 的表现形式千变万化,但是都可以归结为:**import x.y.z** 的形式,当然 **import sys** 也可以看成是 x.y.z 的一种特殊形式。而诸如 from、as 与 import 的结合,实际上同样会进行 **import x.y.z** 的动作,只是最后在当前名字空间中引入的符号各有不同。

## \_\_import\_\_

然后导入模块,虚拟机会调用 \_\_import\_\_,那么我们就来看看这个函数长什么样子。
导入模块,虚拟机会调用 \_\_import\_\_,那么我们就来看看这个函数长什么样子。

~~~C
// Python/bitinmodule.c
Expand All @@ -20,7 +20,7 @@ builtin___import__(PyObject *self, PyObject *args, PyObject *kwds)
static char *kwlist[] = {"name", "globals", "locals", "fromlist",
"level", 0};
PyObject *name, *globals = NULL, *locals = NULL, *fromlist = NULL;
int level = 0; // 表示默认绝对导入
int level = 0; // 默认绝对导入
// 从 PyTupleObject 和 PyDictObject 中解析出需要的信息
if (!PyArg_ParseTupleAndKeywords(args, kwds, "U|OOOi:__import__",
kwlist, &name, &globals, &locals, &fromlist, &level))
Expand All @@ -39,9 +39,9 @@ int PyArg_ParseTupleAndKeywords(PyObject *, PyObject *,
const char *, char **, ...);
~~~

这个函数的作用是参数解析,负责将 args 和 kwds 中所包含的所有对象(指针)按指定的 format 解析成各种目标对象,可以是 Python 的对象,例如 PyListObject、PyLongObject,也可以是 C 的原生对象。我们知道这个 builtin\_\_import\_\_ 里面的参数 args 指向一个 PyTupleObject ,包含了 \_\_import\_\_ 函数运行所需要的参数和信息,它是虚拟机在执行 IMPORT_NAME 指令的时候打包产生的。
这个函数的作用是解析参数,负责将 args 和 kwds 中所包含的所有对象(指针)按指定的 format 解析成各种目标对象,可以是 Python 的对象,例如 PyListObject、PyLongObject,也可以是 C 的原生对象。我们知道 builtin\_\_import\_\_ 里面的参数 args 指向一个 PyTupleObject ,包含了 \_\_import\_\_ 函数运行所需要的参数和信息,它是虚拟机在执行 IMPORT_NAME 指令的时候打包产生的。

然而在这里,虚拟机进行了一个逆动作,将打包后的这个 PyTupleObject 拆开,重新获得当初的参数。Python 在自身的实现中大量使用了这样的打包、拆包策略,使得可变数量的对象能够很容易地在函数之间传递。
然而在这里虚拟机进行了一个逆动作,将打包后的 PyTupleObject 拆开,重新获得当初的参数。Python 在自身的实现中大量使用了这样的打包、拆包策略,使得可变数量的对象能够很容易地在函数之间传递。

而在完成了对参数的拆包动作之后,会进入 PyImport_ImportModuleLevelObject ,这个我们在 import_name 中已经看到了,当然它内部也是调用了 \_\_import\_\_

Expand Down Expand Up @@ -112,15 +112,15 @@ import sklearn.linear_model.ridge

但我们看到 STORE_NAME 是 sklearn,表示只有 sklearn 这个符号暴露在了当前模块的 local 空间里面。可为什么是 sklearn 呢?难道不应该是 sklearn.linear_model.ridge 吗?

其实经过我们之前的分析这一点已经不再是问题了,因为 <font color="blue">import sklearn.linear_model.ridge</font> 并不是说导入一个模块或包叫做 sklearn.linear_model.ridge,而是先导入 sklearn,然后把 linear_model 放在 sklearn 的属性字典里面,再把 ridge 放在 linear_model 的属性字典里面。
其实经过之前的分析这一点已经不再是问题了,因为 <font color="blue">import sklearn.linear_model.ridge</font> 并不是说导入一个模块或包叫做 sklearn.linear_model.ridge,而是先导入 sklearn,然后把 linear_model 放在 sklearn 的属性字典里面,再把 ridge 放在 linear_model 的属性字典里面。

同理 sklearn.linear_model.ridge 代表的是先从 local 空间里面找到 sklearn,再从 sklearn 的属性字典中找到 linear_model,然后在 linear_model 的属性字典里面找到 ridge。因为 linear_model 和 ridge 已经在相应的属性字典里面,我们通过 sklearn 一级一级往下找是可以找到的,因此只需要将符号 skearn 暴露给 local 空间即可。
同理 sklearn.linear_model.ridge 代表的是先从 local 空间里面找到 sklearn,再从 sklearn 的属性字典中找到 linear_model,然后在 linear_model 的属性字典中找到 ridge。因为 linear_model 和 ridge 已经在相应的属性字典里面,我们通过 sklearn 一级一级往下找是可以找到的,因此只需要将符号 skearn 暴露给 local 空间即可。

或者说暴露 sklearn.linear_model.ridge 本身就是不合理的,因为这表示导入一个名字就叫做 sklearn.linear_model.ridge 的模块或者包,但显然不存在。而即便我们创建了这样的一个模块或包,由于 Python 的语法解析规范依旧不会得到想要的结果。不然的话,假设 <font color="blue">import test_import.a</font>,那是导入名为 test_import.a 的模块或包呢?还是导入 test_import 下的 a 呢?
或者说暴露 sklearn.linear_model.ridge 本身就是不合理的,因为这表示导入一个名字就叫做 sklearn.linear_model.ridge 的模块或者包,但显然不存在。而即便我们创建了这样的一个模块或包,但由于 Python 的语法解析规则依旧不会得到想要的结果。不然的话,假设 <font color="blue">import test_import.a</font>,那是导入名为 test_import.a 的模块或包呢?还是导入 test_import 下的 a 呢?

也正如我们之前分析的 test_import.a,我们在导入 test_import.a 的时候,会把 test_import 加载进来,然后把 a 加到 test_import 的属性字典里面,最后只需要把 test_import 返回即可。因为通过 test_import 可以找到 a,或者说 test_import.a 代表的含义就是从 test_import 的属性字典里面获取 a,所以 <font color="blue">import test_import.a</font> 必须要返回 test_import,而且只需返回 test_import。
也正如我们之前分析的 test_import.a,在导入 test_import.a 的时候,会把 test_import 加载进来,然后把 a 加到 test_import 的属性字典里面,最后只需要把 test_import 返回即可。因为通过 test_import 可以找到 a,或者说 test_import.a 代表的含义就是从 test_import 的属性字典里面获取 a,所以 <font color="blue">import test_import.a</font> 必须要返回 test_import,而且只需返回 test_import。

至于 sys.modules 里面虽然存在字符串名为 "test_import.a" 的 key,但这是为了避免重复加载所采取的策略,它依旧表示从 test_import 的属性字典里面获取 a。
至于 sys.modules 里面虽然存在字符串为 "test_import.a" 的 key,但这是为了避免重复加载所采取的策略,它依旧表示从 test_import 的属性字典里面获取 a。

~~~Python
import pandas.core
Expand All @@ -132,7 +132,7 @@ print(pandas.DataFrame({"a": [1, 2, 3]}))
1 2
2 3
"""
# 所以通过 pandas.DataFrame 是可以调用的
# 显然 pandas.DataFrame 是可以调用的
~~~

导入 pandas.core 会先导入 pandas,也就是执行 pandas 内部的 \_\_init\_\_ 文件。虽然 sys.modules 里面同时有 "pandas" 和 "pandas.core",但是暴露在 local 空间的只有 pandas,所以调用 pandas.DataFrame 是完全合理的。至于 pandas.core 显然它无法暴露,因为这不符合 Python 的变量命名规范,变量的名称里面不能出现小数点,它只是单纯地表示从 pandas 的属性字典中加载 core。
Expand All @@ -157,9 +157,9 @@ from sklearn.linear_model import ridge

注意此时的 2 LOAD_CONST 不再是 None 了,而是一个元组,虚拟机将 ridge 放到了当前模块的 local 空间中。并且 sklearn.linear_model 和 sklearn 都被导入了,存在 sys.modules 里面。

但是 sklearn 却并不在当前 local 空间中,尽管它被创建了,但是又被隐藏了。IMPORT_NAME sklearn.linear_model,也表示导入 sklearn,然后把 sklearn 下面的 linear_model 加入到 sklearn 的属性字典里面。
但是 sklearn 却并不在当前的 local 空间中,尽管它被创建了,但是又被隐藏了。IMPORT_NAME 的指令参数是 sklearn.linear_model,也表示导入 sklearn,然后把 sklearn 下面的 linear_model 加入到 sklearn 的属性字典里面。

而之所以 sklearn 没在 local 空间里面,可以这样理解。当只出现 import 的时候,那么我们必须从头开始一级一级向下调用,所以顶层的包必须加入到 local 空间里面。但这里通过 <font color="blue">from ... import ...</font> 把 ridge 导出了,此时 ridge 已经指向了 sklearn 下面的 linear_model 下面的 ridge,那么就不需要 sklearn 了,或者说 sklearn 就没必要暴露在 local 空间里面了,但它确实被导入进来了。
而之所以 sklearn 没在 local 空间里面,可以这样理解。当只出现 import 的时候,由于必须从头开始一级一级向下调用,所以顶层的包必须加入到 local 空间里面。但这里通过 <font color="blue">from ... import ...</font> 把 ridge 导出了,此时 ridge 已经指向了 sklearn 下面的 linear_model 下面的 ridge,那么就不需要 sklearn 了,或者说 sklearn 就没必要暴露在 local 空间里面了,但它确实被导入进来了。

并且 sys.modules 里面也不存在 "ridge" 这个key,存在的是 "sklearn.linear_model.ridge",暴露给 local 空间的符号是 ridge。所以正如上面所说,不管什么导入,都可以归结为 <font color="blue">import x.y.z</font> 的形式,只是暴露出来的符号不同罢了。

Expand Down Expand Up @@ -206,35 +206,35 @@ from sklearn.linear_model import ridge as xxx
"""
~~~

这个和之前的 from & import 一样,只是最后暴露给 local 空间的 ridge 变成了我们自己指定的 xxx。
这个和之前的 from & import 一样,只是最后暴露给 local 空间的 ridge 变成了我们指定的 xxx。

## 与 module 对象有关的名字空间问题

同函数、类一样,每个 PyModuleObject 也有自己的名字空间。一个模块不能直接访问另一个模块的内容,尽管模块内部的作用域比较复杂,比如:遵循 LEGB 规则,但是模块与模块之间的划分则是很明显的。
同函数、类一样,每个 PyModuleObject 也有自己的名字空间。一个模块不能直接访问另一个模块的内容,尽管模块内部的作用域比较复杂,比如 LEGB 规则,但是模块与模块之间的划分则是很明显的。

~~~python
# test.py
name = "古明地觉"
name = "古明地觉"

def print_name():
return name

# main.py
from test1 import name, print_name
name = "古明地恋"
from test import name, print_name
name = "古明地恋"
print(print_name()) # 古明地觉
~~~

执行 main.py 之后,发现打印的依旧是"古明地觉"。我们说 Python 是根据 LEGB 规则进行查找,而 print_name 函数里面没有 name,那么去外层找。可 main.py 里面的 name 是"古明地恋",但打印的依旧是 test.py 里面的 "古明地觉"。为什么?
执行 main.py 之后,发现打印的依旧是"古明地觉"。我们说 Python 是根据 LEGB 规则进行查找,而 print_name 函数里面没有 name,那么去外层找。可 main.py 里面的 name 是"古明地恋",但打印的依旧是 test.py 里面的"古明地觉"。为什么?

还是那句话,模块与模块之间的作用域划分的非常明显,print_name 是 test.py 里面的函数,所以在返回 name 的时候,只会从 test.py 中搜索,无论如何都是不会跳过 test.py、跑到 main.py 里面的
还是那句话,模块与模块之间的作用域划分地非常明显,print_name 是 test.py 里面的函数,所以在返回 name 的时候,只会从 test.py 中搜索,无论如何都不会跳过 test.py、跑到 main.py 里面

再来看个例子:

~~~Python
# test.py
name = "古明地觉"
nicknames = ["小五", "少女觉"]
nicknames = ["小五", "少女觉"]

# main.py
import test
Expand All @@ -251,7 +251,7 @@ print(nicknames) # ['觉大人']
~~~python
# test.py
name = "古明地觉"
nicknames = ["小五", "少女觉"]
nicknames = ["小五", "少女觉"]

# main.py
from test import name, nicknames
Expand All @@ -263,7 +263,7 @@ print(name) # 古明地觉
print(nicknames) # ["少女觉"]
~~~

如果是 from test1 import name, nicknames,那么相当于在当前的 local 空间中新创建变量 name 和 nicknames,它们和 test.py 中的 name 和 nicknames 指向相同的对象。
如果是 from test import name, nicknames,那么相当于在当前的 local 空间中新创建变量 name 和 nicknames,它们和 test.py 中的 name 和 nicknames 指向相同的对象。

而 name = "古明地觉" 相当于重新赋值了,所以不会影响 test.py 里的 name。而 nicknames.remove 则是在本地进行修改,所以会产生影响。

Expand All @@ -288,13 +288,14 @@ x = 1
1
>>> test.y = 2 # 手动添加一个属性(但源文件里面是没有 y = 2 的)
>>>
# 修改 test.py,将 x = 1 改成 x = 11
>>> import importlib
>>> test = importlib.reload(test)
模块被加载
>>> id(test) # reload 后是同一个对象
140712834927872
>>> test.x # 模块中定义的变量被更新了
1
11
>>> test.y # 手动添加的属性仍然存在
2
~~~
Expand All @@ -310,7 +311,7 @@ x = 1
AttributeError: module 'test' has no attribute 'y'
~~~

这种设计保持了模块级别的状态,可以避免破坏其它可能持有该模块引用的代码,但可能导致"幽灵属性"存在。所以实际开发中如果需要完全重新加载一个模块,最好使用删除 sys.modules 的方式,而不是依赖 reload。当然啦,重新加载模块这个需求本身就不常见,大家了解一下就好。
这种设计保持了模块级别的状态,可以避免破坏其它持有该模块引用的代码,但可能导致"幽灵属性"存在。所以实际开发中如果需要完全重新加载一个模块,最好使用删除 sys.modules["xxx"] 的方式,而不是依赖 reload。当然啦,重新加载模块这个需求本身就不常见,大家了解一下就好。

## 小结

Expand Down

0 comments on commit 173c3ef

Please sign in to comment.