從 Ruby 的 method_missing 到雜魚 Common Lisp
在 Ruby 中當調用一個對象不存在的方法時,會觸發解釋器調用該對象的method_missing方法。例如下面的代碼
# -*- encoding: UTF-8 -*-
class A
def method_missing(m, *args, &block)
puts 'now you see me'
end
end
A.new().demo()
運行到方法調用demo()時,由於該方法未定義,於是解釋器會轉而調用方法method_missing,並將相同的方法名(即demo)、參數列表等傳遞給它。其運行結果便是在標準輸出中打印出now you see me這句話。
在 Python 中有method_missing的等價物——__getattr__方法。與 Ruby 不同的是,調用不存在的方法對於 Python 解釋器而言,只是一次尋常的AttributeError異常,然後解釋器會調用對象的__getattr__方法。與前文的 Ruby 代碼類似的寫法如下
class A:
def __getattr__(self, name):
def replacement(*args, **kwargs):
print('now you see me')
return replacement
A().demo()
利用__getattr__可以實現一個透明緩存。例如,假設有一個類Slow,它提供了a、b,以及c等幾個比較耗時的方法。那麼可以實現一個類Cached,由它來代理對Slow類的實例方法的調用、將結果緩存起來加速下一次的調用,再返回給調用方,示例代碼如下
import json
import time
class Slow:
def a(self):
time.sleep(1)
return 2
def b(self):
time.sleep(1)
return 23
def c(self):
time.sleep(1)
return 233
class Cached:
def __init__(self, slow: Slow):
self._slow = slow
self._cache = {}
def __getattr__(self, name):
f = getattr(self._slow, name)
def replacement(*args, **kwargs):
key = json.dumps([args, kwargs])
if key in self._cache:
return self._cache[key]
v = f(*args, **kwargs)
self._cache[key] = v
return v
return replacement
def run_and_timing(f, label):
begin_at = time.time()
v = f()
duration = time.time() - begin_at
print('%s 耗時 %s 秒' % (label, duration))
if __name__ == '__main__':
cached = Cached(Slow())
run_and_timing(lambda: cached.a(), '第一次')
run_and_timing(lambda: cached.a(), '第二次')
在我的機器上運行的結果為
第一次 耗時 1.0018281936645508 秒
第二次 耗時 2.8848648071289062e-05 秒
在 Common Lisp 中有沒有與__getattr__對應的特性呢?有的,那便是廣義函數slot-missing。但可惜的是,它並不適用於調用一個不存在的方法的場景,因為在 Common Lisp 中方法並不屬於作為第一個參數的實例對象,而是屬於廣義函數的(即 Common Lisp 不是單派發、而是多派發的,可以參見這篇文章)。所以調用一個不存在的方法不會導致調用slot-missing,而是會調用no-applicable-method。如下列代碼所示
(defgeneric demo-gf (a)
(:documentation "用於演示的廣義函數。"))
(defclass A ()
())
(defclass B ()
())
(defmethod demo-gf ((a A))
(format t "這是類 A 的實例方法。~%"))
(defmethod no-applicable-method ((gf (eql #'demo-gf)) &rest args)
(declare (ignorable args gf))
(format t "now you see me"))
(defun main ()
(let ((a (make-instance 'B)))
(demo-gf a)))
(main)
假設上述代碼保存在文件no_applicable_method_demo.lisp中,可以像下面這樣運行它們
$ ros run --load ./no_applicable_method_demo.lisp -q
now you see me
當代碼運行到(demo-gf a)時,由於沒有為廣義函數demo-gf定義過參數列表的類型為(B)的方法,因此 SBCL 調用了廣義函數no-applicable-method,後者有applicable 的方法,因此會調用它並打印出now you see me。
如果想利用這一特性來實現透明緩存,那麼必須:
- 為每一個需要緩存的廣義函數都編寫其
no-applicable-method方法; - 手動檢查參數列表的第一個參數的類型是否為特定的類。
如下列代碼所示
(defgeneric a (a))
(defgeneric b (a))
(defgeneric c (a))
(defclass Slow ()
())
(defclass Cached ()
((cache
:accessor cached-cache
:initform (make-hash-table :test #'equal))
(slow
:accessor cached-slow
:initarg :slow)))
(defmethod a ((a Slow))
(sleep 1)
2)
(defmethod b ((a Slow))
(sleep 1)
23)
(defmethod c ((a Slow))
(sleep 1)
233)
(defmethod no-applicable-method ((gf (eql #'a)) &rest args)
(let ((instance (first args)))
(if (typep instance 'Cached)
(let ((slow (cached-slow instance))
(key (rest args)))
(multiple-value-bind (v foundp)
(gethash key (cached-cache instance))
(if foundp
v
(let ((v (apply gf slow (rest args))))
(setf (gethash key (cached-cache instance)) v)
v))))
(call-next-method))))
在我的機器上運行的結果為
CL-USER> (time (a *cached*))
Evaluation took:
1.001 seconds of real time
0.001527 seconds of total run time (0.000502 user, 0.001025 system)
0.20% CPU
2,210,843,642 processor cycles
0 bytes consed
2
CL-USER> (time (a *cached*))
Evaluation took:
0.000 seconds of real time
0.000015 seconds of total run time (0.000014 user, 0.000001 system)
100.00% CPU
29,024 processor cycles
0 bytes consed
2
如果想要讓透明緩存對函數b和c也起作用,則需要重新定義b和c各自的no-applicable-method方法。通過編寫一個宏可以簡化這部分重複的代碼,示例如下
(defmacro define-cached-method (generic-function)
"為函數 GENERIC-FUNCTION 定義它的緩存版本的方法。"
(let ((gf (gensym))
(args (gensym)))
`(defmethod no-applicable-method ((,gf (eql ,generic-function)) &rest ,args)
(let ((instance (first ,args)))
(if (typep instance 'Cached)
(let ((slow (cached-slow instance))
(key ,args))
(multiple-value-bind (v foundp)
(gethash key (cached-cache instance))
(if foundp
v
(let ((v (apply ,gf slow (rest ,args))))
(setf (gethash key (cached-cache instance)) v)
v))))
(call-next-method))))))
然後就可以直接用這個新的宏來為函數a、b、c定義相應的帶緩存的方法了,示例代碼如下
(define-cached-method #'a)
(define-cached-method #'b)
(define-cached-method #'c)
用函數b演示一下,效果如下
CL-USER> (time (b *cached*))
Evaluation took:
1.003 seconds of real time
0.002518 seconds of total run time (0.001242 user, 0.001276 system)
0.30% CPU
2,216,371,640 processor cycles
334,064 bytes consed
23
CL-USER> (time (b *cached*))
Evaluation took:
0.000 seconds of real time
0.000064 seconds of total run time (0.000063 user, 0.000001 system)
100.00% CPU
135,008 processor cycles
0 bytes consed
23
全文完。
閲讀原文