Python中的Package

Python中的Package
ExisfarPython中的Package
init.py
你的理解基本正确,但有一些细节需要补充和澄清。我们来详细讨论一下 __init__.py
的作用以及文件夹和 Python 包的区别。
1. __init__.py
的作用
__init__.py
是 Python 包的标志文件。它的主要作用包括:
-
标识包:
- 如果一个文件夹中包含
__init__.py
文件,Python 会将其视为一个包(Package),而不是普通的文件夹。 - 如果没有
__init__.py
,Python 会将其视为普通文件夹,无法直接通过import
导入。
- 如果一个文件夹中包含
-
初始化包:
__init__.py
文件会在包被导入时执行,通常用于初始化包的内容,例如定义包的接口、导入子模块、设置包级别的变量等。
-
控制包的导入行为:
- 可以在
__init__.py
中定义__all__
变量,控制from package import *
时导入哪些模块。
- 可以在
2. 文件夹和 Python 包的区别
-
普通文件夹:
- 普通文件夹只是一个文件系统目录,Python 不会将其视为包。
- 如果文件夹中没有
__init__.py
,无法直接通过import
导入该文件夹中的模块。
-
Python 包:
- 包含
__init__.py
的文件夹会被 Python 视为包。 - 可以通过
import
导入包或包中的模块。
- 包含
3. 关于“文件夹也可以导入”的疑问
你提到“文件夹也可以导入”,这里需要区分两种情况:
情况 1:文件夹中没有 __init__.py
- 如果文件夹中没有
__init__.py
,Python 不会将其视为包。 - 你仍然可以通过以下方式导入文件夹中的模块:这种方式是通过修改
import sys
sys.path.append('/path/to/folder') # 将文件夹路径添加到 Python 路径
import module_name # 导入文件夹中的模块sys.path
实现的,并不是将文件夹作为包导入。
情况 2:文件夹中有 __init__.py
- 如果文件夹中有
__init__.py
,Python 会将其视为包。 - 可以直接通过
import
导入包或包中的模块:import package_name # 导入包
from package_name import module_name # 导入包中的模块
4. 示例说明
示例 1:普通文件夹
假设有一个文件夹结构如下:
my_project/ |
如果 utils/
中没有 __init__.py
,则 utils
是一个普通文件夹。你可以在 main.py
中这样导入:
import sys |
示例 2:Python 包
如果 utils/
中有 __init__.py
,则 utils
是一个 Python 包。文件夹结构如下:
my_project/ |
你可以在 main.py
中这样导入:
import utils.helper # 直接导入包中的模块 |
5. Python 3.3+ 的隐式命名空间包
从 Python 3.3 开始,引入了隐式命名空间包(Implicit Namespace Packages)。如果一个文件夹中没有 __init__.py
,但它被多个包共享,Python 会将其视为命名空间包。这种情况下,即使没有 __init__.py
,也可以通过 import
导入。
例如:
my_project/ |
在 Python 3.3+ 中,即使 utils/
中没有 __init__.py
,你也可以直接导入:
import utils.helper |
但这种方式通常用于复杂的包结构,而不是常规的包管理。
6. 总结
__init__.py
的作用:标识文件夹为 Python 包,并用于初始化包。- 文件夹和 Python 包的区别:
- 普通文件夹无法直接通过
import
导入。 - Python 包(包含
__init__.py
的文件夹)可以直接通过import
导入。
- 普通文件夹无法直接通过
- 文件夹导入的例外:
- 通过修改
sys.path
可以导入普通文件夹中的模块。 - Python 3.3+ 支持隐式命名空间包,即使没有
__init__.py
也可以导入。
- 通过修改
Numpy Broadcasting
广播(Broadcasting)规则是 NumPy 中用于自动扩展数组形状以便执行元素级操作的一套规则。关键点在于对齐数组的维度时,从数组的“尾部”开始对齐,即比较最后一个维度、倒数第二个维度,以此类推。下面详细说明这一过程:
-
确定维数和对齐方式
如果两个数组的维数不同,在较低维数数组前面补 1,直到它们的维数一致。
例如,一个数组形状为 (60000, 10),另一个是一维数组形状为 (60000, ),补齐后,后一数组形状变为 (1, 60000)(注意:补在最前面)。 -
从尾部对齐维度
对齐后,从最后一个维度开始比较:- 对于 (60000, 10) 来说,最后维度是 10。
- 补齐后的 (1, 60000) 最后维度是 60000。
二者如果既不相等,也没有一个为 1,就不能广播。
-
广播条件
对于两个维度 d1 和 d2,它们是兼容的当且仅当:- d1 == d2,或
- d1 == 1 或 d2 == 1。
如果所有对齐的维度都满足这一条件,则数组可以广播,否则会报错。
-
实际应用
在你的代码中,对分子np.exp(Z)
的形状为 (60000, 10);而分母np.sum(np.exp(Z), axis=1)
的形状为 (60000, )。在广播时,会先将 (60000, ) 补成 (1, 60000),然后从尾部对齐:- 分子的最后一个维度是 10;
- 分母补齐后最后一个维度是 60000。
10 与 60000 不兼容(既不相等,也都不为 1);因此无法广播成功。
解决方法是使用
keepdims=True
:p = np.exp(Z) / np.sum(np.exp(Z), axis=1, keepdims=True)
这样
np.sum(np.exp(Z), axis=1, keepdims=True)
的形状就是 (60000, 1),再从尾部对齐:- 分子的最后一维是 10;
- 分母的最后一维是 1,满足广播条件(1可以扩展为10),因此可以正常运算。
同时输出到终端和文件
这个问题通常是由于 输出缓冲(Output Buffering) 导致的。Python 默认会对标准输出(stdout)进行行缓冲(line-buffered),但如果输出不是终端(比如通过管道 | tee
),Python 可能会使用块缓冲(block buffered),导致输出不会立即刷新。
解决方法
方法 1:强制 Python 无缓冲运行(推荐)
使用 -u
参数(unbuffered mode)运行 Python:
python -u main.py 2>&1 | tee -a output.log |
-u
会强制 Python 不使用缓冲,所有输出会立即刷新。
方法 2:在 Python 代码中手动刷新输出
如果你不能修改启动命令,可以在代码中手动调用 sys.stdout.flush()
:
import sys |
或者在关键位置添加:
print("Loading model cost 0.531 seconds.", flush=True) # Python 3.3+ 支持 |
方法 3:使用 stdbuf
命令(Linux 适用)
stdbuf -oL python main.py 2>&1 | tee -a output.log |
stdbuf -oL
强制行缓冲(-oL
表示stdout
行缓冲,-eL
表示stderr
行缓冲)。
方法 4:使用 unbuffer
(解决伪终端问题)
如果 Python 检测到输出不是终端(TTY),可能会自动缓冲。可以用 unbuffer
(来自 expect
包)模拟终端:
unbuffer python main.py 2>&1 | tee -a output.log |
安装 unbuffer
:
sudo apt-get install expect-dev # Ubuntu/Debian |
为什么会出现缓冲问题?
-
Python 的缓冲机制:
- 终端(TTY):行缓冲(
line-buffered
),遇到\n
就刷新。 - 管道/文件:块缓冲(
block-buffered
),默认 4096 字节或程序结束时才刷新。
- 终端(TTY):行缓冲(
-
tee
不会改变缓冲行为:python main.py | tee output.log
仍然是非终端环境,Python 可能会缓冲输出。
最佳实践
- 推荐
python -u
(最简单):python -u main.py 2>&1 | tee -a output.log
- 长期运行程序(如 nohup):
nohup python -u main.py >> output.log 2>&1 &
- 需要颜色保留:
unbuffer python main.py 2>&1 | tee -a output.log
这样修改后,输出会 实时 显示在终端并写入文件,不会卡住。🚀