format,不只是格式化

2021-02-14 小打小鬧寫點bug

《實戰 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[1]能夠格式化對應類型的數據,控制字符串中的命令也有很多種,常見的有:

列印二進位數字的~B,例如(format t "~B" 5)會列印出 101;列印八進位數字的~O,例如(format t "~O" 8)會列印出 10;列印十六進位數字的~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)列印成單詞true和false

(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函數的詳細介紹[2],CL 愛好者一定不容錯過。

最後,其實 Python 跟 CL 並不怎麼像。每每看到 Python 中的__eq__、__ge__,以及__len__等方法的巧妙運用時,身為一名 Common Lisp 愛好者,我都會流露出羨慕的神情。縱然 CL 被稱為可擴展的程式語言,這些平凡的功能卻依舊無法方便地做到呢。

如果你想要和我交流,歡迎點擊閱讀原文到我的博客上發表評論。

參考資料[1]

format_spec: https://docs.python.org/3/library/string.html#formatspec

[2]

詳細介紹: http://www.ai.mit.edu/projects/iiip/doc/CommonLISP/HyperSpec/Body/sec_22-3.html

相關焦點

  • Python format()格式化輸出方法詳解
    在創建顯示樣式模板時,需要使用{}和:來指定佔位符,其完整的語法格式為:{ [index][ : [ [fill] align] [sign] [#] [width] [.precision] [type] ] }注意,格式中用 [] 括起來的參數都是可選參數,即可以使用,也可以不使用。
  • 史上最快的字符串格式化庫{fmt}及std::format
    在很多場合,字符串格式化的性能越高越好,比如說伺服器的日誌模塊,每秒鐘要打大量的日誌,性能提高一點點就有很明顯的效果。之前在這塊我也是一直用vsnprintf。我曾經見人用boost::format翻了車。boost::format是C++眾多字符串格式化的庫中非常非常慢的一個。所以不要被boost的名頭騙了。
  • 這可能是將String.format格式化講解的最清楚的一篇文章
    八、對整數進行格式化九 、對浮點數進行格式化十、對日期時間進行格式化一、引言String類的format()方法用於創建格式化的字符串以及連接多個字符串對象。format()方法有兩種重載形式。二、重載// 使用當前本地區域對象(Locale.getDefault()),制定字符串格式和參數生成格式化的字符串String String.format(String fmt, Object... args);// 自定義本地區域對象,制定字符串格式和參數生成格式化的字符串String String.format(Locale locale
  • python實踐分享:格式化字符串時使用.format方式還是「%」
    Python中內置的%操作符和.format方式都可用于格式化字符串。先來看看這兩種具體格式化方法的基本語法形式和常見用法。%操作符根據轉換說明符所規定的格式返回一串格式化後的字符申,轉換說明符的基本形式為:%[轉換標記][寬度[.精確度]]轉換類型。
  • Python2.6版本以後的format()方法格式化字符串
    第七十九節:format()方法格式化字符串Python的版本進化到2.6版以後,創造了format()方法使用字符串對象對字符串進行格式化。format()方法格式化字符串的語法是這樣的:str.format(args)它的參數說明如下:str用於指定字符串模板;args就是要轉換的項,有多個項時,使用逗號分隔。
  • java中使用 SimpleDateFormat 格式化日期
    Hi,大家好久不見,今天我們在這裡給大家介紹一下關於Java的小知識,在Java中我們應該如何使用 SimpleDateFormat 格式化日期並顯示,至於運用呢就不和大家做詳細介紹了;接下來就給大家詳細介紹一下如何實現。那我們該如何創建使用呢?
  • 聽說你還在用SimpleDateFormat格式化日期
    ();SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");Date stationTime = dateFormat.parse(dateFormat.format(svcWorkOrderPo.getPayEndTime()));orderDailyStatisticsPo.setStatisticalDate(stationTime
  • 不一樣的 SQL Server 日期格式化
    SQL Server 日期格式化Intro最近統計一些數據,需要按天/按小時/按分鐘來統計,涉及到一些日期的格式化,網上看了一些文章大部分都是使用 CONVERT 來轉換的,SQL Server 從 2012 開始增加了 FORMAT 方法,可以使用 FORMAT 來格式化日期,更標準化,更具可定製性,而且和 C# 裡的日期格式化差不多,可以直接把 C# 裡日期的格式直接拿過來用FORMAT
  • 不簡單的 SimpleDateFormat
    格式化和解析日期是個(痛苦的)日常任務。每天,它都讓我們很頭疼。在 Java 中格式化和解析日期的一種常見方法是使用 SimpleDateFormat。下面是我們用到的一個公共類。(Date target) { return SIMPLE_DATE_FORMAT.format(target); }}你覺得它會像我們預期的那樣進行工作麼?
  • 一文詳解format()函數
    (args, *kwargs) ——> str 返回格式化的字符串。#1.使用位置參數print('"{} {}".format("hello", "world")-->',"{} {}".format("hello", "world") )   # 不設置指定位置,按默認順序print('"{0} {1}".format("hello", "world")-->',"{0} {
  • Python-字符串格式化
    前言Python2.6 開始,新增了一種格式化字符串的函數 str.format(),它增強了字符串格式化的功能。相對於老版的%格式方法,它有很多優點。1.在%方法中%s只能替代字符串類型,而在format中不需要理會數據類型;2.單個參數可以多次輸出,參數順序可以不相同;3.填充方式十分靈活,對齊方式十分強大;4.官方推薦用的方式,%方式將會在後面的版本被淘汰。
  • 不簡單的 Java SimpleDateFormat
    格式化和解析日期是個(痛苦的)日常任務。每天,它都讓我們很頭疼。在 Java 中格式化和解析日期的一種常見方法是使用 SimpleDateFormat。下面是我們用到的一個公共類。這是我們大多數人在 Java 中格式化日期時常犯的錯誤。為什麼?因為我們不了解線程安全。以下是 Java doc 中關於 SimpleDateFormat 的內容:日期格式是不同步的。建議為每個線程創建獨立的格式實例。如果多個線程同時訪問一個格式,則它必須是外部同步的。Tip:當我們使用實例變量時,應始終檢查其是否是一個線程安全類。
  • VBA關於format函數的用法詳解
    On/Off 當值為0時返回 Off,否則返回 Onformat$("100123","Yes/No") 返回值 On自定義格式參數10. "" 不進行格式化 返回值 原值11. 0 佔位格式化,不足補0format$("100123","0000000") 返回值 010012312. # 佔位格式化
  • java日期和時間的格式化
    在編寫程序時,經常需要對日期進行格式化輸出。使用String類的format方法可以實現對日期和時間的格式化輸出。日期的格式化輸出Java提供了日期格式化轉換符用於支持日期的格式化輸出,格式化轉換符如下表所示: 案例1:使用API庫的Date類獲取當前日期和時間信息,並用format()方法將日期格式化為
  • String.format() 圖文詳解,寫得非常好!
    ()方法用於創建格式化的字符串以及連接多個字符串對象。重載// 使用當前本地區域對象(Locale.getDefault()),制定字符串格式和參數生成格式化的字符串String String.format(String fmt, Object... args);// 自定義本地區域對象,制定字符串格式和參數生成格式化的字符串String String.format
  • 如何使用Python 進行格式化輸出?
    首先明確下,什麼是格式化輸出?當我們通過print 語句或者 write 語句向外設進行輸出的時候,往往並不會僅僅只是輸出一個固定的字符串,而是希望把某些變量的值嵌入到字符串中進行輸出,並且對於輸出格式,可能還需要進行控制,例如如何對齊(左對齊,居中對齊,右對齊),小數點保留幾位,使用十進位還是十六進位,是否要使用科學記數法輸出等。格式化輸出,可以幫你靈活方便的控制輸出樣式。
  • Python字符串類型的格式化
    字符串格式化用於解決字符串和變量同時輸出時的格式安排。字符串是程序向控制臺、網絡、文件等介質輸出運算結果的主要形式之一,為了能提供更好的可讀性和靈活性,字符串類型的格式化是運用字符串類型的重要內容之一。Python語言同時支持兩種字符串格式化方法,我們本課題要學習的是format()格式化方法,採用format()方法進行字符串格式化。
  • Python中format函數用法
    format優點format是python2.6新增的一個格式化字符串的方法,相對於老版的%格式方法,它有很多優點。1.不需要理會數據類型的問題,在%方法中%s只能替代字符串類型2.單個參數可以多次輸出,參數順序可以不相同3.填充方式十分靈活,對齊方式十分強大4.官方推薦用的方式,%方式將會在後面的版本被淘汰format應用一:填充(1)通過位置來填充字符串print("hello {0} i am {1}".format
  • 聊一聊:Python格式化字符串
    格式化操作符(%)"%"是Python風格的字符串格式化操作符,非常類似C語言裡的printf()函數的字符串格式化(C語言中也是使用%)。()Python2.6開始,新增了一種格式化字符串的函數str.format(),通過這個函數同樣可以對字符串進行格式化處理。
  • 搞定字符串——format法篇
    實際上,字符串也是一種序列,但是它是不可變的。也就是說,任何一個字符串只要被創建,那麼你只能訪問,任何對它的修改都是不合法的。那麼這種情況,就要用到今天我們要講的內容:「字符串格式化」format法格式化簡單轉換所謂格式化,就是把可變的數據先在字符串中寫好佔位,然後再去用變量替換。