Python模块导入-相对路径问题

19

我正在使用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时,它会尝试从相对路径加载并失败。我的问题是 - 我应该如何以良好、pythonic的方式设置所有路径/设置?我知道每次打开控制台并尝试运行服务器时,可以在shell中运行export PYTHONPATH=...,但我想这不是最好的方法。如果我的模块通过PyPi(或类似的东西)安装,那么它将被安装在/usr/lib/python...路径中,并且将自动加载。

我希望得到有关Python模块开发最佳实践的建议。


4
简短回答:使用 virtualenv 进行隔离,使用 setuptools 创建可安装的软件包并管理依赖项,使用 python setup.py develop 在开发模式下安装该软件包。我正在撰写一个答案,尝试给出如何将这些概念结合在一起的示例。哦 - 不要使用相对导入,你很少需要它们。 - Lukas Graf
2个回答

42

我的Python开发工作流程

这是一个基本的Python包开发流程,结合了我认为社区最佳实践。如果您真的很认真地开发Python包,还有更多要考虑的细节,而且每个人都有自己的喜好,但它应该是一个模板,可以帮助您开始并学习其中的组件。基本步骤如下:

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

virtualenv

首先,我建议使用virtualenv获取隔离环境来开发您的包。在开发过程中,您需要安装、升级、降级和卸载您包的依赖项,您不希望这些操作影响到其他项目或系统。使用virtualenv可以让你在一个单独的环境中安装Python和其他依赖,避免版本冲突和其他问题。

  • 避免污染系统范围内的site-packages,因为您在那里安装的任何包都将对使用系统Python的所有Python应用程序可用,即使您只需要该依赖项进行小型项目。并且它刚刚以新版本安装,覆盖了系统范围内的site-packages中的一个版本,并且与依赖于它的${important_app}不兼容。你明白了。
  • 让系统范围内的site-packages影响开发环境也是不好的,因为您的项目可能依赖于系统Python的site-packages中已有的模块。因此,您忘记正确声明您的项目依赖于该模块,但一切正常,因为它始终存在于您的本地开发框中。直到您发布软件包并尝试安装它或将其推向生产等...在干净的环境中开发可以强制您正确声明依赖项。
  • 版本冲突
所以,一个虚拟环境是一个具有独立的Python解释器和模块搜索路径的隔离环境。它基于你之前安装的Python,但与其相互隔离。
要创建一个虚拟环境,请使用easy_install或pip将virtualenv包安装到您系统范围内的Python中:
sudo pip install virtualenv

请注意,这将是您作为root(使用sudo)安装东西到全局site-packages的唯一时间。此后,所有操作都将在您即将创建的virtualenv中进行。
现在为开发您的软件包创建一个virtualenv:
cd ~/pyprojects
virtualenv --no-site-packages foobar-env

这将创建一个目录树~/pyprojects/foobar-env,它是您的虚拟环境。
要激活虚拟环境,请cd进入该目录并source bin/activate脚本
~/pyprojects $ cd foobar-env/
~/pyprojects/foobar-env $ . bin/activate
(foobar-env) ~/pyprojects/foobar-env $

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

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

一个setuptools包

现在开始创建您的包。基本上,您需要一个带有setup.pysetuptools包来正确声明您的包的元数据和依赖项。您可以通过遵循setuptools文档自己完成此操作,也可以使用Paster templates创建包骨架。要使用Paster模板,请将PasteScript安装到您的虚拟环境中:

pip install PasteScript

让我们为新包创建一个源目录,以保持组织有序(也许您想将项目分成几个包,或稍后从源中使用依赖项):

mkdir src
cd src/

现在创建您的软件包,请执行以下操作:
paster create -t basic_package foobar

在交互界面中回答所有问题。大多数问题都是可选的,只需按ENTER键将其保留为默认值即可。

这将创建一个软件包(或更准确地说,一个setuptools分发),名为foobar

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

通常,在内部创建一个Python软件包(即“具有__init__.py的目录”)。这不是必需的,顶级Python软件包的名称可以是任何有效的软件包名称,但通常惯例是将其命名为与分发名称相同。这就是为什么保持两者分开很重要但不总是容易的原因。因为顶级Python软件包名称是:

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

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

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))

在同一目录下有一个使用models.pyclient.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.

将你的包作为开发egg进行安装

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

python setup.py develop

这将安装requests依赖项和您的包作为开发egg。因此,它链接到您的虚拟环境的site-packages,但仍然位于src/foobar,您可以在其中进行更改,并使其立即在虚拟环境中生效,而无需重新安装您的包。
现在回答您最初的问题,使用相对路径导入:我的建议是不要这样做。现在您已经有了一个合适的setuptools包,已安装并可导入,您当前的工作目录就不再重要了。只需声明该对象所在的完全限定名称,例如from foobar.models import Page或类似方式即可。这使得您的源代码对您自己以及阅读您代码的其他人来说更加易读和易于发现。
现在,您可以通过在激活的虚拟环境中的任何位置执行python client.py来运行代码。python src/foobar/foobar/client.py同样有效,您的包已正确安装,您的工作目录不再重要。
如果您想更进一步,甚至可以为CLI脚本创建setuptools入口点。这将在您的虚拟环境中创建一个bin/something脚本,您可以从shell中运行它。

setuptools控制台脚本入口点

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 了。

一旦你(或其他人)在虚拟环境之外真正安装了你的包,入口点将在 /usr/local/bin/run-foo 或类似位置,这样它就会自动在 $PATH 中。

进一步的步骤

建议阅读:


6
我喜欢这篇文章...没想到会得到这样的回答 :) 谢谢 - ducin
1
不客气。我希望我没有搞错任何路径,并且它可以作为一种HOWTO工作。我省略了一些内容,以避免帖子变得更加冗长,所以如果有什么不够清楚的地方,请随时问我。 - Lukas Graf
2
我尽量不留下这样的评论,但是...这真的非常有帮助。谢谢你。 - serverpunk
非常有帮助的答案。它让我相信setuptools变体与virtualenvs结合是最佳选择,无需任何相对导入的技巧。谢谢! - Simon Hessner
3
当你在项目目录中时,你可以使用 pip install -e . 替代 python setup.py develop - Simon Hessner

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 包名称),并使 client 和 server 成为该包的子包。然后 (a) 如果需要,您可以使用相对导入 (from ...client.client import something),(b) 您的项目更适合重新分发,而不是将两个非常通用的包名称放在 Python 模块层次结构的顶层。

谢谢你的建议。嗯,服务器是用Python编写的,客户端则是用Python、JavaScript和PHP编写的。这就是为什么我将客户端目录和服务器目录分开的原因。你认为这是一个好的解决方案吗? - ducin
这取决于你想要实现什么。如果你正在编写一个应用程序,那么按照最合理的方式组织代码可能更好。如果你正在编写一个库,一个可以放在PyPI上供他人安装的包,那么我认为将整个库包含在一个封闭的包中是更好的选择。 - Matt Anderson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接