【问题标题】:Better way to access an Assoc list in Common Lisp在 Common Lisp 中访问 Assoc 列表的更好方法
【发布时间】:2016-01-22 13:43:02
【问题描述】:

我正在开发一个天气获取应用程序,但我在使用关联列表时遇到了一些问题。我使用 openweathermap 和 convert-to-json 从我的 get-weather 函数返回了以下列表:

((:COORD (:LON . -123.12) (:LAT . 49.25))
 (:WEATHER
  ((:ID . 500) (:MAIN . "Rain") (:DESCRIPTION . "light rain") (:ICON . "10n")))
 (:BASE . "cmc stations")
 (:MAIN (:TEMP . 281.56) (:PRESSURE . 1001) (:HUMIDITY . 93)
  (:TEMP--MIN . 276.15) (:TEMP--MAX . 283.15))
 (:WIND (:SPEED . 3.1) (:DEG . 100)) (:CLOUDS (:ALL . 90)) (:DT . 1453467600)
 (:SYS (:TYPE . 1) (:ID . 3359) (:MESSAGE . 0.0039) (:COUNTRY . "CA")
  (:SUNRISE . 1453478139) (:SUNSET . 1453510389))
 (:ID . 6173331) (:NAME . "Vancouver") (:COD . 200))

我正在尝试访问 :weather :main rain。目前我正在做:

(cdr (second (second (assoc :weather *assoc-list-from-above*))))

有没有更好的办法?

【问题讨论】:

  • 如果这是使用 cl-json,您可能需要考虑将 json 解码为关联列表以外的公司。我认为文档描述了这样做的方法。例如,如果你定义了一个类,它的字段与你期望的匹配,你可以得到一个实例。
  • 这是一个列表,而不是一个数组。
  • 对不起,我写的时候搞砸了。
  • @phlie 你总是只想要:weather :main - 在这种情况下我会建议(cdr (assoc :main (second (assoc :weather report)))) - 还是你正在寻找一种通用的方式来访问任何字段?
  • 访问任何字段的通用方法。

标签: lisp common-lisp


【解决方案1】:

虽然我非常喜欢 @jkiiski 的 json-bind 解决方案,但我想我也会添加以下选项。

如果查询路径在编译时已知,您可以使用以下宏

(defmacro report-get (report &optional key &rest keys)
  (cond
   ((null key) report)
   ((integerp key) `(report-get (nth ,key ,report)  ,@keys))
   (t `(report-get (cdr (assoc ,key ,report)) ,@keys))))

例子:

CL-USER> (report-get *array-from-above* :weather 0 :main)
"Rain"

CL-USER> (report-get *array-from-above* :coord :lon)
-123.12

CL-USER> (macroexpand '(report-get *array-from-above* :weather 0 :main))
(CDR (ASSOC :MAIN (NTH 0 (CDR (ASSOC :WEATHER *ARRAY-FROM-ABOVE*)))))
T

(report-get *array-from-above* :weather 0 :main)中的0是访问天气项集合中的第一项

编辑:忘了说 - 这个宏是setf-able。

CL-USER> (report-get *array-from-above* :weather 0 :main)
"Rain"
CL-USER> (setf (report-get *array-from-above* :weather 0 :main) "Sunny")
"Sunny"
CL-USER> (report-get *array-from-above* :weather 0 :main)
"Sunny"

可能对您的要求没有用,但很高兴知道。

【讨论】:

    【解决方案2】:

    首先,使用一些中间变量使您对关联列表的访问更加清晰可能就足够了。首先,让我们定义一个我们可以解析的输入字符串(以后请尝试在问题中提供这些,因为它们会帮助其他人提供答案):

    (defparameter *input*
      "{\"coord\":{\"lon\":-123.12,\"lat\":49.25},\"weather\":[{\"id\":500,\"main\":\"Rain\",\"description\":\"light rain\",\"icon\":\"10n\"}],\"base\":\"cmc stations\",\"main\":{\"temp\":281.56,\"pressure\":1001,\"humidity\":93,\"temp--min\":276.15,\"temp--max\":283.15},\"wind\":{\"speed\":3.1,\"deg\":100,\"clouds\":{\"all\":90}},\"dt\":1453467600,\"sys\":{\"sunrise\":1453478139,\"sunset\":1453510389},\"id\":6173331,\"name\":\"Vancouver\",\"cod\":200}")
    

    现在,在我看来,您可以更清晰地提取天气字段的主字段的值:

    (let* ((report (cl-json:decode-json-from-string *input*))
           (weather (first (cdr (assoc :weather report))))
           (main (cdr (assoc :main weather))))
      main)
    ;;=> "Rain"
    

    如果您将像这样重复调用 (cdr (assoc ...)),那么基于提供的路径执行此操作的函数将有很大帮助,如 @987654321 所示@。当然,数组索引(例如,您想要天气列表的第一个元素)会使事情变得不那么干净。

    现在,您还可以解码为 CLOS 实例。 CL-JSON 可以将 JSON 解码为匿名 CLOS 类的实例。单独这样做会稍微改变访问权限,但不会改变太多。尽管如此,它仍然使该字段如何访问它的工作更加清晰。请注意,由于天气值现在是一个数组,而不是一个列表,所以我们使用 (aref array 0) 获取第一个元素,而不是 (first list)

    (json:with-decoder-simple-clos-semantics
      (let ((json:*json-symbols-package* nil))
        (let* ((report (json:decode-json-from-string *input*))
               (weather (aref (slot-value report 'weather) 0))
               (main (slot-value weather 'main)))
          main)))
    ;;=> "Rain"
    

    现在,我认为使用 CLOS 类的真正好处是您可以定义自己的类,然后使用change-class 将 CL-JSON 提供给您的实例更改为您自己的类的实例。 在代码中定义类对文档也有很大帮助。对于一个小例子来说,这似乎没什么大不了的,但在编写可维护的代码时,这非常重要。例如,我们现在可以记录这些插槽的预期类型及其含义。这是一个可行的类定义。请注意,人们对命名约定有不同的看法(例如,是使用 wreport-weather 作为访问器还是 weather)。

    (defclass wreport ()
      ((coord
        :accessor wreport-coord
        :documentation "An object with LON and LAT slots.")
       (weather
        :accessor wreport-weather
        :documentation "An array of objects with ID, MAIN, DESCRIPTION, and ICON slots.")
       (base) ;; and so on ...
       (main)
       (wind)
       (dy)
       (sys)
       (id)
       (name)))
    

    现在您可以使用 change-class 将您的对象变成 wreport,然后您可以使用 wreport-weather(连同aref,因为值还是一个数组)来获取子对象,然后你可以使用slot-value(如上),来获取主字段:

    (json:with-decoder-simple-clos-semantics
      (let ((json:*json-symbols-package* nil))
        (let ((x (json:decode-json-from-string *input*)))
          (let* ((wreport (change-class x 'wreport))
                 (weather (aref (wreport-weather wreport) 0))
                 (main (slot-value weather 'main)))
            main))))
    ;;=> "Rain"
    

    为天气元素定义一个子类可能是有意义的,这并不难。由于我们将顶层事物称为 wreport,因此我们可以将较低层事物称为 subreports:

    (defclass subreport ()
      ((id
        :accessor subreport-id
        :documentation "")
       (main
        :accessor subreport-main
        :documentation "A short string containing a concise description of the weather.")
       (description
        :accessor subreport-description
        :documentation "...")
       (icon
        :accessor subreport-icon
        :documentation "...")))
    

    现在,剩下要做的就是在我们使用 change-class 将顶级报表更改为 wreport 的实例之后,我们需要调用 change-class 为其天气数组中的每个元素转换为 子报告。根据change-class 的文档,调用了update-instance-for-different-class。我们可以定义一个 :after 方法来为我们进行转换:

    (defmethod update-instance-for-different-class :after (previous (current wreport) &rest initargs &key &allow-other-keys)
      "When changing an instance of something into a WREPORT, recursively
    change the elements of the WEATHER array (if bound) to elements of
    SUBREPORT."
      (declare (ignore initargs))
      (when (slot-boundp current 'weather)
        (loop for sub across (wreport-weather current)
           do (change-class sub 'subreport))))
    

    如果您对 CLOS 做的不多,那可能有点吓人,但您实际上是在说“在 change-class 完成所有工作之后,还要再进行一次转换. 现在您可以在两个级别使用域适当的访问器:

    (json:with-decoder-simple-clos-semantics
      (let ((json:*json-symbols-package* nil))
        (let ((x (json:decode-json-from-string *input*)))
          (let* ((wreport (change-class x 'wreport))
                 (subreport (aref (wreport-weather wreport) 0))
                 (main (subreport-main subreport))) ;; (slot-value subreport 'main)))
            main))))
    ;;=> "Rain"
    

    这似乎需要做很多工作,而对于一个快速的小脚本来说,这可能是非常好的。但是,如果您暂时需要这些结构,将文档编入代码中会很有帮助。而且,如果您需要构建任何天气报告,那么拥有一个好的域模型将大有帮助。

    【讨论】:

    • 好吧,这更有意义。我会等着看你的嵌套示例。
    • @phlie 好的,这比预期花费的时间要长一些,但我认为现在已经完成了。
    【解决方案3】:

    如果您经常使用这些值,您可能希望将 alist 转换为 CLOS 对象。 json-library 可能有一些东西可以帮助你做到这一点,但这里有一个手动做天气部分的例子。你可以用类似的方式完成其余的工作。

    (defparameter *data*
      '((:COORD (:LON . -123.12) (:LAT . 49.25))
        (:WEATHER
         ((:ID . 500) (:MAIN . "Rain") (:DESCRIPTION . "light rain") (:ICON . "10n")))
        (:BASE . "cmc stations")
        (:MAIN (:TEMP . 281.56) (:PRESSURE . 1001) (:HUMIDITY . 93)
         (:TEMP--MIN . 276.15) (:TEMP--MAX . 283.15))
        (:WIND (:SPEED . 3.1) (:DEG . 100)) (:CLOUDS (:ALL . 90)) (:DT . 1453467600)
        (:SYS (:TYPE . 1) (:ID . 3359) (:MESSAGE . 0.0039) (:COUNTRY . "CA")
         (:SUNRISE . 1453478139) (:SUNSET . 1453510389))
        (:ID . 6173331) (:NAME . "Vancouver") (:COD . 200)))
    
    ;; Define classes for the parts that you're interested in.
    ;; You can leave out the slots you don't need.
    (defclass weather ()
      ((id :initarg :id :reader id)
       (main :initarg :main :reader main)
       (description :initarg :description :reader description)
       (icon :initarg :icon :reader icon)))
    
    ;; This just decodes the :weather part.
    (defun decode-weather (alist)
      (make-instance 'weather
                     :id (cdr (assoc :id alist))
                     :main (cdr (assoc :main alist))
                     :description (cdr (assoc :description alist))
                     :icon (cdr (assoc :icon alist))))
    
    (defparameter *weather* (decode-weather (second (assoc :weather *data*))))
    
    (id *weather*)          ; => 500
    (main *weather*)        ; => "Rain"
    (description *weather*) ; => "light rain"
    (icon *weather*)        ; => "10n"
    

    编辑:添加到其他选项:您可以查看json-bind 实用程序。

    (defparameter *json* "{\"foo\": {\"bar\": 5, \"quux\": 10, \"foobar\":[1, 2, 3]}}")
    (json:json-bind (foo.bar
                     foo.quux
                     foo.foobar)
        *json*
      (format t "bar: ~a~%quux: ~a~%foobar: ~a~%" 
              foo.bar foo.quux foo.foobar))
    ;; output:   
    ;;   bar: 5
    ;;   quux: 10
    ;;   foobar: (1 2 3) 
    

    借用约书亚的*input*

    (defparameter *input*
      "{\"coord\":{\"lon\":-123.12,\"lat\":49.25},\"weather\":{\"id\":500,\"main\":\"Rain\",\"description\":\"light rain\",\"icon\":\"10n\"},\"base\":\"cmc stations\",\"main\":{\"temp\":281.56,\"pressure\":1001,\"humidity\":93,\"temp--min\":276.15,\"temp--max\":283.15},\"wind\":{\"speed\":3.1,\"deg\":100,\"clouds\":{\"all\":90}},\"dt\":1453467600,\"sys\":{\"sunrise\":1453478139,\"sunset\":1453510389},\"id\":6173331,\"name\":\"Vancouver\",\"cod\":200}")
    
    (json:json-bind (weather.main
                     sys.sunrise) *input*
      (format t "Weather main: ~a~%Sys surise: ~a~%" weather.main sys.sunrise))
    

    另一个编辑:如果您有多个weathers,您最好的选择是使用 CLOS(我的选择或 Joshuas)。如果你还需要使用天气以外的其他字段,你可以结合我给出的两个解决方案:

    (json:json-bind (weather sys.sunrise) *input*
      (format t "sys sunrise: ~a~%" sys.sunrise)
      (loop 
         for w in weather
         for dw = (decode-weather w)
         do (format t "Weather: ~a, ~a~%" (main dw) (description dw))))
    

    如果你不想使用 CLOS,你也可以这样做:

    (defun decode-weather-2 (alist)
      (list (cdr (assoc :main alist))
            (cdr (assoc :description alist))))
    
    (json:json-bind (weather sys.sunrise) *input*
      (format t "sys sunrise: ~a~%" sys.sunrise)
      (loop 
         for w in weather
         for (main description) = (decode-weather-2 w)
         do (format t "Weather: ~a, ~a~%" main description)))
    

    【讨论】:

    • 此方法可行,但 CL-JSON 提供了直接解码为 CLOS 对象的方法。请参阅手册中的The CLOS decoder。这似乎是一种最终比解码为 alist,然后将 alist 解码为 CLOS 对象更清洁的方法。看起来对象解码器生成了一个匿名类的实例(此时您仍然可以执行(slot-value obj 'icon) 等),然后使用目标类对change-class 进行简单调用就可以完成其余的工作。
    • 手册中的 CLOS 解码器没有二维 JSON 的示例。因为异次元,我有点失落。
    • @phile 您能否提供您在问题中使用的 JSON 示例?从输出中,我们可以重建它,但这相当乏味。
    • @Joshua {"coord": {"lon":145.77,"lat":-16.92}, "weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04n"}], "base":"cmc stations", "main":{"temp":293.25,"pressure":1019,"humidity":83,"temp_min":289.82,"temp_max":295.37}, "wind":{"speed":5.1,"deg":150}, "clouds":{"all":75}, "rain":{"3h":3}, "dt":1435658272, "sys":{"type":1,"id":8166,"message":0.0166,"country":"AU","sunrise":1435610796,"sunset":1435650870}, "id":2172797, "name":"Cairns", "cod":200} - 注意weather 是一个数组。
    • @GavinLock 在原问题中,weather 的值在(:WEATHER ((:ID . 500) (:MAIN . "Rain") (:DESCRIPTION . "light rain") (:ICON . "10n"))) 中,这只是一个alist,就是CL-JSON 将对象解码为的。是什么让你说它应该是一个数组?
    【解决方案4】:

    你可以写一个函数,传入一个访问路径:

    (defun get-data (list attributes)
      (flet ((get-it (attribute)
               (if (listp attribute)
                   (destructuring-bind (key extractor) attribute
                     (funcall extractor (cdr (assoc key list))))
                 (cdr (assoc attribute list)))))
        (if (cdr attributes)
            (get-data-list (get-it (first attributes)) (rest attributes))
          (get-it (first attributes)))))
    

    路径中的元素可以是键或(键提取器)的列表。提取器需要是一个从返回的关联列表项中提取数据的函数。

    (defparameter *data*
      '((:COORD (:LON . -123.12) (:LAT . 49.25))
        (:WEATHER
         ((:ID . 500) (:MAIN . "Rain") (:DESCRIPTION . "light rain") (:ICON . "10n")))
        (:BASE . "cmc stations")
        (:MAIN (:TEMP . 281.56) (:PRESSURE . 1001) (:HUMIDITY . 93)
         (:TEMP--MIN . 276.15) (:TEMP--MAX . 283.15))
        (:WIND (:SPEED . 3.1) (:DEG . 100))
        (:CLOUDS (:ALL . 90)) (:DT . 1453467600)
        (:SYS (:TYPE . 1) (:ID . 3359) (:MESSAGE . 0.0039) (:COUNTRY . "CA")
         (:SUNRISE . 1453478139) (:SUNSET . 1453510389))
        (:ID . 6173331)
        (:NAME . "Vancouver")
        (:COD . 200)))
    

    例子:

    CL-USER 22 > (get-data *data* '((:weather first) :main))
    "Rain"
    
    CL-USER 23 > (get-data *data* '((:weather first) :icon))
    "10n"
    
    CL-USER 24 > (get-data *data* '(:main :temp))
    281.56
    

    任务:

    • 使其更加健壮,等等。
    • 处理不同的列表:关联列表、属性列表、项目列表……

    【讨论】:

      猜你喜欢
      • 2016-07-11
      • 2018-11-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多