over 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進行網站設計的美好旅程,便由此開始!

 
over 3 years ago

p58-1

一旦我們定義了一個物件變數(實例變數),我們可以在類別內任意的位置使用self來存取他,例如我們可以為Cat類別新增一個幫貓改名字的方法和自我介紹的方法:

class Cat:
    def __init__(self, name):
        self.name = name
    def shout(self):
        print 'Meow'
    def rename(self, new_name):
        self.name = new_name
    def introduce_my_self(self):
        print 'Hello, my name is {0}!'.format(self.name)

又或者我們可以利用物件的名稱在類別的外部存取這些物件變數:

my_cat = Cat('Kitty')
print my_cat.name

其實顯而易見的,對於my_cat而言,在類別外的名稱是my_cat,在類別內我們會用self來指稱自己。

p63-1

你是否覺得將所有的代碼,包括一些基礎的運算、函式的定義和呼叫,甚至是類別的使用都放在一個檔案內,久而久之會使得檔案龐大不堪、修改不易且難以維護呢?別擔心,Python利用模組與套件的方式將程式碼的管理階層化,我們能夠將代碼按照功能切割成數個模組甚至套件,並利用匯入的方式來互相引用,使得程式碼更有章法,維護更容易!就讓我們先從了解模組與套件開始吧。

p62-1

我們將在下一章更進一步地跟讀者們介紹Python的模組與套件,

p190-1

                                                               驗證成功 --> 回傳處理過的cleaned_data
                                                              /
                                驗證成功 --> 根據clean_方法驗證
                              /                               \
原始data --> 根據欄位進行基本驗證                                驗證失敗 --> 引發例外
            (如CharField)     \
                               \
                                  驗證失敗 --> 引發例外,沒有cleaned_data                                           

p193-1

伺服器: 使用者a發出請求 ---> 伺服器給定回應 but 伺服器根本不知道有使用者a
早餐店: 客人a點餐      ---> 老闆娘做早餐   but 老闆娘根本不知道有客人a這號人物

p193-2

伺服器: 使用者a發出請求 ---> 伺服器給定回應 but 伺服器根本沒有確認發出請求的人
早餐店: 客人a點餐      ---> 老闆娘做早餐   but 老闆娘根本沒去看a,也沒想知道他是誰,雖然老闆娘知道有a這號人物

p194-1

伺服器:
使用者a發出請求1 ---> 回應1 -- > 伺服器要求使用者a提供資料(帳密)來確認是使用者a 
                           |---- 伺服器並不知道兩次請求都是使用者a發出的
使用者a發出請求2 ---> 回應2 -- > 伺服器要求使用者a提供資料(帳密)來確認是使用者a

早餐店:
客人a點了第一次餐 ---> 老闆娘做早餐 -- > 老闆娘要客人報出姓名出生年月日和身分證字號
                           |---- 老闆娘並不知道兩次點餐是同一個客人,他還要客人提供那麼多資料
客人a點了第二次餐 ---> 老闆娘做早餐 -- > 老闆娘要客人報出姓名出生年月日和身分證字號

p216-1

如下範例:

mysite/templates/registration/login.html
...
<form method="post" 
 action="{% url 'django.contrib.auth.views.login' %}?next=/index/">
    {% csrf_token %}
    <table>
        {{ form.as_table }}
    </table>

    <input type="submit" value="login" />
</form>
...

p244-1

RequestContext(
    HttpRequest,
    字典(非共有資訊),
    processors=[處理器1,處理器2,...]
)

p314-1

在這裡我們會分成兩種狀況來討論,一種是多個資料庫之間呈現彼此獨立的狀況(非同步的資料庫),一種是多個資料庫之間要進行同步的狀況,但其實不論哪一種情境,我們都只要記得使用--database的選項就能幫助我們指定資料庫。

這邊的提到了兩種同步,讀者們千萬不要搞混了,我們這節討論的是如何將模型與實際的資料庫"同步",而討論的情境又分為資料庫之間需要"同步"或各自獨立,兩個同步是完全不同的喔!

p314-2

那在實際的程式碼中,要怎麼樣來使用不同的資料庫呢?首先讓我們看到如何使用模型存取指定的資料庫:

>>> from yourapp.models import AModel
>>> a = AModel.objects.using('auth').get(id=1)

使用時加上using("資料庫名稱"),就能夠從指定的資料庫中取出我們的資料。

而想要將資料存進指定的資料庫也很簡單:

>>> a.save(using='auth')

我們在save方法後面加上using的參數就可以了。

以上的操作若無指定using則會當成要使用default資料庫。

 
almost 4 years ago

  • 章節簡介
  • 模組與套件
    • 模組與匯入
    • 名稱空間(namespace)
    • 匯入到頂層空間
    • 不安全的匯入
    • 套件
  • 第三方程式庫
    • PyPI
    • Python的套件管理程式-pip
    • 常用套件簡介
    • 上傳自己的套件
  • 虛擬環境
    • 使用virtualenv
    • 使用virtualenvwrapper
    • 使用虛擬環境來解決問題
  • 本章小結

章節簡介

在這個章節我們會學到:

  • 什麼是模組?什麼是套件?
  • 名稱空間的使用與匯入的手法
  • 如何使用與管理第三方程式庫
  • 使用虛擬環境讓任務能妥善進行

在了解了基本的Python程式設計之後,我們有必要再花一個章節來探討Python中的模組與套件。透過使用現成的模組與套件,我們可以減少大量重複而不必要的開發,同時也能夠使我們的設計更有架構跟章法,本章將會由模組和套件的概念說起,包含匯入、名稱空間的使用等議題,接著會說明如何取得和安裝第三方套件,最後還會介紹如何使用虛擬環境來讓我們的工作更順利。

模組與套件

模組與匯入

一個模組簡單來說就是一個Python檔案,而在模組中(一個Python檔案)會出現的不外乎就是運算、函式與類別了。而我們主要還是想要利用函式跟類別這兩部分。我們簡單看一個匯入模組的例子:

test.py
import sys

print sys.argv

接著我們來執行這個程式:

$ python test.py hello world
['test.py', 'hello', 'world']

沒錯!就是那麼簡單!使用import關鍵字來匯入sys模組(此模組是Python內建的模組,他也是一個Python檔案),並且透過模組名稱與.來取用該模組中的一個資料sys.argv,這其實是一個標準的清單,裡面的第零個元素就是我們的Python檔名,第一個元素是緊跟在test.py後面的第一個命令列參數,第二個元素是第二個命令列參數,以此類推。

sys.argv是個很常用的清單,他能夠幫助我們取得命令列上的參數。

我們也可以幫模組取個別名:

test.py
import sys as s

print s.argv

在這裡我們透過as為sys模組取了一個別名s,往後我們要使用到該模組的東西時,可以改用s來存取。

還有一點值得一提的是,我們可以用一個import敘述匯入多個模組:

import os, sys

名稱空間(namespace)

談到模組就不得不提到名稱空間了,名稱空間顧名思義就是收集名稱的空間,每個名稱空間只會認得自己空間內的名字。這是什麼意思呢?讓我們打開Python shell做點試驗吧:

>>> dir()
['__builtins__', '__doc__', '__name__', '__package__']
>>> import sys
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', 'sys']

dir是一個內建函式,他能夠列出指定名稱空間中所有的名稱,當我們不提供參數的時候,dir會列出shell中最上層名稱空間中的名字。我們發現,當我們匯入sys後,頂層名稱空間中多了'sys'這個name。

>>> dir(sys)
[..., '_mercurial', 'api_version', 'argv', ...]
>>> dir(argv)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'argv' is not defined

如果我們再往sys裡面看,我們可以找到argv這個名字。但是為什麼dir(argv)會出現錯誤呢?這是由於頂層空間中並沒有argv這個名字,所以一旦要使用我們便不認識他了,那該怎麼辦呢?沒關係,起碼shell認識sys這個名字,而sys又認識argv,我們透過.就可以跨過一層又一層,存取到我們要的東西:

>>> dir(sys.argv)

這跟模組有什麼關係呢?原來是當我們匯入模組時,Python會用模組的名稱建立一個名稱空間,該空間收集了模組中函式、類別的名字,我們透過.做階層式的存取,便能取用這些函式和類別為己用了。

匯入到頂層空間

我們在這裡還要介紹一個匯入的手法:

>>> from sys import argv
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', 'argv', 'sys']

from sys import argv的手法會避免建立以sys為名的名稱空間,而直接將argv加入頂層空間,這樣做的好處是,當我們要存取argv的時候,不必再透過sys,我們可以直接取用argv。

我們也可以透過*來一次匯入所有名字到頂層空間:

>>> from sys import *

但要注意一點:import越多東西,Python負擔越大,所以我們應該盡量只import會使用到的範圍。

不安全的匯入

不安全的匯入會造成名稱衝突,我們應當設法避免,讓我們先舉個例子,我們設想有兩個模組M1和M2,他們擁有一個同名的函式叫作func,當我們這樣匯入:

from M1 import *
from M2 import *

我們會發現名稱衝突的問題,究竟我們呼叫func的時候是呼叫哪個模組的呢?答案是會呼叫後匯入模組的func。雖然Python仍能運行,但是這個重複的名稱會讓我們在之後的使用上造成誤會進而產生錯誤。

在此我們也能夠看出名稱空間設計的目的,它能透過階層性的存取(加入了一些足以辨別不同的前綴名稱)來避免名稱衝突,以下列出一些不太安全的匯入方式:

import os.path as path
from os import path
from os.path import *

上述的匯入方式均會使得os這個前綴消失使得名稱衝突的可能性大增。 讀者們可能會發現,使用from import語法幾乎都會遭遇危險性上升的問題,沒錯!這是一個較不推薦的匯入手法,可是在一些較複雜的模組或套件中,我們依然會適度地採用這種匯入以避免過長的階層存取,例如在Django中。

套件

模組就是一個檔案,而套件就是一個目錄!一個擁有著__init__.py檔案的目錄就會被Python視為一個套件,一個套件裡面收集了若干相關的模組或是套件,簡單來說套件就是個模組庫、函式庫。

假設我們自己寫了個套件叫做my_package:

my_package/
├── __init__.py
├── m1.py
├── m2.py
└── sub_package
    ├── __init__.py
    ├── m3.py
    └── m4.py

這個目錄底下要有一個__init__.py的檔案以確保Python認定它為套件,還有幾個模組跟一個子套件,子套件底下也有一個__init__.py和若干模組。

接著我們來試著匯入,我們在my_package底下加入一個檔案main.py

main.py
import m1
import sub_package
import sub_package.m3

當我們要實際匯入一個模組的時候,我們會省略.py這個副檔名,接著我們可以匯入子套件sub_package或是裡面的模組m3,我們發現匯入套件的方式跟匯入模組是一模一樣的。

接著我們執行main.py,我們會發現在my_package底下會出現一個新的檔案m1.pyc,在sub_package底下也會出現m3.pyc,這些pyc檔是Python的位元組碼檔案。當Python第一次匯入模組時,會對該模組進行位元組編譯,產生出一個byte code檔,這些經過編譯的檔案與函式可以運行的更快。

第三方程式庫

除了官方內建的程式庫之外,Python還有大量的第三方套件來支援,這使得我們不用重新發明輪子,即可使用前人的心血結晶來幫助我們完成任務,但是這些套件究竟放在哪裡呢?而我們又該如何來使用他們呢?請看本章的介紹與說明。

PyPI

PyPI是Python Package Index的縮寫,這是Python的第三方套件集中地,目前蒐集了超過五萬個的第三方套件,幾乎所有能想像到的功能,都可以在這找到合適的套件。所以我們有必要介紹一個方便強大的套件管理程式-pip,它能夠利用簡單的指令和步驟幫助我們從PyPI上下載想要的套件並作妥善的安裝,同時也能很方便地管理他們。

Python的套件管理程式-pip

pip是Python的套件管理工具,它集合下載、安裝、升級、管理、移除套件等功能,藉由統一的管理,可以使我們事半功倍,更重要的是,也避免了手動執行上述任務會發生的種種錯誤。

pip的安裝非常簡單,請前往pip的官方文件網站:

在install pip章節可以找到一個名為get-pip.py的檔案,讀者可以將內容複製到編輯器上再另存新檔,接著在終端機中切換到get-pip.py的目錄並執行:

$ python get-pip.py

便可以順利安裝pip囉。

上述方法是作者認為最簡單且最通用的作法,get-pip.py目前支援了CPython直譯器的2.6、2.7、3.1、3.2、3.3和3.4版本,也支援Mac OS、Linux和Windows作業系統。另外,在最新版本的Python安裝包中,pip預設會被安裝到電腦上。

當然對於一些Linux的發行版本而言,使用套件管理程式是更快的方式:

$ sudo apt-get install python-pip (apt工具)
$ sudo yum install python-pip     (yum工具)

更新pip也非常容易:

$ pip install -U pip           (Linux & Mac)
$ python -m pip install -U pip (Windows)

我們可以用list來列出目前有安裝的套件:

$ pip list
...(列出已安裝的套件及其版本)...

假設我們現在想要安裝科學計算套件Numpy,我們可以用install指令來幫助我們:

$ pip install numpy

要解除安裝可以用uninstall:

$ pip uninstall numpy

search指令可以幫助我們在線上找尋相關的套件:

$ pip search numpy
...(列出跟numpy有關的套件)...

help指令可以幫助我們了解pip的使用方式或某個套定指令的使用方式:

$ pip help
...(列出pip的使用方式和指令)...
$ pip help search
...(列出search指令的使用方式)...

常用套件簡介

在這個小節,我們會列出列出一些有名、有用或有趣的第三方套件,而這些套件可以單獨使用或是配合Django來完成更強大的功能,令人高興的是,他們都可以從pip直接安裝:

網站框架

套件名稱 簡述
Django 完整而強大的Web框架,也就是這本書介紹的內容囉
Pyramid 另一個完整強大的web框架
web2py Google app engine預設使用的框架
flask 相較於前三個是輕量的網站框架

圖片處理

套件名稱 簡述
PIL 可對圖片進行縮放、切割、旋轉等各類操作
Pillow 早先大家使用PIL,但年久失修後,出了一個fork的版本,就是Pillow

科學計算

套件名稱 簡述
Numpy 支援非常多的科學計算,包含矩陣運算、線性代數、傅立葉轉換等。可說是集大成者,大多數科學計算套件皆有使用
Matplotlib 可以畫出各種圖型如長條圖、分佈圖、立體圖等
pandas 提供特殊資料結構,具有數據處理和資料分析的功能
scikit-learn 機器學習的套件,包含內建的分群分類計算、回歸、統計等功能

命令列操作及遠端登入

套件名稱 簡述
fabric 可以直接撰寫shell命令,透過fabric執行,也支援遠端登入和自定義shell命令
paramiko 提供遠端登入和部分指令呼叫的功能

測試

套件名稱 簡述
django-nose 更多選擇及設定的Django測試套件,也可產生更詳盡的資訊

網路爬蟲

套件名稱 簡述
Scrapy Python爬蟲框架之一,可以輕易的與Django合作

文件剖析器

套件名稱 簡述
beautifulsoup 可以處理HTML、XML等標記檔案
lxml 可以處理HTML、XML等標記檔,使用xpath選取內容

自然語言處理

套件名稱 簡述
nltk 理論基礎及功能很強大的語言處理套件,但相對低階,需花一些時間熟悉才能流利使用
textblob 較高階的分詞、分句、語言分析工具
jieba 針對中文的分詞、分句、語言分析工具

網路請求用戶端

套件名稱 簡述
requests 可以模擬各種網路請求,如:get、post、put、delete等
pycurl 看名字便知道是在Linux、Unix系統上的命令:curl的python版本

背景程序、定時任務

套件名稱 簡述
celery 可輕易地編寫、呼叫非同步及背景程序,或是執行定時任務(cronjob)

資料庫介接

套件名稱 簡述
mysql-python MySQL資料庫介接套件,Django連接MySQL時預設使用的套件
psycopg2 PostgreSQL資料庫介接套件,Django連接PostgreSQL時預設使用的套件
pymongo MongoDB資料庫介接套件

上傳自己的套件

我們看了、用了那麼多別人寫的套件,自己會不會很想寫一個呢?本章前面的內容已經足夠讓大家了解如何自己撰寫一個套件並在本地端使用,但是我們要如何將這個套件分享給全世界呢?那當然就是要把套件上傳到PyPI囉!(這樣子所有人都可以用pip來下載和安裝你的套件呢)。這個小節就是要教大家怎麼作。

如果打算要上傳自己的套件,那要先去PyPI上註冊取得帳號跟密碼喔。

首先,大家要知道,如果自己的Python專案不只想要能從現行或相對的資料夾中匯入,而想要能夠安裝到Python環境中,一定要在專案的目錄底下撰寫setup.py檔。這邊針對還要上傳到PyPI,來客製化我們的setup.py,我們先來檢視一下一個待上傳的專案結構和和我們要加入的幾個設定檔位置:

MyProject/
├── README
├── pkgA
│   ├── __init__.py
│   ├── modA_1.py
│   └── modA_2.py
├── pkgB
│   ├── __init__.py
│   └── modB.py
├── runner
├── .pypirc  # 加入.pypirc
└── setup.py  # 加入setup.py

我們注意到原本該專案底下有兩個套件pkgApkgB(通常我們一個專案裡面會有數個套件,而我們在上傳專案的同時要將所屬的套件全數上傳),裡面分別有數個模組跟標示其為套件結構的__init__.py。接著我們會有一個說明檔README(用來介紹該專案的結構與使用方式,在此便不細談)和一個執行檔(整個專案的執行腳本碼)runner

接著我們要在原本的專案中加入兩個設定檔,分別是setup.py.pypirc檔,我們會逐一介紹這些檔案該如何撰寫。

首先來看到setup.py:

setup.py
from distutils.core import setup

setup(
    name = 'MyProject',
    packages = ['pkgA', 'pkgB'],
    scripts = ['runner'],
    version = '1.0',
    description = 'My first project',
    author = 'dokelung',
    author_email = 'dokelung@gmail.com',
    keywords = ['Good Project'],
    classifiers = [],
)

我們必須要從distutils.core(distutils是Python內建的套件)中匯入setup函式,此函式會幫助我們進行安裝,讓我們來了解一下此函式中每個參數的意義:

欄位名稱 描述
name 專案名稱(與專案目錄同名)
packages 要安裝的套件名稱
scripts script名稱,通常代表一個執行檔,不一定有
version 版本
description 專案描述
author 作者
author_email 作者信箱
keywords 這個專案的一些關鍵字

這邊稍微解釋一下scripts,這邊要寫在scripts裡的是整個專案的執行檔,他可能用到了專案裡面的套件。為了要將該檔案同時也裝到使用者的系統上,我們需要把他也標註上去,否則後面我們利用pip安裝的時候就只會安裝package而不會安裝執行檔了。而這邊所謂對執行檔的安裝,其實也就是把指定的scripts放到一個可執行路徑裡,例如/usr/bin/中,如此使用者在安裝完後可在任何地方運行該script(其實我們能夠指定安裝的路徑,但如果只給script名稱,那他會被放在預設的位置)。這邊有一點一定要注意,script的名字千萬不要跟他要匯入的pakcage同名了,這會導致一些匯入上的失誤。

至此,已經擁有一個漂亮的安裝檔了(同時也能支援PyPI發佈)。

接著,我們來看看.pypirc檔,唯有建立此設定檔才能讓我們傳東西到PyPI上面:

.pypirc
[distutils]
index-servers =
    pypi 

[pypi] 
repository: https://pypi.python.org/pypi
username: (此處填帳號)
password: (此處填密碼)

如果是Windows的使用者,請打開終端機(命令提示字元),進行環境變數的設定:

set HOME=C:\Users\Owner\

接著將我們在C槽的使用者資料夾(其實就是C:\Users)裡面新增一個子目錄Owner,把我們的.pipyrc複製一份放到該資料夾下,就算設定完成囉。

如果是Linux或是Mac的使用者,也請將.pypirc複製到家目錄底下:

$ cp .pypirc ~/.pypirc

上述設定檔備妥後,就到了最後一個階段,首先註冊:

$ python setup.py register -r pypi

接著上傳:

$ python setup.py sdist upload -r pypi

太好了,你成功的讓全世界都能看到你的作品了,上PyPI看看你的package頁面吧。終於,我們嘗到了甜美的果實,緊接著利用pip下載我們的專案(和裡面的套件)並安裝到自己的電腦看看:

$ pip install MyProject

如果讀者有用pip在自己的電腦上安裝了上傳的專案,該專案底下的套件就可以在本機端任何地方匯入了。

然後你就可以昭告天下,你也是Python的貢獻者了。

虛擬環境

接下來我們要向大家介紹虛擬環境,在正式開始之前呢,讓我們看看兩個情境:

情境一:如果同時有多個Python專案在進行,而每個專案需要的環境都不同(比如說,套件所需的版本不同),總不可能把全部的環境都混在一起(這一定會造成混亂的),當然我們也不可能為了每個專案各弄一台電腦!

情境二:有十個人一起開發同一個大型的專案,要怎麼確保這十個人的開發環境都相同呢? a使用Windows電腦,b使用Linux電腦,c則是使用Mac,這樣怎麼一起合作開發一個專案呢?

其中有一個解決方法就是虛擬環境,Python的虛擬環境可以在同一個系統上建立各自"獨立"且"乾淨"的開發環境,讓每個專案的套件不會相互覆蓋或影響,也確保同一個專案裡的開發人員能以相同的環境進行開發,免去相依性或衝突的問題,而可以更專心在開發或維護的工作上,這些優點讓虛擬環境成為開發者常用的工具。當然,在Python中,虛擬環境也有不同套件可以選擇,後面我們要簡介的兩個虛擬環境套件分別是virtualenvvirtualenvwrapper,接著就來看看該怎麼使用吧!

使用virtualenv

首先,讓我們用pip來安裝virtualenv吧:

$ pip install virtualenv

安裝完成後,要建立新的虛擬環境很簡單,我們先切換目錄到想要建立虛擬環境的位置,接著只需要執行virtualenv 虛擬環境名稱,便會出現類似下面的資訊(假設要建立的虛擬環境取名為test_env):

$ virtualenv test_env
New python executable in test_env/bin/python
Installing setuptools, pip...done.

我們在當前的位置可以看到新建的test_env目錄,其中包含了binlibinclude三個資料夾,這便是我們的虛擬環境了。環境建立好之後,我們得要啟動他,只要執行:

$ source test_env/bin/activate
(test_env)$

如果讀者用的是Windows的話,建立起來的虛擬環境底下應該會包含了Include、Lib、Scripts三個目錄,我們要啟動該環境只要去執行Scripts底下的activate就可以了:test_env\Scripts\activate

就會發現現在的提示字符(prompt)換成了虛擬環境的提示符,該提示符使用了括號括起了啟動的虛擬環境名稱。這時可以來驗證一下,當前的環境是不是有別於原本系統的環境(我們想知道我們是不是已經踏入世界了),執行pip list看一下有什麼套件,是不是和系統層的套件不一樣呢?神奇吧!一個新的、乾淨的、獨立的開發環境就此誕生。

我們可以在這個環境底下使用Python shell或是運行Python腳本碼,甚至在此環境底下開發Django都沒問題,只要記得在工作前進入虛擬環境就可以了。至於退出也很簡單,只要執行deactivate便可關閉虛擬環境,回到系統層,如下所示:

(test_env)$ deactivate
$

如果再也用不到這個虛擬環境,那麼就直接將test_env目錄刪除,便可以移掉這個環境的全部囉!

使用virtualenvwrapper

在Windows底下使用wirtualenvwrapper有點複雜,甚至要動用Windows的powershell,我們不建議讀者這麼作,但如果有興趣的話,可以參考http://docs.python-guide.org/en/latest/dev/virtualenvs/裡面的教學。

接著讓我們來試試另一個套件virtualenvwrapper,一樣用pip安裝:

$ pip install virtualenvwrapper

接著需宣告一個WORKON_HOME在環境變數裡,這樣virtualenvwrapper才能參照這個位置建立虛擬環境,通在會指定在家目錄下:

$ export WORKON_HOME=$HOME/.virtualenvs

接著再執行virutalenvwrapper的執行檔或腳本:

$ source /usr/local/bin/virtualenvwrapper.sh

如果不想每次手動執行的話,可以將加入WORKON_HOME參數執行腳本的動作寫入.bashrc等設定檔裡,每次開啟終端機時便會自動執行。

~/.bashrc
...
export WORKON_HOME=$HOME/.virtualenvs
source /usr/local/bin/virtualenvwrapper.sh

接著就可以來新建虛擬環境了,執行mkvirtualenv 虛擬環境名稱後便會新建並直接啟動這個虛擬環境:

$ mkvirtualenv test_env
New python executable in test_env/bin/python
Installing setuptools, pip...done.
(test_env)$

但如果在下次開啟新的終端機要怎麼啟動呢? 就靠workon指令,而且workon指令有自動補齊功能會列出目前所有的虛擬環境,簡單操作如下:

$ workon (然後按一下TAB)
test_env    test_env2
$ workon test_env
(test_env)$

而退出則和virtualenv相同,執行deactivate即可:

(test_env)$ deactivate
$

如果想要刪掉現有的環境,則執行rmvirtualenv 虛擬環境名稱便可以完成:

$ rmvirtualenv test_env
Removing test_env...
$ workon (然後按一下TAB)
test_env2

virtualenv另外有提供列出虛擬環境(lsvirtualenv)、複製虛擬環境(cpvirtualenv)、快速切換至虛擬環境或套件的目錄(cdvirtualenv)、對所有環境操作(allvirtualenv)等附加的功能,在此不再贅述,如有需要請參考線上文件。

使用虛擬環境來解決問題

我們回到剛剛的兩個情境,虛擬環境究竟是怎麼幫我們解決問題的呢?

先看看情境一,我們不需要為每個專案都準備一個系統,但我們為每個專案都準備一個虛擬環境,當我今天要開發專案A的時候,我們切進專案A的虛擬環境中,不論是安裝套件或是運行代碼都在虛擬環境A中完成,當結束工作後,退出虛擬環境。而開發另外一個專案的時候也是如此,簡單來說,我現在要開發哪個專案、需要哪個環境,我就進入相應的虛擬環境中。

那情境二的解決也非常簡單,我們可以將建立好的虛擬環境,統一複製配給參與專案的每一個人,方法有:一、打包處擬環境的目錄,或是二、通常會使用pip freeze指令將套件列表輸出在一個requirements.txt檔案,其他人再依檔案內容安裝套件。另外有兩點要注意,第一點是每個人的電腦都需要安裝過虛擬環境套件。第二點是當某個人的虛擬環境有所變動時(比如說新安裝了某套件或是套件升級等),所有人的虛擬環境也需要做相應的更新。

最後,如果可以的話,我們希望讀者在演示範例或自行開發的時候能夠使用虛擬環境,這樣好習慣的養成對未來會大有幫助的。

當然,懶得這樣做或是為了學習上的方便,我們不會要求一定要使用虛擬環境,本書中的所有內容是否使用虛擬環境都是適用的。

本章小結

本章向讀者介紹了模組和套件的概念,透過這樣對程式碼作分層式的管理,再加上名稱空間的輔助,可以使我們的程式管理和運用更清晰明瞭。同時我們也介紹了PyPI和pip工具,往後讀者們可以依據需要去下載相應的套件(我們要學習的Django也可以使用pip下載喔),讓我們的網站或是開發更加強大。最後,我們也介紹了虛擬環境的概念,希望能夠幫助讀者擁有一個好的開發環境。至此,我們對於Python的介紹也告一段落了,準備了那麼久的時間,是時候讓我們的主人公-Django登場了,請看下一章!

 
about 4 years ago

  • 前言
    • 這篇心得講些什麼
    • 誰適合看這篇心得
  • 利用Github管理你的專案
    • 開啟一個新專案
    • 觀察repository狀態
    • 將檔案on stage
    • 提交檔案
    • 將repository上傳到遠端
    • 建立可下載的專案包
    • 更新發佈
    • 複習
  • 上傳套件到PyPI
    • 設定setup.py
    • 設定setup.cfg
    • 建立.pypirc設定檔
    • 上傳到PyPI
    • 利用pip下載及安裝
    • 複習
  • 結語
  • 參考資料

前言

說實在的從碩班開始就累積了一些專案,有些是為了研究寫的,有些是個人興趣的小作品,不過就一直放在Lab的server上面覺得有點可惜,前幾天想要把他們整理整理在對外發佈,但這不像是寫blog,打打字放放圖就好了,source code的下載,相關的說明文件,日後版本的更新都是一門學問,只好硬著頭皮好好的搞懂了一下Github和PyPI,本篇算是一個心得隨筆,記錄著這三天以來累積的心得。

這篇心得講些什麼

  1. 簡單的單人git技術(只是用到,不是教學)
  2. 如何使用Github管理你的專案
  3. 發佈一個可供人下載的專案包
  4. Python package的打包
  5. 上傳到PyPI與使用pip下載

誰適合看這篇心得

  1. 對git略懂略懂的人
  2. 專案用Python寫的朋友(不然後半段介紹發佈到PyPI的部份就派不上用場了XD)
  3. 不過還是私心希望越多人看越好。

廢話不多說,馬上來介紹流程。

利用Github管理你的專案

在開始之前,我預設各位都有個想要管理的專案,我給他一個代號:MyProject
以下是他的目錄結構:

MyProject-|--runner
          |--pkgA---|--__init__.py
          |         |--modA_1.py
          |         |--modA_2.py
          |--pkgB---|--__init__.py
                    |--modB.py

然後讀者們已經有了一個Github的帳號了,這邊我們用dokelung這個名稱。

開啟一個新專案

開啟的方法有兩種,一種是使用git init,另一種是直接在Github網站上面點選New repository,我們採用後者:

  1. 輸入repository的名稱: MyProject
  2. 輸入repository的敘述: an example
  3. 勾選initialize this repository with a README
  4. 按下Create repository

Github的repository其實有public和private的差別,但是private需要錢錢,所以我們不用。
但由於public的關係,我們放上去的專案都會變成open source的,這要特別注意。

repository在Github中就是專案的意思。

接著我們來看看MyProject的repository頁面,這個頁面的URL會長這樣:

https://github.com/dokelung/MyProject

我們可以看到在頁面中還有一個HTTPS clone URL,會以repository的名稱來命名:

https://github.com/dokelung/MyProject.git

這是個很重要的位址,只要有他,我們就能隨時透過網路來取得這個專案。

接著我們在本地端,使用git來複製(clone)這個專案:

$ git clone https://github.com/dokelung/MyProject.git

你會發現在當前目錄下多了一個以MyProject為名的目錄,還有一個說明檔README.md
這個目錄不單單是個普通的目錄,還是一個已經使用git來管理的目錄。
不信可以用git status指令來確認:

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

nothing to commit, working directory clean

我們稍微來解釋一下上面的動作:

  1. git status可以幫助我們知道目前專案中各個檔案更動與被追蹤的情形
  2. On branch master告訴我們現在的工作位於master這條分支上,
  3. nothing to commit, working directory clean告訴我們目前這個專案目錄是乾淨的,相較於上ㄧ次的commit是沒有更動的。

觀察repository狀態

接著我們將專案目錄下的所有檔案跟目錄都放到MyProject底下,現在在這個repository目錄底下的結構:

MyProject-|--REDAME.md
          |--runner
          |--pkgA---|--__init__.py
          |         |--modA_1.py
          |         |--modA_2.py
          |--pkgB---|--__init__.py
                    |--modB.py

然後運行git status:

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    這邊應該會列出所有你放進來的東西(除了README.md)

nothing added to commit but untracked files present (use "git add" to track)

事實上,每當repository中有檔案更動(包含新增、移入、刪除或修改等),git status總是會指出更動的部份。
目前的更動是,git發現了一群沒看過的檔案。

將檔案on stage

若我們想要讓這些檔案被真正的納入repository中(現在只是在該目錄下,實際上還沒加入repository),我們必須使用git add指令:

$ git add runner
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   runner

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    除了README.md和runner之外的所有東西

git add會將指定的檔案狀態成為stage,講簡單一點就是ready to commit的意思!
反之,unstage的檔案將不會被commit:有兩種狀況會產生unstage檔案:

  1. 未被追蹤的檔案
  2. 被追蹤但是被修改過而且尚未被add的檔案

關於第二點,我們可以藉由修改一個原本onstage的檔案再用git add來觀察。
總之,利用git status可看的清清楚楚所有的狀態。
我們只要記住,所有需要被提交的檔案都要確定是stage狀態的。

大家可以將專案裡面的檔案都git add進去囉。

有個情況比較特別,當某個on stage的檔案被刪除後,若要加入這項刪除的資訊,請用git rm

提交檔案

使用git commit做提交。

$ git commit -m "first commit"

"first commit"是該次提交的註解。
所謂提交,就好像是一次存檔,我們可以任意的更動專案,並使用git add讓檔案on stage,調整出一個要存檔的狀態(stage狀態的檔案們就是要被存檔的檔案),然後用git commit來進行真正的存檔。

本篇不是git教學,就不贅述了XD

將repository上傳到遠端

光在本地端存檔還不夠,我們既然要用Github管理我們的專案,自然要將檔案提交到遠端囉,使用git push就好:

$ git push

由於我們的repository是從Github建的,所以預設的遠端就是Github囉。另外,如果日後在遠端有任何修改,先用git pull將遠端merge回來,在git push推出去。

到這邊,我們已經能夠在遠端好好管理我們的專案了,很棒吧。

建立可下載的專案包

首先,我們要對重要的commit(重要的存檔,也就是你想要發佈的那次存檔)製作一個標籤,使用git tag即可:

$ git tag v1.0 -m "first realease"

通常tag的名稱會是一個版本號,要不要使用v來標示版本都可以,-m後面跟著的一樣是該標籤之註解。
接著,我們將標籤推到遠端:

$ git push --tags origin master

如此一來,我們將可以獲得可供下載的專案包:

https://github.com/dokelung/MyProject/tarball/v1.0

或是在Github的頁面上也有相應的zip檔或tar.gz檔下載URL。

更新發佈

新手可能會有一個問題,當我們需要更新某個已發佈的專案包而不是建立一個新版本時,該怎麼做呢?
通常會想到的辦法是移除舊有的版本發佈,再重新建立一個,這讓過程很麻煩,因為你需要:

  1. 刪除本地端tag
  2. 刪除遠端tag
  3. 建立本地端tag
  4. 建立遠端tag

上述動作是針對同一個版本的標籤,因為不同版本的標籤我們直接新建立就好,沒有刪除的問題。

但是只要用-f選項就可以強制更新囉(下面假設v1.0標籤已經存在):

git tag v1.0 -f -m "update release"
git push -f --tags origin master

複習

我們重點整理複習一下:

  1. 整備本地專案
  2. 上Github開新repository
  3. 利用HTTPS clone URL和git clone將repository拉到本地
  4. 將本地專案放入git repository在本地的目錄
  5. 使用git add將所有檔案加入追蹤
  6. 使用git commit將stage上的檔案進行提交
  7. 使用git push將repository提交到遠端

若該次commit想要發佈,則:

  1. 使用git tag建立版本標籤
  2. 使用git push --tags提交標籤至遠端

若專案有檔案被修改,重複步驟5-7。
若同一版本的專案要重新發佈,使用-f選項。

上傳套件到PyPI

緊接著是第二個部分,若我們的專案是python package或是python script,如何利用PyPI將之散播到全世界。

設定setup.py

相信大家都知道,如果自己的python專案想要讓大家能夠安裝到python環境中,一定要撰寫setup.py,這邊針對還要上傳到PyPI,來客製化我們的setup.py,我們先來檢視一下目前的結構和等等我們要加入的幾個設定檔位置:

MyProject-|--REDAME.md
          |--setup.py  <-- 加入setup.py
          |--setup.cfg <-- 加入setup.cfg
          |--.pypirc   <-- 加入.pypirc(注意,此檔不要加入stage提交出去!)
          |--runner
          |--pkgA---|--__init__.py
          |         |--modA_1.py
          |         |--modA_2.py
          |--pkgB---|--__init__.py
                    |--modB.py

然後:

setup.py
from distutils.core import setup

setup(
    name = 'MyProject',
    packages = ['pkgA', 'pkgB'],
    scripts = ['runner'],
    version = '1.0',
    description = 'My first project',
    author = 'dokelung',
    author_email = 'dokelung@gmail.com',
    url = 'https://github.com/dokelung/MyProject',
    download_url = 'https://github.com/dokelung/MyProject/tarball/v1.0',
    keywords = ['Good Project'],
    classifiers = [],
)

以下針對各欄位進行說明:

欄位名稱 描述
name 專案名稱
packages 要安裝的套件名稱
scripts script名稱,通常代表一個執行檔,不一定有
version 版本
description 專案描述
author 作者
author_email 作者信箱
url 專案頁面,這邊我們填上Github的專案位址
download_url 下載路徑,還記得我們剛剛傳上標籤後得到的URL嗎?就是他
keywords 這個專案的一些關鍵字

這邊稍微解釋一下scripts,這邊要寫在scripts裡的可能是整個專案的執行檔,用到了專案裡面的套件,為了要將該檔案同時也裝到使用者的系統上,我們需要把他也標註上去,否則後面我們利用pip安裝的時候就只會安裝package而不會安裝執行檔了。
而這邊所謂的安裝,其實也就是把指定的scripts放到一個可執行路徑裡,例如/usr/bin/中,如此使用者在安裝完後可在任何地方運行該script。

有兩點要說明一下,第一個是我們能夠指定安裝的路徑,但如果只給script名稱,那他會被放在預設的位置。
第二點是,一定要注意script的名字不要跟他要匯入的pakcage同名了,這會導致一些import上的失誤。

至此,已經擁有一個漂亮的安裝檔了(同時也能支援PyPI發佈)。

設定setup.cfg

因為我們的README.md是個markdown file,所以我們得新增一個setup.cfg的檔案:

[metadata]
description-file = README.md

建立.pypirc設定檔

唯有建立此設定檔才能讓我們傳東西到PyPI上面:

.pypirc
[distutils]
index-servers =
    pypi 

[pypi] 
repository: https://pypi.python.org/pypi
username: dokelung
password: 寫自己的吧,我才不會告訴你我的咧!

再次提醒大家,既然裡面有密碼明碼,大家千萬不要把它給commit出去。

上傳到PyPI

終於到了最後一個階段,首先註冊:

$ python setup.py register -r pypi

接著上傳:

$ python setup.py sdist upload -r pypi

太好了,你成功的讓全世界都能看到你的作品了,上PyPI看看你的package頁面吧。

利用pip下載及安裝

這應該是過程中最讓人開心的部分了,我們可以嘗到甜美的果實,利用

$ pip install MyProject

我們可以在任何有裝pip的電腦中下載及安裝我們的專案,很酷吧!
而如果日後我們的專案有任何修改,除了上一大節提到的重新提交或發佈外,如果要在PyPI上面進行更新,一定要重新上傳過喔。

複習

又到了複習的時間囉,我們來看看後半段怎麼做的:

  1. 撰寫setup.py
  2. 撰寫setup.cfg如果README是markdown file
  3. 撰寫.pypirc
  4. python setup.py register註冊
  5. python setup.py sdist upload上傳

若有任何更動,請先整理Github再修改setup.py的內容(如更新版本號或是加入新套件或更正URL)重複4-5。

結語

原本想要寫個簡明的流程,沒想到比我想的篇幅還要大,我想我應該有點完美主義者的傾向XDD
再者,由於這篇心得不是git的教學,所以有許多部分沒有細講,建議大家去瀏覽一下相關的教學,有個滿推薦的blog大家可以在參考資料的地方看到。

參考資料

推薦的git教學

 
about 4 years ago

這邊是我作的Python教學投影片(中文),已經是2012的事情了,且許多缺漏的章節未補,望日後補齊。
以下提供pdf檔的連結:

M2 - Python 簡介
M3 - Python 程設入門
M4 - 資料與數值
M7 - 控制流程
M8 - 例外
M9 - 函式
M10 - 基本檔案IO
M11 - 模組與套件

 
about 4 years ago

在本篇中要講述一些有關於template(模版)的進階技巧。學習了相關的知識之後,讀者們可以寫出架構更好的頁面,同時也會發現,我們能省下大量的時間。


  • 主題1-重複利用模板
    • 匯入模板
    • 模板繼承
      • 模板繼承的步驟
      • 模板繼承的策略
      • 模板繼承的原則
      • 利用模板繼承更新我們的網站
  • 主題2-RequestContext與Context處理器
    • Context處理器
    • 使用render_to_response
    • 使用render
    • Django預設的處理器
  • 主題3-自定義過濾器
    • 前置作業
    • 過濾器函數
    • 註冊過濾器
    • 載入過濾器
    • 使用裝飾器

主題1-重複利用模版

當我們花了時間撰寫了一個模版後(尤其是寫了一堆html標籤累的要死),卻無法重複利用他,這可是會讓人心情沮喪的。好在模板的設計本身就是一個可再利用的資源,透過使用同一個template,可以讓相似度高的頁面(視圖)共用同一個模板。但是我們難道沒辦法利用已經寫好的template作為新template的一部分嗎?

如果你還不能明白我的意思,我就講的再直接一點:封裝、匯入、繼承...
這些在程式設計上耳熟能詳的代名詞有沒有讓你有一些想法呢?

其實撰寫一個模版我們就運用了封裝的技巧,透過參數化的方式,使得普通的html頁面變成能接受context填寫而重複利用的樣板。

那匯入或是繼承呢?

匯入模版

使用{% include %}標籤就可以將模板匯入,以餐廳王的首頁來說:

mysite/templates/index.html
<html>
    <head>
        <title>Index</title>
        <meta charset="utf-8">
    </head>
    <body>
        <h2>歡迎來到餐廳王</h2> <p><a href="/accounts/register/">註冊</a></p>
        {% if request.user.is_authenticated %}
            <p>
                {{request.user}} 您已經登入囉~
                <a href="/accounts/logout/">登出</a>
            </p>
            <a href="/restaurants_list/">餐廳列表</a>
        {% else %}
            <p>您尚未登入喔~<a href="/accounts/login/">登入</a></p>
        {% endif %}
    </body>
</html>

我們可以將之拆解為:

index.html
<html>
    <head>
        <title>Index</title>
        <meta charset="utf-8">
    </head>
    <body>
        {% include 'body.html' %}
    </body>
</html>

body.html
<h2>歡迎來到餐廳王</h2> <p><a href="/accounts/register/">註冊</a></p>
    {% if request.user.is_authenticated %}
        <p>
            {{request.user}} 您已經登入囉~
            <a href="/accounts/logout/">登出</a>
        </p>
        <a href="/restaurants_list/">餐廳列表</a>
    {% else %}
        <p>您尚未登入喔~<a href="/accounts/login/">登入</a></p>
    {% endif %}

當然我們也可以考慮反向的匯入,也就是說,在index.html中保留<body>的部份而include<head>等其他內容。
這完全看哪一部分的html代碼是能重複利用的。

我們將{% include %}標籤的完整用法整理如下:

{% include '模版名稱(路徑)' %}

模版名稱(路徑)是一個字串,所以記得用單引號或是雙引號括起來,這個名稱可以是一個模板目錄下的template file,也可以是一個在模板目錄下包含了其子目錄在內的路徑。

如果模板名稱(路徑)有誤,在除錯模式下會顯示TemplateDoesNotExist錯誤,在一般模式下則會略過。

模板繼承

不過使用者深入去思考後會發現,{% include %}標籤其實並不是那麼好用,怎麼說呢?

我們舉個例子,假設我們有一個要套用到所有頁面的樣式如下:

<!doctype html>
<html>
    <head>
        <title>(頁首標題)</title>
    </head>
    <body>
        網頁選單
        <h1>(頁面標題)</h1>
        <h1>說明</h1>
        <p>(內容)</p>
        頁尾
    </body>
</html>

我們可以將整個頁面樣式區分為所有頁面共有的部分和自定義的部份:

共有部分:<html><head><body>等標籤,網頁選單、頁尾等
自定義部分(以小括號括起來的部分):頁首標題、頁面標題、內容

若使用{% include %}我們必須選定上述兩部分其中一個當做基礎,再包含進其他的部份。

舉例來說,我們可以產生以下幾個html(為了讓大家了解,我就暴力法拆解給大家看吧):

A.html
<!doctype html>
<html>
    <head>
B.html
</head>
<body>
    網頁選單
C.html
    <h1>說明</h1>
D.html
頁尾
    </body>
</html>
final.html
{% include 'A.html' %}
        <title>(頁首標題)</title>
{% include 'B.html' %}
        <h1>(頁面標題)</h1>
{% include 'C.html' %}
        <p>(內容)</p>
{% include 'D.html' %}

我們發現產生一個頁面需要花上5個html檔,且檔案零碎。

這使得include的動作異常複雜,當共有部分和自定義部分交錯的越嚴重時,這會讓需要include的數量大幅提昇(這還不打緊,反而是撰寫每一個被include進來的頁面才痛苦,不但要寫超多個html檔,還要忍受這些零碎的痛苦。)

問題就在於這些被重複利用的部份必須完全被切割!
所以我們比較鼓勵讀者使用Django的模版繼承。

怎麼做呢,我們直接來看一個例子,我們首先要制定一個基礎的模版,他應該要包含所有共同部份:

base.html
<!doctype html>
<html>
    <head>
        <title>{% block title %}{% endblock %}</title>
    </head>
    <body>
        網頁選單
        <h1>{% block pagetitle %}{% endblock %}</h1>
        <h1>說明</h1>
        <p>{% block content %}{% endblock %}</p>
        頁尾
    </body>
</html>

記得,可以在每個需要自定義的地方,使用模板區塊。
一個模板區塊以{% block BLOCKNAME %}為開頭,以{% endblock %}為結尾。可以填入內容也可以留白,這個區塊的內容會讓繼承此基礎模板的子模板覆寫,這與物件導向中關於類別的繼承與覆寫很類似。

接著使用{% extends %}標籤讓子模板去繼承基礎模板:

final.html
{% extends 'base.html' %}
{% block title %}(頁首標題){% endblock %}
{% block pagetitle %}(頁面標題){% endblock %}
{% block content %}(內容){% endblock %}

{% extends TEMPLATENAME %}標籤的參數TEMPLATENAME可以是一個字面的字串,也可以是一個變量。當使用變量的時候,可以動態地更換繼承的模板達到一些特殊的效果。當然,不論用字串或是變量,該參數所指名的模板應該要位於任何我們能夠存取到的模板目錄中。

若我們有重新定義某區塊的內容,則基礎模板中該區塊的內容會被我們覆寫,否則使用基礎模板中的內容。

模板繼承的步驟

我們會發現,經過繼承設計的模板,更簡潔,更完整。我們在下面整理一下關於模板設計的幾個要點:

1. 定義基礎模板,並在其中使用{% block BLOCKNAME %}和{% endblock %}來定義模板區塊。
2. 利用{% extends BASE_TEMPLATE_NAME %}讓子模板繼承基礎模板。
3. 使用模板區塊來覆寫(填寫)基礎模板的內容,未被覆寫的區塊將會採用原基礎模板的內容。

模板繼承的策略

至於繼承的策略我們大致在這裡分析一下,常見的設計手法是一種三層式的設計:

第一層: 撰寫一個全站的base模板,應該包含全站等級的頁首/頁尾和選單。
第二層: 針對幾個特定的功能集合撰寫不同的區域模板(繼承base模板),應該包含特定功能的選單及功能。
第三層: 每一個特定功能的模板實作,應該要根據分類繼承區域模板。

模板繼承的原則

那設計的原則也有以下兩點:

  1. 基礎模板中的{% block %}區塊越多越好,雖然並不是每個區塊都要求繼承他的子模板來覆寫,但是預留更多可變得位置,會使得模板設計上更靈活。
  2. 如果發現有大量重複的代碼在存在於各個模板中,這些應該考慮抽取出來放置在基礎模板。這與OOP的精神相同。

接下來我們便使用前面介紹的模板繼承概念來整頓一下我們的餐廳王網站吧!

利用模板繼承更新我們的網站

由於我們目前的網站結構還算簡單,我們只採用兩層的繼承架構。
那第一步便是為整個網站製作一個基礎模板base.html:

mysite/templates/base.html
<!doctype html>
<html>
    <head>
        <title>{% block title %}{% endblock %}</title>
        <meta charset="utf-8">
    </head>
    <body>
        <nav>
            | <a href="/restaurants_list/">餐廳列表</a> |
            {% if request.user.is_authenticated %}
                {{ request.user }}
                <a href="/accounts/logout/">登出</a>
            {% else %}
                <a href="/accounts/login/">登入</a> /
                <a href="/accounts/register/">註冊</a>
            {% endif %}
        </nav>
        <h2>{% block h2 %}{% endblock %}</h2>
        {% block content %}{% endblock %}
    </body>
</html>

這個基礎模板包含了頭尾的一些基礎標籤以及一個全站通用的nav bar(網站選單),和幾個待填空的模板區塊。
這裡總共用了四個模板區塊,有在瀏覽器的title block,各頁面的標題h2 block和內容content block。

在這裡我們也利用{% request.user %}對已登入的使用者顯示他們的username。

接著,利用繼承base.html,我們來整頓其他的頁面,我們以首頁做個示範,至於其他的頁面,就留待讀者自行更動調整:

mysite/templates/index.html
{% extends 'base.html' %}

{% block title %} 首頁 {% endblock%}
{% block h2 %} 餐廳王 {% endblock %}

{% block content %}
    <p>歡迎來到餐廳王</p>
{% endblock %}

是不是清爽許多了呢?
我們會發現,透過全站統一繼承一個基礎模板,不但使的各頁面的代碼更簡短清晰,更可以簡單的共用統一的風格跟選單。
讀者們還會發現,當下一次要新增一個模板時,會輕鬆不少。

主題2-RequestContext與Context處理器

還記得在第三篇模板中我們曾經詳細地介紹過使用模板的方式嗎?
那該篇筆記中,我們最後得出來的結論是:使用render_to_response函式來完成創建模板到填寫模板到回應的動作。
如果讀者們對於基礎的觀念已經有點淡忘了,可以回到該篇筆記查查,因為我們要利用最基礎的手法來闡述RequestContext的用途。

最原始的模板處理步驟,我們首先得用django.template.Template類別來創造一個模板。
接著使用djanog.template.Context來設置一些待填的變量,Context是一個純然由使用者打造的類字典物件,缺點很明顯,每一次要使用時都必須要手動設置。

解決這個問題就得靠django.template.RequestContext這個物件,他與Context類似,但比Context多了兩項資訊,其一是HttpRequest,其二是Context處理器,這讓我們得以使用一個已經設置好若干訊息(透過HttpRequest和Context處理器)的Context來填充模板,而不需要每次都手動建造(即便locals函數已經能夠幫我們省下很多時間了,但仍然不足)。

我們來舉個例子:

from django.template import loader, Context

def view1(request):
    ...
    t = loader.get_template('template1.html')
    c = Context({
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR'],
        'message': 'View 1'
    })
    return t.render(c)

def view2(request):
    ...
    t = loader.get_template('template2.html')
    c = Context({
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR'],
        'message': 'View 2'
    })
    return t.render(c)

這兩個視圖函數都會使用Context來填寫各自的模版,我們發現,'user'和'ip_address'兩項資訊根本一模一樣,卻必須要在兩個view function裡面都手動設置,這其實非常累贅。

Context處理器

一個解決的辦法便是使用Context processor(Context處理器),Context處理器其實只是一個function,這個函數以HttpRequest物件為參數,並會回傳一個包含了Context共有資訊的字典。我們看看上面的範例要怎麼用Context處理器加以改良:

from django.template import loader, RequestContext

def context_proc(request):
    return {
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR']
    }

def view1(request):
    ...
    t = loader.get_template('template1.html')
    rc = RequestContext(request, {'message': 'View 1'}, processors=[context_proc])
    return t.render(rc)

def view2(request):
    ...
    t = loader.get_template('template2.html')
    rc = RequestContext(request, {'message': 'View 2'}, processors=[context_proc])
    return t.render(c)

我們透過context_proc將view1和view2中Context共有的部分:'user'和'ip_address'提取出來,並且在兩個視圖函式中改用RequestContext,如此便可以省去不少需要手動設置的功夫了!

同時,我們也觀察到RequestContext的結構:

RequestContext(HttpRequest, 字典(非共有資訊), processors=[處理器1,處理器2,...])

第一個參數要給定一個HttpRequest物件,第二個參數是非共有資訊集合成的字典,另外還有一個名為processors的參數,這是一個處理器清單(list),裡面擺置用來處理共有資訊的處理器們。

使用render_to_response

但光是這樣我們還不滿足,我們之前的快捷手段:render_to_response難道派不上用場了嗎?當然不!
只需要多給予一個context_instance的參數就可以囉:

from django.template import loader, RequestContext
from django.shortcuts import render_to_response

def context_proc(request):
    return {
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR']
    }

def view1(request):
    ...
    return render_to_response(
        'template1.html',
        {'message': 'View 1'}, 
        context_instance = RequestContext(request, processors=[context_proc])
    )

def view2(request):
    ...
    return render_to_response(
        'template2.html',
        {'message': 'View 2'}, 
        context_instance = RequestContext(request, processors=[context_proc])
    )

不過看起來,代碼並沒有減少多少,這是因為我們將原本設置Context資訊的功夫花在設置Context處理器了。
難道Context處理器並非一個好的選擇?
不,這依然是個優秀的處理手段,因為Django允許我們在settings.py中設置processors,這樣就不需要每次都要填寫processrs了,很棒吧。

實際的作法非常簡單,在settings.py中設置TEMPLATE_CONTEXT_PROCESSORS元組,將想要加入的處理器路徑加入即可,這會導致任何一個RequestContext物件都會含有該些處理器所提供的資訊。

比如說處理器context_proc位於mysite/mysite/views.py中,我們只需:

mysite/mysite/settings.py
...
from django.conf import global_settings
...
TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + 
                             ("mysite.views.context_proc",)

就可以省略RequestContext中處理器的設置,視圖函數可簡化為:

from django.template import loader, RequestContext
from django.shortcuts import render_to_response

def context_proc(request):
    return {
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR']
    }

def view1(request):
    ...
    return render_to_response(
        'template1.html',
        {'message': 'View 1'}, 
        context_instance = RequestContext(request)
    )

def view2(request):
    ...
    return render_to_response(
        'template2.html',
        {'message': 'View 2'}, 
        context_instance = RequestContext(request)
    )

這邊稍微來解釋一下settings.py中的作法,這邊給讀者一個概念,並非所有的設定值都會出現在專案的settings.py中,很多的設定值會被寫在django裡面的global_settings.py,只要使用者沒有自行在自己的settings.py中覆寫這些參數,Django會使用global_settings.py的預設值。

我們現在想要設置TEMPLATE_CONTEXT_PROCESSORS,這是一個元組,在global_settings.py中早就有若干Context處理器被設置在其中,若我們貿然將TEMPLATE_CONTEXT_PROCESSORS覆寫掉可能會造成不少錯誤,為了避免這一點,我們得先將原始的處理器元組匯入,再串接上新的處理器。

還有一個問題是,如果不同的處理器提供了相同名稱的變量怎麼辦?這個時候將會依照處理器被看到的順序設置,後看到的處理器會覆蓋掉前面處理器中相同名稱的變量。

使用render

好了,做到這裡,讀者們可能會覺得這個主題已經告一段落了,但是,難道沒有再更精簡的手法嗎?
其實有的,有個函數比起render_to_response更神奇,那就是render函式:

from django.shortcuts import render

def context_proc(request):
    return {
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR']
    }

def view1(request):
    ...
    return render(request, 'template1.html', {'message': 'View 1'})

def view2(request):
    ...
    return render(request, 'template2.html', {'message': 'View 2'})

render函式比需要由django.shortcuts中匯入,他跟render_to_response的功能幾乎一樣,除了他強制使用RequestContext作為Context之外,所以我們得出一個結論:

需要用到處理器及HttpRequest資訊的時候用render
否則用render_to_response

render的使用方式如下:

render(HttpRequest, TEMPLATENAME, DICTIONARY)

第一個參數是HttpRequest物件,第二個參數是模板名稱,最後一個是非共有資訊的字典。
讀者若有感悟,應回到前面的筆記,將有使用到RequestContext的地方都改為用render

Django預設的處理器

不過讀者也許會很好奇,在global_settings.py中究竟有哪些預設的處理器,又各提供了哪些有用的資訊(變量)給模板呢?
打開global_settings.py就能夠看到TEMPLATE_CONTEXT_PROCESSORS的預設值:

(
    "django.contrib.auth.context_processors.auth",
    "django.core.context_processors.debug",
    "django.core.context_processors.i18n",
    "django.core.context_processors.media",
    "django.core.context_processors.static",
    "django.core.context_processors.tz",
    "django.contrib.messages.context_processors.messages"
)

有興趣的讀者可以前往Django官網進行詳細的了解。

在結束這個主題之前,我們回憶一下在權限筆記中,我們曾經用過RequestContext來提供perms變量:

mysite/restaurants/restaurants_list.html
...
        {% if perms.restaurants.can_comment %}
            <th>評價</th>
        {% endif %}
...

這就是RequestContext的威力。

主題三-自定義過濾器

自己創造一個過濾器是再簡單也不過的事情了,只要兩個步驟:

1. 寫一個過濾器函數
2. 註冊該函數
3. 載入過濾器

除此之外當然還有一些前置作業要做,就讓我們來一一說明吧!

前置作業

首先我們在restaurants這個APP中新增一個templatetags目錄,這個目錄將會放置我們自定義的過濾器模組或標籤模組,為了使templatetags成為一個python package,大家千萬別忘了建立一個__init__.py

接著我們在該目錄下建立一個python module: myfilters.py,顧名思義就是用來撰寫我們的過濾器函數的地方啦。我們會發現現在的結構為:

mysite-|--manage.py
       |--templates(子結構省略)
       |--mysite(子結構省略)
       |--restaurants--|--__init__.py
                       |--admin.py
                       |--models.py
                       |--tests.py
                       |--views.py
                       |--forms.py
                       |--templatetags--|--__init__.py
                                        |--myfilters.py

過濾器函數

接著便讓我們來撰寫過濾器函數。
讀者要有一個概念就是,過濾器本身是一個函數,我們來看一個簡單的例子,還記得我們的menu模板嗎:

mysite/restaurants/templates/menu.html
<tr>
    <td> {{ food.name }} </td>
    <td> {{ food.price }} </td>
    <td> {{ food.comment }} </td>
    <td> {% if food.is_spicy %} 辣 {% else %} 不辣 {% endif %} </td>
</tr>

當初我們為了辣與不辣的顯示採用了if/else標籤,我們現在試著換一個方式,用過濾器來幫我們解決,我們想要利用以下的方式來完成一樣的功能:

mysite/restaurants/templates/menu.html
<tr>
    <td> {{ food.name }} </td>
    <td> {{ food.price }} </td>
    <td> {{ food.comment }} </td>
    <td> {{ food.is_spicy|yes_no:"辣/不辣" }} </td>
</tr>

這裡整理一下這個過濾器的要素:

  1. 名字叫做yes_no
  2. 接受一個布林值的主要參數
  3. 接受一個額外的字串參數Y/N,如果主要參數為真則輸出Y,否則輸出N

要怎麼做呢?
首先打開myfilters.py:

mysite/restaurants/templatetags/myfilters.py
def yes_no(bool_value, show_str):
    if bool_value:
        return show_str.partition('/')[0]
    else:
        return show_str.partition('/')[1] 

我們寫了一個名叫yes_no的函數,並且需要兩個參數,在這裡,函數的名字不需要跟我們自定義的過濾器名稱一樣,但是選擇相同名稱通常是比較好的作法,bool_value是過濾器的第一個參數,他負責接收模板中pipe符號(|)左邊的值;而show_str是第二個參數,會用來接收過濾器中的額外參數,以下是簡單的對應示意:

{{ food.is_spicy|yes_no:"辣/不辣" }}
   ------------- ------ --------
          2.        1.     3.    
def yes_no(bool_value, show_str):
    ------ ----------  --------
       1.       2.        3.

值得注意的是,過濾器函數我們總是要保證他是對的,也就是說,我們不允許該函數能夠拋出例外。這是相當合理的要求,因為任何的例外都會導致網站顯示錯誤,我們應該想辦法對於不可避免的例外採行捕捉,並且回傳一個空白字串好讓事情圓滿(不過這裡我們就不多做示範囉)。

註冊過濾器

下一步我們要將他註冊給django知道,我們利用django.template中的Library來做到這一點:

mysite/restaurants/templatetags/myfilters.py
from django import template
...
register = template.Library()
register.filter('yes_no',yes_no)

在這裡我們使用register的filter函數來幫助我們註冊,filter函數的第一個參數是過濾器的名稱,第二個參數是他對應的過濾器函數(讀者應該明白了為什麼過濾器函數的名稱可以不同於過濾器名稱了吧!)

要注意的是,千萬別自作聰明的這樣寫:

template.Library().filter('yes_no',yes_no)

或是:

reg = template.Library()
reg.filter('yes_no',yes_no)

總之沒有把register(名字也要一模一樣)給建立出來就是會出錯誤!要小心!

當然,為了快捷,django也允許我們使用方便的裝飾器來註冊:

@register.filter(name='yes_no')
def yes_no(bool_value, show_str):
...

透過@register.filter裝飾器可以讓被修飾的函數自動註冊為一個過濾器,該裝飾器的參數name是個可選的參數,用來定義過濾器的名稱,如果使用者不提供該參數,django預設會使用過濾器函數的名稱作為過濾器名稱。

載入過濾器

最後我們可以在模板裡面載入他囉:

mysite/restaurants/templates/menu.html
{% load myfilters %}
...
                <tr>
                    <td> {{ food.name }} </td>
                    <td> {{ food.price }} </td>
                    <td> {{ food.comment }} </td>
                    <td> {{ food.is_spicy|yes_no:"辣/不辣" }} </td>
                </tr>
...

使用{% load %}標籤可以使我們載入myfilter中的過濾器,在這裡,我們只能載入restaurantsapp中的過濾器。

這邊讀者要特別注意,templatetags或是register等等的名稱一定要百分之百跟教學中的一模一樣,至於自定義過濾器的模組名稱可以自由發揮,要多寫幾個模組也無所謂,只要記得使用前要將之載入。

 
about 4 years ago

django_note_logo.png

本頁短網址:http://goo.gl/FMXLNG


本系列筆記主要依循Django Book的脈絡,也參考官方文件等相關資料,並且用作者喜歡的學習順序和範例來說明。
希望對學習Django的朋友們能有一些幫助,也不失為作者備忘的參考。

本系列筆記使用Django1.6.5Django1.7 [註1] 和Python2.7.6,並以mysite作為專案名字來示範。


先備知識

  • Python與其OO技術
  • 基本的網頁觀念(至少要知道HTML)

目次

筆記 主題 更新日期 延伸閱讀
-1 Python快速入門 2014.06.21
0 Django介紹與安裝 撰寫中
1 建置與環境設定 2014.10.13
2 視圖與URL 2014.10.13
3 模板初探 2014.10.13
4 模板的變量與標籤 2014.10.20
5 模型與資料庫 2014.10.21 Django Migrations - a Primer
6 後台管理系統admin 2014.10.13
7 使用者互動與表單 2014.10.13
8 表單的驗證與模型化 2014.10.13
9 Cookies與Sessions 2014.10.13
10 用戶的登入與登出 2014.10.13
11 權限與註冊 2014.10.13
12 模板進階技巧 2014.10.27
13 視圖進階技巧 規劃中
14 模型進階技巧 規劃中

更新日誌

  • 2014.10.27
    • 新增筆記-模板進階技巧,目前包含三個主題,日後陸續補上一些值得探討的主題。
  • 2014.10.21
    • 更新版本至1.7。加入migration的介紹於筆記(5)。
    • 更新本目錄。
    • TODO: 撰寫一篇詳細介紹migration技術的筆記。

參考資料


[註1] 本系列筆記開始寫作時,作者使用Django1.6.5版本,而至2014.10.21,更新為Django1.7版,而後所新撰寫之筆記與更新之內容也以1.7版為主。為了相容於舊版本的文章,關於新版的更動部分會特別提出說明。在各篇筆記的目錄中,若有1.7版延伸介紹的部份會用(1.6.5 & 1.7)表示,只適用於1.7版的內容會用(1.7)來表示。

 
about 4 years ago

  • 匿名用戶 vs. 具名用戶
  • 註冊
    • 建立新用戶
    • 更改用戶密碼
    • 註冊用戶
  • 權限
    • 建立權限
      • 使用Meta Class來新增權限
      • 操作Permission模型來建立權限物件
    • 權限的新增、移除與判定
    • Django自帶權限
    • 使用權限
  • 群組與群組權限

我們之所以要打造一個個人化的web應用,就是為了對於不同的用戶提供不同的資訊和功能,這裡所謂"不同的用戶"可能指:

  1. 匿名用戶與具名用戶
  2. 不同的具名用戶
  3. 不同群的具名用戶

而關於如何提供這些不同用戶之間的差異化或客製化,就得靠一些判定身分的作法或是權限,我們將在這裡探討這些議題。

匿名用戶 vs. 具名用戶

我們首先討論要如何對匿名用戶與具名用戶做出差異化,我們通常會想要限制匿名用戶對於某些功能或頁面的存取。
關鍵就在於對HttpRequest物件的user屬性進行is_authenticated的判斷。
透過is_authenticated方法,我們能很容易地判定當下的使用者是匿名或是具名,詳細的作法有三,如下表

方法說明 優點 缺點
透過html來規範操作功能 對使用者來說直觀易理解 無法擋下用URL存取頁面的手段
於視圖函式中對於匿名用戶進行限制,使用重導或顯示錯誤 可以完全擋下匿名使用者的存取 太繁瑣,對使用者也不直觀
於視圖函式中對於匿名用戶進行限制,使用修飾符login_required 可以完全擋下匿名使用者的存取 對使用者不直觀

第一種方法我們再稍早之前已經有討論過了,雖然我們的確可以在模版中透過判定用戶的登入決定要顯示哪些資訊或連結,但我們無法阻止使用者透過GET方法直接存取某些頁面。舉例來說,用戶未登入前無法由首頁獲得餐廳列表的超連結,但是如果我們利用URLpattern/restaurants_list/還是可以進入餐廳列表的頁面。

第二種方法,是在視圖函式中先用is_authenticated進行判斷,如果發現是匿名用戶,則馬上將其重導到其他頁面,或是回應以一個錯誤訊息的頁面。我們利用限制匿名用戶存取餐廳列表頁面做範例:

mysite/restaurants/views.py
...
def list_restaurants(request):
    if not request.user.is_authenticated():
        return HttpResponseRedirect('/index/')
    restaurants = Restaurant.objects.all()
    return render_to_response('restaurants_list.html',locals())
...

上述代碼會讓匿名用戶被強制重導回首頁。我們也可以製作一個顯示錯誤訊息的頁面來提醒使用者,為了簡化,我們就不附上該頁面的html代碼了:

mysite/restaurants/views.py
...
def list_restaurants(request):
    if not request.user.is_authenticated():
        return render_to_response('error.html')  # 一樣要記得此模版要放置在正確的templates路徑下

    restaurants = Restaurant.objects.all()
    return render_to_response('restaurants_list.html',locals())
...

但我們還不滿足,我們希望當判定用戶未登入時,將其重導至login頁面,並且在登入成功後,再將其重導回來(這才是對使用者負責的設計),我們可以這樣做:

mysite/restaurants/views.py
...
def list_restaurants(request):
    if not request.user.is_authenticated():
        return HttpResponseRedirect('/accounts/login/?next={0}'.format(request.path))
    restaurants = Restaurant.objects.all()
    return render_to_response('restaurants_list.html',locals())
...

這裡用到前面講到的利用GET方法傳遞next欄位的技術,讓使用者成功登入後可以重導至next欄位的URL,這裡我們希望重導到的頁面就是餐廳列表的url pattern:/restaurants_list/,我們發揮不寫死的精神,使用request.path來提供該url pattern。

第二種方法雖然解決了第一種方法的缺陷,但顯得太繁瑣了,我們需要對每個需要判定登入的視圖撰寫減查碼和重導(起碼花兩行),其實Django提供我們一種快捷的作法,那就是法三:使用login_required修飾符。

mysite/restaurants/views.py
...
from django.contrib.auth.decorators import login_required  # 記得import進來!

...
login_required
def list_restaurants(request):
    restaurants = Restaurant.objects.all()
    return render_to_response('restaurants_list.html',locals())
...

以上代碼跟法2的代碼基本上是等效的,@login_required會檢查使用者是否登入,若已登入,正常執行修飾的視圖函式,反之會重導至/accounts/login/,並附上一個查詢字串,以該頁面的URL作為查詢字串中next欄位的值。

讀者在這裡也可以發現,@login_required也使用了默認的登入URL pattern "/accounts/login/"

使用@login_required固然減少了瑣碎的過程也能做到完全的防範,但是對於使用者來說畢竟不直觀,意思是說,如果在使用者面前展現的多數超連結都是登入限定的,那根本讓人一頭霧水,我何必要點了之後才知道他不可用呢(目前指的是對匿名使用者而言不可用)?所以,真正好的設計,應該是結合法1和法3,直觀上讓匿名用戶不會誤入登入限定的頁面,也避免了有心人士想透過URL直接存取頁面。

註冊

接著,在我們往下討論不同具名用戶之間的權限問題前,我們先來多新增幾個用戶(不然也沒得示範),當然我們要教大家的是如何使用代碼來進行註冊而非使用admin。其實這並不是什麼新鮮事,新增一個用戶不過是新增一個用戶模型的實例,還記得模型與資料庫的章節嗎?如果忘記了也沒關係,我們走一遍流程,大家順便複習一下:

首先先進入django shell

$ python manage.py shell

建立新用戶

接著我們必須要匯入User模型,並且利用模型管理器的create來產生一個模型物件並存入資料庫:

>>> from django.contrib.auth.models import User
>>> new_user = User.objects.create(username="peter",password="test123")
>>> new_user
<User: peter>

更改用戶密碼

如果想要更改用戶的密碼,可以用user物件裡的set_password方法,之所以不直接去修改password屬性,是因為密碼是經過加密的,我們通常無法自己處理這方面的手續。以下是簡單的修改範例:

>>> new_user.set_password("hello123")
>>> new_user.is_staff = True
>>> new_user.save()

我們在這不但更改了密碼,更改了使用者的狀態(is_staff屬性設為True可讓該使用者登入admin後台),最後別忘記使用save方法來儲存變更。現在,讀者們可以透過新的使用者登入admin後台來測試看看。

關於密碼的加密,Django用的是加入隨機值的hash演算法,讀者若有興趣,可自行找資料深入了解。

註冊用戶

了解了如何建立用戶之後,我們可以來研究如何實現註冊的功能,這其實相當簡單,我們需要一個處理註冊的視圖函式和一個註冊用的頁面,當然還有最重要的,用來註冊的表單。表單的實現方式有很多種,我們可以使用最原始土法煉鋼的方式,在註冊用的模版上面手動加入表單及其元件,最後利用POST方法就可以在視圖函式裡得到使用者資訊,並利用剛剛才講過的方法創建新的使用者。我們也可以將表單給模型化,以提供更優質的驗證。

但我們在這裡要用更偷懶的方式,那當然就是Django中auth應用中內建的註冊表單模型UserCreationForm:

mysite/mysite/views.py
...
from django.contrib.auth.forms import UserCreationForm
...
def register(request):
    if request.method == 'POST':
        form = UserCreationForm(request.POST)
        if form.is_valid():
            user = form.save()
            return HttpResponseRedirect('/accounts/login/')
    else:
        form = UserCreationForm()
    return render_to_response('register.html',locals())

UserCreationForm是繼承自forms.ModelForm的表單模型,ModelForm是一種特殊形式的表單,當我們發現表單欄位與某資料庫模型的欄位相同時(通常會發生在該表單的填寫就是為了產生某資料庫模型的物件),我們可以使用ModelForm避免一些不必要的手續,此處我們並不打算深入探究自定義ModelForm的寫法,我們只要了解如何從ModelForm產生一個資料庫模型的物件並且將之存入資料庫。

其實這件事情也容易得很,一共只有兩個步驟:

1. 填入欄位資料並創造表單物件
2. 使用save方法以表單中的內容生成資料並存入資料庫

我們會發現UserCreationForm並是遵循這個模式,我們將request.POST這個類字典當做引數(當然裡面包含了該表單所需的各個欄位內容:帳號跟密碼)來生成一個表單物件form,在驗證了內容的合法性後,使用save方法生成一個User物件並存入database,最後重導回/accounts/login/讓通過註冊的用戶可以立即登入網站。當然,若是輸入不合法,則會回到註冊頁面,並且透過表單模型使得各欄位不需重填。

而如果是第一次呼叫該視圖(沒有POST資料),則會產生一個空表單讓使用者輸入。

小小提醒,資料庫模型要產生實例時,參數使用的是以各欄位名稱為名的關鍵字引數,表單模型產生實例時,使用的是字典型態的引數。

接著是註冊頁面的撰寫,我們選擇了formas_p方法來生成表單:

mysite/templates/register.html
...
    <body>
        <h1>註冊新帳號</h1>
    
        <form action="" method="post">
            {{ form.as_p }}
            <input type="submit" value="註冊">
        </form>
    </body>
...

當然別忘了urls.py的設定:

mysite/mysite/urls.py
...
from views import here, math, new_math, welcome, index, register
...
urlpatterns = patterns('',
...
    url(r'^accounts/register/$',register),
)

最後透過index.html來增加註冊這個超連結選項:

mysite/templates/index.html
...
    <body>
        <h2>歡迎來到餐廳王</h2> <p><a href="/accounts/register/">註冊</a></p>
        {% if request.user.is_authenticated %}
...

權限

接著我們來介紹權限,有的時候,我們的web應用只需要區分用戶是具名的還是匿名的,但有的時候,不同的具名用戶之間,也有在資訊獲得或是功能選項上的差異,此時便要透過權限來規範各用戶間的使用情形。

其實,權限只是一個標誌,被賦予了某種權限的用戶並非自動可以使用某功能,而必須透過開發者的對權限的判定,才來決定要怎麼做,這一點跟要使用authenticate方法還滿像的。

建立權限

權限(Permission)也是一種Django內建的模型,主要包含了三個欄位如下

欄位名稱 說明
name 權限名稱
codename 實際運用在代碼中的權限名稱
content_type 來自一個模型的content_type

這邊有兩點要注意,第一點是:codename是實際運用在判定權限代碼中的名字,有點類似一個用戶的帳號,而name就好像是用戶名稱,通常只是拿來顯示,好閱讀的。第二點是,每一個權限都會跟一個資料庫模型綁定,都會屬於一種資料庫模型,這也是需要content_type的原因。

而要新增權限有兩種方式,一種透過各資料庫模型中的Meta Class來設定,另外一種可以透過操作Permission模型來建立新物件,我們將在下面示範這兩種作法:

使用Meta Class來新增權限

我們來對餐廳的評論新增一個權限,我們打開mysite/restaurants/models.py:

mysite/restaurants/models.py
...
class Comment(models.Model):
    ...
    class Meta:
        ordering = ['date_time']
        permissions = (
            ("can_comment", "Can comment"),  # 只有一個權限時,千萬不要忘了逗號!

        )

permissions變數是一個元組,我們可以為該模型增加一至數種權限,而該袁組的每個元素又是一個有兩元素的元組,第一個元素是codename字串,第二個元素是name字串,至於不需要content_type的原因很簡單,我們在模型下直接定義了權限,content_type會由Django自動地默默幫我們取得。

記得,由於權限也是模型,也有資料庫,所以我們必須要同步資料庫:

$ python manage.py syncdb

操作Permission模型來建立權限物件

首先進入shell:

$ python manage.py shell

接著我們匯入Comment模型和Permission模型:

>>> from restaurants.models import Comment
>>> from django.contrib.auth import Permission
>>> from djnago.contrib.contenttypes.models import ContentType
>>> content_type = ContentType.objects.get_for_model(Comment)
>>> permission = Permission.objects.create(
                     codename='can_comment',
                     name='Can comment',
                     content_type=content_type
                 )

為了取得content_type,我們使用了get_for_model方法,這個方法需要一個要綁定的模型作為引數。

用上述兩種方法新增了權限之後,讀者們可以登入admin後台,從使用者那邊,我們將可以在可選權限欄內看到新增的權限。

權限的新增、移除與判定

要對某用戶新增與移除權限最快的方式就是利用admin後台,但是我們依然要說明一下如何利用代碼進行(假設讀者已經註冊了一個用戶叫dokelung):

>>> from django.contrib.auth.models import User, Permission
>>> user = User.objects.get(username='dokelung')
>>> perm = Permission.objects.get(codename='can_comment')
>>> user.has_perm('restaurants.can_comment')
False
>>> user.user_permissions.add(perm)
>>> user.has_perm('restaurants.can_comment')
False
>>> user = User.objects.get(username='dokelung')
>>> user.has_perm('restaurants.can_comment')
True
>>> user.user_permissions.remove(perm)
>>> user = User.objects.get(username='dokelung')
>>> user.has_perm('restaurants.can_comment')
False

我們發現可以用User.user_permissions.addUser.user_permisssions.remove來為某個使用者新增或刪除一個權限,也可以用User.has_perm來查看使用者是否具備某種權限,但有一點很弔詭,經過測試之後發現,若沒有向管理器重新取得User物件的話,has_perm無法反應即刻性的結果。

下表列出了用戶權限的幾個重要方法:

方法 說明 範例
add 讓用戶新增權限,參數是一或多個Permission物件 user.user_permissions.add(perm)
remove 讓用戶刪除權限,參數是一或多個Permission物件 user.user_permissions.remove(perm)
has_perm 確認某用戶是否具備某種權限,參數是一個字串,形式為<模型名稱>.<權限的codename> user.has_perm('restaurants.can_comment')
clear 清除某用戶的所有權限 user.user_permissions.clear()

Django自帶權限

如果使用了Django的authapp,則Django會自動對所有專案中被安裝的APP底下所有的模型創建以下三種權限:

1. add權限
2. change權限
3. delete權限

這三種權限對於admin後台來說,分別會給使用者帶來以下操作上的限制:

  1. 擁有某資料庫模型add(新增)權限的用戶才能在admin中檢視該模型的新增表單或新增一個實體物件(資料)。
  2. 擁有某資料庫模型change(修改)權限的用戶才能在admin中檢視該模型的修改表單或修改一個實體物件(資料)。
  3. 擁有某資料庫模型delete(刪除)權限的用戶才能在admin中刪除一個實體物件(資料)。

要注意的是,這三個權限對於admin來說都只限制了用戶對於某種模型的操作,也就是說,我們沒有辦法指定一個用戶能否對其中一筆特定的資料有某種權限,只能指定某個用戶對該種資料有某種權限。

舉例來說,我們允許用戶對於餐廳評論有新增的權限,但無法允許用戶只對第三筆餐廳評論有某種權限。

還有一點要知道,除了在admin系統之外,以上這些自帶權限都只是一種標誌,他究竟規範了什麼還有賴使用者自行定義,簡單的來說,就是Django提供給我們一些標誌,至於看到標誌可以幹嘛或不能幹嘛,我們可以自己決定,當然,在admin中,Django已經幫我們決定了。

使用權限

使用權限的道理跟判定用戶是否具名一樣,要靠我們自己的判斷,而對於不同的權限到底對於web app的操作有哪些差異,也需要靠我們自己來決定跟撰寫,關於不同權限的用戶之間要如何做出差異化,有一下手法(類似如何對匿名用戶與具名用戶做出差異化):

方法說明 優點 缺點
透過html來規範操作功能 對使用者來說直觀易理解 無法擋下用URL存取頁面的手段
於視圖函式中利用權限對用戶進行限制,使用重導或顯示錯誤 可以完全擋下不具權限使用者的存取 太繁瑣,對使用者也不直觀
於視圖函式中對於匿名用戶進行限制,使用修飾符user_passes_testpermission_required 可以完全擋下匿名使用者的存取 對使用者不直觀

我們一樣對這三種方法,各進行一次示範,但在開始之前,請讀者起碼先註冊兩個用戶,一個擁有can_comment權限,而另外一個沒有。

首先我們可以在html頁面上直接判定權限,假設我們只允許擁有can_comment權限的具名用戶進行評價的話:

mysite/restaurants/
...
        <table>
            <tr>
                <th>選取</th>
                <th>店名</th>
                <th>電話</th>
                <th>地址</th>
                {% if perms.restaurants.can_comment %}
                    <th>評價</th>
                {% endif %}
            </tr>
            {% for r in restaurants %}
                <tr>
                    <!-- <td> <input type="radio" name="id" value="{{r.id}}"> </td> -->
                    <td> <a href="/menu/{{r.id}}/"> menu </a> </td>
                    <td> {{ r.name }} </td>
                    <td> {{ r.phone_number }} </td>
                    <td> {{ r.address }} </td>
                    {% if perms.restaurants.can_comment %}
                        <td> <a href="/comment/{{r.id}}/"> comment </a> </td>
                    {% endif %}
                </tr>
            {% endfor %}
        </table>
...

這裡使用的是{{perms}}這個變量,我們知道一個變量之所以可以用來填寫模板,必須要含在context中並傳給模板。我們偷懶通常會用locals函數。那在這裡一樣,{{perms}}不可能會憑空出世,我們得要將當下使用者的各種權限含在context中並且傳給模板才能使用。但這顯然花功夫,Django其實已經提供了這種處理的機制,只要我們使用包含了HttpRequest物件的context就可以,而這樣的context,在Django中是一個內建的context子類別:RequestContext

我們直接來看看如何使用RequestContext:

mysite/restaurants/views.py
...
from django.template import RequestContext
...
@login_required
def list_restaurants(request):
    restaurants = Restaurant.objects.all()
    print request.user.user_permissions.all()
    return render_to_response('restaurants_list.html',
                               locals(),
                               context_instance=RequestContext(request))
...

只有兩點,第一個是記得從django.template中匯入RequestContext,另一個是多給render_to_response一個可選的引數context_instance,並將其設為RequestContext(request)。如此一來,restaurants_list.html模板便具備了{{perms}}變量了。

我們回到剛剛的html,利用perms.restaurants.can_comment可以得知當下的用戶是否具備了restaurants這個app中的can_comment權限,一個標準的{{perms}}變量使用法為:

{{ perms.<app名稱>.<權限名稱> }}

讀者們如果已經暈頭轉向了,沒關係,只要把握住基本的要點就可以了,畢竟我們還沒有正式談到RequestContext,我們將在之後的筆記中詳細地討論他,讀者最後會發現使用RequestContext的模板不但具有perms變量,還有user等其他重要的變量,更重要的是,這是我們解決CSRF的重要手段。

然而,跟檢查用戶是否登入一樣,html的限制無法擋住使用URL對頁面直接存取,解決之道便是使用視圖函式來做一個檢查跟限制:

mysite/restaurants/views.py
...
def comment(request,id):
    if request.user.is_authenticated and request.user.has_perm('restaurants.can_comment'):
        ...
    else:
        return HttpReponseRedirect('/restaurants_list/')

同樣地,使用重導或顯示錯誤都可以。

當然我們也可以使用方便的修飾符,首先我們將剛剛的檢查式寫成一個function:

def user_can_comment(user):
    return user.is_authenticated and user.has_perm('restaurants.can_comment')

接著我們可以使用@user_passes_test:

mysite/restaurants/views.py
...
from django.contrib.auth.decorators import login_required, user_passes_test
...
@user_passes_test(user_can_comment, login_url='/accounts/login/')
def comment(request,id):
...

這個修飾符需要兩個參數,一個是用來判斷權限通過與否的函式,另外一個關鍵字參數是一個login的url,他將會在權限測試失敗時將使用者重導回登入頁面,當然,該URL也可以填寫其他頁面的URL,只是使用login頁面作為重導目標跟參數名稱比較一致,另外,該修飾符會很好心地附上一個next查詢字串在重導的URL後面,期待能將使用者在正確登入後重新導向原先的頁面。

但是由於這種登入檢查+某種權限檢查是一種常態,於是發展出另外一個更便捷的修飾符:@permission_required,用法如下:

mysite/restaurants/views.py
...
from django.contrib.auth.decorators import login_required, permission_required
...
@permission_required('restaurants.can_comment', login_url='/accounts/login/')
def comment(request,id):
...

這個修飾符一樣需要兩個參數,只是他的第一個參數直接給定權限的名稱,也不需要寫一個判定函式,因為他預設會檢查用戶是否登入(is_authenticated)和是否具備指定的權限。其他的部份跟@user_passes_test一模一樣。

當然別忘了,這兩個修飾符都需要匯入。

還有一個議題是如何檢查使用者呼叫通用視圖(這是啥,可以吃嗎?)的權限,由於還沒介紹通用視圖,我們也留到以後再說。

群組與群組權限

本篇最後一個議題是有關於群組的,我們可以將使用者加入到不同的群組,並且統一賦予群組一些權限,讓管理上更方便。
舉例來說:使用者a如果加入了x群組和y群組,他將會自動獲得這兩個群組所有的權限。

我們用以下的代碼做示範,請大家先進入django shell:

>>> from django.contrib.auth.models import User, Group, Permission
>>> user = User.objects.get(name='dokelung')
>>> p1 = Permissions.objects.get(codename='add_comment')
>>> p2 = Permissions.objects.get(codename='can_comment')
>>> g1 = Group.objects.create(name='group1')              # 新增一個新群組group1

>>> g2 = Group.objects.create(name='group2')              # 新增一個新群組group2

>>> g1.permissions.add(p1)                                # 為group1群組增加add_comment權限

>>> g2.permissions.add(p2)                                # 為group2群組增加can_comment權限

>>> user.groups.add(g1,g2)                                # 將dokelung加入group1群組與group2群組

>>> user = User.objects.get(name='dokelung')              # 重新獲取user

>>> user.has_perm('restaurants.add_comment')
True
>>> user.has_perm('restaurants.can_comment')
True

我們會發現,群組的權限屬性叫做permissions而非group_permissions,千萬別搞錯。
而一樣要注意的是,user必須重新由管理器獲取才能正確顯示權限。

群組一些常用的群組方法如下表:

方法 說明 範例
add 讓用戶加入群組,參數是一或多個群組 user.groups.add(group)
remove 讓用戶離開群組,參數是一或多個群組 user.groups.remove(group)
clear 讓用戶離開所有群組 user.groups.clear()

但為了方便,通常我們會利用admin後台來管理群組及其權限。

本篇花了大量的篇幅詳細介紹了關於用戶系統的註冊和權限問題,再加上上一篇講到的用戶登出入的機制,便可以打造個人化的web應用了。現在,我們對於Django的各項基本要素和功能都有了個初步的了解,接下來我們要逐步的強化我們的專案,並且將整個MTV架構的進階議題帶給大家。讓我們接著往前進吧!

 
about 4 years ago

  • 用戶
  • 登入
    • 確認使用者身分
    • 登入使用者並保持其登入
    • 根據用戶身分確認並規範使用者的操作功能
  • 登出
  • 使用內建的login/logout視圖
    • 內建login做的事
    • 內建login提供給呼叫模版的變量
    • 提供重導URL給內建login
      • 於settings.py中設定LOGIN_REDIRECT_URL
      • 透過POST方法傳送next(或是其他的REDIRECT_FIELD_NAME)欄位及其值
      • 透過GET方法傳送next(或是其他的REDIRECT_FIELD_NAME)欄位及其值
    • 對於內建的login視圖使用別的模版名稱

在上一節中學習了cookie和session這兩個重要的機制之後,我們可以來面對將web個人化的問題了。概略來說,我們希望我們的web應用是非匿名的,每個使用者都有一組帳號和密碼,用來核對他的身分,並且能夠將這個身分保持直到登出為止。這種身分的保持,可以使得整個web對該使用者呈現客製化的效果,包括資料、偏好以及功能的差異化。

用戶

一個用戶是一個web裡具有正式身分的使用者,我們為了能夠存取用戶的資訊,我們會想要建立model。但在Django中不用那麼麻煩,django.contrib套件中的authapp,便內建了user的模型。不論讀者想要自己刻或是使用現成的模型,都沒問題。不過我們先來看看要如何使用auth app中的User model。

首先我們一樣先打開settings.py:

mysite/mysite/settings.py
...
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',  # 確認此行有加

    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'restaurants',
)

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
    #'django.middleware.csrf.CsrfViewMiddleware',

    'django.contrib.auth.middleware.AuthenticationMiddleware',  # 確認此行有加

    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
...

authapp和AuthenticationMiddleware給安裝上去,如果這是第一次安裝的話,別忘了要同步資料庫喔。

$ python manage.py syncdb

有了auth,我們就等於有了一個內建的用戶權限系統,讀者們可以進入admin後端看看,我們會發現有使用者群組這兩個屬於auth應用的model,包含了使用者的名稱、姓氏、名字、電子郵箱和工作人員狀態等欄位。除非讀者們的web有特殊需求,不然這個模型已足夠使用。

登入

因為使用了內建的用戶系統,我們暫且先將註冊的部分放下,因為我們可以手動透過admin後端來新增使用者,我們先來看看要如何設計一個登入的機制。要知道,我們所期待的登入包含了下面三個步驟:

1. 藉由比對資料庫中的用戶資訊,確認使用者的身分(通常使用帳號及密碼)
2. 保持該使用者的登入狀態直到登出或預設時限
3. 根據用戶身分確認並規範使用者的操作功能

確認使用者身分

要確認使用者的身分,必須要求使用者給予一些資訊,而絕大多數的情況,我們需要帳號與密碼。首先,讓我們先來設計一個頁面用來輸入帳號密碼,當然,這需要表單以及對應的視圖函式:

mysite/templates/login.html
...
    <body>
        <form action="" method="post">
            <label for="username">用戶名稱:</label>
            <input type="text" name="username" value="" id="username"> <br />
            <label for="password">用戶密碼:</label>
            <input type="password" name="password" value="" id="password"> <br />
            <input type="submit" value="登入" />
        </form>
    </body>
...

因為登入是有關於整個web的操作,所以他應該隸屬於整個project而非單一一個app,所以我們選擇在mysite/templates下撰寫login.html這個登入頁面,並且在mysite/mysite/views.py中撰寫視圖函式。

登入的頁面非常簡單,包含了一個使用POST方法的form,會要求使用者輸入帳號密碼,然後會有一個submit button可以登入,為了簡化說明,我們這裡就簡單的略過表單的模型化與表單正確性的檢查,但讀者在實作自己的登入表單時,務必要確實做好。

mysite/mysite/views.py
...
from django.contrib import auth  # 別忘了import auth

...
def login(request):

    if request.user.is_authenticated(): 
        return HttpResponseRedirect('/index/')

    username = request.POST.get('username', '')
    password = request.POST.get('password', '')
    
    user = auth.authenticate(username=username, password=password)

    if user is not None and user.is_active:
        auth.login(request, user)
        return HttpResponseRedirect('/index/')
    else:
        return render_to_response('login.html') 

與之對應的視圖函式login包含幾個重要的部份,首先,我們可以發現HttpRequest物件中包含了一個user屬性,他代表了當前的使用者。

如果用戶已經登入,則HttpRequest.user是一個User物件,也就是具名用戶。
如果使用者尚未登入,HttpRequest.user是一個AnonymousUser物件,也就是匿名用戶。

下表列出User物件中的屬性:

屬性 說明
username 使用者的帳號,由字母、數字和底線組成
first_name 名字
last_name 姓氏
email 電子郵箱
password 加密(經編碼)過後的密碼
is_staff 真假值。若為True,該用戶可登入admin後端
is_active 真假值。若為True,該用戶可登入
is_superuser 真假值。若為True,該用戶擁有全權限
last_login 用戶上一次登入的日期與時間
date_joined 用戶被創建的日期與時間

下表則列出User物件中幾個重要的方法:

方法 說明
get_username( ) 取得用戶帳號
is_anonymous( ) 是否是匿名用戶,永遠回傳False; 若為AnonymousUser物件,則永遠回傳True
is_authenticated( ) 用戶是否認證過,永遠回傳True; 若為AnonymousUser物件,則永遠回傳False
get_full_name( ) 回傳完整的姓名
get_short_name( ) 只回傳名字
set_password(password) 設定密碼,會自動編碼加密,不包含User物件的儲存
check_password(password) 確認密碼,正確會回傳True,會自動編碼加密才比較

由上表可知,對AnonymousUser來說,is_authenticated方法會返回一個False值,而User會拿到True,所以is_authenticated方法是用來判定當下的使用者是否認證過(比對過身份)的重要函式。

順帶一提,is_anonymous和is_authenticated這兩個方法對於User和AnonymousUser來說算是一種多型概念的表現。

所以如果使用者已經認證過,我們將他重導回首頁(別擔心,我們等等會開始著手設計index.html)。
如果還是匿名用戶,我們試圖從request.POST中拿取表單中的帳密資訊(如果POST中沒有username或是password的話,我們填給他一個空值,讓他在接下來的檢查中產生失敗),並且使用auth中的authenticate方法來確認用戶。

登入使用者並保持其登入

auth.authenticate方法接受兩個引數,分別是usernamepassword,如果帳密資訊正確,authenticate方法會回給我們一個具名用戶的User物件,否則我們會拿到None

為了謹慎,我們還須檢查user.is_active,確認該用戶的帳戶沒有被凍結。只要兩道檢查任何一個有問題,我們便回應一個login頁面讓使用者重新輸入帳密,否則使用auth.login方法可以幫助我們真正登入使用者並保持他的登入狀態。

auth.login需要一個HttpRequest物件和一個User物件當做引數,他會利用Django的session(前一節介紹那麼久的session終於登場了!)將這個具名用戶保存在該session中,這也代表了該用戶的登入狀態得以跨頁面的保存直到session結束或登出。

這邊稍微來整理一下:

1. auth.athenticate負責進行用戶的認證
2. auth.login負責進行用戶登入狀態的保持

所以通常一個完整的login視圖函式都會用到上述兩個方法,可驗證用戶後保持其狀態。其實讀者可以發現,Django中所謂的登入,代表在當下的HttpRequest中的user屬性是一個具名的User物件。

根據用戶身分確認並規範使用者的操作功能

為了讓上述的功能被完整展示,我們便來製作一個首頁:

mysite/templates/index.html
<html>
    <head>
        <title>Restaurant King</title>
        <meta charset="utf-8">
    </head>
    <body>
        <h2>歡迎來到餐廳王</h2>
        {% if request.user.is_authenticated %}
            <p>{{request.user}} 您已經登入囉</p>
            <a href="/restaurants_list/">餐廳列表</a>
        {% else %}
            <p>您尚未登入喔~<a href="/accounts/login/">登入</a></p>
        {% endif %}
    </body>
</html>

在這裡唯一值得一提的是,我們利用is_authenticated來判斷是否登入(根據前面的結論,所謂登入不過就是user屬性是User物件,所謂未登入,指的是user屬性是AnonymousUser物件),所以一個登入的用戶其is_authenticated方法的回傳值一定是True的。

is_authenticated指代表了該用戶是經過認證的(也就是具名的),他不會檢查該用戶的帳號是否被凍結。

透過判斷登入與否,提供給匿名與使用者不同的資訊與功能,透過html來規範操作功能是一個重要的手段。另一個可行的辦法是透過視圖函式,我們將在後面提到,在此之前,我們記得要幫index.html撰寫一個對應的view function:

mysite/mysite/views.py
...
def index(request):
    return render_to_response('index.html',locals())

別忘了設定一下我們的urls.py不然一切都是白搭。

mystie/mysite/urls.py
...
from views import login, index
...
urlpatterns = patterns('',
    ...
    url(r'^accounts/login/$',login),
    url(r'^index/$',index),
)

這裡讀者們可能會很好奇,為何需要使用/accounts/login/這個pattern,選擇/login/不是更簡單嗎?
這是因為/accounts/login/是Django默認的登入pattern(/accounts/logout/也是默認值),這個pattern對於某些Django的函式而言是參數的預設值,使用默認的pattern可以使得我們再使用到這些函式時減少一些負擔,不過若讀者沒有這些顧慮的話,使用任何想要的pattern都是可以的。
順帶一提,讀者可以透過settins.py來設定此一默認值,LOGIN_URL可以修改成任意想要的默認pattern。

登出

登出的手段很簡單,使用auth中的logout函數即可,我們先撰寫登出的view function:

mysite/mysite/views.py
...
def logout(request):
    auth.logout(request)
    return HttpResponseRedirect('/index/')

logout函式比起login就簡單多了,auth.logout方法會將用戶登出,然後跟login函式一樣,我們要記得在views.py中,從django.contrib中匯入auth

logout用在匿名用戶上也不會產生錯誤,這點要稍微注意一下。

接著我們需要調整一下index.html:

mysite/templates/index.html
...
        {% if request.user.is_authenticated %}
            <p>
                {{request.user}} 您已經登入囉~
                <a href="/accounts/logout/">登出</a>
            </p>
            <a href="/restaurants_list/">餐廳列表</a>
        {% else %}
            <p>您尚未登入喔~<a href="/accounts/login/">登入</a></p>
        {% endif %}
...

最後,加入新的pattern/action對應到urls.py中:

mysite/mysite/urls.py
...
from views import login, logout, index  # 記得匯入該function

...
urlpatterns = patterns('',
...
    url(r'^accounts/login/$',login),
    url(r'^accounts/logout/$',logout),  # 加入此對應

    url(r'^index/$',index),
)

現在讀者們可以利用早先在admin中建立的帳號來測試登入與登出的功能囉(這裡使用超級使用者或一般使用者都沒問題,但記得帳戶若被凍結則無法登入)。

使用內建的login/logout視圖

其實Django早在其內部便幫我們撰寫好了登入與登出的視圖函式,我們大可不必手動刻一個(但自己刻過一遍了解背後原理也是不錯啦,各位讀者不要打我),使用的方法如下,我們只要使用django.contrib.auth.views中的loginlogout函式即可:

mysite/mysite/urls.py
...
from views import index
from django.contrib.auth.views import login, logout  # 利用內建的view funciton

...
urlpatterns = patterns('',
...
    url(r'^accounts/login/$',login),
    url(r'^accounts/logout/$',logout),
    url(r'^index/$',index),
)

這兩個視圖函式預設使用的模版是registration/login.htmlregistration/logged_out.html,我們先試著將我們的mysite/templates/login.html複製一份到mysite/templates/registration/中(請讀者新增一個registration目錄到templates底下吧!)在試著登入一次。

天啊,又碰到CSRF的問題了,這是因為內建的login函式有強制使用到CSRF的功能,為了解決這個問題,我們在login.html的表單中加入{% csrf_token %}這個標籤,如下:

mysite/templates/registration/login.html
<form action="" method="post">
        {% csrf_token %}  # 加入此標籤
        <label for="username">用戶名稱:</label>
        <input type="text" name="username" value="" id="username"> <br />
        <label for="password">用戶密碼:</label>
        <input type="password" name="password" value="" id="password"> <br />
        <input type="submit" value="登入" />
    </form>

關於更多有關CSRF的議題跟詳細的討論,我們留待後面的章節,這裡要注意的是,雖然我們並沒有開啟csrf的功能(看看自己的settings.py吧),但是內建的login還是會啟動保護。

不過因為視圖函式和模版有密不可分的關係(模版的變量依賴視圖提供,而模版的表單內容也會提供給視圖),所以當我們使用了內建的視圖搭配自定的模版時,要小心中間可能造成的資訊代溝(所以建議讀者們可以去看auth.views.login的原始碼,這裡很好心地附在下面)。

auth.views.login
def login(request, template_name='registration/login.html',
          redirect_field_name=REDIRECT_FIELD_NAME,
          authentication_form=AuthenticationForm,
          current_app=None, extra_context=None):
    """
    Displays the login form and handles the login action.
    """
    redirect_to = request.POST.get(redirect_field_name,
                                   request.GET.get(redirect_field_name, ''))

    if request.method == "POST":
        form = authentication_form(request, data=request.POST)
        if form.is_valid():

            # Ensure the user-originating redirection url is safe.

            if not is_safe_url(url=redirect_to, host=request.get_host()):
                redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)

            # Okay, security check complete. Log the user in.

            auth_login(request, form.get_user())

            return HttpResponseRedirect(redirect_to)
    else:
        form = authentication_form(request)

    current_site = get_current_site(request)

    context = {
        'form': form,
        redirect_field_name: redirect_to,
        'site': current_site,
        'site_name': current_site.name,
    }
    if extra_context is not None:
        context.update(extra_context)
    return TemplateResponse(request, template_name, context, current_app=current_app)

我們僅針對auth.views.login做以下幾點重要的說明:

內建login做的事

如果使用者透過GET方法呼叫該函式,則login會呼叫模版回應一個帶有登入表單的頁面(當然還是得自己寫,並且要相容),而且該表單將會POST到相同的URL位置也就是說會呼叫login函式自己處理。

如果使用者透過POST方法呼叫該函式,則login會試著登入使用者,如果成功,會重導至next鍵值對應的URL,如果沒有next值,則會重導至settings中的LOGIN_REDIRECT_URL(當然沒設定的話他也有個預設值/accounts/profile/)。如果不成功,則會重新顯示登入表單。

內建login提供給呼叫模版的變量

login函式在最後會呼叫registration/login.html模版,而提供給模版的主要有下列變量(另外還有sitesite_name):

變量 說明
form AuthenticationForm的物件,用來做authenticate的確認的,有username和password兩個欄位
next 用在登入成功後重導的URL,可能包含查詢字串

其實next只是個預設的名稱,透過設定settings.py中的REDIRECT_FIELD_NAME可以設定成任意的字串。我們可以利用這些變量寫出一個更完整的login.html:

mysite/templates/registration/login.html
...
    {% if form.errors %}
        <p>Your username and password didn't match. Please try again.</p>
    {% endif %}

    <form method="post" action="{% url 'django.contrib.auth.views.login' %}">
        {% csrf_token %}
        <table>
            <tr>
                <td>{{ form.username.label_tag }}</td>
                <td>{{ form.username }}</td>
            </tr>
            <tr>
                <td>{{ form.password.label_tag }}</td>
                <td>{{ form.password }}</td>
            </tr>
        </table>

        <input type="submit" value="login" />
    </form>
...

提供重導URL給內建login

我們常常會碰到一個狀況,登入之後要將使用者帶至某個特定的頁面(也許是首頁,也許是登入前的頁面),為了讓內建的login能夠照我們的意思來重導(如果是我們自己寫的login當然沒問題,便如前面的範例,我們重導回/index/),我們必須提供重導URL給login,作法有下列幾種,我們以想要重導回/index/做示範:

於settings.py中設定LOGIN_REDIRECT_URL

這是最簡單直接的方法

mysite/mysite/settings.py
...
LOGIN_REDIRECT_URL = "/index/"

透過POST方法傳送next(或是其他的REDIRECT_FIELD_NAME)欄位及其值

通常會使用hidden type的input元件

mysite/templates/registration/login.html
...
        <input type="submit" value="login" />
        <input type="hidden" name="next" value="{{ next }}" />  # 利用此行
    </form>
...

這裡有個點要注意, 我們利用名為next的隱藏元件來傳送重導URL,而這個URL卻是來自login函式提供的變量{{ next }}

透過GET方法傳送next(或是其他的REDIRECT_FIELD_NAME)欄位及其值

透過URL pattern/accounts/login/?next=/index/中的查詢字串來提供next欄位。

不過說起來第二種方法還是依據第一種方式的設定,只能算是解決next值傳遞的方式而不算是設定一個next欄位的方法。
而我們對於預設的重導行為會偏向使用第一種方式進行設定,對於重導回登入前一個頁面的行為會偏向使用第三個方法。
另外,沒有特殊需求的話,建議各位讀者遵循使用next這個欄位名稱,可以在使用上避免一些需要調整的負擔,也能有較好的一致性。

對於內建的login視圖使用別的模版名稱

這必須要透過進階的視圖函式技巧,我們將留到後面的章節說明,不過如果先給出一個作法,我們可以對urls.py做出以下的更動:

mysite/mysite/urls.py
...
from views import index
from django.contrib.auth.views import login, logout

urlpatterns = patterns('',
        ...
    url(r'^accounts/login/$',login,{'template_name':'login.html'}),
    url(r'^accounts/logout/$',logout),
    url(r'^index/$',index),
)

如此我們便可以將使用的模版更換成任意的模版了。

提醒一下,這裡模版名稱的路徑必須在模版目錄底下,以前面章節的設定來說,此名稱必須位於mysite/templates下。

對於登出來說,我們也可以撰寫一個相應的模版,但有趣的是,如果我們沒有設定,Django預設會用admin的登出頁面作為登出頁面。

本節學習了如何實作login以及logout的功能,不過這顯然不夠,如果沒有對於登入的身分做出資訊與功能的差異化的話,登入與登出也就變得沒意義了,所以我們將在下一個筆記中對權限進行討論,同時也讓展現了如何實作用戶系統中的註冊功能。

 
about 4 years ago

  • Http協定的不足
    • 根本不知道使用者
    • 知道使用者但不會做資料比對
    • 知道使用者,也會比對,但使用者在每一次的請求中都要比對
  • Cookies - 好餅乾,不吃嗎?
    • 設置cookies
    • 讀取cookies
    • 餅乾的問題
  • Session
    • 安裝Session App
    • 使用Session
    • Session資料庫
    • Session cookie
    • Session的持續性
    • 用Session儲存模型物件

在現代的網頁應用中,個人化的web service受到群眾們的歡迎,臉書就是一個最好的例子,我們希望這個web app是認得使用者的,是能夠為使用者客製化的,本篇與下一篇的主題即是在介紹如何利用Django的機制做出個人化的網頁的基礎。

Http協定的不足

既然要認得使用者,那他首先得記住使用者,更精確的說,他必須要保持著某些資訊跟狀態。很可惜的是,單純的Http協定無法滿足我們的需求,因為他就只是非常制式地接收要求,回應要求。每一次的要求,每一次的回應都是獨立的,因為他無法記錄任何狀態,他並不能保持著上一次要求所帶來的任何有用資訊。

當然,透過GET與POST我們依然可以將資料透過新的request帶到下一個頁面,但是,這樣相當麻煩,沒有效率。

舉個例子,這就好像一家早餐店的老闆娘,他完全不在意客人是誰,只專注在客人點了什麼,然後埋首製作早餐。這種營業模式也不是說不行,但少了那麼一點人情味,通常,當老闆娘跟你說出:『先生,今天還是漢堡和大冰奶』的時候,你會有親切的感覺,而且極有可能成為忠實顧客。你看看,老闆娘記著客人的喜好或是從以往點餐的經驗中記住了點什麼,就能夠使得早餐店的經營生色不少,我們何不讓網頁的應用也能夠記住點什麼呢!

為了更清楚的說明,我們將Http協定的不足分為以下三點:

根本不知道使用者

伺服器: 使用者a發出請求 ---> 伺服器給定回應 but 伺服器根本不知道有使用者a
早餐店: 客人a點餐      ---> 老闆娘做早餐   but 老闆娘根本不知道有客人a這號人物

第一點是伺服器或是說web app根本不知道有這個人,他不但認不出客人,他根本不知道有這個客人,全天下的客人對他來說都一樣,客人就只是客人,只要我聽的懂他點餐,就好了。

這個問題的解決之道很簡單,使用資料庫便可以將客人a(使用者a)的資訊記錄下來了。

知道使用者但不會做資料比對

伺服器: 使用者a發出請求 ---> 伺服器給定回應 but 伺服器根本沒有確認發出請求的人
早餐店: 客人a點餐      ---> 老闆娘做早餐   but 老闆娘根本沒去看a,也沒想知道他是誰,雖然老闆娘知道有a這號人物

即使我們的伺服器記錄了用戶的資訊,但不做比對,也是認不出用戶的,這個時候登入或是辨識就成了解決問題的良藥。

知道使用者,也會比對,但使用者在每一次的請求中都要比對

伺服器:
使用者a發出請求1 ---> 回應1 -- > 伺服器要求使用者a提供資料(帳密)來確認是使用者a 
                           |---- 伺服器並不知道兩次請求都是使用者a發出的
使用者a發出請求2 ---> 回應2 -- > 伺服器要求使用者a提供資料(帳密)來確認是使用者a

早餐店:
客人a點了第一次餐 ---> 老闆娘做早餐 -- > 老闆娘要客人報出姓名出生年月日和身分證字號
                           |---- 老闆娘並不知道兩次點餐是同一個客人,他還要客人提供那麼多資料
客人a點了第二次餐 ---> 老闆娘做早餐 -- > 老闆娘要客人報出姓名出生年月日和身分證字號

其實只要客人願意每次都辛苦的提供比對的資訊,老闆娘是可以認得客人的,但是這並不是個好主意(至少對客人來說),就像我們不希望在同一個網站中每次要求一個新頁面時,都要重新登入一次吧,倒不如老闆娘給客人一塊餅乾說:『你拿這個餅乾給我看我就認得你了,身分證可以不用報出來』,於是在餅乾還在的一天,老闆娘都可以直接認得客人!

Cookies - 好餅乾,不吃嗎?

所以餅乾在哪呢?哈哈,餅乾就在這兒呢!大家耳熟能詳的cookie是伺服器儲存在瀏覽器的一小段訊息,每一次使用者透過瀏覽器向伺服器提出要求時,都會雙手奉上伺服器在稍早存在客戶端(瀏覽器)的cookie。透過這些cookies,伺服器便能掌握使用者的狀態。直到cookie失效那天。

cookie是儲存在瀏覽器中的小段訊息,它用來記住一些暫時資訊並且能讓使用者跨頁面使用。

一個cookie其實就只是一個鍵值對,包含了cookie的名稱和cookie的值,我們稍後也會看到,在Django中操作或設置cookies的手段便類似於使用字典。

cookies會伴隨著Http request和response往返客戶端與伺服端,有興趣的朋友可以去了解一下Http協定中cookies的傳送與設置,這邊我們會將精神放在,如何從request中取得cookies所存的資訊,以及如何在response中夾帶我們要設置在客戶端的cookies。

設置cookies

首先我們先來試試看把餅乾給瀏覽器吧,讀者們可以隨意的設定一個url pattern並且讓他對應到視圖函式set_c:

相信各位閱讀過前面的章節都有一定的實力了,這種示範用的代碼筆者就不逐一說明其安放的位置了。

def set_c(request):
    response = HttpResponse('Set your lucky_number as 8')
    response.set_cookie('lucky_number',8)
    return response

要設置cookies,我們要使用HttpResponse物件的set_cookie方法,第一個參數指定了cookie的名稱(鍵),第二個參數指定了cookie的值,set_cookie方法還有許多的參數可以調整,重要的包含設置cookie的有效時間,大家可以參考Django的Documents,我們在此不多做贅述。

讀取cookies

那要如何讀取一個cookie的值呢?這就簡單多了,如同存取POST和GET方法所提交的數據,cookie的取值一樣用字典取值的方法向HttpRequest物件的COOKIES屬性拿取,COOKIES中儲存了所有cookies的鍵值對資訊。如下範例:

def get_c(request):
    if 'lucky_number' in request.COOKIES:
        return HttpResponse('Your lucky_number is {0}'.format(request.COOKIES['lucky_number']))
    else:
        return HttpResponse('No cookies.') 

餅乾的問題

餅乾的確是解決問題的方法,但似乎也有他的問題存在,主要有下兩種問題:

  1. cookies是儲存在瀏覽器端的,而用戶可以關閉cookies的功能這會導致許多行為無效化
  2. Http協定是明文協定,cookies在傳輸過程中容易被攔截、竄改、偽造等,並不安全

為了解決cookies的弱點,我們需要一個更強大的機制:Session

Session

session之所以能夠解決cookies的問題,便在於他是把資訊存在伺服端,等等!那我們何不直接將資料存在資料庫中呢?這一樣能讓資料跨頁面的生存呀?使用資料庫記住這些資訊當然也是可行的辦法,但是資料庫的資料是永久性的,對於這些用完即丟(當然需要持續一段時間啦XD)的暫時資訊,我們還是不勞動資料庫的大駕,畢竟使用者不會想要去刻一個模型出來用吧。(我絕對不會說Django的session還是有用到資料庫的><)

那session是怎麼跨頁面生存的呢,主要有兩種方式:

第一種呢,session會透過cookie儲存一段用以辨識的ID,cookie可以跨頁面生存,session自然也可以囉,透過這個ID,指令搞便可以去伺服端將我們需要的資料取出來。這個方式跟純粹使用cookie來儲存資訊的方式不同,因為現在這個cookie只儲存session的ID(而且是加密過的),我們的資料不會直接暴露給瀏覽器端。

第二種呢,我們一樣可以透過url查詢字符的方式,將session ID附上,這對於cookie功能被瀏覽器端關閉時特別有用。

不過Django的各位使用者,我們暫且不需要擔心這個,我們先使用預設的第一種方式,並且讓Django幫我們處理這個繁雜的手續,我們只要會用就好了。

安裝Session App

要使用session,第一步便是安裝好session的app,打開settings.py

mysite/mysite/settings.py
...
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',      # 確認有安裝

    'django.contrib.messages',
    'django.contrib.staticfiles',
    'restaurants',
)

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',    # 確認有安裝

    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
    #'django.middleware.csrf.CsrfViewMiddleware',

    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
...

如果是第一次設定,請記得一定要同步一下資料庫,才能讓session啟用:

$ python manage.py syncdb

使用Session

當我們安裝好session app之後,HttpRequest物件中就會多了session這個屬性,使用session就跟使用字典一樣容易,讓我們利用視圖函式use_session來做個簡單示範:

def use_session(request):
    request.session['lucky_number'] = 8                               # 設置lucky_number

    if 'lucky_number' in request.session:
        lucky_number = request.session['lucky_number']                # 讀取lucky_number

        response = HttpResponse('Your lucky_number is '+lucky_number)
    del request.session['lucky_number']                               # 刪除lucky_number

    return response

基本上session的用法跟字典(映射型態)完全一樣(提醒大家request.session,session請用小寫),但請遵循下列規則:

  1. 使用字串作為session的鍵值
  2. 不要任意以底線作為session鍵值字串的開頭
  3. 不要對session及其屬性賦值

Session資料庫

前面說過,session還是有用到資料庫的(不然怎麼在後端儲存資料呢!),這也是我們在安裝了session app後還需要同步資料庫的原因,我們平常透過HttpRequestsession屬性來存取這個資料庫中的資料。但其實我們也可以利用操作模型的方式來達到同樣的效果,讓我們打開shell:

$ python manage.py shell

匯入Session模型之後,便可以操作資料庫了:

>>> from django.contrib.sessions.models import Session
>>> s = Session.objects.all()[0]
>>> s.expire_date
datetime.datetime(2014, 8, 30, 3, 55, 42, 878739, tzinfo=<UTC>)

expire_date是該session的有效期限,當超過這個時間時,session即失效。

>>> s.session_data
u'MDZmZGI2ZjZlNTNjYjc2MTlmMDMxM2Y5NTRlNGYzZTg2M2Q0NWJhNjp7Il9hdXRoX3VzZXJfYmFja2VuZCI6ImRqYW5nby5jb250cmliLmF1dGguYmFja2VuZHMuTW9kZWxCYWNrZW5kIiwiX2F1dGhfdXNlcl9pZCI6MX0='
>>> s.get_decoded()
{u'_auth_user_backend': u'django.contrib.auth.backends.ModelBackend',
 u'_auth_user_id': 1}

session_data是經過編碼的,我們必須利用get_decoded()方法來取得編碼後的資料,而這會是一個字典:

Session cookie

我們在上面說過,session ID 是透過cookie來保存的,這個cookie就叫session cookie。我們可以透過sessionid這個cookie名稱來取得cookie,而他的值就是編碼後的session ID。我們透過撰寫一個測試的視圖函數session_test來做一個示範:

from django.contrib.sessions.models import Session

def session_test(request):
    sid = request.COOKIES['sessionid']
    s = Session.objects.get(pk=sid)
    s_info = 'Session ID:' + sid + '<br>Expire_date:' + str(s.expire_date) + 
             '<br>Data:' + str(s.get_decoded())
    return HttpResponse(s_info)

首先我們利用request.COOKIES來取得sessionid這個cookie的值,也就是session ID,接著我們利用模型的get方法去找到在資料庫中的sessions(pk這個屬性指的就是session id),最後我們把expire_date和session的內容都印出來。

我們可以看到他顯示出來的頁面會長的像下面這樣:

Session ID:vtnbu3bpi4eaw634b0wlhv333duzf8am
Expire_date:2014-10-06 03:57:47.741875+00:00
Data:{}

上述方法不是唯一能取得session ID的方法,畢竟session不一定要依附著cookie,我們可以直接讀取session的屬性:session_key來取得session ID:

def session_test(request):
    sid = request.COOKIES['sessionid']
    sid2 = request.session.session_key
    s = Session.objects.get(pk=sid)
    s_info = 'Session ID:' + sid + '<br>SessionID2:' + sid2 + '<br>Expire_date:' + 
              str(s.expire_date) + '<br>Data:' + str(s.get_decoded())
    return HttpResponse(s_info)

結果會發現,兩個方法拿到的session ID一樣。

Session ID:vtnbu3bpi4eaw634b0wlhv333duzf8am
SessionID2:vtnbu3bpi4eaw634b0wlhv333duzf8am
Expire_date:2014-10-06 03:57:47.741875+00:00
Data:{}

這種預設以cookie記錄session ID來達成跨頁面的方式雖然方便,但讀者們還是要確認使用者端的cookie功能是否被開啟,我們可以透過以下步驟來測試:

  1. 利用HttpRequest.session.set_test_cookie()設置測試cookie
  2. 利用HttpRequest.session.test_cookie_worked()來檢查cookie是否被允許使用
  3. 利用HttpRequest.session.delete_test_cookie()來刪除測試cookie

如果cookie未被開啟,那我們得利用url查詢的方式,去傳遞session ID(不過這會相當麻煩)。

Session的持續性

Session的持續性和有效性可以透過settings.py中的參數:SESSION_EXPIRE_AT_BROWSER_CLOSESESSION_COOKIE_AGE來設定。

參數 意義 預設值
SESSION_EXPIRE_AT_BROWSER_CLOSE 決定session是否在瀏覽器關閉時結束 False
SESSION_COOKIE_AGE session(cookie)的有效時間 1,209,600秒,兩週

如果SESSION_EXPIRE_AT_BROWSER_CLOSE被設為True,那當使用者將瀏覽器關閉時,該session自動結束。

用Session儲存模型物件

最後最後要來談到一個很重要的議題,我們常常會希望利用session暫時保存一個或多個模型物件(資料)來跨頁面使用,但是Django的預設狀態會出現錯誤,我們可以來試試看:

mysite/restaurants/views.py
...
def list_restaurants(request):
    restaurants = Restaurant.objects.all()
    request.session['restaurants'] = restaurants  # 試著利用session保存模型物件

    return render_to_response('restaurants_list.html',locals())
...

馬上便出現一個TypeError:

TypeError at /restaurants_list/
[<Restaurant: 派森家常小館>, <Restaurant: 古意得餐廳>] is not JSON serializable

該怎麼辦呢?別擔心,打開settings.py將SERIALIZER改成PickleSerializer吧:

mysite/mysite/settings.py
...
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'

本節介紹了cookie和session這兩個幫助資料進行跨頁面生存的兩大機制,有了他們做基礎,我們便不需要每次都要使用url查詢配合GET方法傳遞資料,也不需要每次用表單+POST方法來從使用者那裏獲取資訊,我們能記也能傳。下一節將會介紹如何使用這些機制來實作個人化的web,包含登入、登出與註冊。