SICP Python 描述 1.2 程式設計元素

發表於2016-08-08

程式語言是操作計算機來執行任務的手段,它也在我們組織關於過程的想法中,作為一種框架。程式用於在程式設計社群的成員之間交流這些想法。所以,程式必須為人類閱讀而編寫,並且僅僅碰巧可以讓機器執行。

當我們描述一種語言時,我們應該特別注意這種語言的手段,來將簡單的想法組合為更復雜的想法。每個強大的語言都擁有用於完成下列任務的機制:

  • 基本的表示式和語句,它們由語言提供,表示最簡單的構建程式碼塊。
  • 組合的手段,複雜的元素由簡單的元素通過它來構建,以及
  • 抽象的手段,複雜的元素可以通過它來命名,以及作為整體來操作。

在程式設計中,我們處理兩種元素:函式和資料。(不久之後我們就會探索它們並不是真的非常不同。)不正式地說,資料是我們想要操作的東西,函式描述了運算元據的規則。所以,任何強大的程式語言都應該能描述基本資料和基本函式,並且應該擁有組合和抽象二者的方式。

1.2.1 表示式

在實驗 Python 直譯器之後,我們現在必須重新開始,按照順序一步步地探索 Python 語言。如果示例看上去很簡單,要有耐心 — 更刺激的東西還在後面。

我們以基本表示式作為開始。一種基本表示式就是數值。更精確地說,是你鍵入的,由 10 進位制數字表示的數值組成的表示式。

表示式表示的數值也許會和算數運算子組合,來形成複合表示式,直譯器會求出它:

這些算術表示式使用了中綴符號,其中運算子(例如+-*/)出現在運算元(數值)中間。Python包含許多方法來形成複合表示式。我們不會嘗試立即將它們列舉出來,而是在進行中介紹新的表示式形式,以及它們支援的語言特性。

1.2.2 呼叫表示式

最重要的複合表示式就是呼叫表示式,它在一些引數上呼叫函式。回憶代數中,函式的數學概念是一些輸入值到輸出值的對映。例如,max函式將它的輸入對映到單個輸出,輸出是輸入中的最大值。Python 中的函式不僅僅是輸入輸出的對映,它表述了計算過程。但是,Python 表示函式的方式和數學中相同。

呼叫表示式擁有子表示式:運算子在圓括號之前,圓括號包含逗號分隔的運算元。運算子必須是個函式,運算元可以是任何值。這裡它們都是數值。當求解這個呼叫表示式時,我們說max函式以引數 7.5 和 9.5 呼叫,並且返回 9.5。

呼叫表示式中的引數的順序極其重要。例如,函式pow計算第一個引數的第二個引數次方。

函式符號比中綴符號的數學慣例有很多優點。首先,函式可以接受任何數量的引數:

不會產生任何歧義,因為函式的名稱永遠在引數前面。

其次,函式符號可以以直接的方式擴充套件為巢狀表示式,其中元素本身是複合表示式。在巢狀的呼叫表示式中,不像巢狀的中綴表示式,巢狀結構在圓括號中非常明顯。

(理論上)這種巢狀沒有任何限制,並且 Python 直譯器可以解釋任何複雜的表示式。然而,人們可能會被多級巢狀搞暈。你作為程式設計師的一個重要作用就是構造你自己、你的同伴以及其它在未來可能會閱讀你程式碼的人可以解釋的表示式。

最後,數學符號在形式上多種多樣:星號表示乘法,上標表示乘方,橫槓表示除法,屋頂和側壁表示開方。這些符號中一些非常難以打出來。但是,所有這些複雜事物可以通過呼叫表示式的符號來統一。雖然 Python 通過中綴符號(比如+-)支援常見的數學運算子,任何運算子都可以表示為帶有名字的函式。

1.2.3 匯入庫函式

Python 定義了大量的函式,包括上一節提到的運算子函式,但是通常不能使用它們的名字,這樣做是為了避免混亂。反之,它將已知的函式和其它東西組織在模組中,這些模組組成了 Python 庫。需要匯入它們來使用這些元素。例如,math模組提供了大量的常用數學函式:

operator模組提供了中綴運算子對應的函式:

import語句標明瞭模組名稱(例如operatormath),之後列出被匯入模組的具名屬性(例如sqrtexp)。

Python 3 庫文件列出了定義在每個模組中的函式,例如數學模組。然而,這個文件為了解整個語言的開發者編寫。到現在為止,你可能發現使用函式做實驗會比閱讀文件告訴你更多它的行為。當你更熟悉 Python 語言和詞彙時,這個文件就變成了一份有價值的參考來源。

1.2.4 名稱和環境

程式語言的要素之一是它提供的手段,用於使用名稱來引用計算物件。如果一個值被給予了名稱,我們就說這個名稱繫結到了值上面。

在 Python 中,我們可以使用賦值語句來建立新的繫結,它包含=左邊的名稱和右邊的值。

名稱也可以通過import語句繫結:

我們也可以在一個語句中將多個值賦給多個名稱,其中名稱和表示式由逗號分隔:

=符號在 Python(以及許多其它語言)中叫做賦值運算子。賦值是 Python 中的最簡單的抽象手段,因為它使我們可以使用最簡單的名稱來引用複合操作的結果,例如上面計算的area。這樣,複雜的程式可以由複雜性遞增的計算物件一步一步構建,

將名稱繫結到值上,以及隨後通過名稱來檢索這些值的可能,意味著直譯器必須維護某種記憶體來跟蹤這些名稱和值的繫結。這些記憶體叫做環境。

名稱也可以繫結到函式。例如,名稱max繫結到了我們曾經用過的max函式上。函式不像數值,不易於渲染成文字,所以 Python 使用識別描述來代替,當我們列印函式時:

我們可以使用賦值運算子來給現有函式起新的名字:

成功的賦值語句可以將名稱繫結到新的值:

在 Python 中,通過賦值繫結的名稱通常叫做變數名稱,因為它們在執行程式期間可以繫結到許多不同的值上面。

1.2.5 巢狀表示式的求解

我們這章的目標之一是隔離程式化思考相關的問題。作為一個例子,考慮巢狀表示式的求解,直譯器自己會遵循一個過程:

為了求出呼叫表示式,Python 會執行下列事情:

  • 求出運算子和運算元子表示式,之後
  • 在值為運算元子表示式的引數上呼叫值為運算子子表示式的函式。

這個簡單的過程大體上展示了一些過程上的重點。第一步表明為了完成呼叫表示式的求值過程,我們首先必須求出其它表示式。所以,求值過程本質上是遞迴的,也就是說,它會呼叫其自身作為步驟之一。

例如,求出

需要應用四次求值過程。如果我們將每個需要求解的表示式抽出來,我們可以視覺化這一過程的層次結構:

SICP Python 描述 1.2 程式設計元素

這個示例叫做表示式樹。在電腦科學中,樹從頂端向下生長。每一點上的物件叫做節點。這裡它們是表示式和它們的值。

求出根節點,也就是整個表示式,需要首先求出枝幹節點,也就是子表示式。葉子節點(也就是沒有子節點的節點)的表示式表示函式或數值。內部節點分為兩部分:表示我們想要應用的求值規則的呼叫表示式,以及表示式的結果。觀察這棵樹中的求值,我們可以想象運算元的值向上流動,從葉子節點開始,在更高的層上融合。

接下來,觀察第一步的重複應用,這會將我們帶到需要求值的地方,並不是呼叫表示式,而是基本表示式,例如數字(比如2),以及名稱(比如add),我們需要規定下列事物來謹慎對待基本的東西:

  • 數字求值為它標明的數值,
  • 名稱求值為當前環境中這個名稱所關聯的值

要注意環境的關鍵作用是決定表示式中符號的含義。Python 中,在不指定任何環境資訊,來提供名稱x(以及名稱add)的含義的情況下,談到這樣一個表示式的值沒有意義:

環境提供了求值所發生的上下文,它在我們理解程式執行中起到重要作用。

這個求值過程並不符合所有 Python 程式碼的求解,僅僅是呼叫表示式、數字和名稱。例如,它並不能處理賦值語句。

的執行並不返回任何值,也不求解任何引數上的函式,因為賦值的目的是將一個名稱繫結到一個值上。通常,語句不會被求值,而是被執行,它們不產生值,但是會改變一些東西。每種語句或表示式都有自己的求值或執行過程,我們會在涉及時逐步介紹。

注:當我們說“數字求值為數值”的時候,我們的實際意思是 Python 直譯器將數字求解為數值。Python 的直譯器使程式語言具有了這個意義。假設直譯器是一個固定的程式,行為總是一致,我們就可以說數字(以及表示式)自己在 Python 程式的上下文中會求解為值。

1.2.6 函式圖解

當我們繼續構建求值的正式模型時,我們會發現直譯器內部狀態的圖解有助於我們跟蹤求值過程的發展。這些圖解的必要部分是函式的表示。

純函式:具有一些輸入(引數)以及返回一些輸出(呼叫結果)的函式。內建函式

可以描述為接受輸入併產生輸出的小型機器。

SICP Python 描述 1.2 程式設計元素

abs是純函式。純函式具有一個特性,呼叫它們時除了返回一個值之外沒有其它效果。

非純函式:除了返回一個值之外,呼叫非純函式會產生副作用,這會改變直譯器或計算機的一些狀態。一個普遍的副作用就是在返回值之外生成額外的輸出,例如使用print函式:

雖然這些例子中的printabs看起來很像,但它們本質上以不同方式工作。print的返回值永遠是None,它是一個 Python 特殊值,表示沒有任何東西。Python 互動式直譯器並不會自動列印None值。這裡,print自己列印了輸出,作為呼叫中的副作用。

SICP Python 描述 1.2 程式設計元素

呼叫print的巢狀表示式會凸顯出它的非純特性:

如果你發現自己不能預料到這個輸出,畫出表示式樹來弄清為什麼這個表示式的求值會產生奇怪的輸出。

要當心print!它的返回值為None,意味著它不應該在賦值語句中用作表示式:

簽名:不同函式具有不同的允許接受的引數數量。為了跟蹤這些必備條件,我們需要以一種展示函式名稱和引數名稱的方式,畫出每個函式。abs函式值接受一個叫作number的引數,向它提供更多或更少的引數會產生錯誤。print函式可以接受任意數量的引數,所以它渲染為print(...)。函式的可接受引數的描述叫做函式的簽名。

相關文章