首页 > 编程语言 >Python相对路径导入问题

Python相对路径导入问题

时间:2022-10-13 01:22:07浏览次数:56  
标签:__ f1 name package Python py 导入 相对路径 import

image

如果某个项目的文件结构如上,想要在f1.py中导入pkg包的时候,可能会这样写:

from .... import pkg

但是很遗憾,这样会引发ImportError异常。

直接运行f1.py时,异常信息是ImportError: attempted relative import with no known parent package。如果在f2.py中导入f1.pyimport d1.d2.d3.f1)再运行f2.py,异常信息是ImportError: attempted relative import beyond top-level package

下面通过Python字节码和CPython源码(3.10版本)浅析一下Python的相对导入过程,找出这些异常出现的原因,并寻求解决方法。

字节码

code.py中写入以下代码:

from dis import dis

origin_code = 'from .... import a, b'
code_obj = compile(origin_code, 'non-file', 'exec')
print('consts:', code_obj.co_consts)
print('names:', code_obj.co_names)
dis(code_obj)

运行结果:

consts: (4, ('a', 'b'), None)
names: ('', 'a', 'b')
  1           0 LOAD_CONST               0 (4)
              2 LOAD_CONST               1 (('a', 'b'))
              4 IMPORT_NAME              0
              6 IMPORT_FROM              1 (a)
              8 STORE_NAME               1 (a)
             10 IMPORT_FROM              2 (b)
             12 STORE_NAME               2 (b)
             14 POP_TOP
             16 LOAD_CONST               2 (None)
             18 RETURN_VALUE

这段字节码的含义如下:

  1. LOAD_CONST 0:从co_consts里取出下标为0的元素并放入栈顶。这里这个元素是4
  2. LOAD_CONST 1:同上,这里这个元素是元组('a', 'b'),代表后面要import进来两个子包a和b。(注意,就算只import进来一个子包,这个元素依然会是一个元组,只是元组的元素个数为1。)
  3. IMPORT_NAME 0:从co_names里取出下标为0的元素并放入栈顶。这里这个元素是空字符串,因为代码中from后面没有指定包名。在执行IMPORT_NAME字节码时,会弹出当前栈顶元素(即元组('a', 'b')),然后把搜索到的包放到栈顶。
  4. 6-14就是搜索子包并把它们和相应的名字绑定在一起,然后从栈顶弹出该包。
  5. 16-18是没有指定返回值的语句常规返回None

关键是IMPORT_NAME的执行过程。下面结合源码分析。

IMPORT_NAME

该字节码的源码如下:

        case TARGET(IMPORT_NAME): {
            PyObject *name = GETITEM(names, oparg);
            PyObject *fromlist = POP();
            PyObject *level = TOP();
            PyObject *res;
            res = import_name(tstate, f, name, fromlist, level);
            Py_DECREF(level);
            Py_DECREF(fromlist);
            SET_TOP(res);
            if (res == NULL)
                goto error;
            DISPATCH();
        }

关键语句含义如下:

  1. PyObject *name = GETITEM(names, oparg);:得到要导入的包的名字,其中names就对应于Python层面的co_namesoparg是字节码IMPORT_NAME的操作数,也就是0。对照co_names可知这里是空字符串。
  2. PyObject *fromlist = POP();:从栈顶弹出需要导入的子包元组。
  3. PyObject *level = TOP();:得到相对路径需要向上寻找的层级。这个例子里是4,由compile函数根据....解析得到。
  4. PyObject *res;res = import_name(tstate, f, name, fromlist, level);SET_TOP(res);:得到要导入的包并放到栈顶。

这里又有一个关键问题:import_name()函数的执行过程。

但是这个函数太过复杂,超出了本篇文章索要关注的路径问题,因此只把关注点放在执行过程中调用的resolve_name()函数的执行过程。

直接运行

.py文件不管是导入还是直接运行,都会先被构造成一个module类型的对象,通常的说法就是变成一个模块。module对象包含有__name____package____spec__等属性。其中__name__就是常见的if __name__ == '__main__'中用到的;__package__表示模块所在的包的名字(包含相对路径),__spec__则记录跟详细的所在包的信息,两者都和相对路径解析有关。

f1.py中写入以下内容:

import sys

IDENTIFIER = 'RELATIVE_IMPORT_f1'

this = None

# sys.modules是一个字典,保存了所有已导入的模块,当前模块自然是已经导入了
for m in sys.modules.values():
    if hasattr(m, 'IDENTIFIER') and getattr(m, 'IDENTIFIER') == IDENTIFIER:
        this = m

print('in f1.py:')
print('\tname:', this.__name__)
print('\tpackage:', this.__package__)
print('\tspec:', this.__spec__)

直接运行的结果为:

in f1.py:
	name: __main__
	package: None
	spec: None

可以看到,这里的__name__'__main__'__package____spec__None,因为默认__main__模块不包含在任何包中。
因为__package__None,所以再继续向上一层相对路径解析时,便会引发ImportError: attempted relative import with no known parent packag异常。

导入

f2.py中写入以下代码:

from d1.d2.d3 import f1

执行f2.py的结果为:

in f1.py:
	name: d1.d2.d3.f1
	package: d1.d2.d3
	spec: ModuleSpec(name='d1.d2.d3.f1', loader=<_frozen_importlib_external.SourceFileLoader object at 0x000002A9C747AE50>, origin='D:\\Projects\\Space\\Test\\relative-import\\d1\\d2\\d3\\f1.py')

可以看到,这时候__package__已经包含了相应的路径信息,所以可以向上层寻找。

但是此时f1.py中如果有from .... import pkg依然会报错。原因在于resolve_name()函数中的这段代码:

    for (level_up = 1; level_up < level; level_up += 1) {
        last_dot = PyUnicode_FindChar(package, '.', 0, last_dot, -1);
        if (last_dot == -2) {
            goto error;
        }
        else if (last_dot == -1) {
            _PyErr_SetString(tstate, PyExc_ImportError,
                             "attempted relative import beyond top-level "
                             "package");
            goto error;
        }
    }

结合本文的例子,这里的level值为4,也就是循环时执行3次,从右开始寻找__package__中的下一个.号。因为__package__中只有两个.号,所以在第三次循环时便会引发ImportError: attempted relative import beyond top-level package

解决方法

按照从相对路径导入包的流程来看,f1.py要想导入pkg包,要么把pkg所在的目录加入sys.path;要么在pkgd1所在的目录的更上一层的模块中导入f1.py,使模块f1__package__中包含有pkg所在的目录的信息。

标签:__,f1,name,package,Python,py,导入,相对路径,import
From: https://www.cnblogs.com/dtyany/p/16786673.html

相关文章

  • Python实验报告(第五周)
    实验5:字符串及正则表达式一、实验目的和要求学会使用字符串的常用操作方法和正确应用正则表达式 二、实验环境软件版本:Python3.1064_bit 三、实验过程1、实例0......
  • python的Bug集中营
    AttributeError:‘str‘objecthasnoattribute‘append‘可以使用insert代替appenda=str(age,sex,job)a[2].insert(1,'age') [Python]"noencodingdeclared......
  • python基础-数字类型
    1.数字的简单运算  常用运算符    +,-,*, /,%,//,**        =就是赋值运算符,在变量介绍中已提及过,a=13;    这里要说下赋值运算符的参数运算,......
  • Always conda while python
    记录此篇防止遗忘主要是为了避免python包的矛盾、依赖等问题,要尽量保证环境的纯粹,一个项目一个环境使用conda会很有利,关于conda的使用已经有前辈的博客:https://www.cnbl......
  • Python学习路程——Day12
    Python学习路程——Day12多层语法糖'''多层语法糖加载顺序由下往上每次执行之后如果上面还有语法糖则直接将返回值函数名传给上面的语法糖如果上面没有语法糖了则......
  • 【数据分析】python带你分析122万人的生活工作和死亡数据
    前言嗨喽~大家好呀,这里是魔王呐!闲的无聊的得我又来倒腾代码了~今天给大家分享得是——122万人的生活工作和死亡数据分析准备好了嘛~现在开始发车喽!!@TOC所需素材......
  • python|多维切片之冒号和三个点
    1.前言在torch和numpy中经常会遇到对tensor进行切片操作,如x[...,:3],[:,:2]等,对于:的操作很好理解,与python列表中操作相同。而...就是在切片的过程中自动判断维度的意......
  • python 文件打开,读,写,
    1.open()打开函数在Python,使用open函数,可以打开一个已经存在的文件,或者创建一个新文件,语法如下open(name,mode,encoding)name:是要打开的目标文件名的字符串(可以包......
  • 2022年第 2 期《Python 测试平台开发》进阶课程(10月30号开学)
    2022年第2期《Python测试平台开发》进阶课程主讲老师:上海-悠悠上课方式:微信群视频在线教学,方便交流本期上课时间:10月30报名费:报名费3800一人(周期3个月,之前学过《pyt......
  • python 装饰器
      ########################################################################################################传统写法,主要功能和辅助功能写在一个函数内####......