Python 動態從文件中導入類或函數的方法


假設模塊文件名是data_used_to_test.py,放在tests文件夾下

文件夾結構如下:

project
    |-tests
        |-data_used_to_test.py

文件內包含一個test_class類:

class test_class():
    def test_func(arg):
        return "hello {}".format(arg)

代碼全部基於 Python3.6.4

  1. 使用imp

    1. 用imp.find_module查找模塊

      In [1]:file, pathname, description = imp.find_module('data_used_to_test', path=['tests/'])
      
      In [2]: file
      Out[2]: <_io.TextIOWrapper name='tests/data_used_to_test.py' mode='r' encoding='utf-8'>
      
      In [3]: pathname
      Out[3]: 'tests/data_used_to_test.py'
      
      In [4]: description
      Out[4]: ('.py', 'r', 1)
      
    2. 用imp.load_module將找到的模塊加載到sys.modules中

      In [5]: mod = imp.load_module('test_load', file, pathname, description)
      In [6]: mod
      Out[6]: <module 'test_load' from 'tests/data_used_to_test.py'>
      

      這時候sys.modules里會多一條'test_load'的記錄,值就是mod的值。

    3. 這時候就可以直接通過mod訪問包內的對象了

      In [7]: mod.test_class().test_func('x')
      Out[7]: 'hello x'
      

      這個方法的優點是

      1. 簡單,容易實現。
      2. 不用對pyc文件做特殊處理

      缺點是可定制性太低。不適合框架使用

      1. 無法動態修改模塊源代碼, 開放的api必須要很穩定,不會經常變動。
      2. 可以訪問的對象一開始就要制定成固定名稱。無法動態注冊訪問。
    4. 上面的問題2可以用getattr解決,讓我們更進一步。

      In [8]: tmp = getattr(mod, 'test_class')
      
      In [9]: tmp
      Out[9]: test_load.test_class
      In [10]: tmp().test_func('l')
      Out[10]: 'hello l'
      

      這樣就可以通過事先在外部模塊中調用准備好的注冊函數,

      把外部模塊中的類或函數注冊到一個全局的單例變量中,

      實現動態的模塊加載和對象訪問。

      但仍然無法解決問題 1.

      所以就需要另一種模塊加載方法。

  2. 顯式的定義一個加載函數

    import imp
    import sys
        
    def load_module(module_name, module_content):
        if fullname in sys.modules:
            return sys.modules[fullname]
            
        mod = sys.modules.setdefault(url, imp.new_module(module_name))
        mod.__file__ = module_name
        mod.__package__ = ''
            
        # if *.py
        code = compile(module_content, url, 'exec')
        # if *.pyc 有問題,我運行一直報錯
        # code = marshal.loads(module_content[8:])
            
        exec(code, mod.__dict__)
        # 2的寫法是 exec code in mod.__dict__
        # 其實就是讓code在環境里運行一下,所以這里可能會有注入漏洞
        return mod
    

    這個函數接受模塊的名字和源碼內容,並使用 compile() 將其編譯到一個代碼對象中, 然后在一個新創建的模塊對象的字典中來執行它。

    下面是這個函數的使用方式:

    In[1]: module_content = open('tests/data_used_to_test.py').read()
        
    In[2]: mod = load_module('test_import', module_content)
    

    后面就和 1.4 一樣了。

    這樣可以同時解決 1.3 中提出的兩個問題。

    因為你是先將源碼作為普通文件讀進來的,也就可以做各種修改后再注冊到sys.modules中。

    Pocsuite使用的就是這種方法。雖然我覺得他們當時可能也沒太弄明白。

    當然這還是有不足的。

    1. 上面的兩種方法都只支持簡單的模塊。並沒有嵌入到通常的import語句中。並不支持更高級的結構比如包。
    2. 不夠酷
  3. 自定義導入器

    PythonCookbook上給了自定義導入器的兩種方式

    1. 創建一個元路徑導入器
    2. 編寫一個鈎子直接嵌入到 sys.path 變量中去

    還沒看明白,PEP302的文檔太長了,大概知道是繼承importlib.abc來做的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM