更好的处理 Python 多工程 import 依赖

来源:岁月联盟 编辑:exp 时间:2011-12-14

话说, 这段时间需要开发一个项目, 新项目对现有的几乎所有项目都有依赖。 豆瓣现存的几个大项目,基本都是围绕豆瓣主站shire的依赖, 也就是说, 其他项目对shire的单项依赖,  这些项目在需要主站shire模块的时候, 无一例外的将shire的工程路径加入到其他工程的sys.path中, 熟悉Python Import的人一定马上会意识到, 这个并不是什么好方法, 因为这样会造成Python Import的搜索路径产生一些不确定因素, 也就是Import  搜索空间污染, 比如说, 我在anduin(豆瓣说)项目中import 主站(shire)的工程,from luzong.user import User, 看起来好像是很magic的东西, 但是, 假如anduin中也有个luzong目录呢(当然这个没有), 这样就会造成搜索冲突, 严重点的可能隐士的错误的import了其他模块。 也就是说, 这个是很不确定的, 很有危险性的一个事情, 那豆瓣那些个大项目是怎么处理这个magic的危险的呢, 答: 每个新工程的顶级目录都与现存的各个工程的顶级目录不重名。

好吧, 一听就知道是个很蛋疼的想法, 我在skype上问了下anrs, 貌似他是遵守这个规定的。但是,随着内部工程量增多, 每个工程的命名都要考虑其他工程的命名是非常非常蛋疼的事情, 在我个人来看是非常要不得的, 那怎么办呢? 在这篇blog中我就循序渐进的介绍下我这两天探索到的方法, 真的非常的magic, 非常的好用:)

首先, 我们想到的是, 为了不污染import的导入空间, 我们何不把每个项目作为一个可以导入的顶层模块, 这样每个项目的内容都在自己独立的命名空间之内, 就不会出现那种很magic的隐式命名空间污染。 好吧, 这个好办, 在每个工程的顶层目录添加一个__init__.py 的空文件, 然后我们再开发一个类似import_project的东西:

def import_project(path):
    sys.path.insert(0, os.path.dirname(path))
    project = __import__(os.path.basename(path))
    sys.path.remove(os.path.dirname(path))
    globals()[os.path.basename(path)] = project
    return project

然后,我们在我们项目的初始化文件中import依赖的project:

conf.__init__.py

shire = import_project(cfg.deps.shire)

使用的时候

from conf import shire
from shire.luzong.user import User

哇塞, 确实很magic, 是不是这样就完了呢, 答案往往不是那么简单, 不是那么如你所愿, 想一想anduin工程, 在豆瓣服务器上, 我们开发的时候部署了很多用来测试的工程, 比如shuo-test, anduin-test, auduin ,  如果按照上面的方法, 线上的肯定没什么问题, 因为shire就是shire, 那如果我测试的那个工程顶级名目换成shire-test呢, 这下就不能处理了, 所以就有了后边的东西, 诸君且往下看:)

经过了半天的苦苦码字, 终于弄出来了一个还能用的东西, 思想就是, 对import部分进行类似勾子的hack, 让他不对这些shuo-test, anduin-test等类似的magic shring有任何的依赖。 于是开发一个函数, 这个函数实现类似普通import的功能, 但不会造成项目之间搜索空间污染的情况, 而且不依赖于这些类似shuo-test, anduin-test 的magic string, 下面我就来介绍一下我的东西, 完了之后再给出一个demo :)

码字了一天开发了下面这个东西 import_helper.py

# libs.import_helper
# -*- coding: utf-8 -*-
# luoweifeng@douban.com

import os
import sys

def import_deps(locals, path, imps=None):
    from conf import cfg
    pro_name, m_path = path[:path.find('.')], path[path.find('.')+1:]
    re_pro_path = cfg.getByPath('deps.' + pro_name)
    re_pro_dir = os.path.dirname(re_pro_path)
    re_pro_name = os.path.basename(re_pro_path)

    # import project
    sys.path.insert(0,re_pro_dir)
    project = __import__(re_pro_name)
    sys.path.remove(re_pro_dir)
    locals[pro_name] = project

    imp_module = re_pro_name + '.' + m_path
    __import__(imp_module)
    module = getattr(project, m_path)

    # add imps to locals
    if imps:
        for imp in imps:
            if isinstance(imp, tuple):
                locals[imp[1]] = getattr(module, imp[0])
            else:
                locals[imp] = getattr(module, imp)

 

 

主要的逻辑在这个文件中, 使用起来非常方便, 而且可以处理from XX import A as B等形式。 下面我就做个demo来演示一下:

1.  创建测试环境

$cd && mkdir  -p test/test11  test22 && cd test

$touch test11/__init__.py  && echo “age = 1″ > test11/app.py

$cd test22

我们这里创建了一个测试目录test, 两个工程test11 和test22, 这个test11 的工程名叫做test(尽管他的目录是test11, 但是就像shuo-test之于anduin一样:)

2. 创建配置文件(这里为了简单期间, 我用了个config的东西)

$mkdir conf && emacs config.cfg

path_prefix : `os.environ['HOME']`
deps :
{
    test : $path_prefix + '/test/test11'
}

$emacs conf/__init__.py

import os
import posixpath
from config import Config

config_file = posixpath.abspath('conf/dimholt.cfg')
cfg = Config(file(config_file))

 

 ok , 我们的配置文件就配置好了, 这里, 我配置了我这个工程依赖一个叫做test的工程, 这个工程的目录在我的主目录下test/test11.

3. 将import_helper.py(文章后边提供下载)放在test22目录下的libs目录下。
4. 使用演示

启动python解释器
>>> from libs.import_helper import import_deps
>>> import_deps(locals=locals(), 'test.app')
这样在你的工程中就可以访问test.app了, 如果想模拟from A import B
>>> import_deps(locals=locals(), 'test.app',['age'])
这样就会导入一个叫做age的变量, 如果还想模拟import A as B
>>> import_deps(locals=locals(), 'test.app',
    [('age', 'g_age')])
再来个全面开花的
>>> import_deps(locals=locals(), 'test.app',
    ['age', ('name', 'screen_name')])

怎么样, 是不是很magic

总结一下, 使用这种方法之后解决了两个工程间import的问题, 一个就是import搜索路径污染, 我们通过把每个依赖的工程的import空间限制在每个工程名下面来实现, 第二个就是工程别名问题, 我们使用比较magic的这个开发的函数来解决。 个人感觉还是非常好用的,除了需要多输入一些东西。 但是好处是非常明显的, 还是值得的。

安, 北京:)

后记: (bugfixed)

话说, 本想今天好好看emacs的, 结果突然想到了一个问题, python locals和globals的处理方式是不同的, globals()是可以被修改的, 而locals()是不能被修改的, 所以上面的是不能用的, 尝试了各种试图修改locals的方法都不是很好, 所以更改了下工程的使用方式, 再加了一个对项目import的支持:

import_helper.py

# libs.import_helper
# -*- coding: utf-8 -*-
# luoweifeng@douban.com

import os
import sys

def import_deps(path, imps=None):
    if '.' not in path:
        pro_name, m_path = path, ''
    else:
        pro_name, m_path = path[:path.find('.')], path[path.find('.')+1:]
    from conf import cfg
    re_pro_path = cfg.getByPath('deps.' + pro_name)
    re_pro_dir = os.path.dirname(re_pro_path)
    re_pro_name = os.path.basename(re_pro_path)

    sys.path.insert(0,re_pro_dir)
    project = __import__(re_pro_name)
    sys.path.remove(re_pro_dir)

    if not m_path:
        return project

    imp_module = re_pro_name + '.' + m_path
    __import__(imp_module)
    module = getattr(project, m_path)

    if imps:
        return [getattr(module, imp) for imp in imps]
    return module

使用的时候:

>>> from libs.import_helper import import_deps

可以直接import 顶层工程module
>>>shire = import_deps('shire')

也可以import任意工程外module
>>>user = import_deps('shire.luzong.user')

可以指定类似from import的方式
>>>User = import_deps('shire.luzong.user', ['User'])

当然如果你想模拟as的操作
>>>ShireUser = import_deps('shire.luzong.user', ['User'])

后边的部分可以是个list, 表示import多个参数
>>>get_user_rank, User = import_deps('shire.luzong.user',

['get_user_rank', 'User'])

总结, 因为locals空间不能修改, 所以使用这个方法来处理, 如果您能确定可以使用globals空间, 那加上globals的也行, 就跟以前那个方法一样, 不过传递globals参数, 在module层是没有locals的, 在module层传递locals其实是用的globals,切记。

 摘自 python.cn

图片内容