在Common Lisp中访问Assoc列表的更好方法

在Common Lisp中访问Assoc列表的更好方法,lisp,common-lisp,Lisp,Common Lisp,我有一个天气预报的应用程序,我正在工作,我有点麻烦与协会名单。我使用openweathermap从get weather函数返回了以下列表,并将其转换为json: ((:COORD (:LON . -123.12) (:LAT . 49.25)) (:WEATHER ((:ID . 500) (:MAIN . "Rain") (:DESCRIPTION . "light rain") (:ICON . "10n"))) (:BASE . "cmc stations") (:MAIN (

我有一个天气预报的应用程序,我正在工作,我有点麻烦与协会名单。我使用openweathermap从get weather函数返回了以下列表,并将其转换为json:

((: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))
我正在尝试访问:天气:主雨。目前我正在做:

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

有更好的方法吗?

如果您经常使用这些值,您可能希望将列表转换为CLOS对象。json库可能有一些东西可以帮助您做到这一点,但这里有一个手动执行天气部分的示例。剩下的你也可以照样做

(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"
编辑:添加到其他选项:您可以查看该实用程序

借用Joshua的
*输入*

(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))
另一个编辑:如果您有多个
weather
s,您最好的选择是使用CLO(我的选择或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)))

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

现在,在我看来,这里是如何更清晰地提取天气场主字段的值的:

(let* ((report (cl-json:decode-json-from-string *input*))
       (weather (first (cdr (assoc :weather report))))
       (main (cdr (assoc :main weather))))
  main)
;;=> "Rain"
如果您像这样重复调用(cdr(assoc…),那么基于提供的路径执行此操作的函数将非常有用,如中所示。当然,数组索引(例如,您想要天气列表的第一个元素)会使事情变得不那么干净

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

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

现在,您可以使用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"
为weather元素定义一个子类可能是有意义的,这并不难。由于我们称顶层内容为wreport,因此我们可以称底层内容为子报告s:

(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,将其转换为子报表。根据的文档,称为。我们可以定义一个:在为我们进行转换的方法之后:

(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方面做得不多,这可能有点吓人,但您实际上是在说“在更改类完成所有工作后,再进行一次转换。现在您可以在两个级别上使用域适当的访问器:

(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"

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

您可以编写一个函数在上,传递访问路径的位置:

(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)))))
路径中的元素可以是键或(键提取器)列表。提取器需要是从返回的assoc列表项中提取数据的函数

(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
任务:

  • 使它更健壮等
  • 处理不同的列表:关联列表、属性列表、带项目的列表

虽然我非常喜欢@jkiiski的
json绑定
解决方案,但我认为我还应该添加以下选项

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

(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
中的
0
(上面的报告get*数组*:weather 0:main)
用于访问天气项目集合中的第一个项目

编辑:忘记提及-此宏可设置为
setf

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"

可能对您的需求不太有用,但很好知道。

如果使用CL JSON,您可能想考虑将JSON解码为一个非关联列表的公司。我认为文档描述了这样做的方法。例如,如果您定义了一个字段与您期望的字段相匹配的类,则可以得到一个实例。它是一个列表,不是数组。很抱歉我写的时候把它弄糟了。@phlie你总是想要
:weather:main
-在这种情况下,我建议
(cdr(assoc:main(second(assoc:weather report)))
-或者你在寻找一种访问任何字段的通用方法吗?一种访问任何字段的通用方法
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
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"