关于编程语言:format不只是格式化

52次阅读

共计 5897 个字符,预计需要花费 15 分钟才能阅读完成。

《实战 Common Lisp》系列次要讲述在应用 Common Lisp 时能派上用场的小函数,心愿能为 Common Lisp 的振兴做一些渺小的奉献。MAKE COMMON LISP GREAT AGAIN。

序言

写了一段时间的 Python 后,总感觉它跟 Common Lisp(下文简称 CL)有亿点点像。例如,Python 和 CL 都反对可变数量的函数参数。在 Python 中写作

def foo(* args):
    print(args)

而在 CL 中则写成

(defun foo (&rest args)
  (print args))

Python 的语法更紧凑,而 CL 的语法表意更清晰。此外,它们也都反对关键字参数。在 Python 中写成

def bar(*, a=None, b=None):
    print('a={}\tb={}'.format(a, b))

而在 CL 中则是

(defun bar (&key (a nil) (b nil))
  (format t "a=~A~8Tb=~A~%" a b))

只管 CL 的 &key 依然更清晰,但申明参数默认值的语法的确是 Python 更胜一筹。

仔细的读者可能发现了,在 Python 中有一个叫做 format 的办法(属于字符串类),而在 CL 则有一个叫做 format 的函数。并且,从下面的例子来看,它们都负责生成格式化的字符串,那么它们有相似之处吗?

答案是否定的,CL 的 format 几乎就是格式化打印界的一股泥石流。

format的根本用法

无妨从下面的示例代码动手介绍 CL 中的format(下文在不引起歧义的状况下,简称为format)的根本用法。首先,它须要至多两个参数:

  • 第一个参数管制了 format 将会把格式化后的字符串打印到什么中央。t示意打印到规范输入;
  • 第二个参数则是本文的配角,名为管制字符串(control-string)。它领导 format 如何格式化。

听起来很神秘,但其实跟 C 语言的 fprintf 也没什么差异。

在管制字符串中,个别会有许多像占位符个别的命令(directive)。正如 Python 的 format 办法中,有各式各样的 format_spec 可能格式化对应类型的数据,管制字符串中的命令也有很多种,常见的有:

  • 打印二进制数字的 ~B,例如(format t "~B" 5) 会打印出 101;
  • 打印八进制数字的 ~O,例如(format t "~O" 8) 会打印出 10;
  • 打印十进制数字的~D
  • 打印十六进制数字的 ~X,例如(format t "~X" 161) 会打印出 A1;
  • 打印任意一种类型的~A,个别打印字符串的时候会用到。

另外,format的命令也反对参数。在 Python 中,能够用下列代码打印右对齐的、左侧填充字符 0 的、二进制模式的数字 5

print('{:0>8b}'.format(5))

format函数也能够做到同样的事件

(format t "~8,'0B" 5)

到这里为止,你可能会感觉 format 的管制字符串,不过就是将花括号去掉、冒号换成波浪线,以及参数语法不一样的 format 办法的翻版罢了。

接下来,让咱们进入 format 的黑科技领域。

format的高级用法

进制转换

后面列举了打印二、八、十,以及十六进制的命令,但 format 还反对其它的进制。应用命令 ~R 搭配参数,format能够打印数字从 2 到 36 进制的所有状态。

(format t "~3R~%" 36)   ; 以 3 进制打印数字 36,后果为 1100
(format t "~5R~%" 36)   ; 以 5 进制打印数字 36,后果为 121
(format t "~7R~%" 36)   ; 以 7 进制打印数字 36,后果为  51
(format t "~11R~%" 36)  ; 以 11 进制打印数字 36,后果为  33
(format t "~13R~%" 36)  ; 以 13 进制打印数字 36,后果为  2A
(format t "~17R~%" 36)  ; 以 17 进制打印数字 36,后果为  22
(format t "~19R~%" 36)  ; 以 19 进制打印数字 36,后果为  1H
(format t "~23R~%" 36)  ; 以 23 进制打印数字 36,后果为  1D
(format t "~29R~%" 36)  ; 以 29 进制打印数字 36,后果为  17
(format t "~31R~%" 36)  ; 以 31 进制打印数字 36,后果为  15

之所以最大为 36 进制,是因为十个阿拉伯数字,加上二十六个英文字母正好是三十六个。那如果不给 ~R 加任何参数,会应用 0 进制吗?非也,format会把数字打印成英文单词

(format t "~R~%" 123) ; 打印出 one hundred twenty-three

甚至能够让 format 打印罗马数字,只有加上 @ 这个修饰符即可

(format t "~@R~%" 123) ; 打印出 CXXIII

天晓得为什么要内置这么冷门的性能。

大小写转换

你,作为一名仔细的读者,可能留意到了,format~X 只能打印出大写字母,而在 Python 的 format 办法中,{:x}能够输入小写字母的十六进制数字。即便你在 format 函数中应用 ~x 也是有效的,因为命令是大小写不敏感的(case insensitive)。

那要怎么实现打印小写字母的十六进制数字呢?答案是应用新的命令~(,以及它配套的命令~)

(format t "~(~X~)~%" 26) ; 打印 1a

配合 :@修饰符,一共能够实现四种大小写格调

(format t "~(hello world~)~%")   ; 打印 hello world
(format t "~:(hello world~)~%")  ; 打印 Hello World
(format t "~@(hello world~)~%")  ; 打印 Hello world
(format t "~:@(hello world~)~%") ; 打印 HELLO WORLD

对齐管制

在 Python 的 format 办法中,能够管制打印出的内容的宽度,这一点在“format的根本用法”中曾经演示过了。如果设置的最小宽度(在下面的例子中,是 8)超过了打印的内容所占据的宽度(在下面的例子中,是 3),那么还能够管制其采纳左对齐、右对齐,还是居中对齐。

在 CL 的 format 函数中,不论是 ~B~D~O,还是~X,都没有管制对齐形式的选项,数字总是右对齐。要管制对齐形式,须要用到~< 和它配套的~>。例如,上面的 CL 代码能够让数字在八个宽度中左对齐

(format t "|~8<~B~;~>|" 5)

打印内容为 |101 |~< 跟后面提到的其它命令不一样,它不耗费管制字符串之后的参数,它只管制 ~<~>之间的字符串的布局。这意味着,即便 ~<~>之间是字符串常量,它也能够起作用。

(format t "|~8,,,'-<~;hello~>|" 5)

下面的代码运行后会打印出 |---hello|:8 示意用于打印的最小宽度;三个逗号(,)之间为空,示意疏忽~< 的第二和第三个参数;第四个参数管制着打印后果中用于填充的字符,因为 - 不是数字,因而须要加上单引号前缀;~;是外部的分隔符,因为它的存在,hello成了最右侧的字符串,因而会被右对齐。

如果 ~<~>之间的内容被 ~; 分隔成了三局部,还能够实现左对齐、居中对齐,以及右对齐的成果

(format t "|~24<left~;middle~;right~>|") ; 打印出 |left    middle     right|

跳转

通常状况下,管制字符串中的命令会耗费参数,比方 ~B~D等命令。也有像 ~< 这样不耗费参数的命令。但有的命令甚至能够做到“一参多用”,那就是 ~*。比方,给~* 加上冒号润饰,就能够让上一个被耗费的参数从新被耗费一遍

(format t "~8D~:*~8D~8D~%" 1 2) ; 打印出       1       1       2

~8D 耗费了参数 1 之后,~:*让下一个被耗费的参数从新指向了 1,因而第二个 ~8D 拿到的参数依然是 1,最初一个拿到了 2。只管管制字符串中看起来有三个 ~D 命令而参数只有两个,却仍然能够失常打印。

format 的文档中一个不错的例子,就是让 ~*~P搭配应用。~P能够依据它对应的参数是否大于 1,来打印出字母 s 或者什么都不打印。配合 ~:* 就能够实现依据参数打印出单词的复数或复数模式的性能

(format t "~D dog~:*~P~%" 1) ; 打印出 1 dog
(format t "~D dog~:*~P~%" 2) ; 打印出 2 dogs

甚至你能够组合一下后面的毕生所学

(format t "~@(~R dog~:*~P~)~%" 2) ; 打印出 Two dogs

条件打印

命令 ~[~]也是成对呈现的,它们的作用是选择性打印,不过比起编程语言中的if,更像是取数组某个下标的元素

(format t "~[~;one~;two~;three~]~%" 1) ; 打印 one
(format t "~[~;one~;two~;three~]~%" 2) ; 打印 two
(format t "~[~;one~;two~;three~]~%" 3) ; 打印 three

但这个个性还挺鸡肋的。想想,你必定不会平白无故传入一个数字来作为下标,而这个作为下标的数字很可能自身就是通过 position 之类的函数计算出来的,而 position 就要求传入待查找的 item 和整个列表 sequence,而为了用上~[ 你还得把列表中的每个元素硬编码到管制字符串中,颇有背道而驰的滋味。

给它加上冒号修饰符之后倒是有点用途,比方能够将 CL 中的真(NIL以外的所有对象)和假(NIL)打印成单词 truefalse

(format t "~:[false~;true~]" nil) ; 打印 false

循环打印

圆括号和方括号都用了,又怎么能少了花括号呢。没错,~{也是一个命令,它的作用是遍历列表。例如,想要打印出一个列表中的每个元素,并且两两之间用逗号和空格离开的话,能够用下列代码

(format t "~{~D~^, ~}" '(1 2 3)) ; 打印出 1, 2, 3

~{~} 之间也能够有不止一个命令,例如下列代码中每次会耗费列表中的两个元素

(format t "{~{\"~A\": ~D~^, ~}}" '(:a 3 :b 2 :c 1))

打印后果为 {"A": 3, "B": 2, "C": 1}。如果把这两个format 表达式拆成用循环写的、不应用 format 的等价模式,大概是上面这样子

; 与 (format t "~{~D~^, ~}" '(1 2 3)) 等价
(progn
  (do ((lst '(1 2 3) (cdr lst)))
      ((null lst))
    (let ((e (car lst)))
      (princ e)
      (when (cdr lst)
        (princ ","))))
  (princ #\Newline))

; 与 (format t "{~{\"~A\": ~D~^, ~}}" '(:a 3 :b 2 :c 1)) 等价
(progn
  (princ "{")
  (do ((lst '(:c 3 :b 2 :a 1) (cddr lst)))
      ((null lst))
    (let ((key (car lst))
          (val (cadr lst)))
      (princ "\"")
      (princ key)
      (princ "\": ")
      (princ val)
      (when (cddr lst)
        (princ ","))))
  (princ "}")
  (princ #\Newline))

这么看来,~{的确能够让使用者写出更紧凑的代码。

参数化参数

在后面的例子中,只管用 ~R 搭配不同的参数能够将数字打印成不同进制的模式,但毕竟这个参数是固化在管制字符串中的,局限性很大。例如,如果我想要定义一个函数 print-x-in-base-y,使得参数x 能够打印为 y 过程的模式,那么兴许会这么写

(defun print-x-in-base-y (x y)
  (let ((control-string (format nil "~~~DR" y)))
    (format t control-string x)))

format 的灵活性,容许使用者将命令的前缀参数也放到管制字符串之后的列表中,因而能够写成如下更简练的实现

(defun print-x-in-base-y (x y)
  (format t "~VR" y x))

而且不只一个,你能够把所有参数都写成参数的模式

(defun print-x-in-base-y (x
                          &optional y
                          &rest args
                          &key mincol padchar commachar commainterval)
  (declare (ignorable args))
  (format t "~V,V,V,V,VR"
          y mincol padchar commachar commainterval x))

祝贺你从新创造了 ~R,而且还不反对:@修饰符。

自定义命令

要在 CL 中打印形如 2021-01-29 22:43 这样的日期和工夫字符串,是一件比拟麻烦的事件

(multiple-value-bind (sec min hour date mon year)
    (decode-universal-time (get-universal-time))
  (declare (ignorable sec))
  (format t "~4D-~2,'0D-~2,'0D ~2,'0D:~2,'0D~%"
          year mon date hour min))

谁让 CL 没有内置像 Python 的 datetime 模块这般欠缺的性能呢。不过,借助 format~/命令,咱们能够在管制字符串中写上要调用的自定义函数,来深度定制打印进去的内容。以打印上述格局的日期和工夫为例,首先定义一个后续要用的自定义函数

(defun yyyy-mm-dd-HH-MM (dest arg is-colon-p is-at-p &rest args)
  (declare (ignorable args is-at-p is-colon-p))
  (multiple-value-bind (sec min hour date mon year)
      (decode-universal-time arg)
    (declare (ignorable sec))
    (format dest "~4D-~2,'0D-~2,'0D ~2,'0D:~2,'0D~%"
            year mon date hour min)))

而后便能够间接在管制字符串中应用它的名字

(format t "~/yyyy-mm-dd-HH-MM/" (get-universal-time))

在我的机器上运行的时候,打印内容为2021-01-29 22:51

后记

format能够做的事件还有很多,CL 的 HyperSpec 中有对于 format 函数的具体介绍,CL 爱好者肯定不容错过。

最初,其实 Python 跟 CL 并不怎么像。每每看到 Python 中的 __eq____ge__,以及__len__ 等办法的奇妙使用时,身为一名 Common Lisp 爱好者,我都会流露出艳羡的神气。纵然 CL 被称为可扩大的编程语言,这些平庸的性能却仍旧无奈不便地做到呢。

浏览原文

正文完
 0