---
# System prepended metadata

title: '[Python] import不在package中的多個同名module'
tags: [python]

---

# [Python] import不在package中的多個同名module
## 問題
有一次需要import 多個**同名**檔案，而這些檔案卻又不在package內，只是零散的module。

原先打算透過 `sys.path` 把module的目錄加進去直接import，做法如下：
```python=
import sys

sys.path.insert(0, './folder_01')
import utils as utils_01

sys.path.insert(0, './folder_02')
import utils as utils_02

print(utils_01)
print(utils_02)
```
> <module 'utils' from './folder_01/utils.py'>
> <module 'utils' from './folder_01/utils.py'>

folder_01和folder_02底下都各有一個utils.py，輸出結果卻是相同的，也就是folder_02底下的utils.py沒有成功被import。

看起來是已經load過的module會放到`sys.modules`裡面，而已經存在的module就不會再次load，而是拿現有的來用。 

所以如果只用`sys.path`看起來不能處理這個問題，而且如果重複交替使用這兩個module的時候，`sys.path`不做額外處理就會不斷被加長。


---
## Delete imported module
刪除`sys.modules`內已經被load過的module。
```python=
import sys

sys.path.insert(0, './folder_01')
import utils as utils_01
print(utils_01)

del sys.modules['utils']  # 加入這行

sys.path.insert(0, './folder_02')
import utils as utils_02
print(utils_02)
```
> <module 'utils' from './folder_01/utils.py'>
> <module 'utils' from './folder_02/utils.py'>

這樣的結果算是正確了，但每次使用都得delete module，並確保新的module目錄在`sys.path`中要優先被找到。

---
## 使用importlib
在 https://stackoverflow.com/questions/6031584/importing-from-builtin-library-when-module-with-same-name-exists 這篇找到的做法：
```python=
import importlib
import sys

def import_same_name(file_path, module_name):
    spec = importlib.util.spec_from_file_location(module_name, file_path)
    module = importlib.util.module_from_spec(spec)
    sys.modules[module_name] = module
    spec.loader.exec_module(module)
    return module

module_01 = import_same_name('./folder_01/utils.py', 'utils_01')
module_02 = import_same_name('./folder_02/utils.py', 'utils_02')
print(module_01)
print(module_02)
```
> <module 'utils' from './folder_01/utils.py'>
> <module 'utils' from './folder_02/utils.py'>

---
## 避免重複import及自動改名的機制
延續上一個部分，在`import_same_name` function加入一些更便於使用的機制：
* 先去`sys.modules`找是否有module的path與要加入的module path重複，若有則直接使用。
* module_name自動加入postfix編號，這樣一來使用`import_same_name`時就不用額外幫module取不同的名字。
```python=
import importlib
import sys

def search_module_by_path(file_path):
    '''
    If module file path exists in sys.modules, return it.
    '''
    for v in sys.modules:
        try:
            if file_path == v.__file__:
                return v
        except AttributeError:
            pass
    return None

def import_same_name(file_path, module_name):
    module = search_module_by_path(file_path)
    if module is not None:  # module has been imported.
        return module

    # 'utils' -> 'utils_2' if 'utils_0' and 'utils_1' exists.
    i = 0
    module_name += '_{:d}'
    while module_name.format(i) in sys.modules:
        i += 1
    module_name = module_name.format(i)  
    
    spec = importlib.util.spec_from_file_location(module_name, file_path)
    module = importlib.util.module_from_spec(spec)
    sys.modules[module_name] = module
    spec.loader.exec_module(module)
    return module

module_01 = import_same_name('./folder_01/utils.py', 'utils')
module_02 = import_same_name('./folder_02/utils.py', 'utils')
print(module_01)
print(module_02)
```
> <module 'utils_0' from './folder_01/utils.py'>
> <module 'utils_1' from './folder_02/utils.py'>

---
## 紀錄
* 在使用[Delete imported module](#Delete-imported-module)的方法時，如果先用`utils_01` create instance後，再照步驟import utils_02，先前的instance是可以正確運作的。
* 看起來最後的方法可能也可以透過以下方法完成，但很不方便就是了。
```python=
import sys
import utils
sys.modules['utils_01'] = sys.modules.pop('utils')
import utils_01
```

###### tags: `python`