Python中的Package

Python中的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/
├── main.py
└── utils/
├── helper.py
└── math_tools.py

如果 utils/ 中没有 __init__.py,则 utils 是一个普通文件夹。你可以在 main.py 中这样导入:

import sys
sys.path.append('./utils') # 将 utils 文件夹路径添加到 Python 路径
import helper # 导入 helper.py

示例 2:Python 包

如果 utils/ 中有 __init__.py,则 utils 是一个 Python 包。文件夹结构如下:

my_project/
├── main.py
└── utils/
├── __init__.py
├── helper.py
└── math_tools.py

你可以在 main.py 中这样导入:

import utils.helper  # 直接导入包中的模块
from utils import math_tools # 另一种导入方式

5. Python 3.3+ 的隐式命名空间包

从 Python 3.3 开始,引入了隐式命名空间包(Implicit Namespace Packages)。如果一个文件夹中没有 __init__.py,但它被多个包共享,Python 会将其视为命名空间包。这种情况下,即使没有 __init__.py,也可以通过 import 导入。

例如:

my_project/
├── main.py
└── utils/
├── helper.py
└── math_tools.py

在 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. 确定维数和对齐方式
    如果两个数组的维数不同,在较低维数数组前面补 1,直到它们的维数一致。
    例如,一个数组形状为 (60000, 10),另一个是一维数组形状为 (60000, ),补齐后,后一数组形状变为 (1, 60000)(注意:补在最前面)。

  2. 从尾部对齐维度
    对齐后,从最后一个维度开始比较:

    • 对于 (60000, 10) 来说,最后维度是 10。
    • 补齐后的 (1, 60000) 最后维度是 60000。
      二者如果既不相等,也没有一个为 1,就不能广播。
  3. 广播条件
    对于两个维度 d1 和 d2,它们是兼容的当且仅当:

    • d1 == d2,或
    • d1 == 1 或 d2 == 1。
      如果所有对齐的维度都满足这一条件,则数组可以广播,否则会报错。
  4. 实际应用
    在你的代码中,对分子 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("Building prefix dict from the default dictionary ...")
sys.stdout.flush() # 强制刷新缓冲区

或者在关键位置添加:

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
sudo yum install expect # CentOS/RHEL

为什么会出现缓冲问题?

  1. Python 的缓冲机制

    • 终端(TTY):行缓冲(line-buffered),遇到 \n 就刷新。
    • 管道/文件:块缓冲(block-buffered),默认 4096 字节或程序结束时才刷新。
  2. tee 不会改变缓冲行为

    • python main.py | tee output.log 仍然是非终端环境,Python 可能会缓冲输出。

最佳实践

  1. 推荐 python -u(最简单):
    python -u main.py 2>&1 | tee -a output.log
  2. 长期运行程序(如 nohup)
    nohup python -u main.py >> output.log 2>&1 &
  3. 需要颜色保留
    unbuffer python main.py 2>&1 | tee -a output.log

这样修改后,输出会 实时 显示在终端并写入文件,不会卡住。🚀