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