更好的处理 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 = __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

免责声明:文章转载自《更好的处理 Python 多工程 import 依赖》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇SAP名词解释-售达方|送达方java——testNG——工作复习——xml详解下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

随便看看

【FFMPEG】关于硬解码和软解码

一、一些命令1、显示所有可用的硬件加速器[root@tranCodeing~]#ffmpeg-hwaccelsffmpegversion4.1Copyright(c)2000-2018theFFmpegdevelopersbuiltwithgcc4.8.5(GCC)20150623(RedHat4.8.5-39)configuration:--prefix=...

ubuntu用命令连接无线网络

拔下USB网卡并重新插入。无法使用上述步骤成功连接AP。可以使用以下步骤成功连接AP。参考:1.打开无线网卡iwconfigwlan0txpoweron2.列出无线网络iwlistwlan0scan3.如果要连接到网络MyHome,请输入命令iwconfigwlan 0sessiond“MyHome”。如果网络已加密,密码为0123456789,请输入命令i...

如何在linux下安装idea

[通过正式安装包安装]http://www.jetbrains.com/在官方网站上下载相应版本。终极旗舰社区版本,将其解压缩到本地对应目录,然后执行/idea.sh命令。安装后,可以在启动程序中找到创意图标。...

SkyWalking 服务端配置

在安装基于Docker的ElasticSearch时,在为什么需要链接跟踪一章中,我们介绍了几种SkyWalking存储解决方案。官方推荐的解决方案是ElasticSearch,因此我们需要首先安装Elastic搜索。...

JWT加密解密

token2、使用https传输协议。这点是最主要的,前面3的未必能够100%保证安全)JWT由三部分组成,可以把用户名、角色等无关紧要的信息保存到Payload部分。Header:base64enc  //eyAiYWxnIjoiSFMyNTYiLCJUWVBFIjoiSldUIn0=Payload:base64enc  //用户的关键信息eyJ1c2Vy...

Vmware安装Linux打开一直黑屏的四个解决方法

方法1:以管理员身份打开cmd,输入netshwinsocksreset,按enter键,然后重新启动计算机方法2:关闭虚拟机,编辑虚拟机设置,选择硬件中的虚拟机设置、删除加速3D图形前面的复选框,然后再次启动虚拟机。最终的解决方案:我一开始使用的是14版,但上述方法都不能解决黑屏问题。然后我使用版本12来解决这个问题。...