about 3 years ago

  • 章節簡介
  • Python特性與應用
    • 動態語言
    • 直譯語言
    • 強定型的語言
    • 多風格的語言
    • 優雅而高效的語言
    • 像膠水一般的語言
    • 能跨平台的語言
    • 擁有豐富資源的語言
    • 高度活躍的語言
    • 小結
  • Python的下載與安裝
    • Mac
    • Windows
    • Linux
  • Python的基礎
    • 進入Python shell
    • Python的文件與求助
    • 關於運算
    • 關於變數
    • 關於資料型態
    • 關於物件參照
    • 撰寫多個運算
    • 封裝、參數化與函式
    • 類別與物件
  • 資料型態
    • 整數(Integer)與浮點數(Float)
    • 布林值(Boolean)
    • 字串(String)
    • 清單(List)
    • 元組(Tuple)
    • 字典(Dictionary)
    • 巢狀結構
    • 使用群集的好處
    • 可變與不可變
  • 運算
    • 賦值運算
    • 空運算
    • 自運算與增強運算
    • 比較運算
    • 邏輯運算
    • 身分運算
    • 隸屬運算
  • 流程控制
    • 有條件的運算
    • 重複的運算
      • while迴圈
      • break與continue
      • for迴圈
      • Comprehension
  • 輸入與輸出
  • 例外與捕捉
  • 函式
    • 內建函式
      • 工廠函式
      • 全部或任何
      • 最大與最小
      • 產生連續的整數
      • 群集的長度
    • 自定義函式
      • 函式的呼叫
      • 遞迴
      • 參數的預設值
    • 綴星運算式
    • 變數的有效範圍
    • 閉包與裝飾器
  • 物件導向
    • 自定義類別
    • 物件導向三大特性
      • 封裝
      • 繼承
      • 多型
  • 本章小結

章節簡介

在這個章節我們會學到:

  • Python的特色、優勢與應用
  • 如何建立可以運行Python的環境
  • Python的基礎語法和編程方法
  • 物件導向程式設計的概念

作為Django的基礎,我們免不了要先介紹這個偉大而又令人心醉神迷的語言-Python,在這個章節中,我們將會帶領各位讀者,迅速而簡要地了解Python,包括了基本的運算、資料型態、流程控制、例外捕捉、模組套件、物件導向和其他的部份進階技術。而這些也將成為我們日後理解並且使用Django的基礎。

讀者可以依照自己的狀況選擇是否閱讀本章,或是只閱讀部分內容,建議完全沒有Python經驗的讀者能夠從頭到尾瀏覽一次,而有一些經驗的讀者可以迅速地翻閱當做複習,並且留意一下進階技術的介紹,至於高手們則可以當做一個備考的章節,有需要可以回來此處翻閱。另外,由於本書的主題是Django,Python的篇幅筆者認為不宜過大,所以會盡可能簡要的概述,讀者們若有深入學習的必要(想必是有的!),歡迎去尋找各類適合學習的教材,作者也會適時地提供網路上不錯的教材或是推薦的參考書籍給大家。

Python特性與應用

在本章的最一開始,首先要向大家介紹一些Python的特性,這無非是想要讓讀者了解Python的威力與定位,從而能將Python的能力發揮地更徹底,同時也堅定大家使用Python的決心。

動態語言

Python是一個高階的動態語言,何謂動態語言呢?動態語言是指在執行時可以改變其結構的語言,包括變數參照型態的改變,函式、物件、和代碼的動態引進等。像是PHP、Ruby、JavaScript等都是動態語言,而與之相對的是像C、C++一類的靜態語言。

因為動態和可變造成的靈活性,動態語言具備了易學易寫的特性,在某種角度而言,這樣的語言比靜態語言更高階,更能處理複雜和瑣碎的事情,在學習和開發上的效率通常較高,所以近幾年來這些語言相當活躍。尤其在網際網路發達之後,Web服務和雲端科技等技術儼然成為資訊界的重要領域,而在這裡,動態語言是極具優勢的。我們當然也不諱言,動態語言的缺點還是存在的,效率不彰,對底層的控制性較差,都是靜態語言仍占一席之地的原因。

不過情況沒有我們想的那麼糟,動態語言的執行效率雖然無法與靜態語言相比,但是經過許多前輩與高手的努力,速度上面已經相差不遠,尤其是當我們要處理的任務不需要如此高效的時候(比如說記帳程式的效率要求一定低於運算物理方程式吧),使用一款在寫作上高效的語言比起使用一款執行上高效的語言是正確多了。我們總不會希望用靜態語言多花兩個月寫出來的記帳程式只比動態語言的執行速度快0.000001秒吧。

千萬別小看寫作上的高效,一個語言運作和運用的方式會導致完成任務所需時間的不同,如果讀者們有學習過C語言,可以想像Python跟C在完成一個工作上面的複雜度有多麼懸殊。而這種寫作上的高效和能面對複雜任務的能力正是動態語言所擁有的。

直譯語言

Python也是所謂的直譯語言,雖然在概念上,動態與直譯的意義大不相同,不過這兩個特性卻幾乎會一起出現:動態語言幾乎都是直譯語言。

那到底什麼是直譯語言呢?這要從編譯語言講起。一款編譯語言在寫完後,需要使用編譯器來將之轉為電腦能讀懂的機器碼,再讓電腦去執行。而直譯語言則是在要執行時,才透過直譯器一行一行地轉化為機器碼。聽起來兩者似乎都需要經過翻譯,看起來沒什麼不同,那讓我們舉個例子來說明。

這就好像我們寫出來的一篇中文文章必須翻譯成外文後才能讓外國人讀懂,編譯語言利用編譯器進行整篇文章的翻譯,不僅翻譯還潤飾跟調整架構(還講究信達雅呢!),碰到英國人就翻譯成英文,碰到日本人就翻譯成日文,往後若有人想讀這篇文章,就直接去找看的懂得翻譯文章來讀就好了。(在這裡不同國家的外國人相當於不同的作業系統,同樣的程式碼若要到Windows執行則需要Windows的編譯器,若要到Mac執行則需要Mac的編譯器。)而直譯語言則是請了一個即時翻譯(直譯器),他接收一句中文,吐出一句外文,每次外國人要看文章時,都需要有這個翻譯在旁邊。

透過這個比喻,我們可以大致分析兩者的優缺點,編譯語言的執行速度一定比較快,因為外國人可以直接看翻譯好的文章而不必等待翻譯一句一句翻譯,但是直譯語言的可攜性一定更好,因為一篇英文文章拿到日本就沒有人看的懂了(別想太多,這裡只是比喻!),但是我卻可以帶著我的中文文章,在英國找個英文翻譯,在日本找個日本翻譯,而且有翻譯在馬上就能翻譯,不需要等到編譯完成(當文章太長的時候,是受不了整篇翻完再看的)。

Python就具備了這樣高可攜性跟立即執行的特性,這讓Python異常適合學習跟測試。

強定型的語言

Python也是個強定型語言。這個特性就比較好理解了,問讀者一個問題,一個整數的10加上一個字串(符號的序列)"100"等於多少?對於弱定型的語言,這個答案就是110,即使字符是不能夠與整數作加法運算,但還是"猜測"是要做10+100的整數運算,從而得出110的結果。強定型語言則會認為兩者型態根本不應該做加法而導致錯誤。乍看之下弱型語言是比較有彈性且聰明的,但是這種聰明完全出自於猜測與自作主張,很有可能引起意想不到的錯誤,強型語言透過較嚴格的規範來減少錯誤的發生機率。

優雅而高效的語言

Python是極其優雅的語言,不論是本身的語法設定,或是從而衍生出來的設計慣例,都在在地表現出Python的簡潔和一致性,當讀者有了一定的撰碼經驗之後,將會對這種化繁於簡,樸實而高雅的語言感到激動與興奮。同樣的任務Python能夠花更少的時間、更少的程式碼完成,這說明了我們使用Python來工作可以大大地提昇效率。同時,寫作完成後的原始碼,也具備高度的可讀性,這對於讀的時間比寫的時間多的程式設計領域來說無疑是一大優點。

像膠水一般的語言

有人說Python就是一種膠水語言,他可以扮演一個整合的角色,調度使用不同語言與不同技術開發成的模組,不論作為腳本碼或是程式主體都是很優秀的。同時,他與C的嵌合度極高,我們能夠使用這些在執行效率上表現更好的語言去撰寫極需高效的、需要大量運算的部分,再用Python去調度或是嵌合。

能跨平台的語言

由於有直譯器的地方就能運行Python,所以Python是能夠跨平台的語言,我們撰寫完成的Python原始碼能夠在幾乎所有的平台上面執行,包含Windows、Linux、Mac等主流作業系統,這也代表著我們完成的作品能夠更容易地推廣散佈出去。

擁有豐富資源的語言

由於Python的活躍,使得它擁有極其豐富的教學、文件與第三方程式庫,這讓Python在推廣、學習與應用上擁有極大的力量。尤其是數量龐大的套件,更帶給開發者充足的支援,下面就列出Python在各個領域上的應用:

領域 重要套件、模組或工具 簡述
Web程式 Django、Pyramid、TurboGears、web2py、Zope、Flask Web的開發是Python最大的應用領域之一,透過能與網頁伺服器溝通的WSGI介面,使得Python能夠成為伺服器端的腳本碼,用以開發動態網站。
伺服器程式 Twisted Python對各種網路協定的支援非常完善,所以被廣泛地用於編寫高效能的伺服器軟體,有些還支援非同步的機制和平行處理的功能。
網路爬蟲 Scrapy Python的易用性與對網路協定的支援,所以非常適合拿來編寫網路爬蟲。
GUI開發 Tkinter、wxPython、PyQt Python支援視窗介面的程式開發,特色是靠著Python的跨平台能力,能夠在各個作業系統上達到完美的相容。
作業系統 Ubiquity、Anaconda Python是許多作業系統的標準元件,也就是內建在系統裡了,我們可以透過Python來撰寫腳本碼,用以管理或統籌各項系統工作。除此之外,很多Linux系統的套件管理工具也使用Python撰寫。
科學計算 NumPy、SciPy、Matplotlib Python中有非常多成熟且功能強大的科學計算套件,能讓工程師或科學家們透過這些工具編寫科學計算的程式,領域涵蓋了數學、物理學、生物學等範圍,而計算得出的數據或結果,都有相當好的圖形展示支援。
其他 - 許多知名的公司或組織都會在其內部使用Python,如YouTube、Google、Yahoo!、NASA等。

高度活躍的語言

一個語言的發展跟能力往往與他的活躍程度有關,越多人關注、越多人投入的語言能夠獲得更多的資源挹注,包括直譯器跟語法上面的修正與改良、第三方資源的開發以及設計方法跟思維上的進步,光是說教材的數量與好壞就足以說明活耀對程式語言的重要性。幸好Python正是一門高度活躍的語言,不僅應用面廣、使用者眾,更有許多大大小小的社群不間斷地對Python做出貢獻,下面就列出一些比較知名的Python社群,大家可以多去了解,甚至參與:

以下有關於台灣的Python社群資料,引用自Python台灣使用者群組 http://wiki.python.org.tw/ 的介紹,基本上也是各社群的自我介紹。

社群名稱 簡述
Python Software Foundation Python軟體基金會,簡稱為PSF,負責推廣與維護Python語言的非營利性組織,PSF目前握有Python的智慧財產權。
Python Conference 簡稱Pycon,是Python的年度會議,Pycon匯集了來自各領域的Python使用者,透過各種主題的演講和交流,能讓所有參與者在技術、視野上有所成長,現在世界各地都有各區域的Pycon,台灣也有Pycon Taiwan喔。
PyHUG 活動於新竹周邊的 Python 程式員。非常歡迎你加入我們的聚會!
Taipei.py 活動於台北周邊的 Python 玩家,歡迎來玩!
Tainai.py 動於台南周邊的 Python 人客,歡迎來吃!
PyLadies @ Taiwan! 專屬於女生的 Python 愛好者聚會,專供喜歡 Python 的女生、想學 Python 的女生、想瞭解 Python 的女生,彼此交流、交換心得,聊天認識朋友的地方。
Kaohsiung Python User Group 高雄地區 Python 使用者的交流園地
花蓮.py 活動於花蓮周邊的 Python 玩家,歡迎來花蓮玩!
Taichung.py Python台中聚會,歡迎大家來玩!Have Fun!
Django Girls Taipei 活動於台北,專屬女孩的(Django, Python Web)程式工作坊

Python的版本

如果讀者並非首次接觸Python,應該都知道現行在市面上的Python主要有兩個版本,分別是Python2以及Python3,聽起來Python3似乎就是Python2的進階版,但顯然不是如此,這兩個語言雖然大體而言相似,卻並非完全相容的語言。首先就底層的實作面而言,這兩個版本在基本的資料結構上就有顯著的不同,更不用說在語法上的落差,這導致我們在使用上必須做出選擇。

基本上會推行Python3,那一定代表著官方認為3版是個較優秀的版本(事實上也確實如此),不過由於Python的許多第三方套件與資料庫都是使用2版寫成,許多也都還沒擴充到3版,使得Python2的使用者仍然龐大,本書也鑒於此,會希望讀者們能夠使用Python2來進行開發,不但確保在講解上跟讀者實際操作時的一致性,也能保證大部分的套件都是有支援的。至於有讀者擔心學習Python2會不會是個較差的選擇,其實讀者們可以放心,Python2.7以後的版本也大量引進了3版的特點(意思是許多3版擁有的能力2版也有了),同時,兩者的差異,其實僅需要一點點的提示便能克服,只要熟悉了Python2,日後讀者可以輕易地轉換到Python3的環境中,畢竟,他們都叫做Python不是嗎?

學Python準沒錯

其實很多初學程式的朋友常常會猶豫該選擇哪一種語言,也有很多朋友喜歡爭論哪種語言最好。作者的見解是:不同的領域、不同的任務的最佳語言是大不相同的。我們必須將程式語言這個工具,應用在它所適合的任務上,同時要能發揮該語言的特性。

所以不必猶豫,學習Python、選擇Python是絕對不會錯的事情,而使用以Python為基礎的Django來設計網站也絕對是聰明的選擇。讀者們若有疑慮,請在繼續閱讀本書前盡皆放下,唯有帶著全然接受的心,才能夠真正學習心事物,況且,程式語言只是個工具,而使用工具來解決問題才是我們所追求的不是嗎?

Python的下載與安裝

由於Python是種直譯語言,我們有必要準備一款直譯器(interpreter)來執行Python語句,在Python已經紅遍大街小巷的今天,Python直譯器(或稱為Python實作)的種類也早就不只一種了,以下列出幾款常見的直譯器:

直譯器名稱 簡述
CPython 我們一般提的Python就是CPython,他也是Python官方維護和支援的直譯器。至於為什麼叫CPython,那是因為這個直譯器是以C寫成的,所以他與C的相容性也最高。他有個特色是,會將匯入的Python模組編譯成中介碼(.pyc檔),直後要執行的時候便可以由中介碼開始執行,不用再從最原始的Python語言開始直譯。
PyPy 他是以Python語言的子集合rPython實作而成的直譯器,他能夠將Python編譯成其他的語言,通常來說,運行速度比起CPython來的快,但是並不完全相容於CPython(但是相當接近了)。
Jython Jython是用Java實作的直譯器,能讓Python使用JVM來執行,所以他能夠利用Java的各種函式庫與資源。
IronPython 可與.NET結合的Python。
ZhPy 就是傳說中的周蟒,支援使用繁/簡中文來編寫程式的Python,簡單來說就是Python的中文版。

在本書中,我們一律以CPython為主,而CPython的直譯器可以由Python的官方網站取得:

要安裝Python是很簡單的事情,甚至有許多作業系統本身便自建了Python在內(Python2),如Mac OS或是Linux等,我們只需要確保安裝的版本是符合我們需求的即可:

打開終端機(terminal):

$ python --version

如果執行上述指令成功,代表系統中已經安裝好了Python。上述指令可以得到Python直譯器的版本號,我們希望大家都能夠使用2.7.8的版本來學習,不過基本上2.7以上的版本基本上都不會有什麼問題。

如果讀者們的電腦尚未安裝好Python,那請跟著我們一步一步的來下載跟安裝。

首先,讓我們進入官網,在頁面的中央,可以找到Downloads的字樣,點選後網站會自動判定電腦的作業系統,出現下載的連結,請讀者們選取Download Python2.7.8(請下載Python2),就能輕鬆地下載好安裝檔,接著請參閱以下幾個小節的教學,筆者將會分述在Mac、Linux和Windows系統上的安裝方法。

Mac

點開安裝檔後,會見到Python.mpkg這個檔案,請右鍵點選打開(這樣做是為了避免系統因為未識別的開發者而拒絕執行),接著只要跟著安裝檔的說明即可順利成功安裝。接著進入終端機以python --version測試即可。

Windows

點開安裝檔,按照安裝檔說明一步一步裝妥即可,Python預設將會被安裝在C:\Python27,我們多花一點時間來設定環境變數,讓Python可以在任何路徑下被執行,我們對電腦按一下右鍵,接著點選內容,在左側可以看到進階系統設定,接著點選下方的環境變數,在系統變數下面找到Path這個變數,點選後按編輯,在最後面加入兩個路徑:C:\Python27C:\Python27\Scripts,請記得使用分號(;)來隔開每個路徑。

環境變數設定
電腦(右鍵) > 內容 > 進階系統設定 > 環境變數 > 選取系統變數Path > 編輯 > 加入指令路徑 > 確認設定

接著我們進入命令提示字元,鍵入python --version確認安裝成功。

Linux

Linux的安裝,我們將使用任何發行版都能使用的方式:下載tar ball(壓縮包)並由原始碼編譯,首先,下載並用xz(壓縮工具)解壓縮並將打包解開:

$ wget http://www.python.org/ftp/python/2.7.8/Python-2.7.8.tar.xz
$ xz -d Python-2.7.8.tar.xz
$ tar -xvf Python-2.7.8.tar

接著就會得拿到Python-2.7.8的資料夾,接著進入資料夾,利用Linux安裝最有名的安裝指令序列:./configuremakemake install,但這裡我們需在設定時給予前綴/usr/local/且不使用install而是altinstall,避免一些可怕的事情發生,指令如下:

$ ./configure --prefix=/usr/local
$ make && sudo make altinstall

完成後,Python2.7.8的執行檔便是/usr/local/bin/python2.7了,接著,我們將系統預設的Python(/usr/bin/python)透過連結改為我們手動編譯的新版本:

$ sudo ln -sf /usr/local/bin/python2.7 /usr/bin/python

如果你想把python2.7指令也換成版本2.7.8那就再執行:

$ sudo ln -sf /usr/local/bin/python2.7 /usr/bin/python2.7

接著以python --version驗證一下就完成囉!

Python的基礎

進入Python shell

Python語言由於是直譯式的語言,所以它也提供了一個互動式的介面,我們稱之為Python shell,進入Python shell的方法很簡單,讓我們打開終端機(windows中叫做命令提示字元)並輸入python:

$ python
這裡會有一些python直譯器的資訊...
然後會出現一個提示字符(prompt)
>>>

接著我們可以在prompt後面(>>>符號的後面)輸入任何合法的Python語句,讓我們來試試個簡單的指令:

>>> 1+1
2

沒錯,python就好像計算機一樣地正確列出了1+1的結果。

Python的文件與求助

在這個小節,也就是正式進入到Python的概念與語法之前,我們要先跟各位介紹Python提供的支援文件要在哪裡取得,而我們在學習或是開發時有碰到困難時又要怎麼樣獲得幫助。

首先,大家絕對要知道,Python的官方網站是:http://www.python.org/,而Python2的完整線上文件在https://docs.python.org/2/,絕大多數關於語言的用法和使用範例都可以在這裡找到。但是我們可否從Python shell中獲得幫助呢?答案當然是肯定的了。讓我們來實際操作看看:

>>>  help(sorted)
Help on build-in function sorted in module __builtin__:

sorted(...)
    sorted(iterable, cmp=None, key=None, reverse=False) --> new sorted list

我們可以利用help來幫助我們查詢函式、類別等等的資料,好像是一個內建的求助系統,大家在沒有網路或是想要快速查詢時可以善加利用。

函式或是類別不懂沒關係唷,之後都會介紹到,大家只要記得,不懂或是不確定的東西可以用help來幫助我們查詢。

關於運算

讓我們回到Python本身,剛剛我們所做的運算:1+1,是由兩個運算元-整數"1"和一個運算符"+"所構成的,所謂的運算元就是運算的對象,運算符是運算的動作,有了動作和對象的配合,我們能夠產出結果,也就是整數"2",我們要記得,任何Python上的運算都是對運算元執行一些動作,並且產生結果(或是一個正式的名稱:返回值return value),我們可能會想要記下這個結果,或者將結果做為另一個運算的運算元。

那要如何記下這個結果呢?

我們使用變數:

>>> result = 1+1
>>> result
2
>>> result+2
4

這裡,result就是我們的變數,他可以記錄任何運算的結果,而等號=在Python中並非相等的意思,而是一種指派的動作,這會將等號左邊的值指派給右邊,result = 1+1的意思是將1+1的結果指派給result,也就是讓result儲存2,這種運算有個名稱:賦值運算

最後我們發現,利用這個結果可以再進行另一個運算result+2

關於變數

在之前我們看到的變數,就好比一個容器,能夠儲存各式運算結果的值。但是變數的命名可不能馬虎,要遵從以下規則:

變數的命名規則
1. 以底線或英文字母開頭字符
2. 以底線,英文字母和數字為後續字符
3. 不可與關鍵字(保留字)相同
4. 大小寫有別

第一點和第二點很好理解,而所謂關鍵字是指在Python語言中已經具有特殊意義的一些字,如果使用了這些字作為變數,會讓Python直譯器產生混淆,為了避免這種狀況,這類型的名稱會被任定為不合法的名稱,並且強制出現錯誤。

第四點告訴我們,Python是case sensitive的,resultResult是兩個完全不同的單字。

我們來看一些例子,以下都是合法的命名:

result
_result
result_2
Result_2

而以下的變數命名是不合法的:

$result
2_result
for

不遵守規則的變數名稱就等著看到錯誤:SyntaxError: invalid syntax

變數的命名除了硬性的規則還有軟性的建議(又稱為慣例),不過由於屬於細節,我們不在此敘述,但建議不了解的讀者可以多找些資料來了解。

Python程式碼的風格或慣例可以找PEP8的文件來參考:https://www.python.org/dev/peps/pep-0008,什麼是PEP呢?PEP是Python Enhancement Proposals的簡稱,也就是Python改進提案。PEP會蒐集Python社群中對於Python未來發展方向或是功能支援上的意見,為將來Python的新特性提出文件提案。重要的PEP有PEP1(說明了基礎的PEP概念)、PEP8(Python的程式碼風格與慣例指引)、PEP20(知名的Python之禪)等。

關於資料型態

我們回到最一開始的例子:1+1,我們說過,1是運算的對象,而運算的對象會是一些資料,而資料會有他相應的形態,像是這個例子中的1就是"整數"的形態。

幾乎所有的資料(對象)都有自己的形態,但Python中存在一個不具備任何型態的資料:None
None就是,什麼都不是,什麼都沒有,跟其他語言中的null是差不多的意思。

我們會再之後的章節介紹到其他重要的資料結構,包含浮點數、字串、清單、字典等。

在Python中的每種資料型態都有自己的特徵,比如說浮點數(也就是小數)永遠會伴隨著小數點,清單的特徵是中括號[],字典的特徵是大括號{}等,只要記住每種資料伴隨的特徵,我們可以很輕易地辨認他們或是使用他們。

還有一點要注意的是,不同型態的資料都有他們可以配合的動作(運算符),我們可以整數進行減法,但可不能對字串(一些字符的序列)作減法。

關於變數參照

介紹過資料型態後,有一個重要的觀念大家務必要了解,那就是變數參照。

>>> result = 2

在這個例子中,result這個變數儲存了2這個整數,但事實上,Python並沒有讓result直接記錄著2,而是找了一個可以儲存2這個整數的空間(容器),並讓result參考到了該空間,這好像是說result其實只是一張標籤,上面寫了儲存物的儲存位置而已。我們說變數result參照了2這個整數。

那參照的重要性在哪裡呢?我們來看以下的例子:

>>> result = 2
>>> result = 3.0
>>> result = 'hello'
>>> result = [1,2,3]

我們發現,一個變數不但能夠儲存不同型態的資料,還能夠隨時更換儲存的資料型態。

這在靜態語言中是不可能的事情,因為一個真正的容器只能裝合適的內容物,就像裝水要用水瓶一樣,靜態語言的變數就如同這些真正的容器,所以每個變數也都具有特定的型態並且只能裝該型態的資料,這也是為什麼靜態語言都需要進行變數宣告的原因。

而Python的變數並非真正的容器,他不過是張標籤,參照了某個裝資料的容器罷了(好像是隔層儲存!),當我需要他記錄別種形態的資料時,我們只要用一個合適的容器裝著該資料,並且改改標籤,變換一下參照的容器就好了。

這點有好也有壞,好處是,這對於程式設計師而言簡直是太方便了,減少了宣告的工作量,變數也能夠隨時更換所儲存資料的類型。但是缺點則是隔層的存取畢竟是慢一點點,還有更主要的,當有錯誤發生時,我們比較難發現。因此我們必須要非常清楚某個變數中目前參照的資料型態究竟是什麼!

撰寫多個運算

我們常常會發現,要做的運算不只一個,我們需要若干個運算,依照一定的順序進行計算,但是在Python shell中一行一行的鍵入實在太花時間也太麻煩了,我們能不能一次寫完所有運算並一次執行呢?

很簡單,便如同其他程式語言一樣,我們將多個運算依照順序擺放在一個檔案中,並且檔名以.py結尾(這並非強迫性的,但是這是個好的慣例,讓大家都明白這個檔案是Python程式碼),比如說我們寫了一個簡單的加法:

test.py
a = 10
b = 10
result = a + b
print result  # result的結果是20

我們將這三個運算(又稱敘述)放置在test.py這個檔案中,接著我們只要在OS shell:

$ python test.py
20

就可以執行上述的多個運算囉!

在這邊python是直譯器的名稱,也是你電腦上面執行Python的指令,後面緊跟著的是python file的名稱。

我們注意到這邊出現了一個print敘述,print會將後面所列出的資料內容印出來,在這裡result的值在print的時候是20,所以我們執行完該檔案之後,會看到畫面上印出了20。

#號代表的是註解(commment),所謂的註解就是純粹給人看的一些說明,而這些說明完全會被Python給忽略掉,所以#號後面的內容將不會被執行,通常我們會對重要或者是需要抽象層次解說的程式碼下註解。

接下來的範例碼,讀者可以選擇使用Python shell來演示或是寫成一個.py檔來執行。

封裝、參數化與函式

有的時候,我們會重複地利用到一些常見的運算的組合,比如說我們考慮到計算五個整數的算數平均數:

result = (1+2+3+4+5)/5

當我們需要知道另外一組時,又要撰寫一次:

result = (1+1+1+1+1)/5

這樣不但沒效率,且在運算更複雜的時候會更令人心煩。
我們可以透過封裝的方式,將這些運算包裝起來,並且進行參數化,使其成為一個可重複利用的函式:

result = avg(1,2,3,4,5)
result_2 = avg(1,1,1,1,1)

我們不再實際地撰寫這些運算,而是去呼叫包裝好的函式:

函式的呼叫
函式名稱(參數1,參數2,...)

透過函式名稱加上一個帶有參數的小括號,我們便能呼叫函式幫我們執行這些包裝好的運算,當然我們需要一些運算的材料,那就是我們稱之為參數的東西了。參數的數量視需要而定,也可以完全不需要參數,但是無論有沒有參數,呼叫函式時小括號總是要伴隨著函式名稱出現才行。

Python中有需多內建好的函式,稱之為built-in function,比如我們可以用內建函式abs來計算絕對值:

>>> abs(-10)
10
>>> abs(2)
2

然而我們也可以自己包裝設計函式,不過那留到本章稍後一點再說明了。

類別與物件

還記得剛剛講的資料型態嗎,其實每一種型態就是一種類別(class),比如說整數一種資料型態,也是一種類別,而一個類別的實際的例子,稱為實例(instance),又稱物件(object)。比如說100就是整數類別的實例,我們也稱它是物件。

其實一種類別是資料跟運算的集合體,就像整數包含了本身的數值跟加法減法等運算一樣,我們稱資料的本體為類別或物件的屬性(attribute),而屬於該類別的運算(通常包裝成函式)稱之為方法(method),我們舉個簡單的例子:

假設我們有一個類別叫做Cat,my_cat是Cat的實例,我們可以如此使用:

>>> my_cat.name
'Kity'
>>> my_cat.shout()
Meow

我們利用點.來取得一個類別實例(物件)的屬性或是呼叫他的方法(函式),my_cat.name會取得這隻貓的名字,而my_cat.shout()讓這隻貓執行叫這個動作而發出'Meow'的聲音。

就跟函數一樣,除了內建的幾種類別之外,我們也能夠自定自己的類別(像是剛剛的Cat就是作者自定的),自定的方法我們一樣留到後面統一來談,現在只要知道如何取得屬性跟呼叫方法即可。

資料型態

在本節,將會向讀者介紹幾種Python常見且重要的資料型態,包含整數、浮點數、字串、清單、字典等以及他們的基本運算。

整數(Integer)與浮點數(Float)

我們來看一些整數做運算的例子:

>>> 2 - 3
-1
>>> 2 * 3
6
>>> 6 / 4
1
>>> 6 % 4
2
>>> 2 ** 6
64

上例示範了如何做減法(-)、乘法(*)、除法(/)、取餘數(%)和指數運算(2**6代表2的六次方!),我們會發現一個有趣的現象,對於整數做除法的結果會得到一個整數的商,而不會得到一個小數。

小數也就是我們所謂的浮點數,基本的運算也包含了加減乘除,也能夠取餘數,我們快速的來看一下浮點數的運算吧:

>>> 1.2 + 2
3.2
>>> 1.2e2 - 10
110.0
>>> 3.2 / 1.1
2.909090909090909
>>> 4.2 % 3.2
1.0

我們發現,浮點數運算的結果會是浮點數,即使是跟整數一同運算,返回值依然會是浮點數,這是因為Python預設會用比較精確的資料型態來作為運算的結果。

其次,浮點數也支援科學記號,1.2e2代表的是:1.2乘以10的平方,也就是120,減掉10之後結果當然是110.0。

而最後一個算式我們也發現:浮點數居然也支援取餘數,這真是有點神奇阿!

布林值(Boolean)

布林值就是真假值,在Python中我們用True代表真,用False代表假,真與假的運算也只會是真假值,這種運算稱為邏輯運算,詳細的討論請參見運算一節。

字串(String)

我們稱任何一個符號叫做字元,而字元的序列就叫字串,不過在Python中沒有真正的字元,一個字元請大家把它當作長度為1的字串,看以下範例:

>>> string = ''
>>> string_a = 'h'
>>> string_b = 'ello'
>>> string_a + string_b
'hello'

''(沒有內容的字串我們稱為空字串)、'h''ello''hello'都是字串,長度分別為0、1、4、和5,字串必須以成對的引號括住,不論是單引號或是雙引號都可以,例如'hello'"hello"是等價的。

這個機制能夠方便我們寫出含有引號的字串如:

string_c = "It's a good day!"
string_d = 'My name is "dokelung".'

而加號對於字串型態而言並非加法而是串接,所以string_a + string_b的結果是'hello'

另外,我們也可以用三雙引號(""")來製造跨行的字串:

string_e = """
    today is
    a good day!
"""
print string_e

我們會發現,使用三雙引號製造出來的字串不僅能跨行,還可以保留它字面上的形式,比如說第一行的today前面有4個空白,第二行的a前面有1個tab縮排等。讀者們可以自行製造幾個這樣的字串並用print印出來看看,便會明白作者的意思了。對了!這樣的字串有個特別的名字叫做doc string,文件字串。

要取出字串中的某個字元或是部分字串只要用中括號就可以了:

string = "hello world"
first_char = string[0]
first_word = string[0:5]

string[0]代表取出string中第零個字元,我們要知道,在計算機科學的世界裡,一切都是從0開始的,我們所謂的第1個要想成第0個,第2個要想成第1個,所以string[0]會是字串"h"。

我們會給寫在中括號裡面的整數一個名字:索引(index),他表明了指定元素的位置。

而在中括號中加了個冒號,就可以拿取指定範圍的子字串,string[0:5]會拿取第0個字元到第5-1個字元,也就是'hello',這邊要注意的是,結束的標記永遠是比真正結尾的位置多1的。

這種中括號取值或取片段序列(有個正式的名稱:切片(Slicing))的方式將會在後面的序列資料中再度看到,例如清單。

這邊補充一些切片的方法,如果我們想要從頭開始取切片,我們可以捨棄第一個範圍,若我們想要取到原字串的最尾部,可以捨棄第二個範圍:

>>> string[:5]
'hello'
>>> string[6:]
'world'
>>> string[:]
'hello world'

至於兩個範圍都省略時,我們會得到一個原來字串的副本,這也是我們簡易複製字串或是序列的方法。

字串也能夠進行格式化,只要使用字串這種類別的format方法:

string = 'My name is {0}, I am {1} years old.'.format('dokelung', 27)
print string # My name is dokelung, I am 27 years old.

字串'My name is {0}, I am {1} years old.'中有兩個待填的欄位:{0}{1},我們會使用format括號裡面的值來填充,第一個值('dokelung'字串)會填充到標記為0的欄位中({0}),第二個值(整數27)會被填到標記為1({1})的欄位中,這並沒有什麼好奇怪的,我們說過一切由零開始不是嗎!

我們也可以採用另外一種填充方式,不以位置填充,而是認定填充欄位的名字:

string = 'My name is {name}, I am {age} years old.'.format(name='dokelung', age=27)

我們指定'dokelung'用來填充{name},而27用來填充{age}

在字串中,還有一群很特殊的字元叫作跳脫字元(escape charater),這些字元通常代表著一些效果而非符號,比如說:

print 'hello world\n'

我們會發現,\n讓我們多換了一行,這裡的\n跳脫了字串,跳脫了符號,所以才有這樣的名字。
跳脫字元通常都由一個倒斜線加上其他符號組成,所以若要印出倒斜線本身,我們得使用雙倒斜線以免誤會。

下表列出一些常見的跳脫字元:

跳脫字元 說明
\ 倒斜線
\' 單引號
\" 雙引號
\a 鈴響
\b 退格
\n 換行
\t 縮排

關於字串我們還有最後一個問題,我們可以使用非英文的字串嗎?答案是可以的,Python全面支援Unicode編碼,只要我們在Python檔案的最一開頭加上:

# -*- coding: utf-8 -*-

print '哈囉 世界!'

就可以大膽使用中文囉。

關於編碼及Python的編碼,讀者若想深入了解,非常推薦一篇文章:

清單(List)

在這裡我們要來介紹我們的第一個群集物件:清單,他是一個序列,我們能夠存放任何型態的資料在我們的清單中:

>>> lst = ['dokelung', 27, None]

清單的特徵是中括號,裡面可以擺置任意數量的元素,彼此以逗號(,)隔開。
有一點我們要了解, 由於Python是動態語言,使用的是物件參照,所以每個元素可以是同型態的資料也可以是不同型態的資料。

lst這個清單的長度為3,有三個元素,第一個元素為一個字串,第二個元素是個整數,第三個元素是None,若我們想要分別取出這些元素可以這樣做:

>>> lst[0]
'dokelung'
>>> lst[1]
27
>>> lst[-1]
None

在這裡,我們不用lst[2]來拿到第二個元素:None,而使用了lst[-1],代表是該清單的倒數第一個元素。
以此類推,使用負號我們可以取得由尾部數來第幾個元素。

與字串相同,如果我們想要取出清單的部份(子清單),也可以用中括號:

>>> lst[1:]
[27, None]

我們可以很輕易的用這種方式改變清單中的元素:

>>> lst[1] = 28
>>> lst
['dokelung', 28, None]

我們要如何加入一個新的元素到清單中呢,使用append方法:

>>> lst.append('New')
>>> lst
['dokelung', 28, None, 'New']

也可以使用pop來彈出:

>>> first_element = lst.pop(0)
>>> first_element
'dokelung'
>>> lst
[28, None, 'New']

lst.pop(0)將會彈出lst中第零個元素。

我們可以利用sort方法來排序清單中的資料:

>>> lst = [1,3,5,2,4]
>>> lst.sort()
>>> lst
[1, 2, 3, 4, 5]

其他重要的方法請參考Python documents。

元組(Tuple)

元組跟清單非常類似,都是序列,但是元組的每個元素都是不可變動的,而且元組的特徵是小括號():

>>> t = (1, 2, 3)
>>> t[0]
1
>>> t[1] = 0
TypeError

我們發現如果試圖更動元組的元素,將會出現TypeError的錯誤。

另外使用元組有一個要注意的點是:因為單純的小括號在運算裡面有優先運算的意思,比如說:

a = (5+3)*2

所以我們在撰寫單元素的元組時,必須強制加上一個分隔的逗號:

t = (1,)

這樣才能表明t為元組,否則Python會有誤判的情形,如:t=(1),Python會誤判為t=1

>>> a, b = (1, 2)
>>> a, b = b, a
>>> a, b
2, 1

字典(Dictionary)

字典是另一個重要的資料結構,他並非序列,而是一種對應型的資料型態,他的特徵是大括號{}:

>>> dic = {'name':'dokelung', 'age':27}
>>> dic['name']
'dokelung'
>>> dic['age']
27

我們在定義一個字典時,會寫出一對一對的鍵值對(key/value pair),比方說:'name':'dokelung'就是一個鍵值對,'name'是鍵(key),'dokelung'是值(value),我們透過中括號可以用鍵取得對應的值。

我們可以改變一個鍵對應的值或是幫字典增加一個鍵值對,都使用中括號:

>>> dic['age'] = 28
>>> dic
{'age': 28, 'name': 'dokelung'}
>>> dic['gender'] = 'male'
{'age': 28, 'gender': 'male', 'name': 'dokelung'}

至於怎麼樣的資料可以當做鍵,哪種可以當做值呢?
不可變的資料可以當做鍵,而所有資料都可以當做值,何謂不可變的資料呢?讀者們可以參考節最後一小節。
而通常我們會使用字串來當做鍵,表達力跟安全性都比較高。

巢狀結構

既然上述的群集可以放入任何型態的資料,那麼群集裡面有群集就不那麼讓人意外了。
我們看個簡單的例子,我們會發現在清單中有清單:

person = ['dokelung', 27, [1988, 09, 30]]

沒錯!清單中的清單,甚至是清單中的清單的清單都完全沒有問題,他們都是合法的。

我們在來看一個例子,這是個2X5的矩陣:

matrix = [
    [1, 2, 3, 4, 5],
    [5, 4, 3, 2, 1]
]
print(matrix[1][3]) # 結果是2

若我們要取出第二行第四列的元素,用matrix[1][3]就行了。
我們來分析一下,matrix[1]會拿到matrix的第1個元素,也就是清單[5, 4, 3, 2, 1],接著我們再使用matrix[1][3]提取他的第3個元素2。

不只如此,字典中也能擺放字典,甚至清單中擺放字典或字典中擺放清單都不是問題呢!

使用群集的好處

這邊稍微來探討一下使用群集的好處。
比如說今天我們要記錄班上同學的成績:

不使用群集
score_1 = 90
score_2 = 99
score_3 = 45
score_4 = 63
...

這樣做固然沒錯,但是當班上同學一多的時候,不知道要撰寫多少個變數才夠。
相反地,我們可以使用清單:

使用群集
score = [90, 99, 45, 63, ...]

這樣做我們只用到了一個變數,減少變數名稱的使用量是他的第一個好處。

接著我們來算算成績的總和,如果不使用群集:

不使用群集
total = score_1 + score_2 +score_3 ...

會發現由於變數繁多,必須要花大量的時間跟空間來寫出名稱

使用群集
total = sum(score)

很簡單的透過sum這個函式(不要緊張,後面會介紹到),就能加總score這個清單中所有的元素了!可以將元素統一進行運算是第二個好處。

另外還有存取具有系統性、能顯示元素間的關聯性、好管理等都是我們使用群集的理由,其實歸納起來想法很簡單,我用一個東西就能夠裝載著我需要的資料,會比起每個資料用一個東西裝載好,因為我們要搬運的時候,不會需要搬太多東西,這一點很明顯地表現在我們為了變數所花的打字時間。

可變與不可變

資料型態有所謂的可變以及不可變,所謂的可變是指,該變數所參照的物件本身能否被改變。
我們先來看看整數:

a = 1
a = 2

還記得物件參照的概念嗎?a這個變數並非容器本身,他是先參照到1,再參照到2,裝載1的容器跟裝載2的容器是不同的,他改變了標籤上的位置,但是參照到的物件本身並沒有改變,變的是參照的對象。

除了整數之外,浮點數及字串也是不可變的,我們可以簡單的試驗:

string = 'abc'
string[1] = 'x'
TypeError 

我們發現要去更動字串的內容是不可能的,因為他參照到的東西不可變。

再看一個例子:

string = 'abc'
string = 'x'

為什麼這樣不會有錯呢?因為我們並沒有去更動不可變的資料,我們更動的是變數的參照物(就跟整數一樣)。

同樣的,我們來看看清單:

lst = [1, 2, 3]
lst[1] = 0

這樣就是完全合法的代碼,那是因為lst他參照著一序列的物件參照(標籤),標籤自然是可以改的,當我們寫出lst[1]=0的時後,只是將lst參照的第二張標籤參照的對象從2改為0。所以我們說清單是可變的資料型態,當然字典也是。

記得前面提過,字典的鍵一定要以不可變的資料來擔當嗎,我們學習了這裡的內容,變能夠正確地選擇字典的鍵了。

如果讀者不懂原理可以慢慢思考,只要先把握住原則,不要任意去更動不可變的資料,也不要用可變的資料型態當做鍵。

運算

這個小節將會詳細的討論Python中各種重要的運算。

賦值運算

這個運算在前面已經提過了,就是Python會將等號右邊的值賦值給左邊的變數,但是若要講的深入一點,其實是讓左邊的變數參照右邊的資料:

a = 2
a = 'dokelung'
a = 3.1416

而千萬別忘記Python動態的特性,這讓我們能夠隨時變換儲存的資料型態。

空運算

這在Python中是一個特殊的運算,我們會用pass這個關鍵字代表什麼都不作,那讀者一定很好奇那我們就什麼都不寫就好了,但是有的時候Python會有非寫不可的時候,而我們使用pass來讓什麼都不作。

自運算與增強運算

自運算指的是對變數本身作運算,也就是以變數為運算元,運算完後的結果賦值給予變數本身,常見的有遞增或是加總:

a = 1
a = a + 1

上述例子可能讓出接觸程式的人摸不著腦袋,a怎麼就等於a+1了!喔!還記得我們的賦值運算嗎?我們不是在說他們相等,而是把右邊的值給左邊的變數,或是說讓左邊的變數參照右邊的資料,所以a=a+1的意思是我們將a原本的值加上1再存回a中,講簡單一點就是讓a遞增1。

對於自運算,我們可以有一個更簡明的寫法:

a = 1
a += 1

a += 1a = a + 1是完全一模一樣的表示法,當我們看到一個運算符與等號連結時,就代表一個自運算的簡化寫法,我們稱為增強運算:

增強運算
var 運算符= something  <------->  var = var 運算符 something 
                        等同於

我們列出所有的增強運算如下表(假設a=10):

運算符 範例 等價的自運算 結果
+= a += 5 a = a + 5 a為15
-= a -= 5 a = a - 5 a為5
*= a *= 2 a = a * 2 a為20
/= a /= 2 a = a / 2 a為5
%= a %= 3 a = a % 3 a為1
**= a **= 3 a = a ** 3 a為1000

比較運算

當我們需要比較資料的大小或是相等性時,可以使用比較運算,比較運算的結果會是真假值(布林值),也就是前面介紹過的True和False,如下表所列:

運算符 說明 範例 結果
> 運算符左邊的值大於右邊的值,結果為True,否則為False 1 > 0 True
< 運算符左邊的值小於右邊的值,結果為True,否則為False 1 < 0 False
>= 運算符左邊的值大於或等於右邊的值,結果為True,否則為False 0 >= 1 False
<= 運算符左邊的值小於或等於右邊的值,結果為True,否則為False 0 <= 1 True
== 運算符左邊的值等於右邊的值,結果為True,否則為False 0 == 0 True
!= 運算符左邊的值不等於右邊的值,結果為True,否則為False 0 != 0 False

從上表我們知道,要比較是否相等,用的是雙等號(==)而不是等號(=)。
另外,雖然不等於可以使用!=來比較,但是!在Python中可不是否定的意思,我們會想要使用別種方式來表達,請看邏輯運算。

Python的比較運算還有一個特性需要跟大家說明,那就是我們可以同時使用兩個運算符來判定數值的大小:

>>> a = 5
>>> 0 < a <=5
True

真是太美妙了,其他語言必須搭配邏輯運算(0<aa<=5)才能完成的比較運算,Python可以用我們熟知的、簡單的表達式來完成。

邏輯運算

邏輯運算是針對真假值的運算,所以運算出來的結果也會是真假值。主要有三種邏輯運算andornot

  • and運算 : 當兩個運算元都是真的時候結果才是真,否則結果為假。
  • or運算 : 當兩個運算元都是假的時候結果才是假,否則結果為真。
  • not運算 : 真變為假,假變為真。

看以下範例:

>>> True and Ture
True
>>> Ture and False
False
>>> False or False
False
>>> False or True
True
>>> not True
False

邏輯運算式,常和比較運算式用在條件的敘述,例如:

if i>=5 and i<=10:
    print '5<=i<=10'

我們馬上會介紹到if述句!

身分運算

每個資料都存在一個特定的記憶體位置裡,身分運算is就是告訴我們兩個資料是否來自同一個記憶體位置,簡單一點講,這兩個資料是不是同一個!但常常被大家誤解的是,is==的不同,這邊我們要鄭重澄清,==只能判定兩個資料的值相等,而不能判定他們的身分相同,我們看例子:

>>> a = 1
>>> b = 1
>>> lst_1 = [1,2,3]
>>> lst_2 = [1,2,3]
>>> a == b
True
>>> a is b
True
>>> lst_1 == lst_2
True
>>> lst_1 is lst_2
False

在這個例子中,我們很明顯地發現到a跟b的值相同,且他們所參照的整數1是來自同一個位置,這點我們不必太過訝異,Python總是很聰明地只產生並儲存這些不可變的資料一次,當有多個變數都被賦予該值的時候,讓這些變數都參照到同一個資料。為什麼我們說這樣很聰明呢?你想想看,如果每有一個變數的值要設成1,我們就要產生一個新的1,不是很浪費空間嗎?

但是我們看到lst_1和lst_2的例子就不是這樣了,他們的值的確是相同的,但是他們是完全不同的兩份資料!對於可變的資料型態,普通的賦值運算總會產生一個新的資料,當然我們做身分確認時,Python會告訴我們他們不是同一個人。

隸屬運算

隸屬運算in可以運作在字串或是群集中,用以表明某資料是否為另一資料之子集合, 常用在判斷子字串或是確認某元素是否在一群集中:

>>> lst = [1, 2, 3]
>>> a = 1
>>> a in lst
True
>>> 0 in lst
False
>>> string = 'hello world'
>>> 'hello' in string
True

流程控制

通常我們的程式,會被Python由上而下地一行一行解讀並執行,但是難免我們會碰到需要改變這種單調流程的時候,這時候我們需要一些能夠控制流程的語法,這也就是本節要說明的主題。

有條件的運算

首先,最常見的需要控制的場合會出現在有條件的運算上,比如說我們想要用月份來判定季節:

如果 3 <= month <= 5的話
    print '現在是春季'

我們發現,並不是所有的情形都需要印出'現在是春季',只有當某些條件被滿足時,該行才被執行,如果程式只允許我們由上而下的執行走法,我們就無法完成這個任務,所以我們需要能夠進行有條件的運算,"如果"這個字就是重點,只是我們用英文的if:

if 3 <= month <= 5:
    print '現在是春季'

對!就是那麼簡單,不過這邊有好幾點需要好好說明,首先是整個if述句的文法:

if述句
if 條件:
   區塊程式碼(suite)

在if的後面,需要緊跟著一個條件,條件的後面要跟著一個冒號,這個條件會在布林語境中被解析為真或假,當該條件為真,則緊跟在後面的區塊程式碼(block,或在Python中的特殊名稱suite)會被執行,否則會略過suite往後執行。

布林語境就是談論真假、運算真假的情境,比如說if述句中的條件部分,就是一個布林語境作用的地方(條件就是談論真假嘛),在Python中,0、None和所有的空資料(空清單、空字串等)在布林語境中會被解析為False,而除此之外的一切資料都是True。

屬於該if的suite程式碼,每一行都需要以相同的縮排來陳列,比如說我們可以選擇都空四格,一旦縮排不一致,將會導致程式的錯誤,我們來看看以下例子:

if 3 <= month <= 5:
    print '現在是春季'
    print '一年之計在於春'
print '這裡不屬於if區塊'

我們發現緊跟在if述句冒號後面的兩行,一致地進行了4個空白的縮排,Python將會認定此兩行為if的suite,而最後一行的print由於沒有縮排,故是一個獨立的敘述,我們發現如果if的條件成真的話,會印出:

現在是春季
一年之計在於春
這裡不屬於if區塊

如果條件為假會印出:

這裡不屬於if區塊

要取用怎麼樣的縮排是大家的自由,但是Python界中有個不成文的規定,那就是統一用4個空格來縮排,如果讀者嫌麻煩,可以在自己喜歡的編輯器裡將tab以4個空格取代。

有的時候,條件不會那麼簡單,比如說我們將我們的功能作的更完整:

if 3 <= month <= 5:
    print '現在是春季'
else:
    print '現在不是春季'

else是搭配著if延伸出來的語法,當if的條件為真時,屬於else區塊的程式碼將不被執行,反之該區塊的程式碼被執行,if區塊的程式碼被略過,else的後面不需要條件,但是冒號絕對是必須的。

讀者們將會發現,要進入suite之前,一定會先寫出冒號。記住這種架構:冒號後縮排寫出suite。

假設原本month=4,會印出'現在是春季',若month=8會印出'現在不是春季'。

另外,Python提供了一個簡潔的if/else寫法:

string = '現在是春季' if 3 <= month <= 5 else '現在不是春季'

在這種寫法中,如果if後面的條件成立,則結果會是if前面的值,否則結果會是else後面的值。

當然,我們可以一次加入更多的條件:

if 3 <= month <= 5:
    print '現在是春季'
elif 6 <= month <= 8:
    print '現在是夏季'
elif 9 <= month <= 11:
    print '現在是秋季'
else:
    print '現在是冬季'

elif是else if的縮寫,中文可以翻作否則如果,當if的條件不被滿足(不為真)的時候,會由上而下依序檢查每個elif的條件,第一個條件為真的elif區塊程式碼將被執行。

在上例中,假設month=10,由於if的條件:3 <= month <= 5為假且第一個elif的條件:6 <= month <= 8也是假,直到第二個elif的條件才為真,所以我們會執行第二個elif的suite,也就是print '現在是秋季',當然,如果沒有任何一個述句為真,則我們會執行else的suite。

重複的運算

有的時候,運算我們會想要重複多次,畢竟多次撰寫一樣的運算是個滿無謂的行為,比如說我們想要從1加到10,可以這樣做:

_sum = 0
_sum += 1
_sum += 2
_sum += 3
...
_sum += 10

while迴圈

這樣重複的作一樣的事情是非常浪費的,我們會想要用迴圈來讓我們輕易地做到重複,讓我們試試while

_sum = 0
num = 1
while num < 11:
    _sum += num
    num += 1

while使用的方法跟if很像,我們需要一個條件來判斷while的suite要不要被執行,記得後面要跟著一個切換到suite的冒號,接著屬於while的區塊程式碼,都必須以同樣的方式進行縮排。

這跟if不同的是,當while的suite執行完畢之後,他會再度檢查條件,只要符合就會重複地執行suite直到某次的檢查發現條件不成立。這樣子不斷地重複、循環,就好像繞行一個圓型路徑一圈又一圈,所以我們又稱這種重複運算的結構為迴圈

我們發現在上例中,影響迴圈條件的是變數num,我們又稱它為條件變數或是迴圈變數,通常一開始我們設定讓迴圈變數能使得條件成立,並且在迴圈執行中,更改我們的迴圈變數,在我們想要結束該迴圈的時候使得條件不成立。

上例中,num從1開始繞行迴圈,每次的繞行都會遞增1,這使得_sum+=num可以完成1加到10的任務,更重要的,當num增加到11時,由於不滿足條件,Python會讓我們離開迴圈,這樣精準的控制,讓我們剛剛好完成工作。

break與continue

如果while的條件總是成立,我們稱這種情況為無窮迴圈,基本上來說這是個永不停止的迴圈:

while True:
    print 'never stop'

這種情況就會非常恐怖了,但是無窮迴圈也有他的價值,當我們的程式必須等到使用者的一些要求時,就必須使用無窮迴圈,讓程式等待使用者的輸入或是命令,並且再處理完該要求後繼續重複地等待下一個要求。

對於迴圈,我們的控制力不只如此,我們還要介紹到兩個強制介入迴圈的關鍵字:breakcontinue

break是用以中斷迴圈的敘述,不論何時,只要碰到break,馬上跳出該層迴圈:

n = 5
count = 1
while True:
    print 'just print {n} times'.format(n=n)
    if count == n:
        break
    count += 1

這是個比較複雜的例子,乍看之下由於while條件的恆真,這似乎是個無窮迴圈,但是我們在while中設計了一個巢狀的if結構,當count的值與n的值相同時,因為執行break的原因,我們將會跳出該層迴圈。

這邊有幾個概念,第一個,巢狀結構是允許出現在條件結構和重複結構中的,我們可以在while中有if,也允許if中有while,甚至while中有while,if中有if都是可以的。

第二個,break跳出的區塊是由他往外看到的第一個迴圈區塊,因此他不是跳出if而是跳出while,而當迴圈是巢狀結構時,這個判斷就更複雜更需要注意了。

如果今天我們想要印出1-10但是不包含5,我們要怎麼做呢?或許你會想這樣:

num = 1
while num < 5:
    print num
    num += 1
num += 1
while num < 11:
    print num
    num += 1

我們以5為分界點,拆成兩段分別撰寫一個迴圈。但是這樣顯然麻煩,我們可以用continue幫助我們略過:

num = 1
while num < 11:
    if num==5:
        num += 1
        continue
    print num
    num += 1

continue敘述會略過同屬於該層迴圈區塊的其他運算,並且重新再開始一次迴圈,意思他會略過該次迴圈,直接進入下一次迴圈。

我們會發現,break和continue通常都要伴隨著條件判斷式,且他們作用的對象是迴圈。

for迴圈

除了while之外,其實還有一個更常用的迴圈:for迴圈,他會藉迭代來執行重複的運算:

lst = [1, 2, 3]
for num in lst:
    print num

我們稍微講解一下,首先,如同任何學過的流程控制的結構,對於for迴圈,我們必須給定一個冒號並且縮排來進入suite,至於for迴圈要執行幾次並不是靠著條件的判斷來達成,而是靠著迭代的次數。

for item in lst會將lst中的元素一個一個取出,每取出一個便將他賦值(代入)給item,並且執行一次suite,所以,lst中有幾個元素,我們就會重複幾次,而且每次的num值都會是一個lst元素的值。

這種依次取出(探訪)並且進行代入的動作稱為迭代。

for迴圈可以對所有可迭代的對象進行迭代,包含清單、元組、字典甚至字串,迭代字典我們會依照任意的順序取出字典的鍵,迭代字串我們會依序取出字串中的字元。

我們可以稍微來剖析一下for迴圈跟while迴圈的使用時機好了:

  • 當需要重複進行運算的時候使用迴圈(for/while)
  • 當重複的次數可以清楚被計算或當迭代的表現明顯時,使用for迴圈
  • 當重複的次數難以計算(但條件清楚)或是有條件的重複時,使用while

Comprehension

Comprehension是一種利用for迴圈的技術來達成製造清單或字典的快速手段,其實他帶有非常強烈的函數編程的味道,不過這裡不說那麼多,直接看看怎麼做。

假設我們今天有一個清單:

lst = [1, 2, 3, 4, 5]

我們想要製造另一個清單,每個元素是lst中元素的平方,如果讀者們還記得清單怎麼加入元素的話,可能會想這樣做:

lst_sq = []
for num in lst:
    lst_sq.append(num**2)

我們先設置了一個空的清單:lst_sq,接著透過for來迭代lst,取出元素平方後append到lst_sq上,但是我們有個更簡潔的寫法,請看:

lst_sq = [num**2 for num in lst]

就是那麼簡單,我們直接在清單的特徵-中括號中使用for迴圈,取出元素後作平方的動作。這種簡便而快速製造清單的手法就是所謂的list comprehension。

讀者們千萬不要誤解,list comprehension並非一定以清單作為原料(for迭代的對象),但是是為了製造出清單。

除了迴圈,我們還能加入條件判斷的應用,比如說,在剛剛的平方數裡,我們只想要留下偶數:

lst_sq = [num**2 for num in lst if num%2==0]

透過if我們可以保留下符合條件的元素,其餘的會略過。

要製造字典一樣有comprehension的手法:

scores = [88, 90, 100, 65, 78]
score_dic = { student_id:score for student_id, score in enumerate(scores)}

上面的例子稍微複雜,我們解釋一下,我們有個按照學生id排好的分數清單scores,也就是說id為0的學生分數是88,id為1的學生分數是90,以此類推,我們想要將他化做字典,因此使用dictionary comprehension。

enumerate是一個函數,當他和for搭配的時候,他會同時把元素跟元素的位置(索引值)取出來,所以我們在迭代時也要用兩個變數來裝載,最後,我們必須用key:value的型式將兩個值作一個配對以成為字典的一個元素。

Comprehension一直都是一個有爭議的手法,許多人其實並不那麼贊成這種表達方式,但是作者認為這些帶有函數編程風格的python特性,是相當強力且簡潔的工具,我們應當善加利用。

輸入與輸出

本節要來談談Python怎麼做輸出入的,我們知道,最簡單輸出到螢幕的方法就是用print,任何基本的資料都能用print輸出,甚至我們可以用逗號去一次輸出多個資料:

print 'hello world'
print 1, 2, 3
print 'dokelung', 27
print [1, 2, 3]

但有個討厭的狀況是,python的列印會自動換行,所以當今天讀者需要不斷行地列印時,需要使用逗號:

print 1,
print 2,
print 3

如此我們就不會被強制斷行了。

那要如何輸入呢?使用input:

guess.py
answer = 6
while True:
    str_num = input('請輸入一個1-10之間的整數:')
    int_num = int(str_num)
    if int_num==answer:
        print('猜中拉!')
        break

這邊展示了一個完整的例子,我們設計了一個很簡單的猜數字,input函式會讓使用者能從鍵盤輸入,該輸入我們將他存到str_num這個變數之中,在input()內的字串是我們給使用者的輸入提示。

這邊有一點要注意,任何輸入(不管你輸入的是hello或是123)都會被當成字串,而使用int函式可以幫助我們將字串轉為整數。
如果輸入的數字跟答案相符,我們會告訴使用者猜中了,並且中段迴圈,結束程式,否則這個無窮迴圈會無止盡地要求使用者輸入數字,直到猜對答案或是手動中段程式為止。

我們可以將這個檔案運行來試試看:

$ python guess.py

以下是可能的測試結果:

請輸入一個1-10之間的整數:9
請輸入一個1-10之間的整數:1
請輸入一個1-10之間的整數:4
請輸入一個1-10之間的整數:6
猜中拉!

接著我們來談談怎麼透過檔案來執行輸入和輸出,要能夠讀寫檔案,我們需要以下步驟:

1. 開啟檔案
2. 讀取或寫入檔案
3. 關閉檔案

我們先來看看如何開啟檔案,假設我們有一個檔案test_file內容如下:

test_file
Hello world
Today is a good day!

接著我們用簡單的幾行code就能完成讀取檔案:

f = open('test_file')
print f.readline()
print f.readline()
f.close()

open函數需要一個檔名字串作為參數,當我們指定了test_file,Python便會打開該檔,並且產生一個檔案物件,目前我們讓f參照這個檔案物件。

另外要提醒讀者的是,如果open函式只有指定檔名,那預設開啟後是用來被讀取的,如果要開啟一個欲寫入的檔案,那我們需要另外用參數來指定,這會在稍後提到。

下一步,我們可以用readline()方法來讀取一行,記得,他跟input一樣,所讀取的任何資料都會被當成字串。
這邊我們之所以讀取兩次,是因為原本的檔案裡有兩行。

而最後利用close方法來關閉檔案。
我們一定要記得在使用完檔案之後關檔,否則會讓該檔案佔住記憶體的空間,也可能會導致不可預期的錯誤。

執行的結果如下:

hello world

today is a good day

也許這跟讀者的預期不同,原因是當我們讀取檔案中的每一行時,會連同結尾的換行符號\n一起讀到,加上print預設的換行,就多出了我們看到的中間的空白行,其實我們對於字串可以用strip方法來清除字串頭尾一些非必要的字元:

f = open('test_file')
print f.readline().strip()
print f.readline().strip()
f.close()

另外一個可以討論的點是,如果我要回頭去讀取前面的行,我們可以用seek方法:

f = open('test_file')
print f.readline().strip()
print f.readline().strip()
f.seek(0)
print f.readline().strip()
f.close()

我們利用seek將檔案物件讀取的位置移到第一行(其實是第零行),再一次的讀取後,我們可以再度讀到'hello world'。

有一個問題是,有的時候我們並不知道檔案的行數,當我必須要遍讀全部的檔案時會有點麻煩,但是告訴大家一個好消息,for迴圈可以應用在檔案物件上:

f = open('test_file')
for line in f:
    print line.strip()
f.close()

透過for,我們會迭代檔案中的每一行,這讓我們可以輕易地遍讀檔案。

Python有一個功能是環境管理器,我們可以透過with as敘述來簡化我們的檔案讀寫:

with open('test_file') as f:
    for line in f:
        print line

使用這種手法,我們將可以忽略檔案關閉,因為一旦離開with的suite,檔案就會自動被關閉了。

那我們要如何寫檔呢?只要在open函式內多增加一個一個參數,並且提供檔案物件給print:

lst = ["第一行""第二行"]
with open('test_file','w') as f:
    for line in lst:
        print >>f, line

沒錯,就那麼簡單!使用open的第二個參數"w"來指定要寫檔(write),並且在print後面加入兩個大於符號與寫入的目的地(檔案物件)並用,與要寫入的內容隔開,就能夠輕鬆地寫入檔案了!

例外與捕捉

在動態語言中,除了語法上的錯誤造成的Syntax Error幾乎所有的錯誤都來自執行期的錯誤,如同前面見過的TypeError或是NameError等都是。當發生錯誤時,Python會產生回溯(traceback),這讓我們可以檢視錯誤發生的來源,有的時候一個錯誤是另外一個錯誤所引發的,唯有一層一層的檢視,才能找出錯誤。

不過,在本節中要討論的不是除錯,而是辨識並排除掉一些不是錯誤的錯誤,因為錯誤並不全然都是錯誤,這聽起來有點玄,讓我們舉個讀取檔案的例子,首先我們來看看要讀的檔:

test_file
3 4
1 3

20 19
%%%
88 7
1 2 3
0

來看看我們的程式碼:

with open('test_file') as f:
    for line in f:
        a, b = line.strip().split()
        print int(a)+int(b)

我們先對第一次出現的方法split,作解說,split是字串的方法,他能夠將字串依照指定的分隔符號切割,舉例來說,字串string='hello,world'可以利用string.split(',')來切割成'hello'和'world',字串string='hello-world'可以利用string.split('-')來切割成'hello'和'world',如果我們不給定參數,則預設會用空白分割,比如說,字串string='hello world'可以利用string.split()來切割成'hello'和'world'。

因此這個程式首先打開了我們的檔案f,接著利用for迴圈去取出每一行字串,取出的字串先用strip方法作一個清理,接著使用split方法利用空白符號作切割,我們將切割成兩個字串a和b(a, b = line.strip().split()這種寫法稱之為unpack,我們會將拆解完的結果依序賦值給a,b),最後將兩個字串轉成整數後相加。

但我們發現執行的時候出現了ValueError,這很正常,因為檔案中有許多行並不符合兩個整數的格式,有行是空白的,有行有三個整數或只有一個整數,還有行是奇怪的符號,如果我們要利用if/else的手法來排掉這些"例外"的狀況,會顯地非常複雜,每一種可能有誤的格式都需要寫一個if或elif來避開,我們與其花大量的時間來避開錯誤,不如讓錯誤發生,我們再來處理,讓會中斷程式的錯誤僅僅是允許繼續執行的例外:

with open('test_file') as f:
    for line in f:
        try:
            a, b = line.strip().split()
            print int(a)+int(b)
        except:
            pass

例外的捕捉,用的語法是tryexcept,我們先try一段程式碼,如果發生了錯誤,我們立刻停止try,進入except suite作例外發生時的處置。所以當我讀到非兩個整數的行時會導致錯誤發生,但因為我們只是try,我們當場捕捉到了這個錯誤(例外),於是我們就不執行print,而是直接跳至except suite去執行,但因為這邊是個空運算,所以等於是我們略過了所有不符合規格的行。

這裡我們看到了空運算的用途,python不允許任何一個suite是空白的,所以當我們什麼都不想處理時,必須補上一個pass

讀者越熟悉例外,越會覺得他威力無窮,很多很複雜、很瑣碎很難想清楚的狀況,與其利用if/elif/else針對每種狀況寫出處理手段以避開錯誤,不如利用try,大膽執行程式碼,等到錯誤發生時,將之捕捉當成例外。這種手法往往可以讓我們開發的更快,錯誤更少。畢竟,錯誤不一定是錯誤,他可能只是個例外。

函式

我們在前面提過了,我們透過參數化將運算封裝就能得到一個可重複利用且具有彈性的函式,這一節,我們要詳細地討論函式的用法以及自訂義函式的方法。

內建函式

我們之前談過,Python中有內建了許多的函式,我們不需要多做其他的動作就能使用,再這裡就讓我們介紹幾個還沒有看過的內建函式吧!

工廠函式

我們生成資料的方式有兩種,一種是透過字面定義,另外一種就是工廠函式了,我們舉個例子:

a = 5
a = int(5)

a = 5是最常使用的生成方式,簡單的寫出一個字面上的符號5,就能產生一個整數。
而使用內建函式int,也能夠產出一個整數,不過此例中,看不出int的威力,畢竟,寫起來還是麻煩多了。
但是下面的情形,我們就非得使用int不可:

string = '100'
a = int(string)

我們使用int來進行型態的轉換,最常見的情況便如同上例一樣,我們可以將字串的整數(型態是字串,但內容看起來就是整數)轉成真正的字串,這通常在接受使用者輸入和讀檔時是必要的工具。

像int這類的專門負責轉換型態或是製造資料的函式我們稱之為工廠函式,下表列出一些常見的工廠函式與應用:

內建函式 敘述 例子 結果
int 製造整數或將其他型態資料轉為整數 int('99') 99
float 製造浮點數或將其他型態資料轉為浮點數 float('1.73') 1.73
bool 製造布林值或是將其他型態資料轉為布林值 bool('') False
str 製造字串或是將其他型態資料轉為字串 str(20) '20'
list 製造清單或是將其他型態資料轉為清單 list((1,2,3)) [1,2,3]
set 製造集合或是將其他型態資料轉為集合 list(set([1,1,2,2])) [1,2]

我們會發現bool函式可以在非布林語境中得到一個資料的布林值,而set可以將清單轉為集合型態,集合中的元素是不能重複的,這是一個很簡便的方法,讓我們可以將清單中重複的元素過濾掉。

全部或任何

"對於所有"(for all)和"存在一個"(exists)是邏輯上面很重要的概念,假設我們用一個長度為5的清單來記錄班上1號到5號同學考試有沒有及格,如果該生及格,會標示True,否則會標示False:

pass = [True, True, True, False, False]

今天我們想要知道全班是不是都及格了,傳統的作法是:

all_pass = True
for s in pass:
    if s==False:
        all_pass = False
if all_pass:
    print '都及格了,歐啪'
else:
    print '有人不及格'

實在是太麻煩了,Python使用了all函式能支援這種運算:

if all(pass):
    print '都及格了,歐啪'
else:
    print '有人不及格'

all函式可以接受一個清單作為參數,當全部的元素都為真(或是在布林語境為真),則他的回傳值為真,否則為假。
另一個函式any剛好相反,只要有一個人為真,回傳值就是真了:

>>> any(pass)
True

最大與最小

如果讀者有意會到前面的例子,那對於找最大值與最小值,也不會想要用for進行查找比對,直接用內建函式吧:

lst = [1,2,3,4,5]
print max(lst)
print min(lst)

產生連續的整數

range函式能幫我們產生連續的整數,通常會搭配for迴圈來使用:

for num in range(10):
    print num

上面的代碼會印出0到9,原因是range(X)會產生0~x-1的整數序列,透過for迭代後印出,我們也可以對產生的範圍作更進一步的控制:

for num in range(2,7):
    print num

上面代碼會印出2~6。

群集的長度

還有一個非常重要的內建函式,那就是len,此函數可以幫助我們求得一個群集的元素個數:

lst = [1,2,3,4,5]
print len(lst)
dic = {1:1,2:2,3:4}
print len(dic)

不論是清單的長度,或是字典的鍵值對個數,都可以用len來查到!

其餘的內建函式,讀者可以參考Python官網:

自定義函式

除了使用內建的函式之外,我們也可以自己撰寫函式,本小節就來說明自定義函式的方法。

首先我們看一個簡單的例子:

def add(a, b):
    return a + b
print add(1,2)
print add(3,4)

我們自定了一個整數的加法函式,函式的定義要以關鍵字def開頭,後面跟隨著函式的名稱,一個小括號,裡面是這個函式需要的參數,接著是冒號後切換到要縮排的suite。最後,在函式裡面會有一個回傳值。型式整理如下:

def 函式名稱(參數1, 參數2, ...):
    若干運算(敘述)
    return 回傳值1, 回傳值2 ...

函式名稱的命名規則跟變數差不多,參數可有可無,數量也不限,通常函式內部的運算會需要參考到這些變數,比如說我們這邊執行的加法a+b,便是參考了參數。回傳值可以想成是這些被包裝的運算的總結果,不限定只有一個,可以多個也可以都沒有,這跟其他程式語言有極大的差別。

接著,我們再度來檢視函式的呼叫,add(1,2),會呼叫add函數,並把參數a設為1,參數b設為2,最後將a+b的結果回傳,所以add(1,2)可以想像成整數3。

還有一個重點要提醒,由於Python會由上而下地解讀並執行代碼,所以當我們呼叫函式時,一定要確定直譯器已經讀過函式的定義了,也就是說,我們要在定義之後才能呼叫函式。

函式的呼叫

函式其實有兩種呼叫方式,位置呼叫與關鍵字呼叫,比如說我們剛剛的add函數採用add(1,2)就是使用位置呼叫,我們依照參數的位置來傳遞資料,因為def add(a,b):,所以a會設定為1,b會設定為2:

位置呼叫
....add(1, 2)
        |  |
        V  V
def add(a, b):

其實我們也可以用關鍵字呼叫,下面兩次呼叫的結果是相同的:

print add(a=1,b=2)
print add(b=2,a=1)

當使用關鍵字呼叫時,確切地指出傳遞的對象,這樣做的好處是,我們不需要知道參數的確切順序,只要指定的名稱正確,便能正確的傳遞。在本例中,看不出這樣做的好處,原因是對於這個簡單的加法運算而言,a與b其實並沒有差異,我們考慮另一個函數:

def power(base, exp):
    return base ** exp

這是一個計算次方的函式,在這裡,base和exp是有明顯差別的,power(2,3)power(3,2)的結果大不相同,這個時候我們利用關鍵字呼叫便能減少錯誤使用的情況,不然使用者如果並沒有過該函數的定義,他可能不會知道第一個參數代表的是底數還是指數:

print power(base=2,exp=3)
print power(base=3,exp=2)

遞迴

在結束這個主題之前,我們要舉一個稍微複雜的例子,目的是除了講解函式之外,還帶給讀者們一些其他的重要觀念。

還記得我們講過的巢狀結構嗎:

lst = [1, 2, [3, 4, 5], 6, [7, [8, 9]] ]

假設我今天想要把裡面的每個元素(不管這個元素在第幾層了)都列印出來,該怎麼做呢?我們先試試看這樣:

for item in lst:
    print item

會得到這樣的結果:

1
2
[3, 4, 5]
6
[7, [8, 9]]

顯然不是我們想要的,當我們發現元素是清單的時候,應該要再次迭代該清單的項目:

for item in lst:
    if isinstance(item, list):
        for item2 in item:
            print item2
    else:
        print item

isinstance是一個內建函數,可以幫助我們判斷變數的型態,他需要兩個參數,第一個是要測試的資料,第二個是指名要確認的形態,isinstance(data, type)的意思就是問data是type型態的嗎?isinstance(item, list)就是問item是list型態的嗎?

這樣我們可以在元素為清單時,再次迭代,元素不是清單時就直接印出,乍看之下這解決了問題,但是我們卻發現,如果清單位於第二層裡就沒辦法在迭代到了,好了,讀者們會說,那就再度迭代即可,但其實沒有那麼容易,我們並不知道要處理的清單究竟有多少層?

這時候我們只能使用遞迴的手法,但首要的工作,我們需要自定義一個函式:

def print_list(lst):
    for item in lst:
        if isinstance(item, list):
            print_list(item)
        else:
            print item

這個函式是這樣的,我們先迭代一次傳進來的清單,若發現某元素是清單,我們便去呼叫自己本身,否則我們直接把元素印出來。
我們會發現,不管清單位於哪一層,只要我們發現他,我們便呼叫print_list處理,就好像剝洋蔥一樣,一層一層剝開,我們不需要煩惱要剝幾層,只要知道,若沒有剝到最底的一層,我們便要繼續剝。這就是遞迴的手法,這在程式的世界是很重要的技巧,寫在這裡供讀者體會。

參數的預設值

函式的參數使允許有預設值的,這樣我們呼叫函式的時候,不必提供相等數量的參數:

def add(a, b=1):
    return a + b
print add(1,2)
print add(1)

在這裡,我們為參數b設定了一個預設值1,這代表,如果我們只提供了a的值,b會預設使用1,所以add(1,2)的結果是3(a=1,b=2,預設值沒有使用到),add(1)的結果是2(a=1,b使用預設值1)。

我們可以讓多個參數使用預設值:

def add(a, b=1, c=2, d=3):
    return a + b + c + d

但要注意,所有帶有預設值的參數都需要擺置在不帶有預設值參數的後面,而且在使用位置呼叫時,我們無法跳著指定哪些人使用預設值。

綴星運算式

還記得我們使用過星號*來進行unpack的動作嗎?

其實在Python中,*可以提供函式完成一些更進階的功能,我們再次考慮剛剛寫的加法函式:

def add(a,b,c,d):
    return a+b+c+d

如果今天我們要對一個四元素的清單中的元素套用這個加法函式,我們可以透過綴星運算式來完成:

lst = [1,2,3,4]
print add(*lst)

*lst會執行unpacking(拆解)的動作,他會將lst拆解成四個整數1,2,3和4,意思是下面三個寫法是等價的:

add(*lst) 等同於 add(lst[1],lst[2],lst[3],lst[4]) 等同於 add(1,2,3,4)

總而言之,*會將一個有序群集拆解為該群集的元素後再呼叫函式。

綴星運算式不一定用在呼叫的時候,我們也可以在函式定義的地方使用他。如果今天我們要撰寫三個數或四個數的加法:

def add(a,b):
    return a+b
    
def add3(a,b,c):
    return a+b+c+d
    
def add4(a,b,c,d):
    return a+b+c+d

這麼做有幾個壞處,我們不能使用同一個函式名稱add來處理所有的case。這在別的語言之中有overload(多載)來解決了,但Python中沒有這種機制,所以,我們必須對每一種數量的加法都寫一個新的函式,那實在是太麻煩了.

多載(overload)指多個同名的函式可以同時存在,只要他們的參數數量或是型態不同。

我們可以考慮利用清單:

def add(lst):
    return sum(lst)

這樣做已經相當不錯了,但是當呼叫的時候還是要先建立一個清單才能運算,如add([1,2,3,4]),我們想要能夠更直接的呼叫add(1,2,3,4),只要這樣做:

def add(*lst):
    return sum(lst)

位在參數列的*lst會將呼叫該函式所傳進來的所有元素組成一個元組(tuple),所以傳進來的四個整數1,2,3,4將會被收集成元組(1,2,3,4)。有了這個技巧,我們能夠允許任意個資料輸入,最終我們會將之集成一個元組處理。

對於字典類的群集,我們可以使用雙星號**的綴星運算式,這會讓函式以關鍵字的方式呼叫:

def power(base, exp):
    return base ** exp
    
dic = {'base':2, 'exp':3}
print power(**dic)

上例中的**會將dic拆解成base=2和exp=3:

power(**dic) 等同於 power(base=2,exp=3)

而在定義函式上,也能允許任意數量資料輸入:

def power(**dic):
    return dic['base'] ** dic['exp']
    
print power(base=2, exp=3)

Python會將呼叫函式的關鍵字輸入集成一個字典,也就是base=2和exp=3會因為**的使用而形成字典{'base':2,'exp':3}

變數的有效範圍

提到函式,便不得不談到變數的有效範圍,也就是變數被認定(認識)的範圍。

  • 定義在函式外的變數其範圍是一整個Python檔案,又稱全域變數(全域指的是整個Python檔)。
  • 定義在一個函式中的變數稱為區域變數, 其範圍是一整個函式

而以上兩種範圍都必須從變數定義被Python看到開始算起,若是區域變數與全域變數名稱相同產生衝突時,區域內以區域變數為主,區域外以全域變數為主。

我們來看個例子:

def test_scope():
    var1 = 1
    print var1, var2

var1 = 0
var2 = 0

test_scope()
print var1, var2

最後印出的結果是:

1 0
0 0

我們發現var1雖然在函式外有定義過,但是在函式中,由於我們也定義了一個區域函數,所以在印出var1的時候,Python會優先選擇在函式中的定義,反觀var2,由於區域內沒有定義所以會去選擇函式外的全域變數。

至於在函式外的print,當然都會選擇全域變數來印出,許多初學者會認為var1的值已經被改變了,應該會印出1,其實不是,因為這兩個變數是完全不同的兩個變數。

閉包與裝飾器

閉包(closure)是參照了外部環境的函式,什麼意思呢:

def gen_power(base):
    def power(exp):
        return base ** exp
    return power

power2 = gen_power(2)
power3 = gen_power(3)

print power2(3)
print power3(2)

我們在gen_power函式裡面建立了一個位在函式裡的函式(區域函式)power,power函式本身參照了base這個外在變數(base既非power的參數,也不是power的區域變數),所以這種綁定造就了閉包,沒錯,power函式就是閉包。

閉包有怎麼樣的特性呢?我們發現power2 = gen_power(2)會使得power2這個變數參照到一個函式,接著使用power2(3)的時候會計算2的3次方,可是明明power2我們只提供了exp這個參數阿,base打哪來的呢?別忘了,power函式綁定了gen_power的base變數,而base在當時是被設定為2,其實講簡單一點,power2會成為一個計算2的次方的函數。

閉包使得原本在函式結束時就會結束作用的base變數得到了延續。而power2(3)其實就是gen_power(2)(3),透過gen_power(2)得到的函式再利用3去呼叫,這種手法又稱為揉製(curry)。

接著我們介紹裝飾器(decorator),裝飾器是Python獨有的特性,主要是借助閉包的力量,來產生一個能夠修飾函式的函式,這種修飾別人的函式稱為裝飾器。

我們先來看看一個裝飾器該如何使用,再來探討要怎麼樣寫一個裝飾器。

@print_result
def add(*lst):
    return sum(lst)

@print_result
def power(base,exp):
    return base ** exp

add(1,2,3,4,5)
power(2,3)

print_result就是一個裝飾器,當使用的時候必須以@開頭,並且在其後緊跟著他要修飾的函式,print_result的作用是會讓被他修飾過的函式,會自動印出他們的回傳值,例如呼叫add(1,2,3,4,5)將會自動印出15而power(2,3)將會自動印出8,而不需要print add(1,2,3,4,5)print power(2,3)

但大家不要忘記前面講過,裝飾器本身是一個函數,所以

@print_result
def add(*lst):
    return sum(lst)

add = print_result(add)

會具有一樣的效果。

add變數參照的函式傳進print_result被裝飾過後,再度用add變數來參照。
這邊我們會發現原來函式也可以當做參數傳遞,這其實不用太意外,Python中所有的資料(物件)都是可以傳遞的,而函式也是物件。

接著讓我們來瞧瞧該如何撰寫這個裝飾器:

def print_result(func):
    def modified_func(*args,**kwargs):
        print func(*args,**kwargs)
    return modified_func

我們利用閉包寫了一個區域函數modified_func,此函數將會被回傳作為裝飾過的結果。
這個函式的功能應該要跟原本被修飾的函式一模一樣,除了他必須加入列印結果的功能。所以他參考了外部環境的變數func(要被裝飾的函式),藉由呼叫func來完成相同的功能,並且將func的回傳值印出。這就好像是一種包裝的手段,在裝飾器裡,我製造一個新的函式,該函式會呼叫要被裝飾的函式,同時多增加一點功能,最後再吐出這個新函式。

這邊有一個重要的技巧被使用,由於裝飾器本身並無法預測將來要裝飾怎麼樣的函式,對於參數數量或參數名稱不同的函式,我們的modified_func都要能呼叫才行,所以我們使用了綴星運算式。

透過*args**kwargs我們可以接收任意數量的位置引數和關鍵字引數,這兩者之間的搭配可說是恰到好處。

如果今天我們想要為裝飾器增加一些參數,比如說以下這種效果:

@print_result(head='result:')
def add(*lst):
    return sum(lst)

add(1,2,3,4,5)

---(結果)---
result: 15

我們先要理解裝飾器的作用過程,上例中使用裝飾器部分的代碼等同於:

add = print_result(head='result:')(add)

print_result的結果必須要是一個函式,且必須要是一個產生器,所以我們可以這樣寫:

def print_result(head):
    def decorater(func):
        def modified_func(*args,**kwargs):
            result = func(*args,**kwargs)
            print head, result
        return modified_func
    return decorater

我們在裝飾器外面又增加了一層函式,透過閉包,最內層的新函式可以取用到head,我們發現這樣的設計使得呼叫print_result後可以得到一個產生器,最後再傳入要被修飾的函式進行裝飾。

講到這裡大概有讀者想問:如果我們要讓該裝飾器的參數可用可不用,要怎麼做呢?由於這牽涉到更高級的Python技巧,為避免過度複雜,建議大家可以找機會自行了解,我們只要知道裝飾器有這樣的用法即可。

未來在Django中會有大量使用到裝飾器的地方,讀者們至少要熟悉如何使用一個設計好的裝飾器來裝飾函式。

物件導向

還記得類別(class)是什麼嗎?類別其實就是指資料的形態,而所謂的物件(object)便是該型態實際的例子(instance),比如說整數型態的1,字串型態的'hello world'或是清單型態的[1,2,3,4,5],甚至連函式也是物件,事實上,如果是第一次接觸這類程式語言的人,可能會很驚訝,在Python中,無一不是物件。

既然物件如此重要,我們要必要做一些稍微深入的探討,這個小節就是要幫讀者了解最基本的物件導向程式設計原理以及在Python中如何實際運作,而不僅僅只是知道存取屬性或使用方法。

但讓我們我們稍微回憶一下存取屬性與使用方法,一樣假設我們有一個貓的類別Cat(在Python中並不存在這個類別,要靠我們自行定義),而my_cat是他的實例:

print my_cat.name
my_cat.name = 'kitty'
my_cat.shout()

使用.來做屬性的存取與方法的呼叫。

自定義類別

用了那麼多Python內建的類別(型態),我們究竟要如何自定義如同Cat這樣的類別呢,很簡單:

class Cat:
    def __init__(self, name):
        self.name = name
    def shout(self):
        print 'Meow'

就是這樣,只要這簡單幾行,我們便可以擁有一個貓的類別,我們來好好檢視一下自定義類別在定義上的結構

  • class標明了現在我要定義一個類別,就好像我們使用def來定義一個函式一樣
  • 類別的名稱慣例會以大寫開頭,如同本例中的Cat
  • 接著一樣使用冒號,換行後縮排進入suite
  • 接著在類別中我們會定義一連串的方法(函式),但是我們總會以__init__作為第一個函式
  • 每個函式必定以self作為第一個參數

以上幾乎就是簡單定義一個類別所需要知道的東西了,但容我們細細來剖析每個環節,首先就是__init__這個函式,讀者可能覺得這個名稱也太醜了,使用了雙底線作為開頭和結尾,其實這種以__開頭和結尾的方法是Python物件中的特殊方法,代表了特殊的意義,沒有必要請不要使用這種方式命名你的方法。

__init__其實就是initialization(初始化)的意思,又稱為建構方法,每當我們利用類別產生出一個實際物件(實例)的時候,這個方法總會被先呼叫,我們通常會在建構方法中做一些基礎屬性的設定及預設動作的展現。

我們先放下self這個東西來思考一個問題-其他的內建型態都可以透過字面上的定義來建構物件:

a = 5.7
b = 'yes'
c = [1, 2, 3]

可是像Cat這種自訂的類別要怎麼產生實例呢?
我們透過類似函式呼叫的方法就可以了:

my_cat = Cat('Kitty')

在這裡我們好像是呼叫了Cat這個類別,提供了貓的名字後就會生成物件。
其實這種方法我們早就見試過了,a = int('5')實際上就是透過字串作為引數值來生成一個整數(轉換型別其實是這麼一回事)。

建構方法不一定需要self以外的參數,如果建構方法只有一個參數self,那將物件實體化(製造)出來只需要:my_cat = Cat()

但事實上這段程式碼真正的樣貌是:

Cat.__init__(my_cat,'Kitty')

沒錯!當我們利用類別產生物件時,我們其實是去呼叫類別中的__init__方法,並且把參照實例的變數(my_cat)作為第一個參數傳遞給self參數,所以說,self現在會參照my_cat而name會參照'Kitty':

Cat.__init__(my_cat,'Kitty')
                   /
def __init__(self,name)

代碼self.name = name意思就是my_cat.name = 'Kitty',這邊讓我們明白到,self代表了物件本身。

這也是我們要求類別中的方法都要以self作為第一個參數的原因,唯有透過self,我們才能知道現在是哪個物件在使用這個方法,到底是my_cat還是your_cat。更深一層的來講,對於所有來自同一類別的實例而言,他們的屬性在電腦中是分開的儲存的,但是方法是共用的,如果沒有self來指稱是哪個實例,方法可能會不知道現在是哪個物件要運作。

總而言之,請大家務必為任何方法設定self作為第一個參數。

接著來看看self.name = name,我們將參數name的值傳給self.name,在這裡name只是一個普通的變數,當該函式執行完畢後,name就不再存在了(生命週期已經結束),那我們要如何讓貓的名字保留下來(至少在貓活著的時候),並且於函式外面還能存取到呢?

嘿嘿!我們一樣使用self,透過設定self.name(某實例的名字,如my_cat.name),我們可以擁有一個屬於物件的變數,也就是我們一直在講的屬性啦,屬性不但可以跟實例同生共死,還能夠透過實例來存取到,這才是我們要的。

加入self作為前綴的變數才會成為屬性,屬性能夠與物件同生共死,也能在類別中任何方法中存取。

物件導向三大特性

如果對物件導向程式設計略懂略懂,一定會知道三大特性就是封裝、繼承與多型,本小節便會簡要地向讀者展示這三個特性。

封裝

我們在談到函式的時候講過封裝,透過包裝若干運算便能創造出函式,然而物件也是封裝來的,物件不但封裝了若干函式(方法),還封裝了若干的資料(屬性),更重要的一點是,這些屬性通常是供給方法做運算的。我們把同屬一類或同屬一物的資料跟運算打包起來,讓我們能夠輕易且精準地完成任務。

我們考慮一個矩形的類別,要描述一個矩形,我們通常需要長(length)跟寬(width):

class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width
    def perimeter(self):
        return 2 * (self.length + self.width)
    def area(self):
        return self.length * self.width 

如此一來,要計算一個矩形的周長和面積,我們會這樣做:

rec = Rectangle(4,2)
print rec.perimeter()
print rec.area()

這個過程似乎沒有什麼大不了的,也許讀者會覺得一些獨立的function足矣:

def perimeter(length, width):
    return 2 * (length + width)
def area(length, width):
    return length * width 

這兩者的差別在哪呢?我們發現後者在定義函式的時候,每個函式都必須要重新接收一次長跟寬這兩個參數。

這就是一個類別明顯的優勢,對於同一個矩形來說,計算週長和面積所用到的資料是一樣的,所以利用獨立函式來完成任務時,重複且多餘的參數實在令人心煩,類別與物件之所以可以避免,便在於我們將運算所需的資料(長與寬)也一起封裝進去了,減少了傳遞上的麻煩與失誤。

第二個優勢在於,如果使用獨立函式來完成任務,會有資料誤用的情形,怎麼說呢?

area(-1, 0)

上面的代碼我們傳進了負數與0,這完全不合法的長與寬,更不用說如果我任意地傳進字串或清單來惡搞。這實在是因為運算跟資料的分離,導致兩者可能無法配合,像此例就是個誤用的情形。

但使用類別可以有效地解決此情況,因為我們可以確保在Rectangle中,perimeter和area運算所用的資料絕對是正確的長與寬,講的精確一點,如果一開始生成Rectangle的時候我們就保證了資料正確,往後類別中的方法調用資料時就一勞永逸地可以放心使用了,我們可以這樣做:

class Rectangle:
    def __init__(self, length, width):
        if length <= 0 or width <=0:
            raise ValueError('length and width should be positive!')
        self.length = length
        self.width = width

當我們檢查發現長寬非正值的時候,我們可以利用raise來主動引發錯誤ValueError,並且附帶有錯誤訊息,這樣子的機制可以讓我們在最一開始便確認資料的正確性,如果無誤便可以將之與矩形的方法封裝在一起。

如果我們也想對獨立的函式做檢查,那可就多了許多重複而無謂的工作了,畢竟當函式種類繁多時,這樣的檢查並無效率。

封裝將資料及其相應的運算打包在一起,於是運算時不需要重複提供資料,也不怕會誤用資料。

繼承

緊接著我們來談談繼承。
回頭來看看我們的貓,假設現在我們擴充了一些功能:

class Cat:
    def __init__(self, name):
        self.name = name
    def run(self):
        print self.name, 'runs'
    def jump(self):
        print self.name, 'jumps'
    def shout(self):
        print 'Meow'

我們發現Cat類別的物件將會擁有跑及跳的能力,好了,這世界上可不只有貓這種動物,我們也想寫一個Dog類別:

class Dog:
    def __init__(self, name):
        self.name = name
    def run(self):
        print self.name, 'runs'
    def jump(self):
        print self.name, 'jumps'
    def shout(self):
        print 'Bark'

看樣子沒有問題,簡單的很,而且跟Cat類別實在是太像了,除了叫聲不同而已,甚至用複製貼上大法可以加速我撰碼的速度呢!

噢,如果真的是這麼做的話就慘了,大量重複的代碼會造成管理上的困難,更何況我們連複製貼上都懶,世界上有千萬種動物,我們要怎麼搞定這麼多具有相似特性卻略有不同的類別呢?

很簡單!我們抽取出他們的共性,寫成一個Animal類別,再分別繼承他們就好拉!請看:

class Animal:
    def __init__(self, name):
        self.name = name
    def run(self):
        print self.name, 'runs'
    def jump(self):
        print self.name, 'jumps'

class Cat(Animal):
    def shout(self):
        print 'Meow'
        
class Dog(Animal):
    def shout(self):
        print 'Bark'

在類別名稱後面加入小括號與要繼承的類別(我們稱之為父類別或是基礎類別),就可以繼承基礎類別的屬性和方法,然後我們可以為該類別(我們稱之為子類別衍生類別)新增一些屬於自己的方法甚至是覆寫掉一些基礎類別的方法,透過此手法,我們能夠輕鬆創造出更多的類別。

我們來看看如何覆寫掉方法,假設今天我們創造了人類這種Animal,我們想要在建構方法裡面新增人類的身分證號碼:

class Human(Animal):
    def __init__(self, name, id):
        self.name = name
        self.id = id       

透過上面的方法,我們能夠以新定義的__init__取代基礎類別的__init__

說到這裡,且讓我們停下來看一下__init__,這個方法大家千萬不要以為取哪個名字都行,原因是這樣的,我們的所有類別其實都繼承自Python中的object類別,那怕是像Aninmal類別看起來好像沒有繼承別人,但他的確是繼承了object,在這個最最基礎的類別中,__init__早就定義過了,我們不過是在覆寫他而已。

多型

其實多型可算是物件導向的精華,他讓繼承自相同父類別的子類別,能夠在呼叫同名的方法時,能展現出個子類別的實作,這聽起來有點玄,但是很容易理解,我們用剛剛的Cat和Dog類別做個示範:

dog = Dog('Paul')
cat = Cat('Kitty')
dog.shout()
cat.shout()

我們會發現,大家都呼叫的是shout方法,但是結果卻不相同,這看上去沒啥好講的,畢竟我們都個別定義了方法,但這其實是動態語言的福利,要知道,在Python中,變數是沒有型態的,有型態的是變數參照的資料。當我們利用變數呼叫方法時(如:dog.shout()),Python只需要確定當下變數參照的資料是有該方法的就行了(dog有,cat也有)。這會導致一個有趣的現象出現,今天機器狗明明不是動物,但如果MachineDog類別中有定義shout方法,他一樣可以叫,這就是頂頂有名的duck typing

duck typing(鴨子定型):如果他走路像鴨子,而且叫聲像鴨子,那他就是一支鴨子,我們不在乎是哪種物件呼叫了某特定方法,只要該物件有這個方法就行了。

本章小結

本章向各位讀者介紹了Python的特性、優勢與應用,並帶領讀者們下載並安裝Python,建立起一個可以運行Python的環境,同時也盡可能地完整的講述了Python的基本知識跟語法,最後還略微介紹了物件導向程式設計。透過本章的學習,即便讀者是一名Python新手或甚至是程式設計的初學者,相信都能獲得基本的能力,還請大家能夠徹底地吸收本章的知識,以迎接學習Django的挑戰。

我們即將在下一章正式進入到我們的主題-Django,使用Python進行網站設計的美好旅程,便由此開始!

← 一校補充
 
comments powered by Disqus