1. 程式人生 > 實用技巧 >前後端分離開發中動態選單的兩種實現方案

前後端分離開發中動態選單的兩種實現方案

小時候,常被一些可笑的問題困擾——儘管成年以後面臨的疑惑更多,但似乎是因為已經適應了在迷茫中前行,對於未解的問題反倒是失去了那種急於想知道答案的迫切感。比如,站在兩面相對的鏡子中間,會看到無數個自己嗎?對於少時的我,這的確是一個非常魔幻的問題,直到理解了光量子能量衰減,才算找到了答案。

近日,有同學諮詢Python物件的迴圈引用以及垃圾回收問題,結合前些日子遇到的迴圈呼叫和迴圈匯入問題,在整理答案的時候,我忽然意識到,這幾個問題居然和困惑我多年的“兩面鏡子”問題居然有相通之處:看起來都有些魔幻,轉身即是真實的世界!

1. 走向毀滅的函式迴圈呼叫

如果多個函式相互呼叫,構成閉環,就形成了函式的迴圈呼叫。下面的例子中,函式a在其函式體中呼叫了函式b,而函式b在其函式體中又呼叫了函式a,這就是典型的函式迴圈呼叫。

>>> def a():
		print('我是a')
		b()
	
>>> def b():
		print('我是b')
		a()
	
>>> a()

此種情況下,呼叫函式(無論是a函式還是b函式),會發生什麼呢?

>>> a()
我是a
我是b
我是a
我是b
...... # 此處省略了一千餘行
Traceback (most recent call last):
  File "<pyshell#64>", line 1, in <module>
    a()
  File "<pyshell#59>"
, line 3, in a b() ...... # 此處省略了兩千餘行 RecursionError: maximum recursion depth exceeded while pickling an object

很快你就會發現,執行出現了問題,系統連續丟擲異常,大約滾動了幾千行之後,終於結束了執行。最後的提示是:

RecursionError: maximum recursion depth exceeded while pickling an object

意思是說,發生了遞迴錯誤,在序列化(pickle)物件時超過了最大遞迴深度。

原來,迴圈呼叫類似於遞迴呼叫,為了保護堆疊不會溢位,Python環境一般都會設定遞迴深度保護,一旦查過遞迴深度,就會丟擲遞迴錯誤,然後再一層一層退出堆疊。這就是螢幕滾動幾千條錯誤資訊的原因。

關於Python環境遞迴深度,可以通過sys模組檢視和設定。

>>> import sys
>>> sys.getrecursionlimit()
1000
>>> sys.setrecursionlimit(500)
>>> sys.getrecursionlimit()
500

2. 同生共死的物件迴圈引用

函式的迴圈呼叫不難理解,而物件的迴圈引用就有點費解了。什麼是物件的迴圈引用呢?當一個物件被建立時(比如例項化一個類),Python會為這個物件設定一個引用計數器。如果這個物件被引用,比如被關聯到一個變數名,則該物件的引用計數器加1,如果關聯關係取消,則該物件的引用計數器減1。當一個物件的引用計數器為1時(關於這一點,僅憑個人觀察得出,未見權威說法),系統將自動回收該物件。這就是Python的垃圾回收機制。下面的程式碼,藉助於sys模組,可以直觀地看到一個列表物件的引用計數器的變化。

>>> import sys
>>> a = list('abc')
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
>>> del b
>>> sys.getrefcount(a)
2

當多個物件存在相互間的成員引用,一旦形成閉環的時候,就會發生所謂物件的迴圈引用。我們來看一個例子:a和b是類A的兩個例項物件,del這兩個物件的時候,將會呼叫物件的__del__方法,最後顯示“執行結束”。

class A:
    def __init__(self, name, somebody=None):
        self.name = name
        self.somebody = somebody
        print('%s: init'%self.name)
    def __del__(self):
        print('%s: del'%self.name)

a = A('a')
b = A('b')

del a
del b

print('執行結束')

執行結果正如我們所希望的那樣。

a: init
b: init
a: del
b: del
執行結束

然而,當我們建立了例項a和b之後,如果將a.somebody指向b,將b.somebody指向a,那麼就產生了例項間成員相互引用形成閉環的情況。

class A:
    def __init__(self, name, somebody=None):
        self.name = name
        self.somebody = somebody
        print('%s: init'%self.name)
    def __del__(self):
        print('%s: del'%self.name)

a = A('a')
b = A('b')

a.somebody = b
b.sombody = a

del a
del b

print('執行結束')

執行這段程式碼,你會發現,del這兩個物件的時候,物件的__del__方法並沒有被立即執行,而是程式結束之後才被執行的。

a: init
b: init
執行結束
a: del
b: del

這意味著,在程式執行期間,應該被回收的記憶體並沒有正確回收。這樣的問題,屬於記憶體洩漏,應該給予高度重視。通常,我們可以使用gc模組強制回收記憶體。

import gc

class A:
    def __init__(self, name, somebody=None):
        self.name = name
        self.somebody = somebody
        print('%s: init'%self.name)
    def __del__(self):
        print('%s: del'%self.name)

a = A('a')
b = A('b')

a.somebody = b
b.sombody = a

del a
del b

gc.collect()

print('執行結束')

再看執行結果,一切正常了。

a: init
b: init
a: del
b: del
執行結束

3. 轉圈推磨的模組迴圈匯入

相對而言,模組的迴圈匯入的情況一般極少發生。如果發生,一定是模組的功能分割不合理造成的,通過調整模組的定義,可以很容地解決問題。下面用一個最精簡的例子,來演示一下模組迴圈匯入是如何產生的。

名為a.py的指令碼檔案內容如下:

import b

MODULE_NAME = 'a'
print(b.MODULE_NAME)

名為b.py的指令碼檔案內容如下:

import a

MODULE_NAME = 'b'
print(a.MODULE_NAME)

兩個指令碼互相引用,並且各自使用了對方定義的常量MODULE_NAME。無論我們執行哪個指令碼,都會因為模組的迴圈匯入而無法正確執行。

Traceback (most recent call last):
File “a.py”, line 1, in
import b
File “D:\temp\csdn\b.py”, line 1, in
import a
File “D:\temp\csdn\a.py”, line 4, in
print(b.MODULE_NAME)
AttributeError: module ‘b’ has no attribute ‘MODULE_NAME’