python四种可变类型_SICP Python 描述 2.4 可变数据
2.4 可變數(shù)據(jù)
我們已經(jīng)看到了抽象在幫助我們應(yīng)對大型系統(tǒng)的復(fù)雜性時如何至關(guān)重要。有效的程序整合也需要一些組織原則,指導(dǎo)我們構(gòu)思程序的概要設(shè)計。特別地,我們需要一些策略來幫助我們構(gòu)建大型系統(tǒng),使之模塊化。也就是說,它們可以“自然”劃分為可以分離開發(fā)和維護的各個相關(guān)部分。
我們用于創(chuàng)建模塊化程序的強大工具之一,是引入可能會隨時間改變的新類型數(shù)據(jù)。這樣,單個數(shù)據(jù)可以表示獨立于其他程序演化的東西。對象行為的改變可能會由它的歷史影響,就像世界中的實體那樣。向數(shù)據(jù)添加狀態(tài)是這一章最終目標:面向?qū)ο缶幊痰囊亍?/p>
我們目前引入的原生數(shù)據(jù)類型 -- 數(shù)值、布爾值、元組、范圍和字符串 -- 都是不可變類型的對象。雖然名稱的綁定可以在執(zhí)行過程中修改為環(huán)境中不同的值,但是這些值本身不會改變。這一章中,我們會介紹一組可變數(shù)據(jù)類型。可變對象可以在程序執(zhí)行期間改變。
2.4.1 局部狀態(tài)
我們第一個可變對象的例子就是局部狀態(tài)。這個狀態(tài)會在程序執(zhí)行期間改變。
為了展示函數(shù)的局部狀態(tài)是什么東西,讓我們對從銀行取錢的情況進行建模。我們會通過創(chuàng)建叫做withdraw的函數(shù)來實現(xiàn)它,它將要取出的金額作為參數(shù)。如果賬戶中有足夠的錢來取出,withdraw應(yīng)該返回取錢之后的余額。否則,withdraw應(yīng)該返回消息'Insufficient funds'。例如,如果我們以賬戶中的$100開始,我們希望通過調(diào)用withdraw來得到下面的序列:
>>> withdraw(25)
75
>>> withdraw(25)
50
>>> withdraw(60)
'Insufficient funds'
>>> withdraw(15)
35
觀察表達式withdraw(25),求值了兩次,產(chǎn)生了不同的值。這是一種用戶定義函數(shù)的新行為:它是非純函數(shù)。調(diào)用函數(shù)不僅僅返回一個值,同時具有以一些方式修改函數(shù)的副作用,使帶有相同參數(shù)的下次調(diào)用返回不同的結(jié)果。我們所有用戶定義的函數(shù),到目前為止都是純函數(shù),除非他們調(diào)用了非純的內(nèi)建函數(shù)。它們?nèi)耘f是純函數(shù),因為它們并不允許修改任何在局部環(huán)境幀之外的東西。
為了使withdraw有意義,它必須由一個初始賬戶余額創(chuàng)建。make_withdraw函數(shù)是個高階函數(shù),接受起始余額作為參數(shù),withdraw函數(shù)是它的返回值。
>>> withdraw = make_withdraw(100)
make_withdraw的實現(xiàn)需要新類型的語句:nonlocal語句。當我們調(diào)用make_withdraw時,我們將名稱balance綁定到初始值上。之后我們定義并返回了局部函數(shù),withdraw,它在調(diào)用時更新并返回balance的值。
>>> def make_withdraw(balance):
"""Return a withdraw function that draws down balance with each call."""
def withdraw(amount):
nonlocal balance # Declare the name "balance" nonlocal
if amount > balance:
return 'Insufficient funds'
balance = balance - amount # Re-bind the existing balance name
return balance
return withdraw
這個實現(xiàn)的新奇部分是nonlocal語句,無論什么時候我們修改了名稱balance的綁定,綁定都會在balance所綁定的第一個幀中修改。回憶一下,在沒有nonlocal語句的情況下,賦值語句總是會在環(huán)境的第一個幀中綁定名稱。nonlocal語句表明,名稱出現(xiàn)在環(huán)境中不是第一個(局部)幀,或者最后一個(全局)幀的其它地方。
我們可以將這些修改使用環(huán)境圖示來可視化。下面的環(huán)境圖示展示了每個調(diào)用的效果,以上面的定義開始。我們省略了函數(shù)值中的代碼,以及不在我們討論中的表達式樹。
我們的定義語句擁有平常的效果:它創(chuàng)建了新的用戶定義函數(shù),并且將名稱make_withdraw在全局幀中綁定到那個函數(shù)上。
下面,我們使用初始的余額參數(shù)20來調(diào)用make_withdraw。
>>> wd = make_withdraw(20)
這個賦值語句將名稱wd綁定到全局幀中的返回函數(shù)上:
所返回的函數(shù),(內(nèi)部)叫做withdraw,和定義所在位置即make_withdraw的局部環(huán)境相關(guān)聯(lián)。名稱balance在這個局部環(huán)境中綁定。在例子的剩余部分中,balance名稱只有這一個綁定,這非常重要。
下面,我們求出以總數(shù)5調(diào)用withdraw的表達式的值:
>>> wd(5)
15
名稱wd綁定到了withdraw函數(shù)上,所以withdraw的函數(shù)體在新的環(huán)境中求值,新的環(huán)境擴展自withdraw定義所在的環(huán)境。跟蹤withdraw求值的效果展示了 Python 中nonlocal語句的效果。
withdraw的賦值語句通常在withdraw的局部幀中為balance創(chuàng)建新的綁定。由于nonlocal語句,賦值運算找到了balance定義位置的第一幀,并在那里重新綁定名稱。如果balance之前沒有綁定到值上,那么nonlocal語句會產(chǎn)生錯誤。
通過修改balance綁定的行為,我們也修改了withdraw函數(shù)。下次withdraw調(diào)用的時候,名稱balance會求值為15而不是20。
當我們第二次調(diào)用wd時,
>>> wd(3)
12
我們發(fā)現(xiàn)綁定到balance的值的修改可在兩個調(diào)用之間積累。
這里,第二次調(diào)用withdraw會創(chuàng)建第二個局部幀,像之前一樣,但是,withdraw的兩個幀都擴展自make_withdraw的環(huán)境,它們都包含balance的綁定。所以,它們共享特定的名稱綁定,調(diào)用withdraw具有改變環(huán)境的副作用,并且會由之后的withdraw調(diào)用繼承。
實踐指南。通過引入nonlocal語句,我們發(fā)現(xiàn)了賦值語句的雙重作用。它們修改局部綁定,或者修改非局部綁定。實際上,賦值語句已經(jīng)有了兩個作用:創(chuàng)建新的綁定,或者重新綁定現(xiàn)有名稱。Python 賦值的許多作用使賦值語句的執(zhí)行效果變得模糊。作為一個程序員,你應(yīng)該用文檔清晰記錄你的代碼,使賦值的效果可被其它人理解。
2.4.2 非局部賦值的好處
非局部賦值是將程序作為獨立和自主的對象觀察的重要步驟,對象彼此交互,但是各自管理各自的內(nèi)部狀態(tài)。
特別地,非局部賦值提供了在函數(shù)的局部范圍中維護一些狀態(tài)的能力,這些狀態(tài)會在函數(shù)之后的調(diào)用中演化。和特定withdraw函數(shù)相關(guān)的balance在所有該函數(shù)的調(diào)用中共享。但是,withdraw實例中的balance綁定對程序的其余部分不可見。只有withdraw關(guān)聯(lián)到了make_withdraw的幀,withdraw在那里被定義。如果make_withdraw再次調(diào)用,它會創(chuàng)建單獨的幀,帶有單獨的balance綁定。
我們可以繼續(xù)以我們的例子來展示這個觀點。make_withdraw的第二個調(diào)用返回了第二個withdraw函數(shù),它關(guān)聯(lián)到了另一個環(huán)境上。
>>> wd2 = make_withdraw(7)
第二個withdraw函數(shù)綁定到了全局幀的名稱wd2上。我們使用星號來省略了表示這個綁定的線。現(xiàn)在,我們看到實際上有兩個balance的綁定。名稱wd仍舊綁定到余額為12的withdraw函數(shù)上,而wd2綁定到了余額為7的新的withdraw函數(shù)上。
最后,我們調(diào)用綁定到wd2上的第二個withdraw函數(shù):
>>> wd2(6)
1
這個調(diào)用修改了非局部名稱balance的綁定,但是不影響在全局幀中綁定到名稱wd的第一個withdraw。
這樣,withdraw的每個實例都維護它自己的余額狀態(tài),但是這個狀態(tài)對程序中其它函數(shù)不可見。在更高層面上觀察這個情況,我們創(chuàng)建了銀行賬戶的抽象,它管理自己的內(nèi)部狀態(tài),但以一種方式對真實世界的賬戶進行建模:它基于自己的歷史提取請求來隨時間變化。
2.4.3 非局部賦值的代價
我們擴展了我們的計算環(huán)境模型,用于解釋非局部賦值的效果。但是,非局部復(fù)制與我們思考名稱和值的方式有一些細微差異。
之前,我們的值并沒有改變,僅僅是我們的名稱和綁定發(fā)生了變化。當兩個名稱a和b綁定到4上時,它們綁定到了相同的4還是不同的4并不重要。我們說,只有一個4對象,并且它永不會改變。
但是,帶有狀態(tài)的函數(shù)不是這樣的。當兩個名稱wd和wd2都綁定到withdraw函數(shù)時,它們綁定到相同函數(shù)還是函數(shù)的兩個不同實例,就很重要了。考慮下面的例子,它與我們之前分析的那個正好相反:
>>> wd = make_withdraw(12)
>>> wd2 = wd
>>> wd2(1)
11
>>> wd(1)
10
這里,通過wd2調(diào)用函數(shù)會修改名稱為wd的函數(shù)的值,因為兩個名稱都指向相同的函數(shù)。這些語句執(zhí)行之后的環(huán)境圖示展示了這個現(xiàn)象:
兩個名稱指向同一個值在世界上不常見,但我們程序中就是這樣。但是,由于值會隨時間改變,我們必須非常仔細來理解其它名稱上的變化效果,它們可能指向這些值。
正確分析帶有非局部賦值代碼的關(guān)鍵是,記住只有函數(shù)調(diào)用可以創(chuàng)建新的幀。賦值語句始終改變現(xiàn)有幀中的綁定。這里,除非make_withdraw調(diào)用了兩次,balance還是只有一個綁定。
變與不變。這些細微差別出現(xiàn)的原因是,通過引入修改非局部環(huán)境的非純函數(shù),我們改變了表達式的本質(zhì)。只含有純函數(shù)的表達式是引用透明(referentially transparent)的。如果我們將它的子表達式換成子表達式的值,它的值不會改變。
重新綁定的操作違反了引用透明的條件,因為它們不僅僅返回一個值。它們修改了環(huán)境。當我們引入任意重綁定的時候,我們就會遇到一個棘手的認識論問題:它對于兩個相同的值意味著什么。在我們的計算環(huán)境模型中,兩個分別定義的函數(shù)并不是相同的,因為其中一個的改變并不影響另一個。
通常,只要我們不會修改數(shù)據(jù)對象,我們就可以將復(fù)合數(shù)據(jù)對象看做其部分的總和。例如,有理數(shù)可以通過提供分子和分母來確定。但是這個觀點在變化出現(xiàn)時不再成立了,其中復(fù)合數(shù)據(jù)對象擁有一個“身份”,不同于組成它的各個部分。即使我們通過取錢來修改了余額,某個銀行賬戶還是“相同”的銀行賬戶。相反,我們可以讓兩個銀行賬戶碰巧具有相同的余額,但它們是不同的對象。
盡管它引入了新的困難,非局部賦值是個創(chuàng)建模塊化編程的強大工具,程序的不同部分,對應(yīng)不同的環(huán)境幀,可以在程序執(zhí)行中獨立演化。而且,使用帶有局部狀態(tài)的函數(shù),我們就能實現(xiàn)可變數(shù)據(jù)類型。在這一節(jié)的剩余部分,我們介紹了一些最實用的 Python 內(nèi)建數(shù)據(jù)類型,以及使用帶有非局部賦值的函數(shù),來實現(xiàn)這些數(shù)據(jù)類型的一些方法。
2.4.4 列表
list是 Python 中最使用和靈活的洗了類型。列表類似于元組,但是它是可變的。方法調(diào)用和賦值語句都可以修改列表的內(nèi)容。
我們可以通過一個展示(極大簡化的)撲克牌歷史的例子,來介紹許多列表編輯操作。例子中的注釋描述了每個方法的效果。
撲克牌發(fā)明于中國,大概在 9 世紀。早期的牌組中有三個花色,它們對應(yīng)錢的三個面額。
>>> chinese_suits = ['coin', 'string', 'myriad'] # A list literal
>>> suits = chinese_suits # Two names refer to the same list
撲克牌傳到歐洲(也可能通過埃及)之后,西班牙的牌組(oro)中之只保留了硬幣的花色。
>>> suits.pop() # Removes and returns the final element
'myriad'
>>> suits.remove('string') # Removes the first element that equals the argument
然后又添加了三個新的花色(它們的設(shè)計和名稱隨時間而演化),
>>> suits.append('cup') # Add an element to the end
>>> suits.extend(['sword', 'club']) # Add all elements of a list to the end
意大利人把劍叫做“黑桃”:
>>> suits[2] = 'spade' # Replace an element
下面是傳統(tǒng)的意大利牌組:
>>> suits
['coin', 'cup', 'spade', 'club']
我們現(xiàn)在在美國使用的法式變體修改了前兩個:
>>> suits[0:2] = ['heart', 'diamond'] # Replace a slice
>>> suits
['heart', 'diamond', 'spade', 'club']
也存在用于插入、排序和反轉(zhuǎn)列表的操作。所有這些修改操作都改變了列表的值,它們并不創(chuàng)建新的列表對象。
共享和身份。由于我們修改了一個列表,而不是創(chuàng)建新的列表,綁定到名稱chinese_suits上的對象也改變了,因為它與綁定到suits上的對象是相同的列表對象。
>>> chinese_suits # This name co-refers with "suits" to the same list
['heart', 'diamond', 'spade', 'club']
列表可以使用list構(gòu)造函數(shù)來復(fù)制。其中一個的改變不會影響另一個,除非它們共享相同的結(jié)構(gòu)。
>>> nest = list(suits) # Bind "nest" to a second list with the same elements
>>> nest[0] = suits # Create a nested list
在最后的賦值之后,我們只剩下下面的環(huán)境,其中列表使用盒子和指針的符號來表示:
根據(jù)這個環(huán)境,修改由suites指向的列表會影響nest第一個元素的嵌套列表,但是不會影響其他元素:
>>> suits.insert(2, 'Joker') # Insert an element at index 2, shifting the rest
>>> nest
[['heart', 'diamond', 'Joker', 'spade', 'club'], 'diamond', 'spade', 'club']
與之類似,在next的第一個元素上撤銷這個修改也會影響到suit。
由于這個pop方法的調(diào)用,我們返回到了上面描述的環(huán)境。
由于兩個列表具有相同內(nèi)容,但是實際上是不同的列表,我們需要一種手段來測試兩個對象是否相同。Python 引入了兩個比較運算符,叫做is和is not,測試了兩個表達式實際上是否求值為同一個對象。如果兩個對象的當前值相等,并且一個對象的改變始終會影響另一個,那么兩個對象是同一個對象。身份是個比相等性更強的條件。
譯者注:兩個對象當且僅當在內(nèi)存中的位置相同時為同一個對象。CPython 的實現(xiàn)直接比較對象的地址來確定。
>>> suits is nest[0]
True
>>> suits is ['heart', 'diamond', 'spade', 'club']
False
>>> suits == ['heart', 'diamond', 'spade', 'club']
True
最后的兩個比較展示了is和==的區(qū)別,前者檢查身份,而后者檢查內(nèi)容的相等性。
列表推導(dǎo)式。列表推導(dǎo)式使用擴展語法來創(chuàng)建列表,與生成器表達式的語法相似。
例如,unicodedata模塊跟蹤了 Unicode 字母表中每個字符的官方名稱。我們可以查找與名稱對應(yīng)的字符,包含這些卡牌花色的字符。
>>> from unicodedata import lookup
>>> [lookup('WHITE ' + s.upper() + ' SUIT') for s in suits]
['?', '?', '?', '?']
列表推導(dǎo)式使用序列的接口約定增強了數(shù)據(jù)處理的范式,因為列表是一種序列數(shù)據(jù)類型。
擴展閱讀。Dive Into Python 3 的推導(dǎo)式一章包含了一些示例,展示了如何使用 Python 瀏覽計算機的文件系統(tǒng)。這一章介紹了os模塊,它可以列出目錄的內(nèi)容。這個材料并不是這門課的一部分,但是推薦給任何想要增加 Python 知識和技巧的人。
實現(xiàn)。列表是序列,就像元組一樣。Python 語言并不提供給我們列表實現(xiàn)的直接方法,只提供序列抽象,和我們在這一節(jié)介紹的可變方法。為了克服這一語言層面的抽象界限,我們可以開發(fā)列表的函數(shù)式實現(xiàn),再次使用遞歸表示。這一節(jié)也有第二個目的:加深我們對調(diào)度函數(shù)的理解。
我們會將列表實現(xiàn)為函數(shù),它將一個遞歸列表作為自己的局部狀態(tài)。列表需要有一個身份,就像任何可變值那樣。特別地,我們不能使用None來表示任何空的可變列表,因為兩個空列表并不是相同的值(例如,向一個列表添加元素并不會添加到另一個),但是None is None。另一方面,兩個不同的函數(shù)足以區(qū)分兩個兩個空列表,它們都將empty_rlist作為局部狀態(tài)。
我們的可變列表是個調(diào)度函數(shù),就像我們偶對的函數(shù)式實現(xiàn)也是個調(diào)度函數(shù)。它檢查輸入“信息”是否為已知信息,并且對每個不同的輸入執(zhí)行相應(yīng)的操作。我們的可變列表可響應(yīng)五個不同的信息。前兩個實現(xiàn)了序列抽象的行為。接下來的兩個添加或刪除列表的第一個元素。最后的信息返回整個列表內(nèi)容的字符串表示。
>>> def make_mutable_rlist():
"""Return a functional implementation of a mutable recursive list."""
contents = empty_rlist
def dispatch(message, value=None):
nonlocal contents
if message == 'len':
return len_rlist(contents)
elif message == 'getitem':
return getitem_rlist(contents, value)
elif message == 'push_first':
contents = make_rlist(value, contents)
elif message == 'pop_first':
f = first(contents)
contents = rest(contents)
return f
elif message == 'str':
return str(contents)
return dispatch
我們也可以添加一個輔助函數(shù),來從任何內(nèi)建序列中構(gòu)建函數(shù)式實現(xiàn)的遞歸列表。只需要以遞歸順序添加每個元素。
>>> def to_mutable_rlist(source):
"""Return a functional list with the same contents as source."""
s = make_mutable_rlist()
for element in reversed(source):
s('push_first', element)
return s
在上面的定義中,函數(shù)reversed接受并返回可迭代值。它是使用序列的接口約定的另一個示例。
這里,我們可以構(gòu)造函數(shù)式實現(xiàn)的列表,要注意列表自身也是個函數(shù)。
>>> s = to_mutable_rlist(suits)
>>> type(s)
>>> s('str')
"('heart', ('diamond', ('spade', ('club', None))))"
另外,我們可以像列表s傳遞信息來修改它的內(nèi)容,比如移除第一個元素。
>>> s('pop_first')
'heart'
>>> s('str')
"('diamond', ('spade', ('club', None)))"
原則上,操作push_first和pop_first足以對列表做任意修改。我們總是可以清空整個列表,之后將它舊的內(nèi)容替換為想要的結(jié)果。
消息傳遞。給予一些時間,我們就能實現(xiàn)許多實用的 Python 列表可變操作,比如extend和insert。我們有一個選擇:我們可以將它們?nèi)繉崿F(xiàn)為函數(shù),這會使用現(xiàn)有的消息pop_first和push_first來實現(xiàn)所有的改變操作。作為代替,我們也可以向dispatch函數(shù)體添加額外的elif子句,每個子句檢查一個消息(例如'extend'),并且直接在contents上做出合適的改變。
第二個途徑叫做消息傳遞,它把數(shù)據(jù)值上面所有操作的邏輯封裝在一個函數(shù)中,這個函數(shù)響應(yīng)不同的消息。一個使用消息傳遞的程序定義了調(diào)度函數(shù),每個函數(shù)都擁有局部狀態(tài),通過傳遞“消息”作為第一個參數(shù)給這些函數(shù)來組織計算。消息是對應(yīng)特定行為的字符串。
可以想象,在dispatch的函數(shù)體中通過名稱來枚舉所有這些消息非常無聊,并且易于出現(xiàn)錯誤。Python 的字典提供了一種數(shù)據(jù)類型,會幫助我們管理消息和操作之間的映射,它會在下一節(jié)中介紹。
2.4.5 字典
字典是 Python 內(nèi)建數(shù)據(jù)類型,用于儲存和操作對應(yīng)關(guān)系。字典包含了鍵值對,其中鍵和值都可以是對象。字典的目的是提供一種抽象,用于儲存和獲取下標不是連續(xù)整數(shù),而是描述性的鍵的值。
字符串通常用作鍵,因為字符串通常用于表示事物名稱。這個字典字面值提供了不同羅馬數(shù)字的值。
>>> numerals = {'I': 1.0, 'V': 5, 'X': 10}
我們可以使用元素選擇運算符,來通過鍵查找值,我們之前將其用于序列。
>>> numerals['X']
10
字典的每個鍵最多只能擁有一個值。添加新的鍵值對或者修改某個鍵的已有值,可以使用賦值運算符來完成。
>>> numerals['I'] = 1
>>> numerals['L'] = 50
>>> numerals
{'I': 1, 'X': 10, 'L': 50, 'V': 5}
要注意,'L'并沒有添加到上面輸出的末尾。字典是無序的鍵值對集合。當我們打印字典時,鍵和值都以某種順序來渲染,但是對語言的用戶來說,不應(yīng)假設(shè)順序總是這樣。
字典抽象也支持多種方法,來從整體上迭代字典中的內(nèi)容。方法keys、values和items都返回可迭代的值。
>>> sum(numerals.values())
66
通過調(diào)用dict構(gòu)造函數(shù),鍵值對的列表可以轉(zhuǎn)換為字典。
>>> dict([(3, 9), (4, 16), (5, 25)])
{3: 9, 4: 16, 5: 25}
字典也有一些限制:
字典的鍵不能是可變內(nèi)建類型的對象。
一個給定的鍵最多只能有一個值。
第一條限制被綁定到了 Python 中字典的底層實現(xiàn)上。這個實現(xiàn)的細節(jié)并不是這門課的主題。直覺上,鍵告訴了 Python 應(yīng)該在內(nèi)存中的哪里尋找鍵值對;如果鍵發(fā)生改變,鍵值對就會丟失。
第二個限制是字典抽象的結(jié)果,它為儲存和獲取某個鍵的值而設(shè)計。如果字典中最多只存在一個這樣的值,我們只能獲取到某個鍵的一個值。
由字典實現(xiàn)的一個實用方法是get,如果鍵存在的話,它返回鍵的值,否則返回一個默認值。get的參數(shù)是鍵和默認值。
>>> numerals.get('A', 0)
0
>>> numerals.get('V', 0)
5
字典也擁有推導(dǎo)式語法,和列表和生成器表達式類似。求解字典推導(dǎo)式會產(chǎn)生新的字典對象。
>>> {x: x*x for x in range(3,6)}
{3: 9, 4: 16, 5: 25}
實現(xiàn)。我們可以實現(xiàn)一個抽象數(shù)據(jù)類型,它是一個記錄的列表,與字典抽象一致。每個記錄都是兩個元素的列表,包含鍵和相關(guān)的值。
>>> def make_dict():
"""Return a functional implementation of a dictionary."""
records = []
def getitem(key):
for k, v in records:
if k == key:
return v
def setitem(key, value):
for item in records:
if item[0] == key:
item[1] = value
return
records.append([key, value])
def dispatch(message, key=None, value=None):
if message == 'getitem':
return getitem(key)
elif message == 'setitem':
setitem(key, value)
elif message == 'keys':
return tuple(k for k, _ in records)
elif message == 'values':
return tuple(v for _, v in records)
return dispatch
同樣,我們使用了傳遞方法的消息來組織我們的實現(xiàn)。我們已經(jīng)支持了四種消息:getitem、setitem、keys和values。要查找某個鍵的值,我們可以迭代這些記錄來尋找一個匹配的鍵。要插入某個鍵的值,我們可以迭代整個記錄來觀察是否已經(jīng)存在帶有這個鍵的記錄。如果沒有,我們會構(gòu)造一條新的記錄。如果已經(jīng)有了帶有這個鍵的記錄,我們將這個記錄的值設(shè)為新的值。
我們現(xiàn)在可以使用我們的實現(xiàn)來儲存和獲取值。
>>> d = make_dict()
>>> d('setitem', 3, 9)
>>> d('setitem', 4, 16)
>>> d('getitem', 3)
9
>>> d('getitem', 4)
16
>>> d('keys')
(3, 4)
>>> d('values')
(9, 16)
這個字典實現(xiàn)并不為快速的記錄檢索而優(yōu)化,因為每個響應(yīng)getitem消息都必須迭代整個records列表。內(nèi)建的字典類型更加高效。
2.4.6 示例:傳播約束
可變數(shù)據(jù)允許我們模擬帶有變化的系統(tǒng),也允許我們構(gòu)建新的抽象類型。在這個延伸的實例中,我們組合了非局部賦值、列表和字典來構(gòu)建一個基于約束的系統(tǒng),支持多個方向上的計算。將程序表達為約束是一種聲明式編程,其中程序員聲明需要求解的問題結(jié)構(gòu),但是抽象了問題解決方案如何計算的細節(jié)。
計算機程序通常組織為單方向的計算,它在預(yù)先設(shè)定的參數(shù)上執(zhí)行操作,來產(chǎn)生合理的輸出。另一方面,我們通常希望根據(jù)數(shù)量上的關(guān)系對系統(tǒng)建模。例如,我們之前考慮過理想氣體定律,它通過波爾茲曼常數(shù)k關(guān)聯(lián)了理想氣體的氣壓p,體積v,數(shù)量n以及溫度t。
p * v = n * k * t
這樣一個方程并不是單方向的。給定任何四個數(shù)量,我們可以使用這個方程來計算第五個。但將這個方程翻譯為某種傳統(tǒng)的計算機語言會強迫我們選擇一個數(shù)量,根據(jù)其余四個計算出來。所以計算氣壓的函數(shù)應(yīng)該不能用于計算溫度,即使二者的計算通過相同的方程完成。
這一節(jié)中,我們從零開始設(shè)計線性計算的通用模型。我們定義了數(shù)量之間的基本約束,例如adder(a, b, c)會嚴格保證數(shù)學(xué)關(guān)系a + b = c。
我們也定義了組合的手段,使基本約束可以被組合來表達更復(fù)雜的關(guān)系。這樣,我們的程序就像一種編程語言。我們通過構(gòu)造網(wǎng)絡(luò)來組合約束,其中約束由連接器連接。連接器是一種對象,它“持有”一個值,并且可能會參與一個或多個約束。
例如,我們知道華氏和攝氏溫度的關(guān)系是:
9 * c = 5 * (f - 32)
這個等式是c和f之間的復(fù)雜約束。這種約束可以看做包含adder、multiplier和contant約束的網(wǎng)絡(luò)。
這張圖中,我們可以看到,左邊是一個帶有三個終端的乘法器盒子,標記為a,b和c。它們將乘法器連接到網(wǎng)絡(luò)剩余的部分:終端a鏈接到了連接器celsius上,它持有攝氏溫度。終端b鏈接到了連接器w上,w也鏈接到持有9的盒子上。終端c,被乘法器盒子約束為a和b的乘積,鏈接到另一個乘法器盒子上,它的b鏈接到常數(shù)5上,以及它的a連接到了求和約束的一項上。
這個網(wǎng)絡(luò)上的計算會如下進行:當連接器被提供一個值時(被用戶或被鏈接到它的約束器),它會喚醒所有相關(guān)的約束(除了剛剛喚醒的約束)來通知它們它得到了一個值。每個喚醒的約束之后會調(diào)查它的連接器,來看看是否有足夠的信息來為連接器求出一個值。如果可以,盒子會設(shè)置這個連接器,連接器之后會喚醒所有相關(guān)的約束,以此類推。例如,在攝氏溫度和華氏溫度的轉(zhuǎn)換中,w、x和y會被常量盒子9、5和32立即設(shè)置。連接器會喚醒乘法器和加法器,它們判斷出沒有足夠的信息用于處理。如果用戶(或者網(wǎng)絡(luò)的其它部分)將celsis連接器設(shè)置為某個值(比如25),最左邊的乘法器會被喚醒,之后它會將u設(shè)置為25 * 9 = 225。之后u會喚醒第二個乘法器,它會將v設(shè)置為45,之后v會喚醒加法器,它將fahrenheit連接器設(shè)置為77。
使用約束系統(tǒng)。為了使用約束系統(tǒng)來計算出上面所描述的溫度計算,我們首先創(chuàng)建了兩個具名連接器,celsius和fahrenheit,通過調(diào)用make_connector構(gòu)造器。
>>> celsius = make_connector('Celsius')
>>> fahrenheit = make_connector('Fahrenheit')
之后,我們將這些連接器鏈接到網(wǎng)絡(luò)中,這個網(wǎng)絡(luò)反映了上面的圖示。函數(shù)make_converter組裝了網(wǎng)絡(luò)中不同的連接器和約束:
>>> def make_converter(c, f):
"""Connect c to f with constraints to convert from Celsius to Fahrenheit."""
u, v, w, x, y = [make_connector() for _ in range(5)]
multiplier(c, w, u)
multiplier(v, x, u)
adder(v, y, f)
constant(w, 9)
constant(x, 5)
constant(y, 32)
>>> make_converter(celsius, fahrenheit)
我們會使用消息傳遞系統(tǒng)來協(xié)調(diào)約束和連接器。我們不會使用函數(shù)來響應(yīng)消息,而是使用字典。用于分發(fā)的字典擁有字符串類型的鍵,代表它接受的消息。這些鍵關(guān)聯(lián)的值是這些消息的響應(yīng)。
約束是不帶有局部狀態(tài)的字典。它們對消息的響應(yīng)是非純函數(shù),這些函數(shù)會改變所約束的連接器。
連接器是一個字典,持有當前值并響應(yīng)操作該值的消息。約束不會直接改變連接器的值,而是會通過發(fā)送消息來改變,于是連接器可以提醒其他約束來響應(yīng)變化。這樣,連接器代表了一個數(shù)值,同時封裝了連接器的行為。
我們可以發(fā)送給連接器的一種消息是設(shè)置它的值。這里,我們('user')將celsius的值設(shè)置為25。
>>> celsius['set_val']('user', 25)
Celsius = 25
Fahrenheit = 77.0
不僅僅是celsius的值變成了25,它的值也在網(wǎng)絡(luò)上傳播,于是fahrenheit的值也發(fā)生變化。這些變化打印了出來,因為我們在構(gòu)造這兩個連接器的時候命名了它們。
現(xiàn)在我們可以試著將fahrenheit設(shè)置為新的值,比如212。
>>> fahrenheit['set_val']('user', 212)
Contradiction detected: 77.0 vs 212
連接器報告說,它察覺到了一個矛盾:它的值是77.0,但是有人嘗試將其設(shè)置為212。如果我們真的想以新的值復(fù)用這個網(wǎng)絡(luò),我們可以讓celsius忘掉舊的值。
>>> celsius['forget']('user')
Celsius is forgotten
Fahrenheit is forgotten
連接器celsius發(fā)現(xiàn)了user,一開始設(shè)置了它的值,現(xiàn)在又想撤銷這個值,所以celsius同意丟掉這個值,并且通知了網(wǎng)絡(luò)的其余部分。這個消息最終傳播給fahrenheit,它現(xiàn)在發(fā)現(xiàn)沒有理由繼續(xù)相信自己的值為77。于是,它也丟掉了它的值。
現(xiàn)在fahrenheit沒有值了,我們就可以將其設(shè)置為212:
>>> fahrenheit['set_val']('user', 212)
Fahrenheit = 212
Celsius = 100.0
這個新值在網(wǎng)絡(luò)上傳播,并強迫celsius持有值100。我們已經(jīng)使用了非常相似的網(wǎng)絡(luò),提供fahrenheit來計算celsius,以及提供celsius來計算fahrenheit。這個無方向的計算就是基于約束的網(wǎng)絡(luò)的特征。
實現(xiàn)約束系統(tǒng)。像我們看到的那樣,連接器是字典,將消息名稱映射為函數(shù)和數(shù)據(jù)值。我們將要實現(xiàn)響應(yīng)下列消息的連接器:
connector['set_val'](source, value) 表示source請求連接器將當前值設(shè)置為該值。
connector['has_val']() 返回連接器是否已經(jīng)有了一個值。
connector['val'] 是連接器的當前值。
connector['forget'](source) 告訴連接器,source請求它忘掉當前值。
connector['connect'](source) 告訴連接器參與新的約束source。
約束也是字典,接受來自連接器的以下兩種消息:
constraint['new_val']() 表示連接到約束的連接器有了新的值。
constraint['forget']() 表示連接到約束的連接器需要忘掉它的值。
當約束收到這些消息時,它們適當?shù)貙⑺鼈儌鞑ソo其它連接器。
adder函數(shù)在兩個連接器上構(gòu)造了加法器約束,其中前兩個連接器必須加到第三個上:a + b = c。為了支持多方向的約束傳播,加法器必須也規(guī)定從c中減去a會得到b,或者從c中減去b會得到a。
>>> from operator import add, sub
>>> def adder(a, b, c):
"""The constraint that a + b = c."""
return make_ternary_constraint(a, b, c, add, sub, sub)
我們希望實現(xiàn)一個通用的三元(三個方向)約束,它使用三個連接器和三個函數(shù)來創(chuàng)建約束,接受new_val和forget消息。消息的響應(yīng)是局部函數(shù),它放在叫做constraint的字典中。
>>> def make_ternary_constraint(a, b, c, ab, ca, cb):
"""The constraint that ab(a,b)=c and ca(c,a)=b and cb(c,b) = a."""
def new_value():
av, bv, cv = [connector['has_val']() for connector in (a, b, c)]
if av and bv:
c['set_val'](constraint, ab(a['val'], b['val']))
elif av and cv:
b['set_val'](constraint, ca(c['val'], a['val']))
elif bv and cv:
a['set_val'](constraint, cb(c['val'], b['val']))
def forget_value():
for connector in (a, b, c):
connector['forget'](constraint)
constraint = {'new_val': new_value, 'forget': forget_value}
for connector in (a, b, c):
connector['connect'](constraint)
return constraint
叫做constraint的字典是個分發(fā)字典,也是約束對象自身。它響應(yīng)兩種約束接收到的消息,也在對連接器的調(diào)用中作為source參數(shù)傳遞。
無論約束什么時候被通知,它的連接器之一擁有了值,約束的局部函數(shù)new_value都會被調(diào)用。這個函數(shù)首先檢查是否a和b都擁有值,如果是這樣,它告訴c將值設(shè)為函數(shù)ab的返回值,在adder中是add。約束,也就是adder對象,將自身作為source參數(shù)傳遞給連接器。如果a和b不同時擁有值,約束會檢查a和c,以此類推。
如果約束被通知,連接器之一忘掉了它的值,它會請求所有連接器忘掉它們的值(只有由約束設(shè)置的值會被真正丟掉)。
multiplier與adder類似:
>>> from operator import mul, truediv
>>> def multiplier(a, b, c):
"""The constraint that a * b = c."""
return make_ternary_constraint(a, b, c, mul, truediv, truediv)
常量也是約束,但是它不會發(fā)送任何消息,因為它只包含一個單一的連接器,在構(gòu)造的時候會設(shè)置它。
>>> def constant(connector, value):
"""The constraint that connector = value."""
constraint = {}
connector['set_val'](constraint, value)
return constraint
這三個約束足以實現(xiàn)我們的溫度轉(zhuǎn)換網(wǎng)絡(luò)。
表示連接器。連接器表示為包含一個值的字典,但是同時擁有帶有局部狀態(tài)的響應(yīng)函數(shù)。連接器必須跟蹤向它提供當前值的informant,以及它所參與的constraints列表。
構(gòu)造器make_connector是局部函數(shù),用于設(shè)置和忘掉值,它響應(yīng)來自約束的消息。
>>> def make_connector(name=None):
"""A connector between constraints."""
informant = None
constraints = []
def set_value(source, value):
nonlocal informant
val = connector['val']
if val is None:
informant, connector['val'] = source, value
if name is not None:
print(name, '=', value)
inform_all_except(source, 'new_val', constraints)
else:
if val != value:
print('Contradiction detected:', val, 'vs', value)
def forget_value(source):
nonlocal informant
if informant == source:
informant, connector['val'] = None, None
if name is not None:
print(name, 'is forgotten')
inform_all_except(source, 'forget', constraints)
connector = {'val': None,
'set_val': set_value,
'forget': forget_value,
'has_val': lambda: connector['val'] is not None,
'connect': lambda source: constraints.append(source)}
return connector
同時,連接器是一個分發(fā)字典,用于分發(fā)五個消息,約束使用它們來和連接器通信。前四個響應(yīng)都是函數(shù),最后一個響應(yīng)就是值本身。
局部函數(shù)set_value在請求設(shè)置連接器的值時被調(diào)用。如果連接器當前并沒有值,它會設(shè)置該值并將informant記為請求設(shè)置該值的source約束。之后連接器會提醒所有參與的約束,除了請求設(shè)置該值的約束。這通過使用下列迭代函數(shù)來完成。
>>> def inform_all_except(source, message, constraints):
"""Inform all constraints of the message, except source."""
for c in constraints:
if c != source:
c[message]()
如果一個連接器被請求忘掉它的值,它會調(diào)用局部函數(shù)forget_value,這個函數(shù)首先執(zhí)行檢查,來確保請求來自之前設(shè)置該值的同一個約束。如果是的話,連接器通知相關(guān)的約束來丟掉當前值。
對has_val消息的響應(yīng)表示連接器是否擁有一個值。對connect消息的響應(yīng)將source約束添加到約束列表中。
我們設(shè)計的約束程序引入了許多出現(xiàn)在面向?qū)ο缶幊痰母拍睢<s束和連接器都是抽象,它們通過消息來操作。當連接器的值由消息改變時,消息不僅僅改變了它的值,還對其驗證(檢查來源)并傳播它的影響。實際上,在這一章的后面,我們會使用相似的字符串值的字典結(jié)構(gòu)和函數(shù)值來實現(xiàn)面向?qū)ο笙到y(tǒng)。
總結(jié)
以上是生活随笔為你收集整理的python四种可变类型_SICP Python 描述 2.4 可变数据的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: bb弹的枪违法吗
- 下一篇: 电脑装两个硬盘怎么设置主从盘 电脑如何设