文章出處

話說, 這段時間需要開發一個項目, 新項目對現有的幾乎所有項目都有依賴。 豆瓣現存的幾個大項目,基本都是圍繞豆瓣主站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


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()