溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Python如何實(shí)現(xiàn)SICP賦值和局部狀態(tài)

發(fā)布時(shí)間:2023-03-07 10:10:01 來源:億速云 閱讀:117 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要講解了“Python如何實(shí)現(xiàn)SICP賦值和局部狀態(tài)”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“Python如何實(shí)現(xiàn)SICP賦值和局部狀態(tài)”吧!

所謂模塊化,也即使這些系統(tǒng)能夠“自然地”劃分為一些內(nèi)聚(coherent)的部分,使這些部分可以分別進(jìn)行開發(fā)和維護(hù)。

在哲學(xué)上,組織程序的方式與我們對被模擬系統(tǒng)的認(rèn)識息息相關(guān)。接下來我們要研究兩種特色很鮮明的組織策略,它們源自于對于系統(tǒng)結(jié)構(gòu)的兩種非常不同的“世界觀”(world views)。

  • 第一種策略將注意力集中在對象(objects)上,將一個(gè)大型系統(tǒng)看成不同對象的集合,它們的狀態(tài)和行為可能隨著時(shí)間不斷變化。

  • 另一種組織策略將注意力集中在流過系統(tǒng)的信息流(streams of information)上,非常像EE工程師觀察一個(gè)信號處理系統(tǒng)。

這兩種策略都對程序設(shè)計(jì)提出了具有重要意義的語言要求。對于對象途徑而言,我們必須關(guān)注對象可以怎樣變化而又保持其標(biāo)識(identity)。這將迫使我們拋棄前面說講過的計(jì)算的代換模型,轉(zhuǎn)向更機(jī)械式的,理論上也更不容易把握的計(jì)算的環(huán)境模型(environment model)。在處理對象、變化和標(biāo)識時(shí),各種困難的根源在于我們需要在這一計(jì)算模型中與時(shí)間搏斗,如果引入并發(fā)后還將變得更糟糕。流方式將我們的模型中的模擬時(shí)間與求值過程中的事件發(fā)生順序進(jìn)行解耦,我們將通過一種稱為延時(shí)求值(lazy evaluation)的技術(shù)做到這一點(diǎn)。

1 局部狀態(tài)變量

在對象世界觀里,我們想讓計(jì)算對象具有隨著時(shí)間變化的狀態(tài),而這就需要讓每個(gè)計(jì)算對象有自己的一些局部狀態(tài)變量。現(xiàn)在讓我們來對一個(gè)銀行賬戶支取現(xiàn)金的情況做一個(gè)模擬。我們將用一個(gè)過程withdraw完成此事,它有一個(gè)參數(shù)amount表示支取的現(xiàn)金量。如果余額足夠則withdraw返回支取之后賬戶里剩余的款額,否則返回消息Insufficient funds(金額不足)。假設(shè)開始時(shí)賬戶有100元錢,在不斷使用withdraw的過程中我們可能得到下面的響應(yīng)序列:

withdraw(25) # 70
withdraw(25) # 50
withdraw(60) # "In sufficient funds"
withdraw(15) # 35

在這里可以看到表達(dá)式widthdraw(25)求值了兩次,但它產(chǎn)生的值卻不同,這是過程的一種新的行為方式。之前我們看到的過程都可以看做是一些可計(jì)算的數(shù)學(xué)函數(shù)的描述,兩次調(diào)用一個(gè)同一個(gè)過程,總會產(chǎn)生出相同的結(jié)果。

為了實(shí)現(xiàn)withdraw,我們可以用一個(gè)全局變量balance表示賬戶里的現(xiàn)金金額,并將withdraw定義為一個(gè)訪問balance的過程。下面是balancewidthdraw的定義:

balance = 100
def withdraw(amount):
    global balance
    if balance > amount:
        balance = balance - amount
        return balance
    else: 
        return "Insufficient funds"

雖然withdraw能像我們期望的那樣工作,變量balance卻表現(xiàn)出一個(gè)問題。如上所示,balance是定義在全局環(huán)境中的一個(gè)名字,因此可以被任何過程檢查或修改。我們希望將balance做成withdraw內(nèi)部的東西,因?yàn)檫@將使withdraw成為唯一能直接訪問balance的過程,而其他過程只能間接地(通過對withdraw的調(diào)用)訪問balance。這樣才能準(zhǔn)確地模擬有關(guān)的概念:balance是一個(gè)只有withdraw使用的局部狀態(tài)變量,用于保存賬戶狀態(tài)的變化軌跡。

我們可以通過下面的方式重寫出withdraw,使balance成為它內(nèi)部的東西:

def new_withdraw():
    balance = 100
    def inner(amount):
        nonlocal balance
        if balance > amount:
            balance = balance - amount
            return balance
        else:
            return "Insufficient funds"
    return inner

W = new_withdraw()
print(W(25)) # 70
print(W(25)) # 50
print(W(60)) # "In sufficient funds"
print(W(15)) # 35

這里的做法是用創(chuàng)建起一個(gè)包含局部變量balance的環(huán)境,并使它初始值為100。在這個(gè)環(huán)境里,我們創(chuàng)建了一個(gè)過程inner,它以amount作為一個(gè)參數(shù),其行為就像是前面的withdraw過程。這樣最終返回的過程就是new_withdraw,它的行為方式就像是withdraw,但其中的變量確實(shí)其他任何過程都不能訪問的。用程序設(shè)計(jì)語言的行話,我們說變量balance被稱為是封裝new_withedraw過程里面。

將賦值語句與局部變量相結(jié)合,形成了一種具有一般性的程序設(shè)計(jì)技術(shù),我們將一直使用這種技術(shù)區(qū)構(gòu)造帶有局部狀態(tài)的計(jì)算對象。但這一技術(shù)也帶來了麻煩,我們之前在代換模型中說,應(yīng)用(apply)一個(gè)過程應(yīng)該解釋為在將過程的形式參數(shù)用對應(yīng)的值取代之后再求值這一過程。但現(xiàn)在出現(xiàn)了麻煩,一旦在語言中引進(jìn)了賦值,代換就不再適合作為過程應(yīng)用的模型了(我們將在3.1.3節(jié)中看到其中的原因)。我們需要為過程應(yīng)用開發(fā)一個(gè)新模型,這一模型將在3.2節(jié)中介紹?,F(xiàn)在我們要首先檢查new_withdraw所提出的問題的幾種變形。

下面過程make_withdraw能創(chuàng)建出一種“提款處理器”。make_withdraw的形式參數(shù)balance描述了有關(guān)賬戶的初始余額值。

def make_withdraw(balance):
    def withdraw(amount):
        nonlocal balance
        if balance > amount:
            balance = balance - amount
            return balance
        else:
            return "Insufficient funds"
    return withdraw

下面用make_withdraw創(chuàng)建了兩個(gè)對象:

W1 = make_withdraw(100)
W2 = make_withdraw(100)
print(W1(50)) # 50
print(W2(70)) # 30
print(W2(40)) # Insufficient funds
print(W1(40)) # 10

我們可以看到,W1W2是相互完全獨(dú)立的對象,每一個(gè)都有自己的局部狀態(tài)變量balance,從一個(gè)對象提款與另一個(gè)毫無關(guān)系。

我們還可以創(chuàng)建出除了提款還能夠存入款項(xiàng)的對象,這樣就可以表示簡單的銀行賬戶了。下面是一個(gè)過程,它返回一個(gè)具有給點(diǎn)初始余額的“銀行賬戶對象”:

def make_account(balance):
    def withdraw(amount):
        nonlocal balance
        if balance >= amount:
            balance = balance - amount
            return balance
        else:
            return "In sufficient funds"
    def deposit(amount):
        nonlocal balance
        balance = balance + amount
        return balance
    def dispatch(m):
        nonlocal balance
        if m == "withdraw":
            return withdraw
        if m == "deposit":
            return deposit
        else:
            raise ValueError("Unkown request -- MAKE_ACOUNT %s" % m)
    return dispatch

對于make_acount的每次調(diào)用將設(shè)置好一個(gè)帶有局部狀態(tài)變量balance的環(huán)境,在這個(gè)環(huán)境里,make_account定義了能夠訪問balance過程depositwithdraw,另外還有一個(gè)過程dispatch,它以一個(gè)“消息”做為輸入,返回這兩個(gè)局部過程之一。過程dispatch本身將會被返回,做為表示有關(guān)銀行賬戶對象的值。這正好是我們在2.4.3節(jié)中看到過的程序設(shè)計(jì)的消息傳遞風(fēng)格,當(dāng)然這里將它與修改局部變量的功能一起使用。

acc = make_account(100)
print(acc("withdraw")(50)) # 50
print(acc("withdraw")(60)) # In sufficient funds
print(acc("deposit")(40)) # 90
print(acc("withdraw")(60)) # 30

acc的每次調(diào)用將返回局部定義的deposit或者withdraw過程,這個(gè)過程隨后被應(yīng)用于給定的amount。就像make_withdraw一樣,對make_amount的另一次調(diào)用

acc2 = make_acount(100)

將產(chǎn)生出另一個(gè)完全獨(dú)立的賬戶對象,維持著它自己的局部balance。

這里再舉一個(gè)實(shí)現(xiàn)累加器的例子(事實(shí)上該例子在《黑客與畫家》[2]第13章中也有出現(xiàn),被用來說明不同編程語言編程能力的差異)。累加器是一個(gè)過程,反復(fù)用數(shù)值參數(shù)調(diào)用它,就會使得它的各個(gè)參數(shù)累加到一個(gè)和中。每次調(diào)用時(shí)累加器將返回當(dāng)前的累加和。請寫出一個(gè)生成累加器的過程make_accumulator,它所生成的每個(gè)累加器維持著一個(gè)獨(dú)立的和。傳給make_accumulator的輸入描述了和的初始值。其Python實(shí)現(xiàn)代碼如下:

def make_accumulator(sum_value):
    def accumulator(number):
        nonlocal sum_value
        sum_value += number
        return sum_value
    return accumulator

A =  make_accumulator(5)
print(A(10)) # 15
print(A(10)) # 25

當(dāng)然,Common Lisp的寫法將更為簡單:

(defun make_accumulator (sum_value)
   (lambda (number) (incf sum_value number)))

Ruby的寫法與Lisp幾乎完全相同:

def make_accumulator (sum_value)
    lambda {|number| sum_value += number } end

2 引進(jìn)賦值帶來的利益

正如下面將要看到的,將賦值引進(jìn)所用的程序設(shè)計(jì)語言中,將會使我們陷入困難概念問題的叢林之中。但無論如何,將系統(tǒng)看做是帶有局部狀態(tài)的對象的集合,也是一種維護(hù)模塊化設(shè)計(jì)的強(qiáng)有力技術(shù)。先讓我們看一個(gè)簡單的例子:如何設(shè)計(jì)出一個(gè)過程rand,每次它被調(diào)用就會返回一個(gè)隨機(jī)選出的整數(shù)。這里的“隨機(jī)選擇”的意思并不清楚,其實(shí)我們希望的就是對rand的反復(fù)調(diào)用將產(chǎn)生一個(gè)具有均勻分布統(tǒng)計(jì)性質(zhì)的序列。假定我們已經(jīng)有一個(gè)過程rand-update,它的性質(zhì)就是,如果從一個(gè)給點(diǎn)的數(shù)x1開始,執(zhí)行下面操作

x2 = random_update(x1)
x3 = random_update(x2)

得到的值序列x1x2,x3,...將具有我們所希望的性質(zhì)。

實(shí)現(xiàn)random_update的一種常見方法就是采用將xx更新為ax+bax+b取模mm的規(guī)則,其中abm都是適當(dāng)選出的整數(shù)。比如:

def rand_update(x):
    a = int(pow(7, 5))
    b = 0
    m = int(pow(2, 31)) - 1
    return (a * x + b) % m

Knuth的TAOCP第二卷(半數(shù)值算法)[3]中包含了有關(guān)隨機(jī)數(shù)序列和建立起統(tǒng)計(jì)性質(zhì)的深入討論。注意,random_update是計(jì)算一個(gè)數(shù)學(xué)函數(shù),兩次給它同一個(gè)輸入,它將產(chǎn)生同一個(gè)輸出。這樣,如果“隨機(jī)”強(qiáng)調(diào)的事序列中每個(gè)數(shù)與前面的數(shù)無關(guān)的話,由random_update生成的數(shù)序列肯定不是“隨機(jī)的”。在“真正的隨機(jī)性”與所謂偽隨機(jī)序列(由定義良好的確定性計(jì)算產(chǎn)生出的但又具有適當(dāng)統(tǒng)計(jì)性質(zhì)的序列)之間的關(guān)系是一個(gè)非常復(fù)雜的問題,涉及到數(shù)學(xué)和哲學(xué)中的一些困難問題,Kolmogorov、Solomonoff、Chaitin為這些問題做出了很多貢獻(xiàn),從Chaitin 1975[4]可以找到有關(guān)討論。

現(xiàn)在回到當(dāng)前的話題來。我們已經(jīng)實(shí)現(xiàn)好了random_update,接下來在此基礎(chǔ)上實(shí)現(xiàn)rand。我們可以將rand實(shí)現(xiàn)為一個(gè)帶有局部狀態(tài)變量x的過程,其中將這個(gè)變量初始化為某個(gè)固定值rand_init。對rand的每次調(diào)用算出當(dāng)前xx值的random_update值:

def make_rand(random_init):
    x = random_init
    def inner():
        nonlocal x
        x  = rand_update(x)
        return x
    return inner

rand = make_rand(42)
print(rand()) # 705894
print(rand()) # 1126542223

當(dāng)然,即使不用賦值,我們也可以通過簡單地調(diào)用rand_update,生成同樣的隨機(jī)序列。但是這意味著程序中任何使用隨機(jī)數(shù)的部分都必須顯式地記住,需要將x的當(dāng)前值傳給rand_update作為參數(shù),這樣會徒增煩惱。

接下來,我們考慮用隨機(jī)數(shù)實(shí)現(xiàn)一種稱為蒙特卡羅模擬的技術(shù)。

蒙特卡羅方法包括從一個(gè)大集合里隨機(jī)選擇試驗(yàn)樣本,并在對這些試驗(yàn)結(jié)果的統(tǒng)計(jì)估計(jì)的基礎(chǔ)上做出推斷。例如,6/π26/π2是隨機(jī)選取的兩個(gè)整數(shù)之間沒有公共因子(也即最大公因子為1)的概率。我們可以利用這一事實(shí)做出ππ的近似值(這個(gè)定理出自Cesaro,見TAOCP第二卷[3]4.5.2的討論和證明)。

這一程序的核心是過程monte_carlo,它以某個(gè)試驗(yàn)的次數(shù)(trails)以及這個(gè)試驗(yàn)本身(experiment)作為參數(shù)。試驗(yàn)用一個(gè)無參過程cesaro_test表示,返回的是每次運(yùn)行的結(jié)果為真或假。monte_carlo運(yùn)行指定次數(shù)的這個(gè)試驗(yàn),它返回所做的這些試驗(yàn)中得到真的比例。

rand = make_rand(42)
import math
def estimate_pi(trials):
    return math.sqrt(6 / monte_carlo(trials, cesaro_test))

def cesaro_test():
    return math.gcd(rand(), rand()) == 1

def monte_carlo(trials, experiment):
    def iter(trials_remaining, trials_passed):
        if trials_remaining == 0:
            return trials_passed / trials
        elif cesaro_test():
            return iter(trials_remaining - 1, trials_passed + 1)
        else:
            return iter(trials_remaining - 1, trials_passed)
    return iter(trials, 0)

print(estimate_pi(500)) # 3.178208630818641

現(xiàn)在讓我們試一試不用rand,直接用rand_update完成同一個(gè)計(jì)算。如果我們不使用賦值去模擬局部狀態(tài),那么將不得不采取下面的做法:

random_init = 42
def estimate_pi(trials):
    return math.sqrt(6 / random_gcd_test(trials, random_init))

def random_gcd_test(trials, initial_x):
    def iter(trials_remaining, trials_passed, x):
        x1 = rand_update(x)
        x2 = rand_update(x1)
        if trials_remaining == 0:
            return trials_passed / trials
        elif math.gcd(x1, x2) == 1:
            return iter(trials_remaining - 1, trials_passed + 1, x2)
        else:
            return iter(trials_remaining - 1, trials_passed, x2)
    return iter(trials, 0, initial_x)

print(estimate_pi(500)) # 3.178208630818641

雖然這個(gè)程序還是比較簡單的,但它卻在模塊化上打開了一些令人痛苦的缺口,因?yàn)樗枰@式地去操作隨機(jī)數(shù)x1x2,并通過一個(gè)迭代過程將x2傳給random_update作為新的輸入。這種對于隨機(jī)數(shù)的顯式處理與積累檢查結(jié)果的結(jié)構(gòu)交織在一起。此外,就連上層的過程estimate_pi也必須關(guān)心提供隨機(jī)數(shù)的問題。由于內(nèi)部的隨機(jī)數(shù)生成器被暴露了出來,進(jìn)入了程序的其它部分,我們很難將蒙特卡羅方法的思想隔離出來了。反觀我們在程序的第一個(gè)版本中,由于通過賦值將隨機(jī)數(shù)生成器的狀態(tài)隔離在過程rand的內(nèi)部,因此就使隨機(jī)數(shù)生成的細(xì)節(jié)完全獨(dú)立于程序的其它部分了。

由上面的蒙特卡洛方法實(shí)例體現(xiàn)的一種普遍性系統(tǒng)設(shè)計(jì)原則就是:對于行為隨時(shí)間變化的計(jì)算對象(如銀行賬戶和隨機(jī)數(shù)生成器),我們需要設(shè)置局部狀態(tài)變量,并用對這些變量的賦值去模擬狀態(tài)的變化

3 引進(jìn)賦值的代價(jià)

正如上面所看到的,賦值操作使我們可以模擬帶有局部狀態(tài)的對象。然而,這一獲益也有一個(gè)代價(jià),也即使我們的程序設(shè)計(jì)語言不能再用前面所提到過的代換模型解釋了。進(jìn)一步說,任何具有“漂亮”數(shù)學(xué)性質(zhì)的簡單模型,都不可能繼續(xù)適合作為處理程序設(shè)計(jì)語言里的對象和賦值的框架了。

只要我們不適用賦值,以同樣參數(shù)對同一過程的兩次求值一定產(chǎn)生出同樣的結(jié)果,因此就可以認(rèn)為過程是在計(jì)算數(shù)學(xué)函數(shù)。就像我們在之前的章節(jié)中所提到的那樣,不用任何復(fù)制的程序設(shè)計(jì)稱為函數(shù)式程序設(shè)計(jì)。

要理解復(fù)制將怎樣使事情復(fù)雜化了,考慮3.1.1節(jié)中make_withdraw過程的一個(gè)簡化版本,其中不再關(guān)注是否有足夠余額的問題:

def make_simplified_withdraw(balance):
    def simplified_withdraw(amount):
        nonlocal balance
        balance = balance - amount
        return balance
    return simplified_withdraw

W = make_simplified_withdraw(25)
print(W(20)) # 5
print(W(10)) # -5

請將這一過程與下面make_decrementer過程做一個(gè)比較,該過程里沒有用賦值運(yùn)算:

def make_decrementer(balance):
    return lambda amount: balance - amount

make_decrementer返回的是一個(gè)過程,該過程從指定的量balance中減去其輸入,但順序調(diào)用時(shí)卻不會像make_simplifed_withdraw那樣產(chǎn)生累積的結(jié)果。

D = make_decrementer(25)
print(D(20)) # 5
print(D(10)) # 15

我們可以用代換模型解釋make_decrementer如何工作。例如,讓我們分析一下下面表達(dá)式的求值過程:

make_decrementer(25)(20)

首先簡化組合式中的操作符,用25代換make_decrementer體里的balance,這樣就規(guī)約出了下面的表達(dá)式:

(lambda amount: 25 - amount) (20)

隨后應(yīng)用運(yùn)算符,用20代換lambda表達(dá)體里的amount

25 - 20

最后結(jié)果是5。

現(xiàn)在再來看看,如果將類似的代換分析用于make_simplifed_withdraw,會出現(xiàn)什么情況:

make_simplified_withdraw(25)(20)

先簡化其中的運(yùn)算符,用25代換make_simplified_withdraw體里的balance,這樣就規(guī)約出了下面的表達(dá)式(注意,Python的lambda表達(dá)式里不能進(jìn)行賦值運(yùn)算(據(jù)Guido說是故意加以限制從而防止Python成為一門函數(shù)式編程語言),下面這個(gè)式子不能在Python解釋器中運(yùn)行,只是為了方便大家理解):

(lambda amount: balance = 25 - amount)(25)(20)

這里我們沒有代換賦值表達(dá)式里的balance,因?yàn)橘x值符號=的左邊部分并不會進(jìn)行求值,如果代換掉它,得到的25 = 25 - amount根本就沒有意義。

現(xiàn)在用20代換lambda表達(dá)式體里的amount

(balance = 25 - 20)(25)

如果我們堅(jiān)持使用代換模型,那么就必須說,這個(gè)過程應(yīng)用的結(jié)果是首先將balance設(shè)置為5,而后返回25作為表達(dá)式的值。這樣得到的結(jié)果當(dāng)然是錯(cuò)誤的。為了得到正確答案,我們不得不對balance的第一次出現(xiàn)(在=作用之前)和它的第二次出現(xiàn)(在=作用之后)加以區(qū)分,而代換模型根本無法完成這件事情。

這里的麻煩在于,從本質(zhì)上說代換的最終基礎(chǔ)就是,這一語言里的符號不過是作為值的名字。而一旦引入了賦值運(yùn)算=和變量的值可以變化的想法,一個(gè)變量就不再是一個(gè)簡單的名字了?,F(xiàn)在的一個(gè)變量索引著一個(gè)可以保存值的位置(place),而存儲再那里的值也是可以改變的。在3.2節(jié)里將會看到,在我們的計(jì)算模型里,環(huán)境將怎樣扮演者“位置”的角色。

同一和變化

這里暴露出的問題遠(yuǎn)遠(yuǎn)不是簡單地打破了一個(gè)特定計(jì)算模型,它還使得以前非常簡單明了的概念現(xiàn)在都變得有問題了。首先考慮兩個(gè)物體實(shí)際上“同一”(“the same”)的概念。

假定我們用同樣的參數(shù)調(diào)用make_decrementer兩次,就會創(chuàng)建出兩個(gè)過程:

D1 = make_decrementer(25)
D2 = make_decrementer(25)

D1D2是同一的嗎?“是”是一個(gè)可接受的回答,因?yàn)?code>D1和D2具有同樣的計(jì)算行為——都是同樣的將會從其輸入里減去25點(diǎn)過程。事實(shí)上,我們確實(shí)可以在任何計(jì)算中用D1代替D2而不會改變結(jié)果,如下所示:

print(D1(20)) # 5
print(D1(20)) # 5
print(D2(20)) # 5

于此相對應(yīng)的是調(diào)用make_simplified_withdraw兩次:

W1 = make_simplified_withdraw(25)
W2 = make_simplified_withdraw(25)

W1W2是同一的嗎?顯然不是,因?yàn)閷?code>W1和W2的調(diào)用會有不同的效果,下面的調(diào)用顯示出這方面的情況:

print(W1(20)) # 5
print(W1(20)) # -15
print(W2(20)) # 5

雖然W1W2都是通過對同樣表達(dá)式make_simplified_withdraw(25)求值創(chuàng)建起來的東西,從這個(gè)角度可以說它們“同一”。但如果說在任何表達(dá)式里都可以用W1代替W2,而不會改變表達(dá)式的求值結(jié)果,那就不對了。

如果一個(gè)語言支持在表達(dá)式里“同一的東西可以相互替換”的觀念,這樣替換不會改變有關(guān)表達(dá)式的值,這個(gè)語言就稱為是具有引用透明性。而當(dāng)我們的計(jì)算機(jī)語言包含賦值運(yùn)算之后,就打破了引用透明性。

一旦我們拋棄了引用透明性,有關(guān)計(jì)算對象“同一”的意義問題就很難形式地定義清楚了。事實(shí)上,在我們企圖用計(jì)算機(jī)程序去模擬的現(xiàn)實(shí)世界里,“同一”的意義本身就很難搞清楚的,這是由于“同一”和“變化”的循環(huán)定義所致:我們想要確定兩個(gè)看起來同一的事物是否確實(shí)是“同一個(gè)東西”,我們一般只能去改變其中一個(gè)對象,看另一個(gè)對象是否也同樣改變;但如果不觀察“同一個(gè)”對象兩次,看看對象的性質(zhì)是否與另一次不同,我們就能確定對象是否“變化”。由是觀之,我們必須要將“同一”作為一個(gè)先驗(yàn)觀念引入(PS:這里可以參見康德的思想),否則我們就不可能確定“變化”。

現(xiàn)在舉例說明這一問題會如何出現(xiàn)在程序設(shè)計(jì)里?,F(xiàn)在考慮一種新情況,假定Peter和Paul有銀行賬戶,其中有100塊錢。關(guān)于這一事實(shí)的如下模擬:

peter_acc = make_account(100)
paul_acc = make_account(100)

和如下模擬之間有著實(shí)質(zhì)性的不同:

peter_acc = make_account(100)
paul_acc = peter_acc

在前一種情況里,有關(guān)的兩個(gè)銀行賬戶互不相同。Peter所做的交易將不會影響Paul的賬戶,反之亦然。比如,當(dāng)Peter取10塊,Paul取10塊,則Paul賬戶里還有90塊:

peter_acc("withdraw")(10)
print(paul_acc("withdraw")(10)) # 90

而對于后一種情況,這里把paul_acc定義為與peter_acc是同一個(gè)東西,結(jié)果就使現(xiàn)在Peter和Paul共有一個(gè)共同的賬戶,此時(shí)當(dāng)Peter取10塊錢,Paul再取10塊錢后,Paul就只剩80塊錢了:

peter_acc("withdraw")(10)
print(paul_acc("withdraw")(10)) # 80

這里一個(gè)計(jì)算對象可以通過多于一個(gè)名字訪問的現(xiàn)象稱為別名(aliasing)。這里的銀行賬戶例子是最簡單的,我們在3.3節(jié)里還將看到一些更復(fù)雜的例子,例如“不同”的數(shù)據(jù)結(jié)構(gòu)共享某些部分,如果對某一個(gè)對象的修改可能由于“副作用”而修改了另一“不同的”的對象,因?yàn)檫@兩個(gè)“不同”對象實(shí)際上只是同一個(gè)對象的不同別名,當(dāng)我們忘記這一情況程序就可能出現(xiàn)錯(cuò)誤。這種錯(cuò)誤被稱為副作用錯(cuò)誤,特別難以定位和分析。因此某些人(如分布式計(jì)算大佬Lampson)就建議說,程序設(shè)計(jì)語言的設(shè)計(jì)不允許副作用或者別名。

命令式程序設(shè)計(jì)的缺陷

與函數(shù)式程序設(shè)計(jì)相對應(yīng)的,廣泛采用賦值的程序設(shè)計(jì)被稱為命令式程序設(shè)計(jì)(imperative programming)。除了會導(dǎo)致計(jì)算模型的復(fù)雜性之外,以命令式風(fēng)格寫出的程序還容易出現(xiàn)一些不會在函數(shù)式程序中出現(xiàn)的錯(cuò)誤。舉例來說,現(xiàn)在重看一下在1.2.1節(jié)里的迭代求階乘程序:

def factorial(n):
    def iter(product, counter):
        if counter > n:
            return product
        else:
            return iter(counter * product, counter + 1)
    return iter(1, 1)

print(factorial(4)) # 24

我們也可以不通過內(nèi)部迭代循環(huán)(這里假設(shè)Python支持尾遞歸)傳遞參數(shù),而是采用更命令的風(fēng)格,顯式地通過賦值去更新變量productcounter的值:

def factorial(n):
    product, counter = 1, 1
    def iter():
        nonlocal product, counter
        if counter > n:
            return product
        else:
            product = counter * product
            counter = counter + 1
            return iter()
    return iter()

print(factorial(4)) # 24

這樣做不會改變程序的結(jié)果,但卻會引進(jìn)一個(gè)很微妙的陷阱。我們應(yīng)該如何確定兩個(gè)賦值的順序呢?像上面的程序雖然是正確的,但如果以相反的順序?qū)懗鲞@兩個(gè)賦值:

counter = counter + 1 
product = counter * product

就會產(chǎn)生出與上面不同的錯(cuò)誤結(jié)果:

print(factorial(4)) # 120, Wrong!

感謝各位的閱讀,以上就是“Python如何實(shí)現(xiàn)SICP賦值和局部狀態(tài)”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對Python如何實(shí)現(xiàn)SICP賦值和局部狀態(tài)這一問題有了更深刻的體會,具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識點(diǎn)的文章,歡迎關(guān)注!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI