天天看點

SCIP:構造過程抽象--python學習

心智的活動,除了盡力産生各種簡單的認知之外,主要表現為如下三個方面:(1)将若幹簡單認知組合為一個複合的認識,由此産出各種複雜的認知。(2)将兩個認知放在一起對照,不管他們如何簡單或者複雜,在這樣做時,并不能将他們合而為一。由此得到有關他們的互相關系的認知。(3)将有關認識與那些在實際中和它們同在的所有其他認識隔離開,這就是抽象。

所有普遍的認識都是這樣得到的。

--John Locke 有關人類了解的随筆,1960

本文為SICP的一些筆記,用于記錄一些對計算機程式不同的看法,旨在通過數學計算的思路入門程式設計。SCIP是一本關于計算過程的書,計算過程關心資料的操作,建立程式的目的也是為了資料的處理,表現在代碼中便是符号表達式的精心編排,計算過程精密而準确地執行相應程式,初學程式設計的人們就像巫師的徒弟們,學習如何了解和預測咒語的效果,學習并驗證結果,不過,學習程式的危險性遠遠小于巫術。SICP中所有的代碼實踐為scheme(scheme為Lisp的某個版本,Lisp仍是AI領域中擁有理論上最高演算能力的語言),執行過程為解釋器的代碼互動過程,依據同樣的解釋器運作程式原理,也可以用python實作書上的練習題。

程式設計的基本元素

每一種程式設計語言都有三種機制:

  1. 基本的表達形式
  2. 組合的方法
  3. 抽象的方法

基本表達式為程式語言所關心的最簡單的個體,而組合的方法即組合這些簡單的個體成為複雜的元素。再将複雜的元素進行抽象,便可得到一個單元,單元也可以作為一個簡單的個體繼續組合,層層遞進便組成了完整的程式,這也是為什麼許多書中一定會提到

遞歸

在程式中,有兩類基本要素:過程和資料,資料為使用者希望操作的“東西”,而過程就是有關操作這些規則的描述,任何強有力程式設計語言必須表述基本的資料和基本過程,還需提供對過程和資料進行組合和抽象的方法。

表達式

最簡單的程式入門,觀看代碼與解釋器互動,假設鍵盤輸入了一個

表達式

,解釋器将表達式的求值結果顯示出來,最基本的表達式就是數,例如,給一個數

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
           
組合

評估組合的過程有兩步:

  1. 評估子過程
  2. 将表達式的值應用到新的過程

    例如:

>>> (3*((2*4)+(3+5)))
           

可用樹結構

graph TD

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"]

過程的組合

  1. 數字和算術運算是原始資料和過程。
  2. 組合嵌套提供了一種組合操作的方法。
  3. 将名稱與值相關聯的定義提供了有限的限制抽象手段
a = (3*((2*4)+(3+5)))+((10-7)+6)
           

執行個體:采用牛頓法求平方根

\[\sqrt 2 \approx 1.414

\]

計算步驟:

步驟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)
           

程式分解[原問題到子問題的分解]:

graph TD

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·(n-1)·(n-2)···3·2·1

\]

與牛頓法求平方根一樣的思路,為了計算第n次疊代,需要考慮n-1次的結果,階乘可寫為

\[n!=n·(n-1)!

\]

那麼就知道兩種情況的編碼思路:

  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 即可寫成如下疊代:

\[product \leftarrow counter \times product \\

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(a,b)=GCD(b,r)

\]

此時,一個GCD的計算問題連續地歸約到越來越小的整數對,例如

\[GCD(206,40)\\

=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)計算下面的序列之和:

\[\frac{1}{1·3}+\frac{1}{5·7}+\frac{1}{9·11}+···\approx \frac{\pi}{8}

\]

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之間計算定積分的近似值

\[\int_{a}^{b}f dx=[f(a+\frac{dx}{2})+f(a+dx+\frac{dx}{2})+f(a+2dx+\frac{dx}{2})+···]dx

\]

其中

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

無限套娃,可以找到一個不動點:

\[f(x),f(f(x)),f(f(f(x))),...

\]

例如黃金分割率就是下面函數的不動點

\[f(x)=1+\frac{1}{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))