本節書摘來自華章計算機《從問題到程序:用Python學編程和計算》一書中的第2章,第2.11節,作者 裘宗燕,更多章節內容可以訪問云棲社區“華章計算機”公眾號查看。
2.11 補充材料
本書各章的主要內容將圍繞著怎樣通過編程解決計算問題展開,正文中對Python語言的機制只做必要的說明,有些細節情況沒有涉及。另外,用Python編程也有許多有趣而且有用的技術。如果在各章的主要部分詳細羅列,也可能沖淡討論的主線。但是上述兩方面的一些情況也值得介紹。本書采用的方法是在一些章的最后增加稱為“補充材料”一節,補充一些細節,供讀者參考,也供用本書教授課程的教師選用。
除了討論語言細節和編程技術的兩個小節外,有時還總結了一些常用的編程模式。練習中的第1題總是對本章內容做些總結,其中列出本章討論過的重要概念。
2.11.1 語言細節
這里介紹Python語言中的一些與本章內容有關的情況,作為本章內容的補充。這里介紹的情況在本書中并不使用,對于初學者并不重要,可以有選擇地閱讀。提供這部分內容有兩方面意義,首先是使本書中對Python語言的介紹較為完整。另一方面,讀者在進一步使用Python去解決更復雜的問題時,也可能需要用到這里介紹的功能。
整數的二進制、八進制和十六進制字面量形式
Python只有一種整數,即是類型為int的整數。但另一方面,這個語言卻為整數提供了多種不同的字面量描述方式。前面介紹的十進制字面量形式使用最普遍,但Python還提供了二進制、八進制和十六進制的整數字面量形式。現在介紹有關情況。
首先,二進制、八進制和十六進制的整數字面量形式,都由一個引導部分和一個數值部分組成,一個整數仍然寫成一個連續的字符序列。這三種字面量的引導部分都以數字0開頭,在0之后用一個字符相互區分,而后緊接著是一串表示其數值的數字(對于十六進制,其中還能包含一些英文字母,見下)。
二進制整數字面量的引導部分是0b或者0B,隨后表示數值的部分中只能用兩個數字0和1,是一段0/1序列。例如0b110101和0B110100。在b或B之后立刻出現幾個0也允許,不影響結果的數值。二進制數的數值按如下方式計算:
這里實際上假設寫出的二進制字面量包含n + 1位二進制數字。很容易看到,Python的二進制字面量只是整數的一種寫法,例如:
>>>?0b110101
53
>>>?0B01001110
78
>>>
在二進制字面量里出現超出0和1的數字將作為出錯。此外,Python標準函數bin返回與其整數參數對應的二進制字面量形式(結果是一個字符串)。例如:
>>>?bin(78)
'0b1001110'
八進制整數字面量的引導部分用0o或者0O(這兩種形式都很糟糕,0和O太像了),隨后可以寫出任意長的數字0到7的序列。其計值公式是:
例如:
>>>?0O12
10
>>>?0O1234567012345670
45954944846776
與二進制類似,表示數字的部分中不能出現數字8或9。標準函數oct返回與其整數參數對應的八進制字面量形式(是一個字符串)。
十六進制整數字面量的引導部分用0x或者0X。這里有一點麻煩:表示十六進制數的數值需要16個數字,但阿拉伯數字只有10個。Python采用計算機領域的習慣做法,用最前面6個英文字母填補空缺:a或A表示10,b或B表示11,c或C表示12,d或D表示13,e或E表示14,f或F表示15。下面是兩個十六進制字面量:
>>>?0X12
18
>>>?0xdeadbeeffeeddeaf
16045690985374408367
``
十六進制數表示的計值公式與上面類似:

標準函數hex返回其整數參數對應的十六進制字面量字符串。
用二進制、八進制、十六進制字面量都可以描述任意大的整數。
###整數的位運算
計算機里的數據都用二進制編碼的形式表示,位(即二進制位,bit)是最基本的編碼單位,也是最小的數據表示單位。在實際問題中,有些對象的變化情況很簡單,只用一個或幾個位就能表示,如果程序里這種數據很多,把它們表示為某類型的對象,可能造成很大的存儲浪費。一種可能辦法是把多個這類數據對象存入一個整數類型的對象。此外,一些與底層有關的程序可能需要操作二進制位數據。例如,硬件工作狀態信息通常用二進制位串表示,操作硬件時就需要用位串形式發命令。為適應這些情況,Python語言提供了針對整數中二進制位的操作。有關的運算符稱為按位運算符。
先介紹基本的位運算,它們是按位運算符的基礎。一個二進制位只能取值0或1,位運算就是對二進制位的運算,從一個或兩個0/1值出發,算出0/1結果。常用位運算共有四個,其中“否定”為一元運算,其他都是二元運算,運算規則很簡單,見下表:

從這個表可以看出,否定就是1變0而0變1;只有1和1“與”的結果是1,其他情況的結果均為0;0和0“或”的結果為0,其他情況結果都是1;“異或”運算看兩個被操作位是否相同,相同時是1,否則是0。
與上面的位運算相對應,Python語言定義了四個按位運算符,把它們作用于整型對象,得到整型結果。這些按位運算符把整數看成二進制位的序列。按位否定運算符是一元運算符,作用于一個整數類型的運算對象,其余三個都是二元運算符,作用于兩個整數類型的運算對象。在運算時,它們對運算對象的一個個(或一對對)二進制位分別做位運算,得到結果的各位。四個按位運算符是:

看一個例子,假設變量x和y都是16位整數,它們值分別是:
x: 0010,1001,0101,0111
y: 1001,1100,1111,1010
對x和y做各種按位運算,得到的結果如下:
~x 1101,0110,1010,1000
x?&?y 0000,1000,0101,0010
x?|?y 1011,1101,1111,1111
x?^?y 1011,0101,1010,1101
請讀者根據這個例子,對照上面說明,設法弄清各按位運算符的意義。注意,這里整數表示中的逗號只是為了閱讀方便。
這些運算符可以作用于任意大的整數對象,得到任意大的結果。如果兩個整數的長度不同,Python有默認的處理方式,總之不會丟失信息。
除了上面4個按位運算符外,Python還有兩個與二進制位有關的整數運算符,分別是左移運算符“<>”。它們的左邊是被位移的整數對象,右邊的整數表示希望左移或右移的位數。舉例說,對上面的y將有:
y?<
y?>>?3: 0001,0011,1001,1111
可以看到,左移(或右移)空出的二進制位全部補0。
另請注意,上面的這六個運算符都是做運算,產生新的整數對象。例如3 << 3“算出”一個結果(一個新的整數對象24)。假設變量z的值是3,z << 3得到24,但z的值不變。要改變z的值就需要賦值,或者用下面介紹的擴充賦值運算符。
上述運算符的優先級情況有些復雜:一元否定運算符的優先級與一元正負號相同,移位運算符的優先級低于整數加減,三個二元按位運算符的優先級互不相同,從高到低依次為 &、|、^,但都低于移位運算符。所有二元運算符都按從左到右結合。例如x << 3 >> 5相當于(x << 3) >> 5。由于優先級的情況比較復雜,建議少寫過分復雜的表達式,多用括號顯式描述運算的順序。
與上述5個二元位運算符相對應,Python有5個擴充賦值運算符,它們分別基于賦值符之后的第二個參數修改左邊表達式的對象(最簡單情況用變量表示):

例如,x <<= 5效果相當于x = x << 5,但書寫方便,通常效率更高。
###有關浮點數的進一步說明
高級語言的浮點數一般都直接映射到語言系統運行所在的計算機硬件,因此高級語言中的浮點數及浮點數計算直接反映了硬件的相關特征。在計算機設計層面,目前大多數硬件的浮點數計算都采用IEEE 754標準。這里的IEEE是電氣和電子工程師協會的簡寫,它是目前全球最大的一個非營利性專業技術學會。IEEE 754是IEEE頒布的一個浮點數算術系統標準,被大多數計算機硬件廠商采納。Python語言并沒有強制性地規定采用IEEE 754浮點數(如果那樣,在不執行這個標準的硬件上將很難實現Python),但常見環境中運行的Python中的浮點數應該符合這個標準。下面介紹最常用的浮點數情況。
在常規系統上運行的CPython都采用IEEE 754的雙精度浮點數標準作為語言的浮點數。這種浮點數用64位二進制碼表示,其中指數部分用11位,可以表示-1023到1023;表示數值的部分(術語是尾數部分)用52位,另有一個符號位表示正負數。
指數部分的大小決定了浮點數的表示范圍,絕對值最大(最小)大約是±21023的量級。換算到十進制數,表示的范圍(如前面所說)大致為±5×10-324~1.7×10308。尾數的位數決定了浮點數的表示精度,52位二進制數大約相當于十進制的16到17位。超過上述范圍的實數在這里無法表示。即使在范圍內,浮點數表示也受到精度的限制,只能表示數軸上一個個能用二進制編碼形式表示的孤立點。
IEEE 754的具體表示方式還有些細節,但知道上面這些對于初學者已經夠了。從基本編程的需要看,只需了解這種浮點數標準的表示范圍和精度。許多其他語言的實現也采用IEEE 754的雙精度浮點,其他Python實現多半也采用這個標準。
###浮點數舍入轉換
從浮點數轉換到整數,默認轉換方式是舍去小數部分,通常稱為截尾。內置函數round采用另一種轉換方式,通過舍入得到與浮點數最近的整數。但什么是最近呢?中小學的算術里教過“四舍五入”的舍入規則,但顯然這一規則偏向于入,從統計的觀點看,這樣得到的整數值偏大。銀行總按四舍五入付錢就會虧本,長期做下去累積的“入”也會導致虧本很多。為了防止這種情況,人們提出了另一種更為公平的舍入方式。
Python的round函數采用所謂“銀行家舍入”方法,可稱為“四舍六入五取偶”舍入,這也是目前常見計算機硬件采用的舍入計算標準(IEEE 754浮點數標準中的舍入計算標準方法,目前編程語言和工具大都直接借用這一舍入計算標準)。
具體說,如果需要舍入部分的最高位小于等于4和大于等于6,就直接分別舍去或者進位。假設需要舍入的那段數字的最高位是5,如果在這個5之后還有不為0的有效位(也就是說,明確地大于5),轉換時就進位。如果5之后都是0位,則根據5的前一位舍入:前一位是奇數時進位,前一位是偶數時舍去。這里把0也看作偶數。與簡單的四舍五入,“銀行家舍入”在概率上更公平。按照這種規則,可以看到:
round(0.5)
0
round(1.5)
2
round(-0.5)
0
round(-1.5)
-2
### 字符串和換意序列
在寫字符串時有些字符無法直接寫出,換行符是這種字符的典型代表。由于有這種情況,Python(與其他一些語言一樣)引入了換意序列的概念,用一種特殊形式的字符序列(包含兩個或更多字符)表示字符串里的一個字符。換意序列的第一個字符總是下劃線符,也就是說,字符串里出現的下劃線符總表示換意序列開始,后面字符(序列)決定換意序列表示的字符。前面介紹過幾個常用換意字符,下面是它們和另外幾個及其解釋:


其中,“\換行符”表示反斜線后緊跟換行。“\ooo”表示反斜線后緊跟3位八進制數字,這種形式的換意序列表示編碼為八進制為ooo的字符。“\xhh”表示反斜線后有一個x,后跟2位十六進制數字(包括A/B/C/D/E/F),這種換意序列表示編碼為hh的字符。
除上面這些表示形式外,Python還支持用于寫出所有Unicode字符的換意序列形式。這方面的細節這里不進一步介紹了。
用一對三個引號的形式,在字符串字面量中可以包含換行,不必再寫換意序列“\n”。此外,采用三個引號的形式,多數時候也不需要用單引號和雙引號的換意序列“\'”和“\"”。但有時還可能需要,例如下面字面量沒問題:
""""Is?this
the?book
of?you?",?she?asked"""
開頭的連續四個引號被正確解析為一個連續三引號和一個雙引號。但
""""Is?this
the?book
of?you?""""
將報錯。開始的連續四個雙引號可以正確解析,但最后的四個雙引號不行:解釋器看到前三個雙引號,認為它是字符串的結束標志,字符串到此結束。又看到一個雙引號,解釋器認為另一個字符串從這里開始。遇到換行,它認為字符串沒完,報語法錯誤。要正確寫出這個字符串(內容是一對雙引號括起的一句話),可以寫:
""""Is?this
the?book
of?you?""""
或者更簡單的,用一對三個單引號作為字符串括號。
如果在一行里連續寫出幾個字符串,解釋器會自動將其連接成一個字符串:
s?=?'abc'?"123"
s
'abc123'
但是,解釋器只是在處理字符串字面量,才這樣做。例如:
t?=?'456'
s?t
SyntaxError:?invalid?syntax
如果要拼接作為變量值的字符串,必須用拼接運算符(加法符號)。
###基本語句
表達式可以寫在程序中任何應該寫語句的位置,這樣的表達式就構成了一個表達式語句。在一般程序里,把普通表達式(例如算術表達式1 + 2等)作為語句使用意義不大。運行中執行這個語句時就求值該表達式,求出值后該語句的執行完成,表達式的值隨后將被丟掉,不產生任何效果(除了表達式計算可能耗費計算機時間)。
有用的表達式語句主要是函數調用。在Python里,函數調用是一類基本表達式。獨立寫出的函數調用就構成了一個表達式語句。前面程序里已經多次出現這種表達式語句,例如對內置函數print的調用語句。實際上,任何無特定返回值的函數(實際上返回None),通常都以表達式語句的方式使用。
Python允許在一行中寫多個語句,這時要求語句之間加分號。例如:
x?=?len;?y?=?x?+?1
這樣一行仍看作一個語句,在其執行時將順序執行其中的成分語句。最后的成分語句執行完畢時整個語句的執行完成。這種語句是順序語句的一種形式。當然,這種形式中也可以寫任何語句,不僅是賦值語句。例如在一個循環里寫:
x?=?y?+?4;?continue
雖然語言允許上面的形式,有時也會看到有人把幾個簡單語句寫在一行。但在Python編程實踐中,人們不大倡導這種形式。在絕大多數Python程序里,人們堅持一行一個語句的基本規則。在一些情況下用并行賦值語句同時給幾個變量賦值。
###2.11.2 編程技術
本節討論幾個與編程有關的技術問題。
###條件語句與條件表達式
在一些情況下,條件語句和條件表達式都能使用。例如下面同樣函數的兩個定義:
def?abs(x):
if?x?
return?-x
else:
return?x
def?abs(x):
return?-x?if?x?
兩個函數功能完全一樣,但后一個簡單許多。參考這個實例,可以總結出適合使用條件表達式的情況:在需要根據條件,從兩種不同的表達式計算中選一個,而且計算比較簡單時,采用條件表達式可以簡化程序。條件語句適合用于各種賦值情況,其一個分支中可以包含任意復雜的操作序列。用來處理上面問題,是大材小用了。
在后面章節里,還會看到許多使用條件表達式的有意思的例子。
###寫表達式的技術
如果需要寫的計算表達式非常復雜,應該設法做出安排,使寫出的程序代碼清晰易讀,容易檢查表達式書寫的正確性。有兩種方法可以參考。
其一是分解表達式,用一個或幾個中間變量記錄表達式中子部分的結果,而后用這些變量的值組合出最終的表達式。適當的分解有助于保證表達式的可讀性和正確性。
如果確實需要或者希望寫很長的表達式,那就需要安排好表達式的多行格式。首先做好準備,為能把表達式寫在多個行里,可以參考前面基于三邊求三角形函數中條件的寫法,先加入一個括號保證解釋器不會把表達式截斷,而后在每行適當的地方斷行,各行中屬于同層的結構相互對齊(仿照Python語言的格式)。例如,下面是一個長表達式:
x?=?((a1?a2?-?b1?b2?-?c1?c2?-?d1?d2)?+
(a1?b2?+?a2?b1?+?c1?d2?-?c2?d1)?*?i?+
(a1?c2?+?a2?c1?+?b2?d1?-?b1?d2)?*?j?+
(a1?d2?+?a2?d1?+?b1?c2?-?b2?c1)?*?k)
為能寫跨越多行的長表達式,在上面表達式開始加了一個括號。在上述表達式中,我們把屬于同一層的子表達式對齊,分別放在幾行。這樣寫表達式,很容易檢查,容易發現錯誤。IDLE或其他支持Python的開發系統都能幫助維持良好的表達式形式。但如何斷行等,還是需要人做好安排。
###數值計算函數中錯誤情況的返回值
有些數值計算函數不是全函數(相對于參數的類型而言),如果被調用時得到的參數不滿足需要,它將無法返回一個正確的結果。在這種情況下可以考慮報錯(后面介紹),也可以考慮讓函數返回一個特殊值。不同的處理方式各有優缺點。
本章中出現了幾種處理方法:求階乘函數對于負的參數都返回0;計算三角形面積的函數,對于不能構成三角形三邊的參數,返回float("nan")表達式的值。后面還會看到一些情況,這些做法都可以參考。
###本章介紹的Python關鍵字
這里列出本章討論過的Python關鍵字,供讀者參考:from,import,and,or,not,True,False,if,else,elif,for,in,while,break,continue,def,return,None,pass。在總共33個關鍵字中,本章已經介紹了19個。包括:
- 3個特殊字面量,表示邏輯常量的True和False,以及特殊值None;
- 3個邏輯運算符and、or和not(not還有其他使用方式,見后面的介紹);
- 流程控制結構中用6個,if,else,elif、for、in、while;
- 專門語句用4個:break,continue,return,pass;
- 程序包導入語句用from和import;