動態

詳情 返回 返回

從 Ruby 的 method_missing 到雜魚 Common Lisp - 動態 詳情

從 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,它提供了ab,以及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

如果想利用這一特性來實現透明緩存,那麼必須:

  1. 為每一個需要緩存的廣義函數都編寫其no-applicable-method方法;
  2. 手動檢查參數列表的第一個參數的類型是否為特定的類。

如下列代碼所示

(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

如果想要讓透明緩存對函數bc也起作用,則需要重新定義bc各自的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))))))

然後就可以直接用這個新的宏來為函數abc定義相應的帶緩存的方法了,示例代碼如下

(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

全文完。

閲讀原文

Add a new 評論

Some HTML is okay.