# [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`