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

qwer1030274531發表於2020-09-23

在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)  # 重複指定keynew_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.234formatted = '{} = {}'.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.234formatted = 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表示式直接嵌入格式說明符中;


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/30239065/viewspace-2723371/,如需轉載,請註明出處,否則將追究法律責任。

相關文章