Python Import机制备忘-模块搜索路径(sys.path)、嵌套Import、package Import

本文详细解析了Python中模块和包的导入机制,包括模块搜索路径、标准导入流程、嵌套导入行为及包的导入过程。并通过实例展示了不同场景下的导入细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在看《Python源码剖析》,对Python内部运行机制比以前了解的更深入了,感觉自己有机会也可以做个小型的动态脚本语言了,呵呵,当然是吹牛了。目的当然不是创造一个动态语言,目的只有一个:更好的使用Python。看到模块导入那块的时候,终于对模块导入机制比较了解了,以防忘记特记录下来。

模块的搜索路径

模块的搜索路径都放在了sys.path列表中,如果缺省的sys.path中没有含有自己的模块或包的路径,可以动态的加入(sys.path.apend)即可。下面是sys.path在Windows平台下的添加规则。

1、sys.path第一个路径往往是主模块所在的目录。在交互环境下添加一个空项,它对应当前目录。

2、如果PYTHONPATH环境变量存在,sys.path会加载此变量指定的目录。

3、我们尝试找到Python Home,如果设置了PYTHONHOME环境变量,我们认为这就是Python Home,否则,我们使用python.exe所在目录找到lib/os.py去推断Python Home。

如果我们确实找到了Python Home,则相关的子目录(Lib、plat-win、lib-tk等)将以Python Home为基础加入到sys.path,并导入(执行)lib/site.py,将site-specific目录及其下的包加入。

如果我们没有找到Python Home,则把注册表Software/Python/PythonCore/2.5/PythonPath的项加入sys.path(HKLM和 HKCU合并后加入),但相关的子目录不会自动添加的。

4、如果我们没有找到Python Home,并且没有PYTHONPATH环境变量,并且不能在注册表中找到PythonPath,那么缺省相对路径将加入(如:./Lib;./plat-win等)。

总结如下

当在安装好的主目录中运行Python.exe时,首先推断Python Home,如果找到了PythonHome,注册表中的PythonPath将被忽略;否则将注册表的PythonPath加入。

如果PYTHONPATH环境变量存在,sys.path肯定会加载此变量指定的目录。

如果Python.exe在另外的一个目录下(不同的目录,比如通过COM嵌入到其他程序),Python Home将不推断,此时注册表的PythonPath将被使用。

如果Python.exe不能发现他的主目录(PythonHome),并且注册表也没有PythonPath,则将加入缺省的相对目录。

 

标准Import

Python中所有加载到内存的模块都放在sys.modules。当import一个模块时首先会在这个列表中查找是否已经加载了此模块,如果加载了则只是将模块的名字加入到正在调用import的模块的Local名字空间中。如果没有加载则从sys.path目录中按照模块名称查找模块文件,模块文件可以是py、pyc、pyd,找到后将模块载入内存,并加入到sys.modules中,并将名称导入到当前的Local名字空间。

可以看出了,一个模块不会重复载入。多个不同的模块都可以用import引入同一个模块到自己的Local名字空间,其实背后的PyModuleObject对象只有一个。

说一个容易忽略的问题,import只能导入模块,不能导入模块中的对象(类、函数、变量等)。如一个模块A(A.py)中有个函数getName,另一个模块不能通过import A.getName将getName导入到本模块,只能用import A。如果想只导入特定的类、函数、变量则用from A import getName即可。

嵌套Import

嵌套import,我分两种情况,一种是:本模块导入A模块(import A),而A中又有import语句,会激活另一个import动作,如import B,而B模块又可以import其他模块,一直下去。

对这种嵌套比较容易理解,注意一点就是各个模块的Local名字空间是独立的,所以上面的例子,本模块import A完了后本模块只能访问模块A,不能访问B及其他模块。虽然模块B已经加载到内存了,如果要访问还要在明确的在本模块中import B。

另外一种嵌套指,在模块A中import B,而在模块B中import A。这时会怎么样呢?这个在Python列表中由RobertChen给出了详细解释,抄录如下:

[A.py]  
from B import D  
class C:pass  

[B.py]  
from A import C  
class D:pass

为什么执行A的时候不能加载D呢?

如果将A.py改为:import B就可以了。

这是怎么回事呢?

RobertChen:这跟Python内部import的机制是有关的,具体到from B import D,Python内部会分成几个步骤:

  1. 在sys.modules中查找符号"B"
  2. 果符号B存在,则获得符号B对应的module对象<module B>。

    从<module B>的__dict__中获得符号"D"对应的对象,如果"D"不存在,则抛出异常

  3. 如果符号B不存在,则创建一个新的module对象<module B>,注意,这时,module对象的__dict__为空。

    执行B.py中的表达式,填充<module B>的__dict__ 。

    从<module B>的__dict__中获得"D"对应的对象,如果"D"不存在,则抛出异常。

所以,这个例子的执行顺序如下:

1、执行A.py中的from B import D

由于是执行的python A.py,所以在sys.modules中并没有<module B>存在,首先为B.py创建一个module对象(<module B>),注意,这时创建的这个module对象是空的,里边啥也没有,在Python内部创建了这个module对象之后,就会解析执行B.py,其目的是填充<module B>这个dict。

2、执行B.py中的from A import C

在执行B.py的过程中,会碰到这一句,首先检查sys.modules这个module缓存中是否已经存在<module A>了,由于这时缓存还没有缓存<module A>,所以类似的,Python内部会为A.py创建一个module对象(<module A>),然后,同样地,执行A.py中的语句。

3、再次执行A.py中的from B import D

这时,由于在第1步时,创建的<module B>对象已经缓存在了sys.modules中,所以直接就得到了<module B>,但是,注意,从整个过程来看,我们知道,这时<module B>还是一个空的对象,里面啥也没有,所以从这个module中获得符号"D"的操作就会抛出异常。如果这里只是import B,由于"B"这个符号在sys.modules中已经存在,所以是不会抛出异常的。

上面的解释已经由Zoom.Quiet收录在啄木鸟了,里面有图,可以参考一下。

Package(包) Import

包(Package)可以看成模块的集合,只要一个文件夹下面有个__init__.py文件,那么这个文件夹就可以看做是一个包。包下面的文件夹还可以成为包(子包)。更进一步,多个较小的包可以聚合成一个较大的包,通过包这种结构,方便了类的管理和维护,也方便了用户的使用。比如SQLAlchemy等都是以包的形式发布给用户的。

包和模块其实是很类似的东西,如果查看包的类型import SQLAlchemy type(SQLAlchemy),可以看到其实也是<type 'module'>。import包的时候查找的路径也是sys.path。

包导入的过程和模块的基本一致,只是导入包的时候会执行此包目录下的__init__.py而不是模块里面的语句了。另外,如果只是单纯的导入包,而包的__init__.py中又没有明确的其他初始化操作,那么此包下面的模块是不会自动导入的。如:

PA

--__init__.py

--wave.py

--PB1

  --__init__.py

  --pb1_m.py

--PB2

  --__init__.py

  --pb2_m.py

__init__.py都为空,如果有以下程序:

 
  1. import sys
  2. import PA.wave  #1
  3. import PA.PB1   #2
  4. import PA.PB1.pb1_m as m1  #3
  5. import PA.PB2.pb2_m #4
  6. PA.wave.getName() #5
  7. m1.getName() #6
  8. PA.PB2.pb2_m.getName() #7

当执行#1后,sys.modules会同时存在PA、PA.wave两个模块,此时可以调用PA.wave的任何类或函数了。但不能调用PA.PB1(2)下的任何模块。当前Local中有了PA名字。

当执行#2后,只是将PA.PB1载入内存,sys.modules中会有PA、PA.wave、PA.PB1三个模块,但是PA.PB1下的任何模块都没有自动载入内存,此时如果直接执行PA.PB1.pb1_m.getName()则会出错,因为PA.PB1中并没有pb1_m。当前Local中还是只有PA名字,并没有PA.PB1名字。

当执行#3后,会将PA.PB1下的pb1_m载入内存,sys.modules中会有PA、PA.wave、PA.PB1、PA.PB1.pb1_m四个模块,此时可以执行PA.PB1.pb1_m.getName()了。由于使用了as,当前Local中除了PA名字,另外添加了m1作为PA.PB1.pb1_m的别名。

当执行#4后,会将PA.PB2、PA.PB2.pb2_m载入内存,sys.modules中会有PA、PA.wave、PA.PB1、PA.PB1.pb1_m、PA.PB2、PA.PB2.pb2_m六个模块。当前Local中还是只有PA、m1。

下面的#5,#6,#7都是可以正确运行的。

注意的是:如果PA.PB2.pb2_m想导入PA.PB1.pb1_m、PA.wave是可以直接成功的。最好是采用明确的导入路径,对于./..相对导入路径还是不推荐用。

 

<think>好的,我现在需要帮助用户了解如何在Python中使用sys.path.append来添加自定义路径到模块搜索路径中。用户已经提供了一些相关的引用内容,我需要结合这些信息来组织一个清晰、准确的回答。 首先,我得回忆一下sys.path的作用。sys.path是一个列表,里面包含了Python解释器在导入模块时搜索的路径顺序。当用户使用import语句时,Python会按照这个列表中的路径依次查找对应的模块。如果模块不在这些路径中,就会引发ImportError。这时候,用户就需要手动添加路径,而sys.path.append就是一个常用的方法。 接下来,用户的问题是如何正确使用sys.path.append。根据引用中的内容,比如引用[2]提到可以使用绝对路径或相对路径,引用[4]则说明添加的路径的顺序会影响模块查找的优先级。需要注意的是,使用append会将路径添加到列表末尾,而insert(0, path)则会添加到开头,这样优先级更高。但用户的问题主要在于append的使用,所以可能不需要深入讨论insert,除非有必要比较。 然后,我需要考虑用户的使用场景。例如,用户可能有两个项目project1和project2,需要共享common_utils中的模块,如引用[1]中的例子。这时候,他们需要在各自的main.py中添加共享代码的路径。这时候应该使用绝对路径,如'/home/shared_code',或者相对路径,但相对路径可能容易出错,尤其是在不同环境下运行时。 接下来,步骤分解。首先,导入sys模块,这应该是第一步。然后,使用sys.path.append()方法添加路径。例如,sys.path.append('/path/to/your/module')。之后,就可以直接导入该路径下的模块了。需要注意路径的正确性,尤其是斜杠的方向和是否存在拼写错误。 另外,用户可能想知道什么时候应该使用append而不是insert。根据引用[4],因为Python是按顺序搜索的,所以如果使用append,添加的路径会在最后,而如果该模块与现有路径中的模块同名,可能会被先找到的模块覆盖。因此,如果需要优先使用自定义路径中的模块,应该用insert(0, path)。但用户的问题是关于append的,所以这点可以作为扩展说明,但不是重点。 还要提到动态添加和临时性的特点。使用sys.path.append是在运行时修改路径,只对当前会话有效,不会影响其他项目或会话。如果需要永久添加路径,应该考虑设置PYTHONPATH环境变量或者使用.pth文件。 然后,示例部分。根据引用[1]中的示例,用户可能在project1/main.py中添加路径并导入模块。例如,添加绝对路径'/home/shared_code',然后导入common_utils.logger中的setup_logger。另外,可以再举一个相对路径的例子,比如当前目录下的子目录,但需要注意相对路径的基准是当前工作目录,这可能因执行脚本的位置不同而变化,容易出问题,所以推荐使用绝对路径。 可能的问题包括路径错误、权限问题、路径不存在等,需要提醒用户确保路径正确,并且有访问权限。此外,多次添加同一个路径可能会导致列表中存在重复项,但Python处理时不会有问题,只是可能会稍微影响性能。 总结一下,回答的结构应该是先解释sys.path的作用,然后说明使用sys.path.append的方法步骤,接着给出示例,包括绝对路径和相对路径,然后比较append和insert的区别,最后注意事项和常见问题。同时,根据用户提供的引用,正确标注引用来源,如引用[1]、[2]等。 在生成相关问题的时候,可以考虑用户可能进一步想知道如何永久添加路径,或者如何处理模块冲突,以及动态添加路径的优缺点等。例如: 1. 如何永久添加自定义路径到Python模块搜索路径? 2. 使用sys.path.append和sys.path.insert有什么区别? 3. 如何处理不同路径下同名模块的冲突问题? 确保回答准确,并且覆盖用户可能的疑问点。</think>### 如何正确使用`sys.path.append`添加自定义路径到Python模块搜索路径 #### 1. **理解`sys.path`的作用** `sys.path`是Python解释器搜索模块的路径列表,类型为`list`。当使用`import`语句时,Python会按顺序遍历该列表中的路径来查找模块[^3]。默认包含当前脚本所在目录、标准库路径等。 #### 2. **使用方法** 通过`sys.path.append()`动态添加自定义路径: 1. **导入`sys`模块**: ```python import sys ``` 2. **添加路径**: - **绝对路径**(推荐): ```python sys.path.append('/home/shared_code') # 替换为实际路径 ``` - **相对路径**(需谨慎): ```python sys.path.append('../shared_code') # 相对当前脚本的父目录 ``` 3. **导入模块**: ```python from common_utils.logger import setup_logger # 假设目标模块在添加的路径下 ``` #### 3. **示例场景** **场景**:跨项目共享代码(如`project1`和`project2`需共用`/home/shared_code`中的工具模块)[^1]: ```python # 在 project1/main.py 中: import sys sys.path.append('/home/shared_code') # 添加共享代码路径 from common_utils.logger import setup_logger # 导入目标模块 setup_logger() # 使用模块功能 ``` #### 4. **注意事项** - **路径优先级**:`sys.path.append`将路径添加到列表末尾,若需优先搜索,可使用`sys.path.insert(0, path)`[^4]。 - **动态性**:修改仅对当前Python会话有效,重启后需重新添加。 - **路径正确性**:确保路径存在且格式正确(如Linux用`/`,Windows用`\\`)。 - **避免重复**:多次添加相同路径会导致列表冗余(无害但低效)。 #### 5. **与`sys.path.insert`的区别** | 方法 | 作用位置 | 适用场景 | |---------------------|----------|----------------------------| | `sys.path.append` | 列表末尾 | 路径优先级较低时使用 | | `sys.path.insert(0)`| 列表开头 | 需覆盖同名模块或提高优先级 | ---
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值