【问题标题】:python module import - relative paths issuepython模块导入-相对路径问题
【发布时间】:2018-11-19 21:06:38
【问题描述】:

我正在 python 2.7 中开发自己的模块。它位于~/Development/.../myModule 而不是/usr/lib/python2.7/dist-packages/usr/lib/python2.7/site-packages。内部结构是:

/project-root-dir
  /server
    __init__.py
    service.py
    http.py
  /client
    __init__.py
    client.py

client/client.py 包括 PyCachedClient 类。我遇到了导入问题:

project-root-dir$ python
Python 2.7.2+ (default, Jul 20 2012, 22:12:53) 
[GCC 4.6.1] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from server import http
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "server/http.py", line 9, in <module>
    from client import PyCachedClient
ImportError: cannot import name PyCachedClient

我没有将 PythonPath 设置为包含我的 project-root-dir,因此当 server.http 尝试包含 client.PyCachedClient 时,它会尝试从相对路径加载它并失败。我的问题是 - 我应该如何以一种好的 Python 方式设置所有路径/设置?我知道每次打开控制台并尝试运行服务器时,我都可以在 shell 中运行export PYTHONPATH=...,但我想这不是最好的方法。如果我的模块是通过 PyPi(或类似的东西)安装的,我会将它安装在 /usr/lib/python... 路径中,它会自动加载。

我很感激有关 Python 模块开发最佳实践的提示。

【问题讨论】:

  • 非常简短的回答:使用virtualenv 进行隔离,setuptools 用于创建可安装包并管理依赖项,python setup.py develop 用于在开发模式下安装该包。我目前正在写一个答案,试图举例说明如何将这些概念放在一起。哦 - 而且不要使用相对导入,你很少需要。

标签: python module


【解决方案1】:

我的 Python 开发工作流程

这是一个开发 Python 包的基本过程,其中包含我认为社区中的最佳实践。这是基本的——如果你真的很想开发 Python 包,还有更多内容,每个人都有自己的偏好,但它应该作为一个模板来开始,然后了解更多关于所涉及的部分。基本步骤是:

  • 使用virtualenv 进行隔离
  • setuptools 用于创建可安装包和管理依赖项
  • python setup.py develop 在开发模式下安装该软件包

虚拟环境

首先,我建议使用virtualenv 来获得一个独立的环境来开发你的包。在开发过程中,你需要安装、升级、降级和卸载包的依赖项,你不需要想要

  • 你的开发依赖会污染你的系统范围site-packages
  • 您的系统范围内的site-packages 影响您的开发环境
  • 版本冲突

污染您的系统范围site-packages 是不好的,因为您在那里安装的任何包都将可用于您安装的所有使用系统 Python 的 Python 应用程序,即使您的小项目只需要该依赖项。它刚刚安装在一个新版本中,覆盖了系统范围内的site-packages,并且与依赖它的 ${important_app} 不兼容。你明白了。

让您的系统范围内的site-packages 影响您的开发环境是不好的,因为您的项目可能依赖于您已经在系统 Python 的 site-packages 中获得的模块。因此,您忘记正确声明您的项目依赖于该模块,但一切正常,因为它始终存在于您的本地开发盒中。直到你发布你的包并且人们尝试安装它,或者将它推送到生产环境等等......在一个干净的环境中开发会迫使你正确地声明你的依赖关系。

因此,virtualenv 是一个独立的环境,具有自己的 Python 解释器和模块搜索路径。它基于您之前安装的 Python 安装,但与它隔离。

要创建 virtualenv,请安装 virtualenv 包,方法是使用 easy_installpip 将其安装到系统范围的 Python:

sudo pip install virtualenv

请注意,这将是唯一您以 root 身份(使用 sudo)将某些东西安装到您的全局站点包中的时间。之后的一切都将发生在您即将创建的 virtualenv 中。

现在创建一个 virtualenv 来开发你的包:

cd ~/pyprojects
virtualenv --no-site-packages foobar-env

这将创建一个目录树~/pyprojects/foobar-env,这是您的虚拟环境。

要激活 virtualenv,cd 进入其中,source bin/activate script

~/pyprojects $ cd foobar-env/
~/pyprojects/foobar-env $ . bin/activate
(foobar-env) ~/pyprojects/foobar-env $

注意前导点.,它是source shell 命令的简写。还要注意提示是如何变化的:(foobar-env) 表示您在激活的 virtualenv 内部(并且始终需要隔离才能工作)。因此,每次打开新的终端选项卡或 SSH 会话等时都要激活您的环境。

如果您现在在激活的 env 中运行 python,它实际上将使用 ~/pyprojects/foobar-env/bin/python 作为解释器,具有自己的 site-packages 和独立的模块搜索路径。

一个安装工具包

现在开始创建你的包。基本上你会想要一个带有setup.pysetuptools 包来正确声明你的包的元数据和依赖项。您可以按照setuptools documentation 自行完成此操作,或使用Paster templates 创建包骨架。要使用 Paster 模板,请将 PasteScript 安装到您的 virtualenv 中:

pip install PasteScript

让我们为我们的新包创建一个源目录以使事情井井有条(也许您希望将项目拆分为多个包,或者稍后使用源中的依赖项):

mkdir src
cd src/

现在创建你的包,做

paster create -t basic_package foobar

并回答交互式界面中的所有问题。大多数都是可选的,只需按 ENTER 即可保留默认设置。

这将创建一个名为foobar 的包(或更准确地说,是一个setuptools 发行版)。这是这个名字

  • 人们将使用easy_installpip install foobar 来安装您的软件包
  • setup.py 中其他包将使用的名称依赖于您的名称
  • PyPi 上将调用什么

在内部,您几乎总是创建一个名为“__init__.py”的 Python 包(如在“带有__init__.py 的目录中)。这不是必需的,顶级 Python 包的名称可以是任何有效的包名称,但它是将其命名为与发行版相同的通用约定。这就是为什么将两者分开很重要但并不总是那么容易的原因。因为顶级 python 包名称是什么

  • 人们(或您)将使用import foobarfrom foobar import baz 导入您的包

因此,如果您使用了 paste 模板,它已经为您创建了该目录:

cd foobar/foobar/

现在创建您的代码:

vim models.py

models.py

class Page(object):
    """A dumb object wrapping a webpage.
    """

    def __init__(self, content, url):
        self.content = content
        self.original_url = url

    def __repr__(self):
        return "<Page retrieved from '%s' (%s bytes)>" % (self.original_url, len(self.content))

还有一个client.py在使用models.py的同一目录中:

client.py

import requests
from foobar.models import Page

url = 'http://www.stackoverflow.com'

response = requests.get(url)
page = Page(response.content, url)

print page

setup.py中声明对requests模块的依赖:

  install_requires=[
      # -*- Extra requirements: -*-
      'setuptools',
      'requests',
  ],

版本控制

src/foobar/ 是您现在要置于版本控制之下的目录:

cd src/foobar/
git init
vim .gitignore

.gitignore

*.egg-info
*.py[co]
git add .
git commit -m 'Create initial package structure.

将你的包安装为开发鸡蛋

现在是时候在开发模式下安装你的包了:

python setup.py develop

这将安装requests 依赖项和你的 包作为开发蛋。因此,它已链接到您的 virtualenv 的站点包,但仍位于 src/foobar,您可以在其中进行更改并让它们立即在 virtualenv 中处于活动状态,而无需重新安装您的包。

现在对于您最初的问题,使用相对路径导入:我的建议是,不要这样做。现在您已经有了一个正确的 setuptools 包,它已安装并可导入,您当前的工作目录应该不再重要了。只需执行from foobar.models import Page 或类似操作,声明该对象所在的完全限定名称。这使您的源代码对您自己和其他阅读您的代码的人来说更具可读性和可发现性。

您现在可以通过在已激活的 virtualenv 中的任何位置执行 python client.py 来运行您的代码。 python src/foobar/foobar/client.py 工作正常,您的软件包已正确安装,您的工作目录不再重要。

如果您想更进一步,您甚至可以为您的 CLI 脚本创建一个 setuptools 入口点。这将在您的 virtualenv 中创建一个 bin/something 脚本,您可以从 shell 运行该脚本。

setuptools console_scripts 入口点

setup.py

  entry_points='''
  # -*- Entry points: -*-    
  [console_scripts]
  run-fooobar = foobar.main:run_foobar
  ''',

client.py

def run_client():
    # ...

main.py

from foobar.client import run_client

def run_foobar():
    run_client()

重新安装你的包来激活入口点:

python setup.py develop

然后,bin/run-foo

一旦你(或其他人)在 virtualenv 之外真正安装了你的包,入口点将在 /usr/local/bin/run-foo 或类似的地方,它会自动在 $PATH 中。

进一步的步骤

推荐阅读:

【讨论】:

  • 我喜欢这篇文章...没想到会收到这样的答案:) 谢谢
  • 不客气。我希望我没有弄乱任何路径,它就像一个 HOWTO。我遗漏了一些内容以防止帖子变得比现在更长,所以如果有什么不够清楚,请随时询问。
  • 我尽量不让 cmets 像这样离开,但是……这非常有帮助。谢谢。
  • 非常有帮助的答案。它让我确信 setuptools 变体是与 virtualenvs 结合的最佳选择。不需要任何相关的导入黑客。谢谢!
  • 在项目目录中时,您也可以使用pip install -e .,而不是python setup.py develop
【解决方案2】:

所以,您有两个 ,第一个带有名为:

的模块
server         # server/__init__.py
server.service # server/service.py
server.http    # server/http.py

第二个带有模块名称:

client         # client/__init__.py
client.client  # client/client.py

如果您想假设两个包都在您的导入路径 (sys.path) 中,并且您想要的类在 client/client.py 中,那么在您的服务器中您必须这样做:

from client.client import PyCachedClient

您从client 而非client.client 中请求了一个符号,并且根据您的描述,这不是定义该符号的位置。

我个人会考虑制作这个包(即,将__init__.py 放在上一级的文件夹中,并给它一个合适的python 包名称),并将clientserver 作为子包那个包裹。然后 (a) 如果您愿意 (from ...client.client import something),您可以进行相对导入,并且 (b) 您的项目将更适合重新分发,而不是将两个非常通用的包名称放在 python 模块层次结构的顶层。

【讨论】:

  • 感谢您的建议。好吧,服务器是用python编写的,客户端是用python编写的。 javascript和php。这就是我将客户端目录与服务器目录分开的原因。您认为这是一个好的解决方案吗?
  • 这取决于你想要完成什么。如果您正在编写一个应用程序,那么以对您最有意义的方式组织您的代码可能更可取。如果您正在编写一个库,一个可能放在 PyPI 上并供其他人安装的包,那么我认为将您的整个库包含在一个封闭的包中是更可取的。
猜你喜欢
  • 1970-01-01
  • 2010-09-21
  • 1970-01-01
  • 1970-01-01
  • 2020-06-05
  • 1970-01-01
  • 2015-01-07
相关资源
最近更新 更多