Python高效編程之88條軍規(2):你真的會格式化字符串嗎?

在微信公衆號「極客起源」中輸入595586,可學習所有的《Python高效編程之88條軍規》系列文章。

在Python語言中,字符串有多種用途。 能夠用於在用戶界面和命令行實用程序中顯示消息; 用於用於將數據寫入文件和Socket; 用於指定「異常」消息; 用於調試程序。
格式化是將預約義的文本和數據組合成一條人類可讀的消息的過程。Python具備4種不一樣的格式化字符串的方式,這4種方式有的是語言層面支持的,有的是經過標準庫支持的。除其中一種方式外,其餘的格式化方式都有嚴重的缺點,在使用時應該儘可能避免這些缺陷。
1.  C風格的字符串格式化方式
在Python語言中格式化字符串的最多見方法是使用%格式化運算符。預約義的文本模板以格式字符串的形式放在%運算符的左側,要插入模板的數據在%運算符的右側。這些數據能夠是單個值,也能夠是一個元組(不能是列表),表示將多個值插入模板。例如,在這裏我使用%運算符將難以閱讀的二進制和十六進制值轉換爲整數字符串:
a = 0b10111010b = 0xc5cprint('二進制:%d, 十六進程:%d' % (a, b))
執行這段代碼,會輸出以下內容:
二進制:186, 十六進程:3164
格式字符串使用格式說明符(如%d)做爲佔位符,這些佔位符將被%運算符右側的值替換。格式說明符的語法來自C語言的printf函數,該函數已被Python(以及其餘編程語言)繼承。Python支持全部經常使用的printf函數格式化選項。例如%s,%x和%f格式說明符,以及對小數位,填充,填充和對齊的控制。許多不熟悉Python的程序員都以C風格的格式字符串開頭,由於它們熟悉且易於使用。
可是使用C風格的格式化字符串方式,會帶來以下4個問題:
問題1:
若是更改格式表達式右側的元組中數據值的類型或順序,可能會因爲類型轉換不兼容而拋出異常。例如,這個簡單的格式表達式能夠工做:
key = 'my_key'value = 1.234formatted = '%-10s = %.2f' % (key, value)print(formatted)
執行這段代碼,會輸出以下內容:
my_key = 1.23
但如何交換key和value的值,那將會拋出運行時異常:
key = 1.234value = 'my_key'formatted = '%-10s = %.2f' % (key, value)print(formatted)
執行這段代碼,會拋出以下異常:
Traceback (most recent call last): File "/python/format.py", line 12, in <module> formatted = '%-10s = %.2f' % (key, value)TypeError: must be real number, not str
相似地,若是%右側元組中值的順序變化後,一樣會拋出異常。
formatted = '%-10s = %.2f' % (key, value)
爲了不這種麻煩,你須要不斷檢查%運算符的兩側的數據類型是否匹配;此過程容易出錯,由於每次修改代碼,都必須人工檢測數據類型是否匹配。
問題2:
C風格格式化表達式的第2個問題是當你須要在將值格式化爲字符串以前對值進行小的修改時,它們將變得難以閱讀,這是很是廣泛的需求。在這裏,我列出了廚房儲藏室的內容,而沒有進行內聯更改:
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
問題3:
格式化表達式的第3個問題是若是要在格式字符串中屢次使用相同的值,則必須在右側重複該值屢次:
template = '%s loves food. See %s cook.'name = 'Max'formatted = template % (name, name)print(formatted)
執行這段代碼,會輸出以下的內容:
Max loves food. See Max cook.
若是須要對這些重複的值作一些小的修改,這將特別使人討厭的事,並且很是容易出錯。爲了解決這個問題,推薦使用字典取代元組爲格式化字符串提供數據。引用字典中值的方式是%(key),看下面的例子:
old_way = '%-10s , %.2f, %-8s' % (key, value,key) # 重複指定key
new_way = '%(key)-10s , %(value).2f, %(key)-8s' % { 'key': key, 'value': value} # 只須要指定一次key
print(old_way)print(new_way)
執行這段代碼,會輸出以下的內容:
key1 , 1.13, key1 key1 , 1.13, key1
咱們能夠看到,若是須要重複引用%右側的值,在使用元組的狀況下,須要重複指定這些值,如本例中的key。而使用字典,只須要指定一次key就能夠了。
而後,使用字典格式化字符串會引入並加重其餘問題。對於上面的問題2,因爲在格式化以前對值進行了小的修改,因爲%運算符右側存在鍵和冒號運算符,所以格式化表達式變得更長,而且在視覺上更加雜亂。在下面的代碼中,我分別使用字典和不使用指點來格式化相同的字符串以說明此問題:
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
問題4:
使用字典格式化字符串還會帶了第4個問題,就是每一個鍵必須至少指定兩次:在格式說明符中指定一次,另外一次是在字典中指定爲鍵,若是字典值自己是一個變量,也須要再次指定。
soup = 'lentil'formatted = 'Today\'s soup is %(soup)s.' % {'soup': soup} # 這裏再次指定了變量soupprint(formatted)
輸出結果以下:
Today's soup is lentil.
除了重複字符以外,這種冗餘還會致使使用字典的格式化表達式很長。這些表達式一般必須跨多行,格式字符串跨多行鏈接,而且字典賦值每一個值只有一行用於格式化:
menu = { 'soup': 'lentil', 'oyster': 'kumamoto', 'special': 'schnitzel',}template = ('Today\'s soup is %(soup)s, ' 'buy one get two %(oyster)s oysters, ' 'and our special entrée is %(special)s.')formatted = template % menuprint(formatted)
輸出結果以下:
Today's soup is lentil, buy one get two kumamoto oysters, and our special entrée is schnitzel.
因爲格式化字符串很長,可能會跨多行,因此要想了解整個字符串想表達什麼,你的眼鏡必須上下左右來回移動,並且很容易忽略本應該發現的錯誤。那麼是否有更好的格式化字符串的解決方案呢?請繼續往下看:
2. 內建format函數與str.format方法
Python 3添加了對高級字符串格式化的支持,這種格式化方式比使用%運算符的C風格格式化字符串更具表現力。對於單獨的值,能夠經過格式化內建函數來訪問此新功能。例如,下面的代碼使用一些新選項(,用於千分位分隔符,使用^用於居中)來格式化值:
a = 1234.5678formatted = format(a, ',.2f')print(formatted)
b = 'my string'formatted = format(b, '^20s') # 居中顯示字符串print('*', formatted, '*')
運行結果以下:
1,234.57* my string *
您能夠經過調用字符串的format方法來格式化多個值。format方法使用{}做爲佔位符,而不是使用%d這樣的C風格格式說明符。在默認狀況下,格式化字符串中的佔位符按着它們出現的順序傳遞給format方法相應位置的佔位符。
key = 'my_var'value = 1.234
formatted = '{} = {}'.format(key, value)print(formatted)
運行結果以下:
my_var = 1.234
每一個佔位符內能夠在冒號(:)後面指定格式化說明符,用來指定將值轉換爲字符串的方式,代碼以下:
formatted = '{:<10} = {:.2f}'.format(key, value)print(formatted)
運行結果以下:
my_var = 1.23
format方法的工做原理是將格式化說明符與值(上例中的format(value,'.2f'))一塊兒傳遞給內建函數format。而後將 該函數的返回值替換對應的佔位符。可使用__format__方法針對每一個類自定義格式化行爲。
對於C風格的格式化字符串,須要對%運算符進行轉換轉義,也就是寫兩個%,以避免被誤認爲是佔位符。使用str.format方法,也須要對花括號進行轉義。
print('%.2f%%' % 12.5)print('{} replaces {{}}'.format(1.23))
輸出結果以下:
12.50%1.23 replaces {}
在花括號內還能夠指定傳遞給format方法的參數的位置索引,以用於替換佔位符。這容許在不更改format方法傳入值順序的狀況下,更改格式化字符串中佔位符的順序。
formatted = '{1} = {0}'.format(key, value)print(formatted)
輸出結果以下所示:
1.234 = my_var
使用位置索引還有一個好處,就是在格式化字符串中要屢次引用某個值時,只須要經過format方法傳遞一個值便可。在格式化字符串中可使用同一個位置索引引用屢次這個值。
formatted = '{0} loves food. See {0} cook.'.format(name)print(formatted)
輸出結果以下:
Max loves food. See Max cook.
不幸的是,format方法沒法解決上面的問題2,因此在格式化以前須要對值進行小的修改時比較費勁(由於須要對齊參數的位置)。下面的代碼是將%運算符和format方法在一塊兒進行比較,其實同時一樣不容易閱讀。
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))
assert old_style == new_style
儘管format方法使用的格式化說明符還有更多高級選項,例如在佔位符中使用字典鍵和列表索引的組合,以及將值強制轉換爲Unicode和repr字符串:
formatted = 'First letter is {menu[oyster][0]!r}'.format( menu=menu)print(formatted)
運行結果以下:
First letter is 'k'
可是這些功能並不能幫助減小上述問題4中重複key的冗餘性。例如,在這裏,我將在C風格格式化表達式中使用字典的冗長性與將key參數傳遞給format方法的新樣式進行了比較:
old_template = ( 'Today\'s soup is %(soup)s, ' 'buy one get two %(oyster)s oysters, ' 'and our special entrée is %(special)s.')old_formatted = template % { 'soup': 'lentil', 'oyster': 'kumamoto', 'special': 'schnitzel',}
new_template = ( 'Today\'s soup is {soup}, ' 'buy one get two {oyster} oysters, ' 'and our special entrée is {special}.')new_formatted = new_template.format( soup='lentil', oyster='kumamoto', special='schnitzel',)assert old_formatted == new_formatted
這種樣式的噪音較小,由於它消除了詞典中的一些引號和格式化說明符中的一些字符,可是並無達到完美的程度。此外,在佔位符中使用字典鍵和索引的高級功能僅提供了Python表達式功能的一小部分。這種缺少表現力的侷限性使得它從整體上破壞了format方法的價值。
考慮到這些缺點以及仍然存在C風格格式化表達式的問題(上面的問題2和問題4),個人建議是儘可能避免使用str.format方法。瞭解格式化說明符(冒號以後的全部內容)中使用的新的迷你語言以及如何使用格式內置功能是很是重要的。
3. f-字符串
Python 3.6添加了插值格式化字符串(簡稱f字符串)來完全解決這些問題。這種新的語言語法要求您以f字符做爲格式字符串的前綴,這相似於字節字符串以b字符做爲前綴,以及原始(未轉義的)字符串以r字符做爲前綴。
f-字符串將格式字符串的表現力發揮到極致,經過徹底消除提供要格式化的鍵和值的冗餘性,徹底解決了問題4。它們經過容許您引用當前Python範圍中的全部變量做爲格式化表達式的一部分來實現這一點:
key = 'my_var'value = 1.234
formatted = f'{key} = {value}'print(formatted)
輸出結果以下:
my_var = 1.234
格式化的內置迷你語言中的全部相同選項均可以在f-字符串內佔位符後的冒號後面使用,也能夠相似於str.format方法將值強制轉換爲Unicode和repr字符串:
formatted = f'{key!r:<10} = {value:.2f}'print(formatted)
輸出結果以下:
'my_var' = 1.23
在全部狀況下,使用f-字符串進行格式化比使用帶有%運算符和str.format方法的C風格格式化字符串進行格式化要短。在這裏,我按照最短到最長的順序顯示了全部這些格式化方式,以便您能夠輕鬆進行比較:
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}
print(f'f_string:{f_string}')print(f'c_tuple:{c_tuple}')print(f'str_args:{str_args}')print(f'str_kw:{str_kw}')print(f'c_dict:{c_dict}')
輸出結果以下:
f_string:my_var = 1.23c_tuple:my_var = 1.23str_args:my_var = 1.23str_kw:my_var = 1.23c_dict:my_var = 1.23
f-字符串還能夠將完整的Python表達式放在佔位符括號內,經過對使用簡明語法格式化的值進行小的修改,能夠從根本上解決問題2。如今,使用C樣式格式化和str.format方法花費多行的內容如今很容易放在一行上:
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-字符串拆分爲多行。即便比單行版本更長,也比其餘任何多行方法都清晰得多:
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 = 3number = 1.23456print(f'My number is {number:.{places}f}')
f-字符串可讓表達力,簡潔性和清晰度結合在一塊兒,使它們成爲Python程序員最好的內置選項。每當您發現本身須要將值格式化爲字符串時,均可以選擇f-字符串做爲替代。

總結:
1. 使用%運算符的C風格格式化字符串會遇到各類陷阱和冗長的問題;
2.str.format方法在其格式說明符迷你語言中引入了一些有用的概念,但在其餘方面會重複C風格格式化字符串的錯誤,應避免使用;
3. f-字符串是用於將值格式化爲字符串的新語法,解決了C風格格式化字符串最大的問題;
4. f-字符串簡潔而強大,由於它們容許將任意Python表達式直接嵌入格式說明符中;


對本文感興趣,能夠加李寧老師微信公衆號(unitymarvel):


關注  極客起源  公衆號,得到更多免費技術視頻和文章。


本文分享自微信公衆號 - 極客起源(geekculture)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。javascript

相關文章
相關標籤/搜索