淺談 Python 的字串格式化

一直很習慣 f"{datetime.now():%Y}" 這類方便的 python 字串格式化工具,但好像一直沒有系統化的理解這些知識,於是在網路上查了一些資料並做了一些實驗,統整在這篇文章。

modulo

相信寫過 C 的一定不陌生這種寫法:

1
2
3
4
s = '1'
i = 2
f = 3.0
print('%s %d %f' % (s, i, f)) # 1 2 3.000000

缺點就是 % 是一個 binary operator,代表說它只能左右各接受一個參數,也就是說如果要帶入多個參數時,就必須要使用 Tuple 或 Dictionary 之類的資料結構把多個參數當作一個參數傳入,這樣做就會因此失去了一些彈性。

因為很明顯的如果我們在後方使用 Tuple 的話,就沒辦再使用 Dictionary 傳入,也就沒辦法結合兩種資料結構的優勢。

於是乎 format 就出現了。

format

Python 3.0+

來看看 format 是怎麼用的:

1
2
3
4
s = '1'
i = 2
f = 3.0
print('{0} {i} {1}'.format(s, f, i=i)) # 1 2 3.0

format 看 ast 就可以很明顯的看出來是一個 function call 的樣子了,sf 跑到 args,i 跑到 keywords,其實這也就對上 function 上的 *args, **kwargs 這兩個參數。

既然是 function call,後面也是可以放 expression 的:

1
2
3
4
s = '1'
i = 2
f = 3.0
print('{0} {i} {1}'.format(s + 'hi', f - 100, i=i + 10)) # 1hi 12 -97.0

Format Specifiers

前面討論的是某個 instance 要擺在字串的哪個位置,至於那個 instance 要用什麼樣子的方式來呈現,可以透過 Format Specifiers 來決定。

PEP 3101 有提到一些 Standard Format Specifiers,我們可以直接拿出來用,像是一些進制轉換:

1
2
3
4
5
i = 10
print('{0:b}'.format(i)) # 1010
print('{0:d}'.format(i)) # 10
print('{0:x}'.format(i)) # a
print('{0:X}'.format(i)) # A

也可以用一些比較特殊的用法,有時候需要動態決定空格的數量,這時候可以把變數直接當作 Format Specifiers 的一部分,最後再交給 format 格式化:

1
2
3
i = 10
j = 20
"{0:<{1}}".format(i, j) # '10                  '

那我們可以自幹 Format Specifiers 嗎?相信有用過 str 的人也一定幹過自訂 __str__ 這個 function 的事,像是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class mystr:
    def __init__(self):
        self.s = "s"

    def __str__(self):
        return self.s
    
s = mystr()
print("%s" % s) # s
print(s)        # s

format 其實也是相同道理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class mystr:
    def __init__(self):
        self.s = "1234"

    def __format__(self, spec):
        print(spec) # s1,s2,s3
        return str(self)

    def __str__(self):
        return self.s

s = mystr()
print("{0:s1,s2,s3}".format(s)) # 1234

可以透過 parse spec 這個變數來選擇你要 return 回什麼字串。以 datetime 為例:

1
2
3
4
5
6
7
8
    ...
    def __format__(self, fmt):
        if not isinstance(fmt, str):
            raise TypeError("must be str, not %s" % type(fmt).__name__)
        if len(fmt) != 0:
            return self.strftime(fmt)
        return str(self)
    ...

他會呼叫 strftime 這個 function 來格式化字串,一切都變得非常合理了。

f-strings

Python 3.6+

f-strings 其實就是 format 的語法糖,讓你少打 “.format()” 這幾個字,而且也讓整個字串更連續,更易閱讀,以上面的 Standard Format Specifiers 為例:

1
2
3
4
5
i = 10
print(f'{i:b}') # 1010
print(f'{i:d}') # 10
print(f'{i:x}') # a
print(f'{i:X}') # A

細節請參考 PEP 498 – Literal String Interpolation

String Template

最後是比較少用到的 String Template,跟 formatter 的概念類似,只不過他不支援 format specifiers:

1
2
3
from string import Template
t = Template('$a $b $c')
t.substitute(a=1, b=3.0, c="1")

在網路上看到常用的情境是要提供給 User 輸入的資料時,用 String Template 可以提供較好的安全性,像是:

1
2
3
4
5
6
7
SECRET_KEY = 'super secret key'

def func():
    pass

user_input = input()
print(user_input.format(func=func))

如果 user_input 是 {func.__globals__[SECRET_KEY]},就可以直接拿到 super secret key,這時候如果換成 String Template:

1
2
3
4
5
6
7
SECRET_KEY= 'super secret key'

def func():
    pass

user_input = input()
print(Template(user_input).substitute(func=func))

就算 user_input 是 {$func.__globals__[SECRET_KEY]} 也因為在 String Template 中無法執行 Expression 的緣故而只能得到 {<function func at 0x7f291b7100d0>.__globals__[SECRET_KEY]}

總結

其實自從知道 f-string 後,就很少再使用其他種字串格式化方法了,其中一個原因也是因為如果用多打 format 這幾個字,很常就會因為字數過長被 Formatter 給拆成兩行,而且 Formatter 總是會把 Code 拆得很醜,差一個換行、空白還沒對齊真的會讓人看得很痛苦,強迫症無法忍 Orz。

總之紀錄一下一些實驗的過程讓未來的自己參考吧,Python 更新的這麼頻繁,或許過了幾個版本後又長的完全不一樣了。

參考資料

5.6.2. String Formatting Operations

PEP 3101 – Advanced String Formatting

PEP 498 – Literal String Interpolation

Be Careful with Python’s New-Style String Format

comments powered by Disqus