Python 的格式化字串功能
Tags: python
Published: 2022-02-05 16:42:21

看到有的 Python 書上寫『print() 支援參數格式化功能』頗感詫異, 這個格式化功能根本和 print() 八竿子打不著關係, 而是 str 本身格式化字串的功能, 本文就說明幾種格式化功能, 並比較其中的差異。

## 來自 C 世界的 % 運算器

[`%`](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) 的左邊運算元是字串時, 稱為『**插值 (interpolation)』或『格式化 (formatting)』運算器 (operator)**, 左邊的運算元是可以內含**轉換規格 (conversion specifier)** 的字串, 右邊運算元則是提供用來置換的資料, `%` 會依據字串內的轉換規格一一將置換資料代入, 例如：

```python
>>> "圓周率是 %5.2f" % 3.14159
'圓周率是  3.14'
```

字串中的 '%5.2f' 就是轉換規格, 必須以 '%' 開頭, 接續的 '5' 是所要佔用的最小字元寬度, 也就是轉換後至少會是 5 個字元。小數點後的 '2' 是精確度, 它的意義視要轉換的資料類型而定, 最後的 'f' 表示資料是浮點數, 以浮點數來說, 精確度就是小數的位數。所以此例就是小數 2 位加上小數點本身以及個位數 3 共 4 個字, 轉換後的結果還會在 3 的前面補上一個空格, 滿足至少 5 個字元寬度的轉換規格。

如果要轉換的不只一項資料, 就必須以元素組 (tuple) 作為右邊的運算元, 例如：

```python
>>> "圓周率取 %d 位小數是 %05.2f" % (2, 3.14159)
'圓周率取 2 位小數是 03.14'
```

此例中 '%d' 的 'd' 表示整數資料, 由於沒有指定最小字元寬度, 所以轉換結果不會填補空白。'%05.2f' 中 '5' 前面的 '0' 表示轉換後不足字元寬度的部分會補 '0', 而不是預設的空格。

### 以參數指定最小字元寬度或是精確度

最小字元寬度以及精確度也可以由參數傳入, 此時在轉換規格中要改以 '\*' 表示寬度或是精確度, 例如底下就以參數指定精確度：

```python
>>> "圓周率取 %d 位小數是 %05.*f" % (2, 2, 3.14159)
'圓周率取 2 位小數是 03.14'
```

要注意的是傳給 '\*' 的參數要放在置換的資料前面。

### 以字典提供置換資料

你也可以透過以字串為索引鍵的字典物件提供要轉換的資料, 在轉換規格中就在 '%' 後用小括號標註對應元素的索引鍵, 例如：

```python
>>> "圓周率是 %(pi)5.2f" % {'pi': 3.14159}
'圓周率是  3.14'
```

### 在字串中放入 '%' 字元

如果你的字串中需要放入 '%' 字元, 就必須以 '%%' 來表示, 像是這樣：

```python
>>> "百分比符號是 %%" % ()
'百分比符號是 %'
```

如果沒有依循此規則, 會引發例外：

```python
>>> "百分比符號是 %" % ()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: incomplete format
```

Python 會認為這是不完整的轉換規格。

### 小結

`%` 運算器使用的轉換規格來自 C 程式語言的 printf() 函式, 如果想要瞭解完整的轉換規格, 可以參考〈[printf() 格式字串的使用方法](/qKQufOBqTxyw9iJWp5bmkg)〉一文, 或是 Python 官網上的〈[printf-style String Formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting)〉說明文件。

Python [官網上的說明](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting)指出, 由於 `%` 存在一些問題, 因此建議不要使用, 改用接著會介紹的其他方式來建立格式化字串。
 
## str.format() 方法

str 本身就提供有 [`format()`](https://docs.python.org/3/library/stdtypes.html#str.format) 方法可以建立格式化字串, 它使用大括號來標示轉換規格, 最簡單的用法如下：

```python
>>> "圓周率是 {}".format(3.14159)
'圓周率是 3.14159'
```

你也可以加上最小字元寬度、精確度與型別, 例如：

```python
>>> "圓周率是 {:5.2f}".format(3.14159)
'圓周率是  3.14'
```

注意前面必須以半形冒號 ':' 開頭。如果有多項資料, 只要依序傳入即可：

```python
>>> "圓周率取 {:d} 位小數是 {:5.2f}".format(2, 3.14159)
'圓周率取 2 位小數是  3.14'
```

若有需要, 也可以在左大括號後指定以 0 起算的資料項目序號, 例如：

```python
>>> "圓周率是 {1:5.2f} (小數 {0} 位)".format(2, 3.14159)
'圓周率是  3.14 (小數 2 位)'
```
這樣一來, 就不一定要依照順序傳入資料, 甚至同一項資料還可以代換多次, 或是以不同格式呈現, 例如：

```python
>>> "圓周率一般以 {2:05.2f} (小數 {0} 位) 或 {2:.4f} (小數 {1} 位)".format(2, 4, 3.14159)
'圓周率一般以 03.14 (小數 2 位) 或 3.1416 (小數 4 位)'
>>>
```

你可以看到這裡的格式轉換規格和 `%` 運算器非常類似, 也一樣可以用 '0' 來補足位數。

### 巢狀置換

你甚至可以巢狀置換, 也就是用傳入的參數來置換轉換規格的內容, 像是用參數來動態變換精確度, 而不是寫死在轉換規格中, 例如：

```python
>>> "圓周率取 {0} 位是 {1:5.{0}f}".format(2, 3.1415976)
'圓周率取 2 位是  3.14'
```

### 使用具名參數置換資料

你也可以使用具名參數傳入要轉換的資料, 並在轉換規格中指定參數名稱：

```python
>>> "圓周率是 {pi:05.2f} (小數 {precision} 位)".format(precision=2, pi=3.14159)
'圓周率是 03.14 (小數 2 位)'
```

具名參數一樣可以進行巢狀置換, 例如：

```python
>>> "圓周率是 {pi:05.{precision}f} (小數 {precision}  位)".format(precision=2, pi=3.14159)
'圓周率是 03.14 (小數 2 位)'
```

有關完整的轉換規格, 可以參考 Python 官網上〈[Format Specification Mini-Language](https://docs.python.org/3/library/string.html#format-specification-mini-language)〉一文的說明。

### 在字串中放入 '{' 或是 '}' 字元

如果需要在字串中放入 '{' 或 '}', 必須以 '`{{`' 及 '`}}`' 來表示, 例如：

```python
>>> "大括號是 {{}}".format()
'大括號是 {}'
```

不依此規則會引發例外：

```python
>>> "大括號是 }".format()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Single '}' encountered in format string
>>> "大括號是 {".format()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Single '{' encountered in format string
```

### 使用 \_\_format\_\_() 方法自訂格式

實際上真正格式化的工作是由個別資料的 `__format__()` 方法進行, 例如將浮點數依據指定格式轉換成字串的方法如下：

```python
>>> 3.1415976.__format__('5.2f')
' 3.14'
```

這樣做的好處是每種資料只要負責自己的轉換工作, `format()` 再將個別的轉換結果串接起來即可, 甚至自訂類別只要實作了 `__format__()` 方法, 就可以使用在格式化字串中, 以下即是一個簡單的例子：

```python
>>> class MyFloat:
...     def __init__(self, f):
...             self.f = f
...     def __format__(self, format):
...             return "val:" + str(self.f)
...
>>> "{}".format(MyFloat(3.1415976))
'val:3.1415976'
```

## F 字串

F 字串的正式名稱是『**字面格式字串(formatted string literal)**』, 它可以視為 format() 方法的字面描述版, 可以直接用字面描述字串內容以及置換的資料, 例如：

```python
>>> import math
>>> F"圓周率是 {math.pi}"
'圓周率是 3.141592653589793'
```

開頭的 'F' 也可以改用小寫, 它的轉換規格與 `format()` 方法幾乎一樣, 但是改成以運算式的運算結果來置換內容, 例如：

```python
>>> F"圓周率取 {precision} 位是 {math.pi:5.2f}"
'圓周率取 2 位是  3.14'
```

### 巢狀置換

F 字串同樣可以進行巢狀置換, 像是用另一個運算式來動態指定精確度：

```python
>>> F"圓周率取 {precision} 是 {math.pi:5.{precision}f}"
'圓周率取 2 是  3.14'
```

### 在置換結果中顯示運算式

在開發階段, 我們常常會需要顯示特定物件的內容, F 字串還提供有一個便利的功能, 只要在運算式後加上 '=', 就會在轉換結果的開頭加上 '運算式=' 這樣的字首, 便於區別各項資料, 像是這樣：

```python
>>> F"{math.pi=:5.2f}"
'math.pi= 3.14'
```

如果需要, 也可以在 '=' 前後加上適當的空格, 在輸出時也會原封不動顯示：

```python
>>> F"{precision ** 2 = }"
'precision ** 2 = 4'
```

### 使用字典置換資料

由於是使用運算式來指定置換資料, 所以即使是字典內的資料也可以取用, 例如：

```python
>>> d = {"precision":2, "pi":3.1415976}
>>> F"圓周率取 {d['precision']} 位是 {d['pi']:5.2f}"
'圓周率取 2 位是  3.14'
```

### 在字串中放入 '{' 或是 '}' 字元

如果需要在字串中放入 '{' 或 '}', 必須以 '`{{`' 及 '`}}`' 來表示, 例如：

```python
>>> F"大括號是 }}"
'大括號是 }'
>>> F"大括號是 {{"
'大括號是 {'
```

未遵循此規則一樣會引發例外, 例如：

```python
>>> F"大括號是 {"
  File "<stdin>", line 1
    F"大括號是 {"
             ^
SyntaxError: f-string: expecting '}'
>>> F"大括號是 }"
  File "<stdin>", line 1
    F"大括號是 }"
             ^
SyntaxError: f-string: single '}' is not allowed
```

## 樣板字串 (template string)

在 [string 模組](https://docs.python.org/3/library/string.html#module-string)中提供有 [Template 類別](https://docs.python.org/3/library/string.html#template-strings), 可以建立含有置換用佔位器 (substitution placeholder) 的樣板字串, 每個置換用佔位器必須以 '$' 開頭, 並以具名參數的名稱或是字典的索引鍵表示要置換的資料, 例如:

```python
>>> from string import Template
>>> s = Template("圓周率是 $pi")
>>> s.substitute(pi=3.14159)
'圓周率是 3.14159'
>>> s.substitute({'pi':3.14159})
'圓周率是 3.14159'
```

要注意的是 '$' 之後一定要跟著可以識別資料的名稱, 否則會引發例外, 例如：

```python
>>> s.substitute({'pie':3.14159})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\Program Files\Python\lib\string.py", line 121, in substitute
    return self.pattern.sub(convert, self.template)
  File "D:\Program Files\Python\lib\string.py", line 114, in convert
    return str(mapping[named])
KeyError: 'pi'
```

如果你希望找不到對應的具名參數或是索引鍵時, 不要引發例外, 可以改用 `safe_substitute()` 方法, 它會在置換結果中保留原始的置換項目佔位器, 例如：

```python
>>> s.safe_substitute({'pie':3.14159})
'圓周率是 $pi'
```

如果需要在字串中放入 '\$', 可用 '\$\$' 來表示：

```python
>>> Template("錢幣符號是 $$").substitute({})
'錢幣符號是 $'
```

如果只用 '\$', 一樣會引發例外：

```python
>>> Template("錢幣符號是 $").substitute({})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\Program Files\Python\lib\string.py", line 121, in substitute
    return self.pattern.sub(convert, self.template)
  File "D:\Program Files\Python\lib\string.py", line 118, in convert
    self._invalid(mo)
  File "D:\Program Files\Python\lib\string.py", line 101, in _invalid
    raise ValueError('Invalid placeholder in string: line %d, col %d' %
ValueError: Invalid placeholder in string: line 1, col 7
```

例外的說明文字表示單單一個 '\$' 並不是合乎語法的置換用佔位器。如果希望不要引發例外, 一樣可以使用 `safe_substitute()` 方法, 它會在轉換結果中保留 '\$'：

```python
>>> Template("錢幣符號是 $").safe_substitute({})
'錢幣符號是 $'
```

嚴格來說, 樣板字串提供的並不是格式化字串, 而只是可動態置換內容的字串。


## 結語


看完了本文介紹的四種格式化字串的功能後, 以下是我的建議：

- `%` 運算器既然官方文件都建議不要使用, 我們可以放生它, 除非你是 C 語言的終極擁護者, 不然我們就當成沒有這個功能。
- 如果你不需要動態變化字串內容, 可以使用 F 字串直接置換後建立所需的字串, 否則 `str.format()` 應該會是你最好的選擇。
- 如果你並不需要格式化的功能, 只是需要動態置換資料產生新字串, 那麼可以考慮使用**樣板字串**。