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