Python 解決循環引用 (Circular import) 錯誤

Python 解決循環引用 (Circular import) 錯誤

事情是這樣子的:

昨天俺在給 bot 寫資料庫,用來保存槍斃名單。根據我在 Kotlin 上的高雅經驗,爲了避免代碼變成臭大糞,俺們需要給一個資料庫開一個 Class,裏面存儲一個 SQLite 的對象,外加一些資料庫的方法,寫出來是這種感覺:

AppDB.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import sqlite3

class AppDB:
__conn: sqlite3.Connection

def init_db(self):
self.__conn = sqlite3.connect('database.db')
然後創建一些表格
self.__conn.commit()

def execute_sql(self, *query) -> sqlite3.Cursor:
return self.__conn.execute(*query)

def commit(self):
self.__conn.commit()

然後資料庫裏面有一張槍斃名單 User 表,每一個 user 有 id 和 name。

當俺們要把人拉出去槍斃的時候,不直接操作 SQL 命令,因爲如果把 SQL 命令寫得到處都是的話,代碼離屎山也就不遠了。俺們應該把常用的 SQL 命令包裝成函數,美名其曰「Data Access Object」,就像是下面這樣:

UserDAO.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from .AppDB import AppDB

class UserDAO:
__db: AppDB

def __init__(self, db: AppDB):
self.__db = db

def get_name_by_id(self, uid: int) -> str | None:
cursor = self.__db.execute_sql('SELECT name FROM User WHERE id=:uid', {'uid': uid})
row = cursor.fetchone()
if row:
return row[1]
else:
return None

def add_user(self, uid: int, name: str):
self.__db.execute_sql('INSERT INTO User(id, name) VALUES (?,?)', (uid, name))
self.__db.commit()

然後,爲了使用資料庫,俺們手中要持有一個 AppDB 的實例,就叫做 mydb,當俺們要取資料的時候就對 mydb 進行操作。方便起見,把 UserDAO 的實例也保存到 AppDB 實例裏面,對 AppDB.py 稍作修改如下:

AppDB.py
1
2
3
4
5
6
7
8
9
10
11
12
import sqlite3
from .UserDAO import UserDAO

class AppDB:
__conn: sqlite3.Connection
userDao: UserDAO

def init_db(self):
self.__conn = sqlite3.connect('database.db')
然後創建一些表格
self.__conn.commit()
self.userDAO = UserDAO(self) # 把 userDAO 創建出來,然後使用方法如下

比如要根據槍斃 id 查詢槍斃 name,就可以:

mydb.userDAO.get_name_by_id(1234)

這樣就能直接拿到名字了,事情很美好,代碼很簡單,不是嗎?
嘗試運行一下就會發現,Python 不高興了,牠說:

ImportError: cannot import name 'AppDB' from partially initialized module 'AppDB' (most likely due to a circular import)

我們的 UserDAO 和 AppDB 互相 import 讓牠很不爽,仔細一看就會發現:

  1. Python 打開 AppDB.py 並 import 了 sqlite3,然後發現要 import UserDAO
  2. Python 打開 UserDAO.py 一看, 要 from .AppDB import AppDB
  3. 於是再次打開 AppDB.py 一看,裏面並沒有名叫 AppDB 的 class(因爲在第 1 步中,init 卡在了第二行,此時 class AppDB 還不存在)
  4. 然後就爆 Error 了

現在要來思考一個問題,是先有 DB 還是先有 DAO?廢話這還用問?肯定先有 DB 啊,因爲一個 DB 可能有好幾個 DAO,比如 UserDAO PasswordDAO 等,所以我們要對 DAO 來動手,讓牠不能直接 import 資料庫的類。

那該怎麼辦呢?俺們再來看一下 UserDAO 的代碼:

UserDAO.py
1
2
3
4
5
6
7
from .AppDB import AppDB

class UserDAO:
__db: AppDB

def __init__(self, db: AppDB):
self.__db = db

然後發現其實我們可以完全不 import,把那些 AppDB 全部刪掉也可以運行,因爲 Python 是動態語言,只要運行的時候引用的對象存在就沒事,資料類型寫不寫都沒關係,,,

但是這樣子的話,Python 就不會根據資料類型來給俺們檢查代碼是否有錯以及自動補全了,令人不爽。

俺們再看看上面說的第 2 步:

  1. Python 打開 UserDAO.py 一看, 要 from .AppDB import AppDB

是不是知道該怎麼辦了,給你三秒鐘的時間思考,,,

俺們只需要讓 Python 在完整初始化完 AppDB.py 之前,不要 import AppDB 就行了。要達到這點,我們可以把 from .AppDB import AppDB 移到下面。當然你不能移到 class 聲明裏面,因爲 Python 會運行聲明檢查,這一行 import 都會被執行掉引發上面的錯誤。

於是俺們把 from .AppDB import AppDB 移動到 def __init__(self, db: AppDB): 裏面,如此,import 只會在 UserDAO 真正實例化的時候纔會真正 import,而此時 AppDB 早已完成 init,自然也不會找不到類而報錯了。

最後的 UserDAO.py 的代碼如下:

UserDAO.py
1
2
3
4
5
6
7
8
class UserDAO:
# 原本在這裏的 __db 成員變量因爲不能註明類型直接刪掉了
def __init__(self, db): # <- 因爲在這一行還沒 import,參數 db 就不能註明類型了
from .AppDB import AppDB # <- 在這裏 import
self.__db: AppDB = db

def get_name_by_id(self, uid: int) -> str | None:
以下省略

哈哈,是不是很無聊啊。希望以後各位寫資料庫的時候一定要把 SQL 命令包裝成函數喔!