从 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
全文完。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。