当前位置:编程学习 > python >>

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

话说, 这段时间需要开发一个项目, 新项目对现有的几乎所有项目都有依赖。 豆瓣现存的几个大项目,基本都是围绕豆瓣主站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 = __

补充:Web开发 , Python ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,