SCIP:構造過程抽象–面向對象的解釋
心智的活動,除了儘力產生各種簡單的認知之外,主要表現為如下三個方面:(1)將若干簡單認知組合為一個複合的認識,由此產出各種複雜的認知。(2)將兩個認知放在一起對照,不管他們如何簡單或者複雜,在這樣做時,並不能將他們合而為一。由此得到有關他們的相互關係的認知。(3)將有關認識與那些在實際中和它們同在的所有其他認識隔離開,這就是抽象。
所有普遍的認識都是這樣得到的。
–John Locke 有關人類理解的隨筆,1960
本文為SICP的一些筆記,用於記錄一些對計算機程序不同的看法,旨在通過數學計算的思路入門程序設計。SCIP是一本關於計算過程的書,計算過程關心數據的操作,創建程序的目的也是為了數據的處理,表現在代碼中便是符號表達式的精心編排,計算過程精密而準確地執行相應程序,初學程序設計的人們就像巫師的徒弟們,學習如何理解和預測咒語的效果,學習並驗證結果,不過,學習程序的危險性遠遠小於巫術。SICP中所有的代碼實踐為scheme(scheme為Lisp的某個版本,Lisp仍是AI領域中擁有理論上最高演算能力的語言),執行過程為解釋器的代碼交互過程,依據同樣的解釋器運行程序原理,也可以用python實現書上的練習題。
程序設計的基本元素
每一種編程語言都有三種機制:
- 基本的表達形式
- 組合的方法
- 抽象的方法
基本表達式為程序語言所關心的最簡單的個體,而組合的方法即組合這些簡單的個體成為複雜的元素。再將複雜的元素進行抽象,便可得到一個單元,單元也可以作為一個簡單的個體繼續組合,層層遞進便組成了完整的程序,這也是為什麼許多書中一定會提到遞歸
。
在程序中,有兩類基本要素:過程和數據,數據為用戶希望操作的「東西」,而過程就是有關操作這些規則的描述,任何強有力程序設計語言必須表述基本的數據和基本過程,還需提供對過程和數據進行組合和抽象的方法。
表達式
最簡單的程序入門,觀看代碼與解釋器交互,假設鍵盤輸入了一個表達式
,解釋器將表達式的求值結果顯示出來,最基本的表達式就是數,例如,給一個數486
:
mt@mt-P243:~$ python
Python 2.7.17 (default)
[GCC 7.5.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> 486
486
將表示數的表達式組合起來,形成複合表達式
Python 2.7.17 (default)
[GCC 7.5.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> 1000-7
993
>>> 993-7
986
>>> (3*((2*4)+(3+5)))+((10-7)+6)
57
對於複雜的算術式,解釋其也按照基本循環進行操作,讀入-求值-打印
命名和環境
通過給數據命名,通過使用名字進行運算,將名稱標識符稱為變量,將數據存到變量中。解決好命名問題,程序就完成一大半,最基本的表達式為變量賦值,此時數據到變量的過程也是一種抽象:
>>> size = 2
>>> size
2
組合
評估組合的過程有兩步:
- 評估子過程
- 將表達式的值應用到新的過程
例如:
>>> (3*((2*4)+(3+5)))
可用樹結構
root[“3*((2*4)+(3+5))”]–> LT[“3”]
root[“3*((2*4)+(3+5))”]–> RT[“((2*4)+(3+5))”]
RT[“((2*4)+(3+5))”]–>RTL[“(2*4)”]
RT[“((2*4)+(3+5))”]–>RTR[“(3+5)”]
RTL[“(2*4)”] –>RTLL[“2”]
RTL[“(2*4)”] –>RTLR[“4”]
RTR[“(3+5)”] –>RTRL[“3”]
RTR[“(3+5)”] –>RTRR[“5”]
過程的組合
- 數字和算術運算是原始數據和過程。
- 組合嵌套提供了一種組合操作的方法。
- 將名稱與值相關聯的定義提供了有限的限制抽象手段
a = (3*((2*4)+(3+5)))+((10-7)+6)
實例:採用牛頓法求平方根
\]
計算步驟:
步驟1 | 猜測 | 商 | 平均值 |
---|---|---|---|
(1) | 1 | 2/1=2 | (2+1)/2=1.5 |
(2) | 1.5 | 2/1.5 = 1.333 | (1.333+1.5)/2=1.4165 |
(3) | 1.4165 | 2/1.4165 = 1.412 | (1.4165+1.412)/2=1.41425 |
(4) | 1.41425 | 2/1.41425 = 1.4142 | (1.41425+1.4142)/2=1.414225 |
如果不限制條件,計算將一直進行下去,所以為了設計程序來計算平方根考慮計算步驟
1)先猜值
2)計算商
3)計算平均值作為下一輪的猜值
如果不加停止條件,那麼將會一直計算下去,觀察計算結果可以發現猜測值、商還有平均值越來越接近,如果約定一個誤差範圍,就可作為計算的停止條件(good_enough
)。
1)猜值的終止條件
def square(x):
return x*x
def good_enough(guess,x):
if abs(square(guess)-square(x))<0.001:
return True
else:
return False
2)和3)計算平均,作為下一輪猜值的起始,如果結果很好,立即結束,否則繼續猜,迭代過程可寫為
def improve_guess(guess,x):
return (x/guess + guess)/2
def sqrt_iter(guess,x):
print(guess,x)
if good_enough(guess,improve_guess(guess,x)):
print('guess:'+str(guess))
return guess
else:
return sqrt_iter(improve_guess(guess,x),x)
程序可寫為
def sqrt(x):
return sqrt_iter(1.0,x)
程序分解[原問題到子問題的分解]:
root[“sqrt”]–> Node[“sqrt_iter”]
Node[“sqrt_iter”]–>LT[“good_enough”]
Node[“sqrt_iter”]–>RT[“improve”]
RT[“improve”] –> improve_guess
LT[“good_enough”] –> square
LT[“good_enough”] –> abs
「使用許多基本的算術操作,對操作進行組合,通過定義各種複合過程,然後對複合過程進行抽象」
線性迭代和遞歸
考慮階乘
\]
與牛頓法求平方根一樣的思路,為了計算第n次迭代,需要考慮n-1次的結果,階乘可寫為
\]
那麼就知道兩種情況的編碼思路:
- 第1次 n為1
2)第n次 到 (n-1) 的迭代
def factorial1(n):
if n==1:
return 1
else:
return n* factorial(n-1)
用另一種觀點看待問題,1*2然後將結果 *3,再次 *4,直到 n,那麼利用一個計數器counter 即可寫成如下迭代:
counter \leftarrow counter + 1
\]
def fact_iter(product, counter, max_count):
if counter>max_count:
return product
else:
return fact_iter(counter*product, counter+1, max_count)
def factorial2(n):
return fact_iter(1,1,n)
factorial1
採用了先展開後計算的思路,而factorial2
採用了先計算後展開的思路,factorial1
稱為遞歸計算過程(表達式越寫越長),而factorial2
計算過程中表達式未發生改變,factorial2
多了一個變量用於保存中間的結果,這種迭代過程有時也和計算理論中提到的狀態變量類似,計算過程即狀態轉換的過程,同時還有一個(可能有)的停機過程。
最大公約數
兩個整數的最大公約數(GCD)定義為能除盡這兩個數的最大整數,算法基於以下觀察:如果r是a除以b的餘數,那麼a和b的公約數正好是b和r的公約數:
\]
此時,一個GCD的計算問題連續地歸約到越來越小的整數對,例如
=GCD(40,6)\\
=GCD(6,4)\\
=GCD(4,2)\\
=GCD(2,0)\\
=2
\]
def remainder(a,b):
return a%b
def gcd(a,b):
if b==0:
return a
else:
return gcd(b, remainder(a,b))
用高階函數做抽象
上述的過程也就是一類抽象,描述了一些對於數的符合操作,但是同時又不依賴於特定的數–將數作為參數傳入函數。人們對功能強大的程序設計語言有一個必然要求,就是能為公共模式命名,建立抽象,而後直接在抽象的層次上工作。
過程作為參數,
(1)計算從a到b的各個整數之和:
def sum_integers(a,b):
if a > b:
return 0
else:
return a + sum_integers(a+1,b)
(2)計算從a到b的各個整數立方和:
def sum_cubes(a,b):
if a > b:
return 0
else:
return cube(a) + sum_cubes(a+1,b)
(3)計算下面的序列之和:
\]
def pi_sum(a,b):
if a > b:
return 0
else:
return 1/(a*(a+2)) + pi_sum(a+4,b)
明顯看出,三個過程共享着一種公共的基礎模式:從a算出需要加的項的函數,還有用於提供下一個a值的函數,可以通過一個模板描述
def sum_term(term, a, next, b):
if a>b:
return 0
else:
return term(a)+sum_term(term, next(a), next, b)
而計算立方和時,term(a)
為cube(a)
,next(a)
為下一項,根據這個過程,可以改寫上述(1)~(3)的例子
def inc(n):
return n+1
def cube(a):
return a*a*a
def sum_cubes(a,b):
return sum_term(cube,a,inc,b)
有了上面這個模板sum_term
,將其作為基本單元,可以形式化其他概念,例如在a和b之間計算定積分的近似值
\]
其中dx
是一個很小的值,可以將公式轉化為
def integral(f, a, b, dx):
def add_dx(x):
return x + dx
return sum_term(f, a+dx/2, add_dx, b)*dx
用lambda構造過程
在原先寫的pi_sum
函數式,返回了「其輸入值加4的過程」和「其輸入值加2的乘積倒數的過程」,這個過程可以使用輔助函數,也可以使用lambda表達式,python中的labmda
表達式格式為 lambda <表達式的返回值>: 表達式
lambda x:x+4
lambda x: 1/(x*(x+4))
為了實現和原來pi_sum
的過程,可以使用lambda表達式和模板sum_term
實現一樣的功能
def pi_sum(a,b):
return sum_term(lambda x: 1.0 / (x * (x + 2)),a,
lambda x: x+4,b)
尋找函數的不動點
函數調用函數的過程,類似於數學上定義的複合函數的概念,如果f(x)=x
無限套娃,可以找到一個不動點:
\]
例如黃金分割率就是下面函數的不動點
\]
利用程序計算過程如下:
tolerance = 0.00001
def fixed_point(f, first_guess):
def close_enough(v1,v2):
return True if abs(v1-v2)<tolerance else False
def try_guess(guess):
next_guess = f(guess)
if close_enough(next_guess,guess):
return next_guess
else:
return try_guess(next_guess)
return try_guess(first_guess)
print(fixed_point(lambda x:1+1/x, 1))