有時候錯誤訊息只是表象,root cause 並非如字面上的提示如此簡單!今天分享一個跑測試時遇到的坑,使用 django 內建的 test 框架跑測試,所以建立測試 db 由 django 負責,在跑測試前會建立 test db 並把 table 開好。結果這次在跑開表前就看到這個錯誤
django.core.exceptions.ImproperlyConfigured: settings.DATABASES is improperly configured. Please supply the NAME value.
起初懷疑是 database 相關設定被改,於是檢查 settings 部分,發現 code 最近並沒有人做過修改。切到其他 branch 可以跑,但就在目前這隻 branch 會發生問題,所以看來和這隻的修改有關係。
與同事討論後,他說他之前也遇過類似的問題,那時的原因是他在 test 裡把一些 access db 的 code 寫在某些地方,導致 db 還沒建立就去 access db 而發生類似的錯誤。
仔細檢查這次的修改,發現在某些 class 的 __init__ 裡面增加了 default 設定
class AdvanceInvoiceUpdateHandler():
def __init__(self, user, voucher_service=VoucherService()): # set default value
self.user = user
self.voucher_service = voucher_service
而在 VoucherService 的 __init__ 裡面我們有 access db
class VoucherService():
def __init__(self):
company = Company.get_current_company() # access db
但直覺上來說塞在 __init__ 裡面的 default VoucherService() 不應該在還沒建立 AdvanceInvoiceUpdateHandler 的時候 init 啊!
為了確認是否他是兇手,做以下修改
class VoucherService():
def __init__(self):
print(‘I am the ghost.’’)
...
再跑一次測試,果然看到 「I am the ghost.」出現在錯誤訊息之前
做以下修改解決錯誤,把 default VoucherService 往內放即可
class AdvanceInvoiceCreateHandler():
def __init__(self, user, voucher_service=None):
self.user = user
self.voucher_service = voucher_service or VoucherService()
這讓我好奇 python 中 function default arguments 的規則,於是上網研究了下,得到以下結論
- python 的函數是物件
- python 的 def 是「可執行陳述式」
- default arguments 的值會在「跑 def 陳述」時被建立
- mutable object 做 default value 的時候,不能寫在 arguments 旁邊,要到 function 裡面去處理
首先要知道 python 中的東西都是 object,再來要區分出 mutable object 和 immutable object
常見可變物件(指對物件「本身」的值可以改變):
list, dict, set
常見不可變物件(指對物件「本身」的值不可改變):
int, float, string, tuple
對可變物件的 value 做修改,會發現他的記憶體位置並不改變
a = [1, 2]
print(id(a))
>>> 4320093280
a[0] = 2
print(a)
>>> [2, 2]
print(id(a))
>>> 4320093280
這部分很好理解。
那對不可變物件的 value 修改
b = 1000
print(id(b))
>>> 4320093280
b += 10
print(b)
>>> 1010
print(id(b))
>>> 4320093102
原來 b 指到 1000 這個 int object,因為 int object 不可變,所以 python 會先建立一個新的 int object 1010,再把 b 指過去(很像 C 的指標)
回到我們的要討論的初始值問題
如果我們有這個函數:
def test(data=[]):
data.append(1)
return data
會發現這個現象:
>>> test()
[1]
>>> test()
[1, 1]
>>> test()
[1, 1, 1]
這是因為我們設定的 default argment 「data」 所屬的空陣列 [] 在 python 初始化 test 函數時就建立在特殊 class attribute
>>> test.__defaults__
([], )
每次呼叫函數,python 都會從 __defaults__ 裡面去拿預設值,所以
>>> test()
[1]
>>> test.__defaults__
([1], )
>>> test()
[1, 1]
>>> test.__defaults__
([1, 1], )
從這個實驗可以看出
- default data 值是從 class attribute 拿過來設定進去
- 每次跑時對 data 的修改是「可變物件」的修改,因此都會去修改同一個物件
可以看成以下程式
# 呼叫第一次
data = test.__defaults__[0]
data.append(1)
print(data)
>>> [1]
# 呼叫第二次
data = test.__defaults__[0]
data.append(1)
print(data)
>>> [1, 1]
# 後面雷同
如果要設定的 default value 是可變物件,要使用以下方式
def test(data=None):
if data is None:
data = []
把初始值放在函數裡面去給定,這樣就能解決問題!
最後再回頭看一開始遇到的問題
class AdvanceInvoiceUpdateHandler():
def __init__(self, user, voucher_service=VoucherService()):
self.user = user
self.voucher_service = voucher_service
因為我把 voucher_service 的預設值設定為 VoucherService(),所以 python 最初 initial 整份 code 的時候,就先把 VoucherService 初始化放到 __init__ 函數裡的 __default__,那時 django 還沒把 db 建立起來,而 VoucherService 初始化要 access db,程式就發生錯誤了!
真是一個背景知識很高的採坑經驗!
延伸閱讀:
如何解決 Python Decimal.quantize() 發生 InvalidOperation
被新創公司裁員後,我學到的五件事
macOS 中 Python 版本太多如何管理?試試 pyenv 吧!
參考資料
Default Parameter Values in Python
Python constructor and default value [duplicate]
“Least Astonishment” and the Mutable Default Argument
[Python 基礎教學] 什麼是 Immutable & Mutable objects