Python 四種字符串格式化方式
格式化(formatting)是指把數據填寫到預先定義的文本模板里面,形成一條用戶可讀的消息,并把這條消息保存成字符串的過程。
% 格式化
Python 里面最常用的字符串格式化方式是采用 % 格式化操作符。
這個操作符左邊的文本模板叫作格式字符串(format string),我們可以在操作符右邊寫上某個值或者由多個值所構成的元組(tuple),用來替換格式字符串里的相關符號。
例如,下面這段代碼通過 % 操作符把難以閱讀的二進制和十六進制數值,顯示成十進制的形式。
a = 0b10111011
b = 0xc5f
print('Binary is %d, hex is %d' % (a, b))
# >>>
# Binary is 187, hex is 3167
格式字符串里面可以出現 %d 這樣的格式說明符,這些說明符的意思是,% 右邊的對應數值會以這樣的格式來替換這一部分內容。格式說明符的寫法來自 C 語言的 printf 函數,所以,常見的 printf 選項都可以當成 Python 的格式說明符來用,例如 %s、%x、%f 等,此外還可以控制小數點的位值,并指定填充與對齊方式。
但是,C 風格的格式字符串,在 Python 里有四個缺點。
第一個缺點是,如果 % 右側那個元組里面的值在類型或順序上有變化,那么程序可能會因為轉換類型時發生不兼容問題而出現錯誤。例如一個簡單的例子:
key = 'my_var'
value = 1.234
formatted = '%-10s = %.2f' % (key, value)
print(formatted)
# >>>
# my_var = 1.23
但如果把 key 跟 value 互換位置,那么程序就會在運行時出現異常。
reordered_tuple = '%-10s = %.2f' % (value, key)
# >>>
# TypeError: must be real number, not str
如果 % 右側的寫法不變,但左側那個格式字符串里面的兩個說明符對調了順序,那么程序同樣會發生這個錯誤。
reordered_string = '%.2f = %-10s' % (key, value)
# >>>
# TypeError: must be real number, not str
要想避免這種問題,必須經常檢查 % 操作符左右兩側的寫法是否相互兼容。
第二個缺點是,在填充模板之前,經常要先對準備填寫進去的這個值稍微做一些處理,但這樣一來,整個表達式可能就會寫得很長,讓人覺得比較混亂。
下面這段代碼用來羅列廚房里的各種食材,現在的這種寫法并沒有對填入格式字符串里面的那三個值(也就是食材的編號 i、食材的名稱 item,以及食材的數量 count)預先做出調整。
pantry = [('avocados', 1.25),('bananas', 2.5),('cherries', 15),
]
for i, (item, count) in enumerate(pantry):print('#%d: %-10s = %.2f' % (i, item, count))
# >>>
# #0: avocados = 1.25
# #1: bananas = 2.50
# #2: cherries = 15.00
如果想讓打印出來的信息更好懂,那可能得把這幾個值稍微調整一下,但是調整之后,% 操作符右側的那個三元組就特別長,所以需要多行拆分才能寫得下,這會影響程序的可讀性。
for i, (item, count) in enumerate(pantry):print('#%d: %-10s = %d' % (i + 1,item.title(),round(count)))
# >>>
# #1: Avocados = 1
# #2: Bananas = 2
# #3: Cherries = 15
第三個缺點是,如果想用同一個值來填充格式字符串里的多個位置,那么必須在 % 操作符右側的元組中相應地多次重復該值。
template = '%s loves food. See %s cook.'
name = 'Max'
formatted = template % (name, name)
print(formatted)
# >>>
# Max loves food. See Max cook.
如果想在填充之前把這個值修改一下,那么必須同時修改多處才行,例如,如果這次要填的不是 name 而是 name.title(),那就必須提醒自己,要把所有的 name 都改成 name.title()。若是有的地方改了,有的地方沒改,那輸出的信息可能就不一致了。
template = '%s loves food. See %s cook.'
name = 'brad'
formatted = template % (name.title(), name.title())
print(formatted)
# >>>
# Brad loves food. See Brad cook.
為了解決上面提到的一些問題,Python 的 % 操作符允許我們用 dict 取代 tuple,這樣的話,我們就可以讓格式字符串里面的說明符與 dict 里面的鍵以相應的名稱對應起來,例如 %(key)s 這個說明符,意思就是用字符串(s)來表示 dict 里面名為 key 的那個鍵所保存的值。下面通過這種辦法解決剛才講的第一個缺點,也就是 % 操作符兩側的順序不匹配問題。
key = 'my_var'
value = 1.234
old_way = '%-10s = %.2f' % (key, value)
new_way = '%(key)-10s = %(value).2f' % {'key': key, 'value': value} # Original
reordered = '%(key)-10s = %(value).2f' % {'value': value, 'key': key} # Swapped
assert old_way == new_way == reordered
這種寫法還可以解決剛才講的第三個缺點,也就是用同一個值替換多個格式說明符的問題。改用這種寫法之后,我們就不用在 % 操作符右側重復這個值了。
name = 'Max'
template = '%s loves food. See %s cook.'
before = template % (name, name) # Tuple
template = '%(name)s loves food. See %(name)s cook.'
after = template % {'name': name} # Dictionary
assert before == after
但是,這種寫法會讓剛才講的第二個缺點變得更加嚴重,因為字典格式字符串的引入,我們必須給每一個值都定義鍵名,而且要在鍵名的右側加冒號,格式化表達式變得更加冗長,看起來也更加混亂。把不采用 dict 的寫法與采用 dict 的寫法對比一下,可以更明確地意識到這種寫法的缺點。
for i, (item, count) in enumerate(pantry):before = '#%d: %-10s = %d' % (i + 1, item.title(), round(count))after = '#%(loop)d: %(item)-10s = %(count)d' % {'loop': i + 1, 'item': item.title(), 'count': round(count)}assert before == after
第四個缺點是,把 dict 寫到格式化表達式里面會讓代碼變多。每個鍵都至少要寫兩次:一次是在格式說明符中,還有一次是在字典中作為鍵,另外,定義字典的時候,可能還要專門用一個變量來表示這個鍵所對應的值,而且這個變量的名稱或許也和鍵名相同,這樣算下來就是三次了。
soup = 'lentil'
formatted = 'Today\'s soup is %(soup)s.' % {'soup': soup}
print(formatted)
# >>>
# Today's soup is 1enti1.
除了要反復寫鍵名,在格式化表達式里面使用 dict 的辦法還會讓表達式變得特別長,通常必須拆分為多行來寫,同時,為了與格式字符串的多行寫法相對應,定義字典的時候,也要一行一行地給每個鍵設定對應的值。
內置的 format 函數與 str 類的 format 方法
Python3 添加了高級字符串格式化(advanced string formatting)機制,它的表達能力比老式 C 風格的格式字符串要強,且不再使用 % 操作符。
下面這段代碼,演示了這種新的格式化方式。在傳給 format 函數的格式里面,逗號表示顯示千位分隔符,^ 表示居中對齊。
a = 1234.5678
formatted = format(a, ',.2f')
print(formatted)b = 'my string'
formatted = format(b, '^20s')
print('*', formatted, '*')
# >>>
# 1,234.57
# * my string *
如果 str 類型的字符串里面有許多值都需要調整格式,則可以調用 str 的新 format 方法。該方法不使用 %d 這樣的 C 風格格式說明符。而是把格式有待調整的那些位置在字符串里面先用 {} 代替,然后按從左到右的順序,把需要填寫到那些位置的值傳給 format 方法,使這些值依次出現在字符串中的相應位置。
key ='my_var'
value = 1.234
formatted = '{} = {}'.format(key, value)
print(formatted)
# >>>
# my_var = 1.234
可以在 {} 里寫個冒號,然后把格式說明符寫在冒號的右邊,用以規定 format 方法所接收的這個值應該按照怎樣的格式來調整。
formatted = '{:<10} = {:.2f}'.format(key, value)
print(formatted)
# >>>
# my_var = 1.23
這種寫法的效果可以這樣理解:系統先把 str.format 方法接收到的每個值傳給內置的 format 函數,并找到這個值在字符串里對應的 {},同時將 {} 里面寫的格式也傳給 format 函數,例如系統在處理 value 的時候,傳的就是 format(value,‘.2f’)。然后,系統會把 format 函數所返回的結果寫在整個格式化字符串 {} 所在的位置。另外,每個類都可以通過 __format__
這個特殊的方法定制相應的邏輯,這樣的話,format 函數在把該類實例轉換成字符串時,就會按照這種邏輯來轉換。
C 風格的格式字符串采用 % 操作符來引導格式說明符,所以如果要將這個符號照原樣輸出,那就必須轉義,也就是連寫兩個 %。同理,在調用 str.format 的時候,如果想把 str 里面的 {、} 照原樣輸出,那么也得轉義。
print('%.2f%%' % 12.5)
print('{} replaces {{}}'.format(1.23))
# >>>
# 12.50%
# 1.23 replaces {}
調用 str.format 方法的時候,也可以給 str 的 {} 里面寫上數字,用來指代 format 方法在這個位置所接收到的參數值位置索引。以后即使這些 {} 在格式字符串中的次序有所變動,也不用調換傳給 format 方法的那些參數。于是,這就避免了前面講的第一個缺點所提到的那個順序問題。
formatted = '{1} = {0}'.format(key, value)
print(formatted)
# >>>
# 1.234 = my_var
同一個位置索引可以出現在 str 的多個 {} 里面,這些{}指代的都是 format 方法在對應位置所收到的值。這就不需要把這個值重復地傳給 format 方法,于是就解決了前面提到的第三個缺點。
name = 'Max'
formatted = '{0} loves food. See {0} cook.'.format(name)
print(formatted)
# >>>
# Max loves food. See Max cook.
然而,這個新的 str.format 方法并沒有解決上面講的第二個缺點。如果在對值做填充之前要先對這個值做出調整,那么用這種方法寫出來的代碼還是跟原來一樣亂,閱讀性差。
當然,這種 {} 形式的說明符,還支持一些比較高級的用法,例如可以查詢 dict 中某個鍵的值,可以訪問 list 里某個位置的元素,還可以把值轉化成 Unicode 或 repr 字符串。下面這段代碼把這三項特性結合了起來。
menu = {'soup': 'lentil','oyster': 'kumamoto','special': 'schnitzel',
}
formatted = 'First letter is {menu[oyster][0]!r}'.format(menu=menu)
print(formatted)
# >>>
# First letter is 'k'
但是這些特性,依然不能解決前面提到的第四個缺點,也就是鍵名需要多次重復的那個問題。所以并不推薦大家用 str.format 方法。當然,還是必須掌握新的格式說明符所使用的這套迷你語言(mini language),可以在 str 的 {} 里面按照這套迷你語言的規則來指定冒號右側的格式。系統內置的 format 函數也會用到這套規則。
插值格式字符串
Python 3.6 添加了一種新的特性,叫作插值格式字符串(interpolated format string,簡稱 f-string),可以解決上面提到的所有問題。新語法特性要求在格式字符串的前面加字母 f 作為前綴,這跟字母 b 與字母 r 的用法類似。
f-string 把格式字符串的表達能力發揮到了極致,它徹底解決了上文提到的第四個缺點,也就是鍵名重復導致的程序冗余問題。可以直接在 f-string 的 {} 里面引用當前 Python 范圍內的所有名稱,進而達到簡化的目的。
key = 'my_var'
value = 1.234
formatted = f'{key} = {value}'
print(formatted)
# >>>
# my_var = 1.234
str.format 方法所支持的那套迷你語言,也就是在 {} 內的冒號右側所采用的那套規則,現在也可以用到 f-string 里面,而且還可以像早前使用 str.format 時那樣,通過 ! 符號把值轉化成 Unicode 及 repr 形式的字符串。
formatted = f'{key!r:<10} = {value:.2f}'
print(formatted)
# >>>
# 'my_var' = 1.23
同一個問題,使用 f-string 來解決總是比通過 % 操作符使用 C 風格的格式字符串簡單,而且也比 str.format 方法簡單。
f_string = f'{key:<10} = {value:.2f}'
c_tuple = '%-10s = %.2f' % (key, value)
str_args = '{:<10} = {:.2f}'.format(key, value)
str_kw = '{key:<10} = {value:.2f}'.format(key=key, value=value)
c_dict = '%(key)-10s = %(value).2f' % {'key': key, 'value': value}
assert c_tuple == c_dict == f_string
assert str_args == str_kw == f_string
在 f-string 方法中,各種 Python 表達式都可以出現在 {} 里,于是這就解決了前面提到的第二個缺點。我們現在可以用相當簡潔的寫法對需要填充到字符串里面的值做出微調。
pantry = [('avocados', 1.25),('bananas', 2.5),('cherries', 15),
]
for i, (item, count) in enumerate(pantry):old_style = '#%d: %-10s = %d'% (i + 1,item.title(),round(count))new_style = '#{}: {:<10s} = {}'.format(i + 1,item.title(),round(count))f_string = f'#{i+1}: {item.title():<10s} = {round(count)}'assert old_style == new_style == f_string
要是想表達得更清楚一些,可以把 f-string 寫成多行的形式,類似于 C 語言的相鄰字符串拼接(adjacent-string concatenation)。
pantry = [('avocados', 1.25),('bananas', 2.5),('cherries', 15),
]
for i, (item, count) in enumerate(pantry):print(f'#{i+1}: 'f'{item.title():<10s} = 'f'{round(count)}')
# >>>
# #1: Avocados = 1
# #2: Bananas = 2
# #3: Cherries = 15
Python 表達式也可以出現在格式說明符中。例如,下面的代碼把小數點之后的位數用變量來表示,然后把這個變量的名字 places 用 {} 括起來放到格式說明符中,這樣寫比采用硬代碼更靈活。
places = 3
number = 1.23456
print(f'My number is {number:.{places}f}')
# >>>
# My number is 1.235
在 Python 內置的四種字符串格式化辦法里面,f-string 可以簡潔而清晰地表達出許多種邏輯,這使它成為程序員的最佳選擇。如果你想把值以適當的格式填充到字符串里面,那么首先應該考慮的就是采用 f-string 來實現。