Python 中的黑暗角落(三):模块与包

本文详细解析了Python中的模块和包的概念,包括它们的作用、如何使用以及内部运作机制。介绍了如何创建和使用Python模块,解释了模块搜索路径、初始化过程及缓存文件.pyc的作用。此外,还深入探讨了Python包的结构、使用方法及其内部的__init__.py文件的功能。

如果你用过 Python,那么你一定用过 import 关键字加载过各式各样的模块。但你是否熟悉 Python 中的模块与包的概念呢?或者,以下几个问题,你是否有明确的答案?

  • 什么是模块?什么又是包?
  • from matplotlib.ticker import Formatter, FixedLocator 中的 matplotlib 和 ticker 分别是什么?中间的句点是什么意思?
  • from matplotlib.pyplot import * 中,import * 的背后会发生什么?

鲁迅先生说:「于无声处听惊雷」,讲的是平淡时却有令人惊奇、意外的事情。import 相关的模块、包的概念也是如此。如果你对上面几个问题存有疑问,那么这篇就是为你而作的。

模块

为什么要有模块

众所周知,Python 有一个交互式的解释器。在解释器中,你可以使用 Python 的所有功能。但是,解释器是一次性的。也就是说,如果你关掉解释器,那么先前定义、运行的一切东西,都会丢失不见。另一方面,在解释器中输入代码是一件很麻烦的事情;这是因为在解释器中复用代码比较困难。

为此,人们会把相对稳定、篇幅较长的代码保存在一个纯文本文件中。一般来说,我们把这样扩展名为 .py 的文件称为 Python 脚本。为了提高代码复用率,我们可以把一组相关的 Python 相关的定义、声明保存在同一个 .py 文件中。此时,这个 Python 脚本就是一个 Python 模块(Module)。我们可以在解释器中,或者在其他 Python 脚本中,通过 import 载入定义好的 Python 模块。

模块的识别

和 Python 中的其它对象一样,Python 也为模块定义了一些形如 __foo__ 的变量。对于模块来说,最重要的就是它的名字 __name__ 了。每当 Python 执行脚本,它就会为该脚本赋予一个名字。对于「主程序」来说,这一脚本的 __name__ 被定义为 "__main__";对于被 import 进主程序的模块来说,这一脚本的 __name__ 被定义为脚本的文件名(base filename)。因此,我们可以用 if __name__ == "__main__": 在模块代码中定义一些测试代码。

fibonacci.py
1
2
3
4
5
6
7
8
9
10
11
12
def fib_yield(n):
    a, b = 0, 1
    while b < n:
        yield b
        a, b = b, a+b

def fib(n):
    for num in fib_yield(n):
        print(num)

if __name__ == "__main__":
    fib(10)

我们将其保存为 fibonacci.py,而后在 Python 解释器中 import 它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
In [1]: import fibonacci

In [2]: dir(fibonacci)
Out[2]:
['__builtins__',
 '__doc__',
 '__file__',
 '__name__',
 '__package__',
 'fib',
 'fib_yield']

In [3]: print(fibonacci.__name__)
fibonacci

In [4]: fibonacci.fib(5)
1
1
2
3

In [5]: for num in fibonacci.fib_yield(5):
   ...:     print(num)
   ...:
1
1
2
3

可以观察到,fibonacci.py 在作为模块引入时,fibonacci.__name__ 被设置为文件名 "fibonacci"。但若在命令行直接执行 python fibonacci.py,则 if 语句块会被执行,此时 __name__ 是 "__main__"

模块的内部变量和初始化

Python 为每个模块维护了单独的符号表,因此可以实现类似 C++ 中名字空间(namespace)的功能。Python 模块中的函数,可以使用模块的内部变量,完成相关的初始化操作;同时,import 模块的时候,也不用担心这些模块内部变量与用户自定义的变量同名冲突。

module_var.py
1
2
3
4
5
6
7
foo = 0

def show():
    print(foo)

if __name__ == "__main__":
    show()

此处我们在模块 module_var 内部定义了内部变量 foo,并且在函数 show 中引用了它。

1
2
3
4
5
6
7
8
9
10
11
12
In [7]: import module_var
   ...:
   ...: foo = 3
   ...:
   ...: print(foo)
   ...: print(module_var.foo)
   ...:
   ...: module_var.show()
   ...:
3
0
0

值得一提的是,模块的初始化操作(这里指 foo = 0 这条语句),仅只在 Python 解释器第一次处理该模块的时候执行。也就是说,如果同一个模块被多次 import,它只会执行一次初始化。

from ... import ...

模块提供了类似名字空间的限制,不过 Python 也允许从模块中导入指定的符号(变量、函数、类等)到当前模块。导入后,这些符号就可以直接使用,而不需要前缀模块名。

1
2
3
4
5
6
7
8
9
In [8]: from fibonacci import fib_yield, fib

In [9]: fib(10)
1
1
2
3
5
8

值得一提的是,被导入的符号,如果引用了模块内部的变量,那么在导入之后也依然会使用模块内的变量,而不是当前环境中的同名变量。

1
2
3
4
5
6
In [11]: from module_var import show

In [12]: foo = 3

In [13]: show()
0

也有更粗暴的方式,导入模块内的所有公开符号(没有前缀 _ 的那些)。不过,一般来说,除了实验、排查,不建议这样做。因为,通常你不知道模块定义了哪些符号、是否与当前环境有重名的符号。一旦有重名,那么,这样粗暴地导入模块内所有符号,就会覆盖掉当前环境的版本。从而造成难以排查的错误。

模块搜索路径

之前我们都在讨论模块的好处,但是忽略了一个问题:Python 怎样知道从何处找到模块文件?

如果你熟悉命令行,那么这个问题对你来说就不难理解。在命令行中执行的任何命令,实际上背后都对应了一个可执行文件。命令行解释器(比如 cmd, bash)会从一个全局的环境变量 PATH 中读取一个有序的列表。这个列表包含了一系列的路径,而命令行解释器,会依次在这些路径里,搜索需要的可执行文件。

Python 搜寻模块文件,也遵循了类似的思路。比如,用户在 Python 中尝试导入 import foobar,那么

  • 首先,Python 会在内建模块中搜寻 foobar
  • 若未找到,则 Python 会在当前工作路径(当前脚本所在路径,或者执行 Python 解释器的路径)中搜寻 foobar
  • 若仍未找到,则 Python 会在环境变量 PYTHONPATH 中指示的路径中搜寻 foobar
  • 若依旧未能找到,则 Python 会在安装时指定的路径中搜寻 foobar
  • 若仍旧失败,则 Python 会报错,提示找不到 foobar 这个模块。
1
2
3
4
5
6
7
In [14]: import foobar
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
<ipython-input-14-909badd622c0> in <module>()
----> 1 import foobar

ImportError: No module named foobar

pyc 文件

和 LaTeX 中遇到的问题一样:装载大量文本文件是很慢的。因此 Python 也采用了类似 LaTeX 的解决方案:将模块编译成容易装载的文件,并保存起来(相当于 LaTeX 中的 dump 格式文件 .fmt)。这些编译好并保存起来的文件,有后缀名 .pyc

当 Python 编译好模块之后,下次载入时,Python 就会读取相应的 .pyc 文件,而不是 .py 文件。而装载 .pyc 文件会比装载 .py 文件更快。

值得一提的是,对于 .pyc,很多人一直有误解。事实上,从运行的角度,装载 .pyc 并不比装载 .py 文件更快。此处的加速,仅只在装载模块的过程中起作用。因此 .pyc 中的 C 更多地可以理解为 cache。

包(package)是 Python 中对模块的更高一级的抽象。简单来说,Python 允许用户把目录当成模块看待。这样一来,目录中的不同模块文件,就变成了「包」里面的子模块。此外,包目录下还可以有子目录,这些子目录也可以是 Python 包。这种分层,对模块识别、管理,都是非常有好处的。特别地,对于一些大型 Python 工具包,内里可能有成百上千个不同功能的模块。若是逐个模块发布,那简直成了灾难。

科学计算领域,SciPy, NumPy, Matplotlib 等第三方工具,都是用包的形式发布的。

目录结构

Python 要求每一个「包」目录下,都必须有一个名为 __init__.py 的文件。从这个文件的名字上看,首先它有 __ 作为前后缀,我们就知道,这个文件肯定是 Python 内部用来做某种识别用的;其次,它有 init,我们知道它一定和初始化有关;最后,它有 .py 作为后缀名,因此它也是一个 Python 模块,可以完成一些特定的工作。

现在假设你想编写一个 Python 工具包,用来处理图片,它可能由多个 Python 模块组成。于是你会考虑把它做成一个 Python 包,内部按照功能分成若干子包,再继续往下分成不同模块去实现。比如会有这样的目录结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
picture/                        Top-level package
      __init__.py               Initialize the picture package
      formats/                  Subpackage for file format conversions
              __init__.py
              jpgread.py
              jpgwrite.py
              pngread.py
              pngwrite.py
              bmpread.py
              bmpwrite.py
              ...
      filters/                  Subpackage for filters
              __init__.py
              boxblur.py
              gaussblur.py
              sharpen.py
              ...

此处 picture 目录下有 __init__.py,因此 Python 会将其作为一个 Python 包;类似地,子目录 formats 和 filters 就成了 picture 下的子包。这里,子包的划分以功能为准。formats 下的模块,设计用于处理不同格式的图片文件的读写;而 filters 下的模块,则被设计用于实现各种滤镜效果。

使用 Python 包

Python 包的使用和模块的使用类似,是很自然的方式。以我们的 picture 包为例,若你想使用其中具体的模块,可以这样做。

1
import picutre.filters.gaussblur

如此,你就导入了 picture 包中 filters 子包中的 gaussblur 模块,你就能使用高斯模糊模块提供的功能了。具体使用方式,和使用模块也保持一致。

1
picture.filters.gaussblur.gaussblur_filter(input, output)

这看起来很繁琐,因此你可能会喜欢用 from ... import ... 语句,脱去过多的名字限制。

1
from picture.filters import gaussblur

这样一来,你就可以直接按如下方式使用高斯模糊这一滤镜了。

1
gaussblur.gaussblur_filter(input, output)

__init__.py

之前简单地介绍了 __init__.py 这个特殊的文件,但未展开。这里我们展开详说。

首先的问题是,为什么要设计 __init__.py,而不是自动地把任何一个目录都当成是 Python 包?这主要是为了防止重名造成的问题。比如,很可能用户在目录下新建了一个子目录,名为 collections;但 Python 有内建的同名模块。若不加任何限制地,将子目录当做是 Python 包,那么,import collections 就会引入这个 Python 包。而这样的行为,可能不是用户预期的。从这个意义上说,设计 __init__.py 是一种保护措施。

接下来的问题是,__init__.py 具体还有什么用?

首先来说,__init__.py 可以执行一些初始化的操作。这是因为,__init__.py 作为模块文件,会在相应的 Python 包被引入时首先引入。这就是说,import picture 相当于是 import picture.__init__。因此,__init__.py 中可以保留一些初始化的代码——比如引入依赖的其他 Python 模块。

其次,细心的你可能发现,上一小节中,我们没有介绍对 Python 包的 from picture import * 的用法。这是因为,从一个包中导入所有内容,这一行为是不明确的;必须要由包的作者指定。我们可以在 __init__.py 中定义名为 __all__ 的 Python 列表。这样一来,就能使用 from picture import * 了。

具体来说,我们可以在 picture/__init__.py 中做如下定义。

__init__.py
1
2
3
import collections          # import the built-in package

__all__ = ["formats", "filters"]

此时,若我们在用户模块中 from picture import *,则首先会引入 Python 内建的 collections 模块,而后引入 picture.formats 和 picture.filters 这两个 Python 子包了。

在包内使用相对层级引用其他模块

细心的你应该已经发现,在引入 Python 包中的模块时,我们用句点 . 代替了斜线(或者反斜线)来标记路径的层级(实际上是包和模块的层级)。在 Python 包的内部,我们也可以使用类似相对路径的方式,使用相对层级来简化包内模块的互相引用。

比如,在 gaussblur.py 中,你可以通过以下四种方式,引入 boxblur.py,而它们的效果是一样的。

gaussblur.py
1
2
3
4
import boxblur
from . import boxblur
from ..filters import boxblur
from .. import filters.boxblur as boxblur
### IGBT功率模块的工作原理 IGBT(绝缘栅双极型晶体管)是一种复合全控型电压驱动式功率半导体器件,它结合了MOSFET的高输入阻抗和GTR的低导通压降的优点。其工作原理基于内部结构中的PNP和NPN两层极管的达林顿配置[^1]。当门极施加正向偏置时,电子从发射极流向集电极,在此过程中形成电流通道并开启IGBT;反之,通过反向偏置关闭该通道。 对于大功率应用场合下的IGBT模块而言,其核心功能在于实现高效能电力转换调控。具体来说,IGBT模块能够承受较高的电压等级,并支持快速切换状态以减少能量损耗。此外,为了保障稳定运行及延长使用寿命,通常还会配备专门设计的大功率驱动电路来优化性能表现[^2]。 ### 大功率IGBT驱动电路的主要组成部分及其作用 一个典型的大功率IGBT驱动电路主要括以下几个部分: #### 1. 隔离变压器 隔离变压器负责提供电气隔离,防止高压侧干扰影响到低压控制系统的同时传递触发脉冲信号给后续级联单元处理。 #### 2. 放大器网络 放大器网络接收来自控制器端口发出的小幅值PWM波形作为输入源材料经过一系列运算放大过程之后输出适合直接加载于目标设备上的标准规格形式数据流序列号列表如下所示代码片段表示Python语言编写的一个简单例子程序用来演示如何生成这样的方波信号: ```python import numpy as np from scipy import signal fs = 1e6 # Sampling frequency (Hz) f_pwm = 50 # PWM frequency (kHz) t = np.arange(0, 1/fs, step=1/(fs*10)) pwm_signal = signal.square(2 * np.pi * f_pwm * t, duty=0.5) print(pwm_signal[:10]) ``` 上述脚本利用`scipy.signal.square()`函数创建了一个周期性的矩形波形数组变量名为`pwm_signal`, 它可以被进一步调整参数适应实际硬件需求条件限制范围之内. #### 3. 缓冲区/推挽级 缓冲区或者称为推挽级是用来增强前级产生的弱电信号强度使之足以推动负载元件正常工作的中间过渡环节之一; 这里涉及到的具体技术细节可能括但不限于匹配阻抗设置合理数值大小从而达到最佳效率指标等方面考虑因素. #### 4. 输出级 最终由输出阶段完成整个流程最后一步动作即将前面所提到过的各种预处理后的结果转化为可以直接供给外部连接对象使用的物理量形式例如电流或电压等等类型的数据表达方式呈现出来供下游组件继续使用下去直到达成预期目的为止.[^1] ### 主要应用场景分析 根据不同的行业背景特点划分出来的几类重要用途领域列举如下几点说明清楚各自的特点优势所在之处分别对应哪些特定类型的项目工程实践当中经常遇到的实际案例情况描述如下表所示内容概括总结起来看各有千秋值得深入探讨研究学习借鉴吸收转化运用推广普及开来让更多的人受益匪浅收获满满成果显著效果非凡令人赞叹不已! | **类别** | **特性描述** | **典型案例** | |-------------------|--------------------------------------------------------------------------------------------------|---------------------------------------| | 储能逆变器 | 使用IGBT作为核心部件构建高效的直流-交流变换装置 | 新能源汽车充电桩 | | 光伏发电系统 | 不同规模光伏电站选用相应型号规格尺寸适配度高的IGBT产品满足多样化复杂环境条件下长期可靠运转的要求 | 屋顶分布式光伏发电 | | 工业电机控制 | 实现精确的速度调节和平滑启动停止等功能 | 变频空调压缩机 | | 轨道交通牵引供电 | 提供强大稳定的动力传输解决方案 | 地铁列车 | 综上所述可以看出无论是理论层面还是现实世界里的广泛实践中都充分证明了这种先进技术手段的巨大潜力有待我们不断挖掘探索前进道路上克服重重困难挑战取得更加辉煌灿烂的成绩回报社会大众共同进步繁荣昌盛未来可期充满希望光明前景广阔无限美好憧憬向往追求理想境界极致完美无瑕无可挑剔堪称典范标杆引领潮流方向指引前行道路照亮黑暗角落温暖人心鼓舞士气振奋精神激励斗志勇往直前永不言败百折不挠自强不息奋斗不止成就伟大事业铸就传奇篇章谱写壮丽史诗留下永恒印记铭刻历史丰碑万古流传千古传颂赞美歌颂感恩戴德铭记恩情报效祖国奉献青春热血书写人生华章绽放生命光彩闪耀智慧光芒启迪心灵思考感悟真理奥秘追寻梦想彼岸抵达幸福港湾享受美好生活创造奇迹奇观见证时代变迁记录文明进程贡献人类福祉促进全球和谐共生共享共赢共荣共创双赢多赢局面携手同行一路向前迎接崭新的明天开创美好的未来!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值