天天看點

《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串

本節書摘來自異步社群《python 3程式開發指南(第2版•修訂版)》一書中的第2章,第2.4節,作者[英]mark summerfield,王弘博,孫傳慶 譯,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

字元串是使用固定不變的str資料類型表示的,其中存放unicode字元序列。str資料類型可以作為函數進行調用,用于建立字元串對象——參數為空時傳回一個空字元串,參數為非字元串類型時傳回該參數的字元串形式,參數為字元串時傳回該字元串的拷貝。str()函數也可以用作一個轉換函數,此時要求第一個參數為字元串或可以轉換為字元串的其他資料類型,其後跟随至多兩個可選的字元串參數,其中一個用于指定要使用的編碼格式,另一個用于指定如何處理編碼錯誤。

前面我們注意到,字元串是使用引号建立的,可以使用單引号,也可以使用雙引号,但是字元串兩端必須相同。此外,我們還可以使用三引号包含的字元串——這是python對起始端與終端都使用3個引号包含的字元串的叫法,例如:

如果需要在通常的、引号包含的字元串中使用引号,在要使用的引号與包含字元串的引号不同時,可以直接使用該引号,而不需要進行格式化處理操作,但是如果相同,就必須對其進行轉義:

python使用換行作為其語句終結符,但是如果在圓括号内、方括号内、花括号内或三引号包含的字元串内則是例外。在三引号包含的字元串中,可以直接使用換行,而不需要進行格式化處理操作。通過使用n轉義序列,也可以在任何字元串中包含換行。表2-7展示了所有的python轉義序列。有些情況下——比如,編寫正規表達式時,需要建立帶有大量字面意義反斜杠的字元串。(正規表達式将在第12章進行講解。)由于每個反斜杠都必須進行轉義處理,進而造成了不便:

《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串
《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串

解決的方法是使用原始的字元串,這種引号包含的或三引号包含的字元串的第一個引号由字面意義的r引導。在這種字元串内部,所有字元都按其字面意義了解,是以不再需要進行轉義。下面給出了使用原始字元串的phone正規表達式:

如果需要寫一個長字元串,跨越了2行或更多行,但是不使用三引号包含的字元串,那麼有兩種解決方法:

注意上面第二種情況,我們必須使用圓括号将其包含在一起,構成一個單獨的表達式——如果不使用圓括号,就隻有第一個字元串對s進行指派,第二個字元串則會導緻indentationerror異常。python的“idioms and anti-idioms”howto文檔建議總是使用圓括号将跨越多行的任何語句進行封裝,而不使用轉義的換行符,我們努力遵照這一建議。

由于.py檔案預設使用utf-8 unicode編碼,是以我們可以在字元串字面值中寫入任意unicode字元,而不拘形式。我們也可以使用十六進制轉義序列(或使用unicode名)将任意unicode字元放置在字元串内,例如:

上面的情況不能使用十六進制轉義序列,因為本身限定在兩個digits,無法超過0xff。要注意的是,unicode字元名非大小寫敏感,其中的空格也是可選的。

如果需要知道字元串中某個特定字元的unicode字元(賦予unicode編碼中某個字元的整數值),那麼可以使用内置的ord()函數,例如:

類似地,我們也可以将表示有效字元的任意整數轉換為相應的unicode 字元,這需要使用内置的chr()函數:

如果在idle中輸入本身,就輸出其字元串形式。對于字元串,這意味着字元是包含在引号中輸出的。如果隻需要ascii字元,就可以使用内置的ascii()函數,在可能的地方,該函數使用7比特表示形式傳回其參數的對應ascii表示,否則就使用最短的xhh、uhhhh或uhhhhhhhh進行轉義。在本章後面,我們将了解如何實作對字元串輸出的精确控制。

字元串支援通常的比較操作符<、<=、==、!=、>與>=,這些操作符在記憶體中逐個位元組對字元串進行比較。遺憾的是,進行比較時(比如對字元串清單進行排序),存在兩個問題,這兩個問題都影響到每種使用unicode字元串的程式設計語言,不是python特有的問題。

第一個問題是,有些unicode字元可以使用兩種或更多種位元組序清單示。例如,字元Å (unicode字元0x00c5)可以3種不同的方式使用utf-8編碼的位元組表示:[0xe2, 0x84, 0xab]、[0xc3, 0x85]與[0x41, 0xcc, 0x8a]。幸運的是,我們可以解決這一問題。如果我們導入了unicodedata子產品,并以“nfkd”(這是使用的标準化方法,代表normalization form compatibility decomposition")為第一個參數來調用unicodedata.normalize(),則對包含字元Å(使用任意一種有效字元序清單示)的字元串,該函數傳回以utf-8編碼位元組表示的字元串總是位元組序列[0x41, 0xcc, 0x8a]。

第二個問題是,有些字元的排序是特定于某種語言的。一個執行個體是在瑞典語中,ä排序在z之後,而在德語中,ä的排序與其被拼寫為ae時一樣。另一個執行個體是,在英語中,對ø排序時,與其為o一樣,在丹麥語與挪威語中,則排序在z之後。這一類問題還有很多,由于同一個應用程式可能會由不同國家的人(是以所認為的排序順序會不同)使用,使得這一問題變得更加複雜。此外,有時候字元串是不同語言混合組成的(比如,有些是西班牙語,有些是英語),而有些字元(比如箭頭、dingbats與數學符号)并不真正具備有意義的排序位置。

作為一種政策(以便防止出錯),python并不進行推測。在字元串比較時,python使用的是字元串的記憶體位元組表示,此時的排序是基于unicode 字元的,比如對英語就是按ascii順序。對要比較的字元串進行小寫或大寫,會産生更貼近自然英語的排序。标準化一般很少需要,除非字元串來自外部源(比如檔案或網絡socket),但即便是這些情況,一般也不必進行标準化,除非确實需要。我們可以自定義python的排序方法,第3章将進行講述。關于unicode排序的所有問題,在unicode校勘算法文檔(unicode.org/reports/tr10)中有詳細的解釋。

從要素3的講解中我們知道,序列中的單個資料項或者字元串中的單個字元,可以使用資料項存取操作符[]來提取。實際上,這一操作符功能很豐富,其用途不僅僅局限于提取一個資料項或字元,還可以提取項或字元的整個分片(子序列),在這種情況下該操作符被用作分片操作符。

我們首先從提取單個字元開始。字元串的索引位置從0開始,直至字元串長度值減去1,但是使用負索引位置也是可能的——此時的計數方式是從最後一個字元到第一個字元。給定指派操作s = "light ray",圖2-1展示了字元串s中所有有效的索引位置。

《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串

負索引值出人意料地有用,尤其是-1,這個值總是代表字元串的最後一個字元。存取超過範圍的索引位置(或空字元串中的索引位置)會産生indexerror異常。

分片操作符有3種文法格式:

其中,seq可以是任意序列,比如清單、字元串或元組。start、end與step必須都是整數(或存放整數的變量)。我們使用了第一種文法格式:從序列中提取從start開始的資料項。第二種文法從start開始的資料項(包含)到end結束的資料項(不包含)提取一個分片。稍後我們将讨論第三種文法格式。

如果使用第二種文法格式(一個冒号),我們就可以忽略任意的整數索引值。如果忽略了所有起點索引值,就預設為0;如果忽略了終點索引值,就預設為len(seq),這意味着,如果忽略了兩個索引值,比如,s[:],則與s[0:len(s)]是等同的,其作用都是提取——也就是複制整個序列。

給定指派操作s = "the waxwork man",圖2-2展示了字元串s的一些執行個體分片。

在字元串内插入子字元串的一種方法是混合使用帶連接配接的分片,例如:

《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串

實際上,由于文本"wo"在原始字元串中,是以我們也可以寫成s[:12] + s[7:9] + s[12:]達到同樣的效果。

在涉及很多字元串時,使用+進行連接配接、使用+=進行追加等操作并不是特别高效,如果需要連接配接大量的字元串,通常最好使用str.join()方法,下一小節将進行講解。

第三種分片文法格式(兩個冒号)與第二種類似,差別在于不是提取每一個字元,而是每隔step個字元進行提取。與第二種文法類似,也可以忽略兩個索引整數。如果忽略了起點索引值,那麼預設為0——除非給定的是負的step值,此時起點索引值預設為-1;如果忽略終點索引值,那麼預設為len(seq) ——除非給定的是負的step值,此時終點索引值預設為字元串起點前面。不過,不能忽略step,并且step不能為0。如果不需要step,那麼應該使用不包含step變量的第二種文法(一個冒号)。

給定指派操作s = "he ate camel food",圖2-3展示了字元串帶步距的分片的兩個執行個體。

《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串

上面我們使用預設的起點索引值與終點索引值,是以,s[::-2]從該字元串的最後一個字元開始,向該字元串的起點方向,每隔1個字元提取一個字元。類似地,s[::3]從第一個字元開始,向該字元串的終點方向,每隔2個字元提取一個字元。

将分片與步距結合使用也是可能的,如圖2-4所示。

《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串

更常見的情況下,步距是與字元串之外的序列類型一起使用的,但是也存在用于字元串的情況:

step為-1意味着,每個字元都将被提取,方向為從終點到起點——是以會産生反轉的字元串。

由于字元串是固定序列,所有可用于固定序列的功能都可用于字元串,包括使用in進行成員關系測試,使用+=進行追加操作,使用進行複制,使用=進行增強的指派複制等。在這一小節中,我們将在字元串的上下文中讨論所有這些操作,此外還讨論了很多字元串方法。表2-8、表2-9、表2-10總結了所有的字元串方法,除了兩個特别專業的方法(str.maketrans()與str.translate()),這兩個方法将在以後簡要讨論。

《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串
《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串
《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串
《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串
《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串

由于字元串是序列,是以也是有大小的對象,我們可以以字元串為參數來使用len()函數,傳回值是字元串中的字元數(如果字元串為空,就傳回0)。

我們已經知道,在字元串的操作中,+操作符被重載用于實作字元串連接配接。如果需要連接配接大量的字元串,使用str.join()方法是一種更好的方案。該方法以一個序列作為參數(比如,字元串清單或字元串元組),并将其連接配接在一起存放在一個單獨的字元串中,并将調用該方法的字元串作為分隔物添加在每兩項之間,例如:

第一個執行個體或許是最常見的,連接配接一個單獨的字元,這裡是空格。第三個執行個體純粹是連接配接,使用空字元串意味着字元串序列在連接配接時中間不使用任何填充。

str.join()方法也可以與内置的reversed()函數一起使用,以實作對字元串的反轉,比如,"".join(reversed(s))。當然,通過步距也可以更精确地擷取同樣的結果,比如,s[::-1]。

*操作符提供了字元串複制功能:

如上面執行個體所展示的,我們也可以使用複制操作符*的增強版進行指派。

在用于字元串時,如果成員關系操作符in左邊的字元串參數是右邊字元串參數的一部分,或者相等,就傳回true。

如果我們需要在某個字元串中找到另一個字元串所在的位置,有兩種方法,一種是使用str.index()方法,該方法傳回子字元串的索引位置,或者在失敗時産生一個vaueerror異常。另一種是使用str.find()方法,該方法傳回子字元串的索引位置,或者在失敗時傳回-1。這兩種方法都把要尋找的字元串作為第一個參數,還可以有兩個可選的參數,其中第二個參數是待搜尋字元串的起始位置,第三個則是其終點位置。

使用哪種搜尋方法純粹是個人愛好與具體場景,盡管如果搜尋多個索引位置,使用str.index()方法通常會生成更幹淨的代碼,如下面兩個等價的函數所展示的:

《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串

兩個版本的extract_from_tag()函數的作用是完全一緻的。比如,extract_from_tag ("red", "what a rose this is")傳回字元串"rose"。左面這一版本的異常處理部分更清晰地布局在其他代碼之外,明确地表示了如何處理錯誤;右邊版本的錯誤傳回值則将分散了錯誤處理的不同情況。

方法str.count()、str.endswith()、str.find()、str.rfind()、str.index()、str.rindex()與str. startswith()都接受至多兩個可選的參數:起點位置與終點位置。這裡給出兩個等價的語句,并假定s是一個字元串:

可以看出,接受起點與終點索引位置作為參數的方法可以運作在由這些索引值指定的字元串分片上。

下面看另一對等價的代碼,主要是為了明确str.partition()的作用:

《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串

左面的代碼段與右面的代碼段并不完全等價,因為右面還建立了一個新變量i。注意我們可以直接配置設定元組,而不拘于形式。兩邊的代碼都是搜尋/的最右邊出現,如果字元串s為"/usr/local/bin/firefox",那麼兩個代碼段都會産生同樣的結果:('/usr/local/bin', '/', 'firefox')。

我們可以以一個單獨的字元串為參數來調用str.endswith()(以及str.startswith()),比如,s.startswith("from:"),或者使用字元串元組作為參數。下面的語句同時使用str.endswith()與str.lower()來列印檔案名——如果該檔案是一個jpeg檔案:

如果作為參數的字元串至少有一個字元,并且字元串中的每個字元都符合标準,那麼is*()方法(比如isalpha()與isspace())就傳回true,例如:

is*()方法工作的基礎是unicode字元分類,比如,以字元串"n{circled digit two}03"與"203"為參數調用str.isdigit()都會傳回true。出于這一原因,我們不能因為isdigit()函數傳回true就判斷某個字元串可以轉換為整數。

從外部源(其他程式、檔案、網絡連接配接尤其是互動式使用者)接受字元串時,字元串可能包含不需要的開始空白字元與結尾空白字元。我們可以使用str.lstrip()來剝離左邊的空白字元,也可以使用str.rstrip()來剝離右邊的空白字元,或者使用str.strip()同時剝離兩邊的空白字元。我們也可以使用一個字元串作為參數來調用剝離方法,這種情況下,每個字元的每個出現都将被從合适的位置剝離,例如:

我們可以使用str.replace()方法來在字元串内進行替換。這一方法以兩個字元串作為參數,并傳回該字元串的副本(其中第一個字元串的所有出現都被第二個字元串所替代)。如果第二個字元串為空,那麼這一函數的實際效果是删除第一個字元串的所有出現。在csv2html.py程式中,我們将看到str.replace()方法以及其他一些字元串方法的應用執行個體,該程式在本章後面的執行個體一節中介紹。

一個頻繁遇到的需求是将字元串分割為一系列子字元串。比如,我們有一個文本檔案,需要将其中的資料進行處理,要求每行一個記錄,每個記錄的字段使用星号進行分隔。為此,可以使用str.split()方法,并以待分割的字元串作為第一個參數,以要分割的最大子資料段數為可選的第二個參數。如果再不指定第二個參數,該方法就會進行盡可能多的分割。下面給出一個執行個體:

以上面的結果為基礎,可以使用str.split()方法對出生日期與死亡日期進行進一步的分割,以便計算其壽命(給定或接受一個年份值):

上面的代碼中,我們必須使用int()方法将年份從字元串轉換為整數,除此之外,該代碼段是很直接的。我們也可以從fields清單中擷取年份,比如,year_born = int(fields[1].split("-")[0])。

表2-8、表2-9、表2-10中沒有包含兩個方法,即str.maketrans()與str.translate()。str.maketrans()方法用于建立字元間映射的轉換表,該方法可以接受一個、兩個或三個參數,但是我們這裡隻展示最簡單的(兩個參數)調用方式,其中,第一個參數是一個字元串,該字元串中的字元需要進行轉換,第二個參數也是一個字元串,其中包含的字元是轉換的目标,這兩個字元串必須具有相同的長度。str.translate()方法以轉換表作為一個參數,并傳回某個字元串根據該轉換表進行轉換後的副本。下面展示了如何将可能包含孟加拉數字的字元串轉換為英文數字:

從上面可以看出,在str.maketrans()調用内部以及第二個print()調用内部,我們利用了python的字元串字面值連接配接,使得字元串跨越了多行,而不需要對換行進行轉義或使用顯示的連接配接。

我們對空字元串調用了str.maketrans()方法,因為該方法不關心其針對的具體字元串,而隻是對其參數進行處理,并傳回一個轉換表2。str.maketrans()方法與str.translate()方法也可以用于删除字元,方法是将包含待删除字元的字元串作為第三個參數傳遞給str.maketrans()。如果需要更複雜的字元轉換,我們可以建立一個自定義的codec——要了解關于這一主題的更多資訊,可以參閱codecs子產品文檔。

python還有一些其他的庫子產品提供字元串相關的功能。我們已經簡要提及了unicodedata子產品,下一小節将展示該子產品的應用。其他值得關注的子產品還有difflib子產品,用于展示檔案或字元串之間的差别;io子產品的io.stringio類,用于讀、寫字元串,就像對檔案的讀寫操作一樣;textwrap 子產品,該子產品提供了用于包裹與填充字元串的函數與方法。此外,還有一個string子產品,其中定義了一些有用的常量,比如ascii_letters與ascii_lowercase。在第5章中,我們将看到這些子產品的一些應用執行個體。此外,python的re子產品提供了對正規表達式的充分支援——第13章将專注于講述這一主題。

str.format()方法提供了非常靈活而強大的建立字元串的途徑。對于簡單的情況,使用str.format()方法是容易的,但是如果需要進行複雜的格式化操作,就要學習該方法需要的格式化文法。

str.format()方法會傳回一個新字元串,在新字元串中,原字元串的替換字段被适當格式化後的參數所替代,例如:

每個替換字段都是由包含在花括号中的字段名辨別的。如果字段名是簡單的整數,就将被作為傳遞給str.format()方法的一個參數的索引位置。是以,在這種情況下,名為0的字段被第一個參數所替代,名為1的字段則被第二個參數所替代。

如果需要在格式化字元串中包含花括号,就需要将其複寫,下面給出一個執行個體:

如果我們試圖連接配接字元串與數字,那麼python将産生typeerror異常,但是使用str.format()方法可以很容易地做到這一點:

我們也可以使用str.format()方法連接配接字元串(盡管str.join()方法最适合用于這一目的):

在上面的執行個體中,我們使用了一對字元串變量,不過在本小節的大部分,我們在str.format()方法的應用執行個體中都使用字元串字面值,這就是為了友善——實際上,任何使用字元串字面值的執行個體中都可以使用字元串變量,方法是完全一樣的。

替換字段可以使用下面的任意一種文法格式:

另外需要注意的一點是,替換字段本身也可以包含替換字段,嵌套的替換字段不能有任何格式,其用途主要是格式化規約的計算。在對格式化規約進行更細緻的解讀時,我們将展示一個執行個體。現在我們将逐一研究替換字段的每一個組成部分,首先從字段名開始。

字段名或者是一個與某個str.format()方法參數對應的整數,或者是方法的某個關鍵字參數的名稱。我們将在第4章中讨論關鍵字參數,但實際上用起來并不難,是以,這裡我們給出兩個執行個體,以保證本節講述内容的完整性:

上面的第一個執行個體使用了兩個關鍵字參數,分别是who與age,第二個執行個體使用了一個位置參數(到這裡為止隻在這裡使用過)與一個關鍵字參數。要注意的是,在參數清單中,關鍵字參數總是在位置參數之後,當然,我們可以在格式化字元串内部以任何順序使用任何參數。

字段名可以引用集合資料類型——比如,清單。在這樣的情況下,我們可以包含一個索引(不是一個分片)來辨別特定的資料項:

0引用的是位置參數,是以,{0[1]}是清單stock參數的第二個資料項,{0[2]}是清單stock參數的第三個資料項。

後面我們将學習python字典,字典中存儲的是key–value項,字典對象也可以用于str.format()方法,我們這裡展示一個應用執行個體,如果不能很好地了解,也不必擔心,第3章将再次講述這一主題。

就像可以使用整數位置索引來存取清單與元組項一樣,我們可以使用鍵值來存取字典項。

我們也可以存取命名的屬性。假定已經導入math子產品與sys子產品,則可以進行如下一些操作:

總而言之,通過字段名文法,可以引用傳遞給str.format()方法的位置參數與關鍵字參數。如果參數是集合資料類型,比如清單或字典,或參數還包含一些屬性,那麼可以使用[]或.表示法存取所需的部分,圖2-5中勾勒了這一點。

《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串

從python 3.1開始,忽略字段名成為可能,這種情況下,python會自動進行處理(使用從0開始的數值),比如:

如果我們使用python 3.0,那麼這裡使用的格式字元串就必須是"{0} {1} {2}"。在格式化1、2個項目時,使用這種技術是便利的,但對于多個項目的情況,接下來我們看到的技術更便利,并且在python 3.0環境下可以使用。

在結束對字元串格式字段名的讨論之前,提及另一種為格式化字元串指派的相當不同的途徑是有價值的,這涉及一種進階技術,但盡快學會是有用的,因為這種技術非常便利。

目前還在作用範圍内的局部變量可以通過内置的locals()函數通路,該函數會傳回一個字典,字典的鍵是局部變量名,字典的值則是對變量值的引用。現在,我們可以使用映射拆分将該字典提供給str.format()方法,映射拆分操作符為**,可應用于映射(比如字典)來産生一個适合于傳遞給函數的鍵-值清單,比如:

這種文法可能非常怪異——perl程式員倒是會感覺很親切,不過不用擔心,第4章會進行解釋。現在我們需要知道的就是我們可以在格式化字元串中使用變量名,python會通過拆分字典(locals()傳回的字典,或其他字典)來将變量值填充到str.format()方法。比如,我們可以重寫早前看到的“elephant”執行個體,以便其具備更好的格式(帶有更簡單的字段名)。

将字典拆分并提供給str.format()方法時,允許使用字典的鍵作為字段名。這使得字元串格式更易于了解,也易于維護,因為不需要依賴于參數的順序。然而,要注意的是,如果需要将不止一個參數傳遞給 str.format(),那麼隻有最後一個參數才可以使用映射拆分。

在讨論decimal.decimal數字時,我們注意到,這些數可以以兩種方式輸出,例如:

decimal.decimal的第一種展示方式是其表象形式,這種形式的用途是提供一個字元串——該字元串被python解釋時将重建其表示的對象。python程式可以評價python代碼段或整個程式,是以,這種表象形式有時候是有用的。不是所有對象都可以提供這種便于重建的表象形式,如果提供,其形式為包含在尖括号中的字元串,比如,sys子產品的表象形式為字元串""。

第二種是以字元串形式對decimal.decimal進行展示的,這種形式的目标是便于閱讀,是以其着眼點是展示一些讀者感興趣的東西。如果某種資料類型沒有字元串表示形式,但又需要使用字元串進行表示,那麼python将使用表象形式。

python内置的資料類型都知道str.format()方法,在作為參數傳遞給這一方法時,将傳回一個适當的字元串來展示自己。第6章我們将看到,為自定義資料類型添加對str.format()方法的支援是很直接的。此外,重寫資料類型的通常行為并強制其提供字元串形式或表象形式也是可能的,這是通過向字段中添加conversion指定符實作的。目前,有3個這樣的指定符:s,用于強制使用字元串形式;r,用于強制使用表象形式;a,用于強制使用表象形式,但僅限于ascii字元。下面給出一個執行個體:

在上面的執行個體中,decimal.decimal的字元串形式産生的字元串與提供給str.format()(通常情況)的字元串是相同的。同時,在這個比較特定的執行個體中,由于都隻使用ascii字元,是以,表象形式與ascii表象形式之間沒有差別。

下面給出另一個執行個體,這次使用的字元串中包含了電影名,存放在變量movie中。如果使用"{0}".format(movie)列印該字元串,那麼該字元串将原樣輸出,但是如果需要阻止非ascii字元的輸出,就可以使用ascii(movie)或"{0!a}".format (movie),這兩種方法都将生成字元串'u7ffbu8a33u3067u5931u308fu308cu308b'。

到這裡,我們講述了如何将變量值放置在格式化字元串中,以及如何強制使用字元串形式或表象形式。現在,我們開始考慮值本身的格式問題。

整數、浮點數以及字元串的預設格式通常都足以滿足要求,但是如果需要實施更精确的控制,我們就可以通過格式規約很容易地實作。為了更易于掌握相關的詳細資訊,我們分别講述格式化字元串、整數與浮點數,不過圖2-6先給出了包括所有這些對象的通常文法格式。

《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串

對于字元串而言,我們可以控制的包括填充字元、字段内對齊方式以及字段寬度的最小值與最大值。

字元串格式規約是使用冒号(:)引入的,其後跟随可選的字元對——一個填充字元(可以不是))與一個對齊字元(<用于左對齊,^用于中間對齊,>用于右對齊),之後跟随的是可選的最小寬度(整數),如果需要指定最大寬度,就在其後使用句點,句點後跟随一個整數值。

要注意的是,如果我們指定了一個填充字元,就必須同時指定對齊字元。我們忽略了格式規約的符号與類型部分,因為對字元串沒有實際影響。隻使用一個冒号而沒有任何其他可選的元素是無害的,但也是無用的。

下面看一些執行個體:

在倒數第二個執行個體中,我們必須指定左對齊(即便這是預設的)。如果漏掉了<,就将得到:.25,這隻是意味着最大字段寬度為25個字元。

前面我們已經注意到,在格式化規約内部包括替換字段是有可能的,進而有可計算的格式也是可能的。比如,這裡給出了使用maxwidth變量設定字元串最大寬度的兩種方式:

第一種方法使用标準的字元串分片,第二種方法使用内部替換字段。

對于整數,通過格式規約,可以控制填充字元、字段内對齊、符号、最小字段寬度、基數等。

整數格式規約以冒号開始,其後可以跟随一個可選的字元對——一個填充字元(可以不是))與一個對齊字元(<用于左對齊,^用于中間對齊,>用于右對齊,=用于在符号與數字之間進行填充),之後跟随的是可選的符号字元:+表示必須輸出符号,-表示隻輸出負數符号,空格表示為正數輸出空格;為負數輸出符号-。再之後跟随的是可選的最小寬度整數值——其前可以使用字元#引導,以便擷取某種基數進制為字首的輸出(對二進制、八進制、十六進制數值),也可以以0引導,以便在對齊時使用0進行填充。如果希望輸出其他進制資料,而非十進制數,就必須添加一個類型字元——b用于表示二進制,o用于表示八進制,x用于表示小寫十六進制,x用于表示大寫十六進制,為了完整性,也可以使用d表示十進制整數。此外,還有兩個其他類型字元:c,表示輸出整數對應的unicode字元;n,表示以場所敏感的方式輸出數字。

我們可以以兩種不同的方式用0進行填充:

前兩個執行個體使用的填充字元為0,填充位置在符号與數字本身之間(=);後兩個執行個體要求最小寬度為12,并使用0進行填充。

下面給出了一些對齊執行個體:

下面給出一些展示符号字元作用的執行個體:

下面是兩個使用某些類型字元的執行個體:

為整數指定最大字段寬度是不可能的,這是因為,這樣做要求數字是可裁剪的,并可能會使整數沒有意義。

如果我們使用python 3.1,并在格式規範中使用一個逗号,則整數将使用逗号進行分組。例如:

最後一個可用于整數(也可用于浮點數)的格式化字元是n。在給定的字元是整數時,其作用與d相同;在給定的字元是浮點數時,其作用與g相同。n的特殊之處在于,充分考慮了目前的場所,并在其産生的輸出資訊中使用場所特定的十進制字元與分組字元。預設的場所稱為c場所,對這種c場所,十進制字元是一個句點,分組字元是一個空字元串。在程式的起始處添加下面兩行,并将其作為最先執行的語句,通過這種方式,可以充分考慮不同使用者的場所:3

如果将空字元串作為場所傳遞,那麼python會嘗試自動确定使用者的場所(比如,通過檢查lang環境變量),并以c場所為預設場所。下面給出一些執行個體,展示了對整數與浮點數使用不同場所的影響:

雖然n對于整數非常有用,但是對于浮點數的用途有限,因為随着浮點數的增大,就會使用指數形式對其進行輸出。

對于浮點數,通過格式規約,可以控制填充字元、字段對齊、符号、最小字段寬度、十進制小數點後的數字個數,以及是以标準形式、指數形式還是以百分數的形式輸出數字。

用于浮點數的格式規約與用于整數的格式規約是一樣的,隻是在結尾處有兩個差别。在可選的最小寬度後面,通過寫一個句點并在其後跟随一個整數,我們可以指定在小數點後跟随的數字個數。我們也可以在結尾處添加一個類型字元:e表示使用小寫字母e的指數形式,e表示使用大寫字母e的指數形式,f表示标準的浮點形式,g表示“通常”格式——這與f的作用是相同的,除非數字特别大(在這種情況下與e的作用相同——以及幾乎與g等同的g,但總是使用f或e)。另一個可以使用的是%——這會導緻數字擴大100倍,産生的數字結果使用f并附加一個%字元的格式輸出。

下面給出幾個執行個體,展示了指數形式與标準形式:

第一個執行個體中最小寬度為12個字元,在十進制小數點之後有2個數字。第二個執行個體建構在第一個執行個體之上,添加了一個填充字元*,由于使用了填充字元就必須同時也使用對齊字元,是以指定了右對齊方式(雖然對數字而言這是預設的)。第三個執行個體建構在前兩個執行個體之上,添加了符号操作符+,以便在輸出中使用符号。

在python 3.0中,decimal.decimal數值被str.format()當做字元串,而不是數值。這需要一定的技巧來格式化其輸出,從python 3.1開始,decimal.decimal數值能夠被格式化為floats,也能對逗号(,)提供支援,以獲得用逗号進行隔離的組。在下面這個例子中,由于在python 3.1中不再需要字段名,是以這裡将其删除。

如果我們省略格式字元f(或使用格式字元g),則數值将被格式化為'123457e+9'。

python 3.0并不直接支援複數的格式化,從python 3.1開始,才對其提供支援。不過,我們可以很容易地解決這一問題,這是通過将複數的實數部分與虛數部分分别作為單獨的浮點數進行格式化來實作的,比如:

在上面的執行個體中,我們分别存取複數每一部分的屬性,并都将其格式化為浮點數,在小數點後面帶有3個數字。我們還強制對虛數部分輸出符号,并添加字母j。

python 3.1用來格式化複數的文法與用于floats的文法相同:

該方法有一個輕微的缺點,即複數的實部和虛部使用的格式化方法完全相同。但是,如果我們想分别格式化複數的實部和虛部時,則總是可以使用python 3.0技術來分别通路複數的屬性。

在前面的小節中,我們詳細地研究了str.format()方法的格式規約,并給出了很多展示特别之處的代碼snippets。在這裡,我們給出一個雖然小但很有用的執行個體,該執行個體使用了str.format()方法,是以我們可以在真實的上下文中檢視相關的格式規約。該執行個體還使用了我們在前面幾節中講述的其他字元串方法,并引入了一個來自unicod- edata子產品的函數4。

該程式隻包含25行可執行代碼,導入了兩個子產品(sys與unicodedata),并定義了一個自定義函數print_unicode_table()。我們首先看一個運作執行個體,以便了解該程式的行為,之後我們将檢視程式末尾處實際的處理代碼,最後将檢視自定義函數。

《Python 3程式開發指南(第2版•修訂版)》——2.4 字元串

如果不帶參數運作,那麼該程式會生成一個表格,其中包含每個unicode字元,從空格字元開始,直到帶有最高可用字元的字元。如果給定了一個參數,比如執行個體中所展示的,那麼隻列印表格中那些小寫的unicode字元名包含該參數的列。

在完成導入與print_unicode_table()函數的建立後,程式執行到了上面所展示的代碼。這裡假定使用者尚未在指令行中給定一個用于比對的字。如果給定了一個指令行參數,并且是-h或--help,就列印出該程式的使用幫助資訊,并将word設定為0,以作為程式結束的訓示标記;否則,将word設定為使用者輸入的參數的小寫版。如果word不為0,就列印表格。

在列印使用幫助資訊時,我們使用的格式規約隻包括格式名——這裡是參數的位置編号。我們也可以寫成如下的形式:

使用上面的方法時,第一個0代表我們要使用的參數的索引位置,[0]代表該參數内的索引位置——這種寫法是有效的,因為sys.argv是一個清單。

為了保證代碼的清晰,我們使用了兩個空行。該函數的suite的頭兩行用于列印标題行。第一個str.format()用于在40個字元寬的字段中間位置列印“name”,第二個用于在40個字元寬的字段中間位置列印空白字元串,使用的填充字元為-,對齊方式為左對齊。(注意,如果指定了填充字元,就必須也指定對齊方式。)對于第二行,實際上也可以使用如下的替代方法完成:

這裡使用了字元串指派操作符*來建立合适的字元串,并簡單地将其插入到格式化字元串中。第三種替代方法則隻是簡單地輸入40個字元“-”,并使用字元串字面值。

我們在code變量中保持對unicode字元的追蹤,最初将其初始化為用于空格(0x20)的字元,并将變量end設定為最高的可用unicode字元——這裡的設定是可以變化的,依賴于python是否使用ucs-2或ucs-4字元編碼。

在while循環内部,我們使用chr()函數擷取與字元對應的unicode字元。unicodedata.name()函數用于傳回給定的unicode字元的名稱,如果沒有定義字元名,那麼該函數的第二個參數(可選的)是要使用的名稱。

如果使用者沒有指定一個字(word為none),或指定了并且是unicode字元名的小寫版,就列印對應的列。

雖然隻将code變量傳遞給str.format()方法一次,但是在格式化字元串中,該變量實際上使用了三次,第一次是将code作為一個整數在7字元寬的字段内列印(填充字元預設為空格,是以不需要明确指定),第二次是将code作為大寫的十六進制數在5字元寬的字段内列印,第三次是列印與code對應的unicode字元——使用c格式指定符,并在寬度最小值為3個字元的字段中間。注意,在第一種格式規約中,并不是必須要指定類型d,這是因為對整數參數這是預設的。第二個參數是字元的unicode字元名,使用首字母大寫的方式列印,也就是說,每個字的首字母大寫,所有其他字元都小寫。

我們熟悉了功能非常豐富的str.format()方法,在本書中,我們将對其進行充分的介紹和使用。

本質上說,計算機隻能存儲位元組,即8比特的值,如果是無符号數,那麼取值範圍從0x00到0xff,每個字元必須都以某種形式的位元組表示。在計算機技術的早期,研究者們設計的編碼機制是使用一個特定位元組表示某個特定的字元。例如,使用ascii編碼,就用0x41表示a,用0x42表示b,依此類推。在西歐,通常使用的是latin-1編碼,其前127個字元與7比特ascii相同,其餘部分則用于重音字元與歐洲人需要的其他符号。在計算機科學的發展中,研究者還設計了很多種其他編碼方式,其中的大部分仍然在使用中。

遺憾的是,存在太多的編碼方式會帶來很多不便,在編寫國際化軟體時更是如此。一個幾乎被最廣泛采納的标準是unicode編碼,在這種編碼中,unicode為每個字元配置設定一個整數,即字元,就像早期的編碼方式一樣,但是unicode不局限于使用一個位元組表示每個字元,因而有能力使用這種編碼方式表示每種語言中的每個字元。并且,作為對其表示能力的一種增強,其中的前127個unicode字元與7比特ascii表示的前127個字元是相同的。

unicode是如何存儲的?目前,定義了超過100萬個unicode字元,是以,即便使用有符号數字,一個32位整數也足以存放任何unicode 字元,是以,最簡單的用于存儲unicode字元的方式是使用32位整數序列,每個整數代表一個字元。在記憶體中,這是非常便利的,因為我們可以設計一個32位整數數組,數組中的每個元素與某個字元有一對一的對應關系。但對檔案或通過網絡連接配接發送的文本而言,尤其是在文本幾乎都是7比特ascii時,每個整數的4個位元組中最多有3個位元組會是0x00。為避免這樣的浪費,unicode自身有幾種表示方式。

在記憶體中,unicode通常以ucs-2格式(實質上是16比特的無符号整數)表示前65535個字元,或者以usc-4格式(32位整數)表示所有的字元——本書寫作時,共有1114111個。在python編譯時,會設定為使用某一種格式(如果sys.maxunicode為65535,python編譯時就使用ucs-2)。

對存放在檔案中或通過網絡連接配接傳送的資料,情況會更加複雜。如果使用了unicode,那麼字元可以使用utf-8進行編碼——這種編碼中,對前127個字元,每個字元使用一個位元組表示;對其他字元,則使用兩個或更多的位元組數來表示每個字元。對英文文本而言,utf-8是非常緊湊的,如果隻使用了7比特字元,則utf-8檔案與ascii檔案實質上是一樣的。另一種常見的編碼方式是utf-16編碼,這種編碼方式中,對大多數字元使用兩個位元組表示,對其他的一些字元則使用4個位元組表示。對某些亞洲語言,這種編碼方式比utf-8更緊湊,但與utf-8不同的是,utf-16文本應該以一個位元組順序标記開始,以便用于讀取該文本的代碼可以判定位元組對是big-endian還是little-endian。此外,所有舊的編碼格式,比如gb2312、iso-8859-5、latin-1等,實際上都在正常的使用中。

str.encode()方法可以傳回一個位元組序列——實際上是一個bytes對象,在第7章中将進行講述——編碼時根據我們提供的編碼參數進行編碼。使用這一方法,可以更好地了解不同編碼格式之間的差别,以及為什麼進行錯誤的編碼假設或導緻錯誤。

在引号之前使用一個字母b,表示使用的是位元組字面值,而非字元串字面值。作為一種便利,在建立位元組字面值時,我們可以混合使用可列印的ascii字元與十六進制轉義字元。

我們不能使用ascii編碼方式對tage Åsén的名稱進行編碼,因為這種編碼不包括Å字元或任意重音字元,是以,這樣做會導緻産生一個unicodeencodeerror異常。latin-1編碼(即iso-8859-1)是一種8比特的編碼格式,其中包含了這一名稱所需要的所有字元。另一方面,ern b´nk這一artist不夠幸運,因為字元不是一個latin-1字元,不能成功進行編碼。當然,這兩個名稱都可以使用unicode編碼格式進行編碼。要注意的是,對utf-16而言,頭兩個位元組表示的位元組順序标記——解碼函數會根據這一标記确定資料是big-endian還是little-endian,以便分别進行相應的處理。

對str.encode()方法,還有兩點值得注意。第一個參數(編碼名稱)是大小寫不敏感的,連字元與下劃線在其中是等同對待的,是以,“us-ascii”與“us_ascii”被認為是相同的。還有很多别名,比如,“latin”、“latin1”、“latin_1”、“iso-8859-1”、“cp819”,其他一些都是“latin-1”。該方法也可以接受可選的第二個參數,其作用是指定錯誤處理方式。例如,如果第二個參數為“ignore”或“replace”,那麼可以将任何字元串編碼為ascii格式——當然,這會導緻資料丢失——也可以無丢失的情況,如果我們使用“backslashreplace”,就會使用x、u與u等轉義字元替換非ascii字元。例如, artist.encode("ascii", "ignore")會産生b'tage sn',artist.encode("ascii", "replace")會産生b'tage ?s?n',而artist.encode("ascii", "backslashreplace")會産生b'tage xc5sxe9n'。(我們也可以使用"{0!a}".format(artist)得到一個ascii字元串'tage xc5sxe9n'。)

str.encode()方法的complement是bytes.decode()(以及bytearray.decode()),該方法将傳回一個字元串,其中使用給定的編碼格式對位元組進行解碼,例如:

8比特latin-1、cp850(一種ibm pc編碼格式)以及utf-8編碼之間的細弱差别使得猜測編碼格式不太可行,幸運的是,utf-8正在成為明文文本檔案編碼格式的事實标準,是以,後人隻需要熟悉這一格式,甚至不需要知道曾經存在過其他編碼格式。

python的.py檔案使用utf-8編碼,是以,python總是知道字元串字面值要使用的編碼格式。這意味着,我們可以在字元串中輸入任意的unicode字元——隻要使用的編輯器支援5。

在從外部源(比如socket)讀取資料時,python無法知道其使用的編碼格式,是以會傳回位元組序列,并由程式員對其進行相應的解碼。對文本檔案,python采用一種更軟化的方法,即使用本地編碼——除非明确指定編碼格式。

幸運的是,有些檔案格式會指定其編碼格式。比如,我們可以假定xml檔案使用的是utf-8編碼,除非<?xml?>指令明确地指定了不同的編碼格式。是以,閱讀xml時,我們可以提取比如前1000個位元組,尋找其中的編碼規約,如果找到,就使用指定的編碼格式對檔案進行解碼,否則使用預設的utf-8編碼。對使用python所支援的單位元組編碼的任意xml檔案或明文文本檔案,除基于ebcdic的編碼(cp424、cp500)與一些其他編碼(cp037、cp864、cp865、cp1026、cp1140、hz、shift-jis-2004、shift-jisx0213)之外,這一方法應該都可以正常工作。遺憾的是,對多位元組編碼(比如utf-16與utf-32),這一方法不能有效工作。在python package index,pypi.python.org/pypi中,至少有兩個python包可用于檢測檔案的編碼格式。