來源:2.2 Data Abstractionhtml
譯者:飛龍python
協議:CC BY-NC-SA 4.0git
因爲咱們但願在程序中表達世界中的大量事物,咱們發現它們的大多數都具備複合結構。日期是年月日,地理位置是精度和緯度。爲了表示位置,咱們但願程序語言具備將精度和緯度「粘合」爲一對數據的能力 -- 也就是一個複合數據結構 -- 使咱們的程序可以以一種方式操做數據,將位置看作單個概念單元,它擁有兩個部分。github
複合數據的使用也讓咱們增長程序的模塊性。若是咱們能夠直接將地理位置看作對象來操做,咱們就能夠將程序的各個部分分離,它們根據這些值如何表示來從本質上處理這些值。將某個部分從程序中分離的通常技巧是一種叫作數據抽象的強大的設計方法論。這個部分用於處理數據表示,而程序用於操做數據。數據抽象使程序更易於設計、維護和修改。編程
數據抽象的特徵相似於函數抽象。當咱們建立函數抽象時,函數如何實現的細節被隱藏了,並且特定的函數自己能夠被任何具備相同行爲的函數替換。換句話說,咱們能夠構造抽象來使函數的使用方式和函數的實現細節分離。與之類似,數據抽象是一種方法論,使咱們將複合數據對象的使用細節與它的構造方式隔離。數據結構
數據抽象的基本概念是構造操做抽象數據的程序。也就是說,咱們的程序應該以一種方式來使用數據,對數據作出儘量少的假設。同時,須要定義具體的數據表示,獨立於使用數據的程序。咱們系統中這兩部分的接口是一系列函數,叫作選擇器和構造器,它們基於具體表示實現了抽象數據。爲了演示這個技巧,咱們須要考慮如何設計一系列函數來操做有理數。app
當你閱讀下一節時,要記住當今編寫的多數 Python 代碼使用了很是高級的抽象數據類型,它們內建於語言中,好比類、字典和列表。因爲咱們正在瞭解這些抽象的工做原理,咱們本身不能使用它們。因此,咱們會編寫一些不那麼 Python 化的代碼 -- 它並非在語言中實現咱們的概念的一般方式。可是,咱們所編寫的代碼出於教育目的,它展現了這些抽象如何構建。要記住計算機科學並不僅是學習如何使用編程語言,也學習它們的工做原理。編程語言
有理數可表示爲整數的比值,而且它組成了實數的一個重要子類。相似於1/3
或者17/29
的有理數一般可編寫爲:函數
<numerator>/<denominator>
其中,<numerator>
和<denominator>
都是值爲整數的佔位符。有理數的值須要兩部分來描述。工具
有理數在計算機科學中很重要,由於它們就像整數那樣,能夠準確表示。無理數(好比pi
或者 e
或者 sqrt(2)
)會使用有限的二元展開代替爲近似值。因此在原則上,有理數的處理應該讓咱們避免算術中的近似偏差。
可是,一旦咱們真正將分子與分母相除,咱們就會只剩下截斷的小數近似值:
>>> 1/3 0.3333333333333333
當咱們開始執行測試時,這個近似值的問題就會出現:
>>> 1/3 == 0.333333333333333300000 # Beware of approximations True
計算機如何將實數近似爲定長的小數擴展,是另外一門課的話題。這裏的重要概念是,經過將有理數表示爲整數的比值,咱們可以徹底避免近似問題。因此出於精確,咱們但願將分子和分母分離,可是將它們看作一個單元。
咱們從函數抽象中瞭解到,咱們能夠在瞭解某些部分的實現以前開始編出東西來。讓咱們一開始假設咱們已經擁有一種從分子和分母中構造有理數的方式。咱們也假設,給定一個有理數,咱們都有辦法來提取(或選中)它的分子和分母。讓咱們進一步假設,構造器和選擇器如下面三個函數來提供:
make_rat(n, d)
返回分子爲n
和分母爲d
的有理數。
numer(x)
返回有理數x
的分子。
denom(x)
返回有理數x
的分母。
咱們在這裏正在使用一個強大的合成策略:心想事成。咱們並無說有理數如何表示,或者numer
、denom
和make_rat
如何實現。即便這樣,若是咱們擁有了這三個函數,咱們就能夠執行加法、乘法,以及測試有理數的相等性,經過調用它們:
>>> def add_rat(x, y): nx, dx = numer(x), denom(x) ny, dy = numer(y), denom(y) return make_rat(nx * dy + ny * dx, dx * dy) >>> def mul_rat(x, y): return make_rat(numer(x) * numer(y), denom(x) * denom(y)) >>> def eq_rat(x, y): return numer(x) * denom(y) == numer(y) * denom(x)
如今咱們擁有了由選擇器函數numer
和denom
,以及構造器函數make_rat
定義的有理數操做。可是咱們尚未定義這些函數。咱們須要以某種方式來將分子和分母粘合爲一個單元。
爲了實現咱們的數據抽象的具體層面,Python 提供了一種複合數據結構叫作tuple
,它能夠由逗號分隔的值來構造。雖然並非嚴格要求,圓括號一般在元組周圍。
>>> (1, 2) (1, 2)
元組的元素能夠由兩種方式解構。第一種是咱們熟悉的多重賦值:
>>> pair = (1, 2) >>> pair (1, 2) >>> x, y = pair >>> x 1 >>> y 2
實際上,多重賦值的本質是建立和解構元組。
訪問元組元素的第二種方式是經過下標運算符,寫做方括號:
>>> pair[0] 1 >>> pair[1] 2
Python 中的元組(以及多數其它編程語言中的序列)下標都以 0 開始,也就是說,下標 0 表示第一個元素,下標 1 表示第二個元素,以此類推。咱們對這個下標慣例的直覺是,下標表示一個元素距離元組開頭有多遠。
與元素選擇操做等價的函數叫作getitem
,它也使用下標以 0 開始的位置來在元組中選擇元素。
元素是原始類型,也就是說 Python 的內建運算符能夠操做它們。咱們不久以後再來看元素的完整特性。如今,咱們只對元組如何做爲膠水來實現抽象數據類型感興趣。
表示有理數。元素提供了一個天然的方式來將有理數實現爲一對整數:分子和分母。咱們能夠經過操做二元組來實現咱們的有理數構造器和選擇器函數。
>>> def make_rat(n, d): return (n, d) >>> def numer(x): return getitem(x, 0) >>> def denom(x): return getitem(x, 1)
用於打印有理數的函數完成了咱們對抽象數據結構的實現。
>>> def str_rat(x): """Return a string 'n/d' for numerator n and denominator d.""" return '{0}/{1}'.format(numer(x), denom(x))
將它與咱們以前定義的算術運算放在一塊兒,咱們可使用咱們定義的函數來操做有理數了。
>>> half = make_rat(1, 2) >>> str_rat(half) '1/2' >>> third = make_rat(1, 3) >>> str_rat(mul_rat(half, third)) '1/6' >>> str_rat(add_rat(third, third)) '6/9'
就像最後的例子所展現的那樣,咱們的有理數實現並無將有理數化爲最簡。咱們能夠經過修改make_rat
來補救。若是咱們擁有用於計算兩個整數的最大公約數的函數,咱們能夠在構造一對整數以前將分子和分母化爲最簡。這可使用許多實用工具,例如 Python 庫中的現存函數。
>>> from fractions import gcd >>> def make_rat(n, d): g = gcd(n, d) return (n//g, d//g)
雙斜槓運算符//
表示整數除法,它會向下取整除法結果的小數部分。因爲咱們知道g
能整除n
和d
,整數除法正好適用於這裏。如今咱們的
>>> str_rat(add_rat(third, third)) '2/3'
符合要求。這個修改只經過修改構造器來完成,並無修改任何實現實際算術運算的函數。
擴展閱讀。上面的str_rat
實現使用了格式化字符串,它包含了值的佔位符。如何使用格式化字符串和format
方法的細節請見 Dive Into Python 3 的格式化字符串一節。
在以更多複合數據和數據抽象的例子繼續以前,讓咱們思考一些由有理數示例產生的問題。咱們使用構造器make_rat
和選擇器numer
和denom
定義了操做。一般,數據抽象的底層概念是,基於某個值的類型的操做如何表達,爲這個值的類型肯定一組基本的操做。以後使用這些操做來操做數據。
咱們能夠將有理數系統想象爲一系列層級。
平行線表示隔離系統不一樣層級的界限。每一層上,界限分離了使用數據抽象的函數(上面)和實現數據抽象的函數(下面)。使用有理數的程序僅僅經過算術函數來操做它們:add_rat
、mul_rat
和eq_rat
。相應地,這些函數僅僅由構造器和選擇器make_rat
、numer
和and denom
來實現,它們自己由元組實現。元組如何實現的字節和其它層級沒有關係,只要元組支持選擇器和構造器的實現。
每一層上,盒子中的函數強制劃分了抽象的邊界,由於它們僅僅依賴於上層的表現(經過使用)和底層的實現(經過定義)。這樣,抽象界限能夠表現爲一系列函數。
抽象界限具備許多好處。一個好處就是,它們使程序更易於維護和修改。不多的函數依賴於特定的表現,當一我的但願修改表現時,不須要作不少修改。
咱們經過實現算術運算來開始實現有理數,實現爲這三個非特定函數:make_rat
、numer
和denom
。這裏,咱們能夠認爲已經定義了數據對象 -- 分子、分母和有理數 -- 上的運算,它們的行爲由這三個函數規定。
可是數據意味着什麼?咱們還不能說「提供的選擇器和構造器實現了任何東西」。咱們須要保證這些函數一塊兒規定了正確的行爲。也就是說,若是咱們從整數n
和d
中構造了有理數x
,那麼numer(x)/denom(x)
應該等於n/d
。
一般,咱們能夠將抽象數據類型當作一些選擇器和構造器的集合,並帶有一些行爲條件。只要知足了行爲條件(好比上面的除法特性),這些函數就組成了數據類型的有效表示。
這個觀點能夠用在其餘數據類型上,例如咱們爲實現有理數而使用的二元組。咱們實際上不會談論元組是什麼,而是談論由語言提供的,用於操做和建立元組的運算符。咱們如今能夠描述二元組的行爲條件,二元組一般叫作偶對,在表示有理數的問題中有所涉及。
爲了實現有理數,咱們須要一種兩個整數的粘合形式,它具備下列行爲:
若是一個偶對p
由x
和y
構造,那麼getitem_pair(p, 0)
返回x
,getitem_pair(p, 1)
返回y
。
咱們能夠實現make_pair
和getitem_pair
,它們和元組同樣知足這個描述:
>>> def make_pair(x, y): """Return a function that behaves like a pair.""" def dispatch(m): if m == 0: return x elif m == 1: return y return dispatch >>> def getitem_pair(p, i): """Return the element at index i of pair p.""" return p(i)
使用這個實現,咱們能夠建立和操做偶對:
>>> p = make_pair(1, 2) >>> getitem_pair(p, 0) 1 >>> getitem_pair(p, 1) 2
這個函數的用法不一樣於任何直觀上的,數據應該是什麼的概念。並且,這些函數知足於在咱們的程序中表示覆合數據。
須要注意的微妙的一點是,由make_pair
返回的值是叫作dispatch
的函數,它接受參數m
並返回x
或y
。以後,getitem_pair
調用了這個函數來獲取合適的值。咱們在這一章中會屢次返回拆解這個函數的話題。
這個偶對的函數表示並非 Python 實際的工做機制(元組實現得更直接,出於性能因素),可是它能夠以這種方式工做。這個函數表示雖然不是很明顯,可是是一種足夠完美來表示偶對的方式,由於它知足了偶對惟一須要知足的條件。這個例子也代表,將函數當作值來操做的能力,提供給咱們表示複合數據的能力。