Python之禪

星期四 23 May 2019   even  
教學

前言

在所有版本的python,都可以執行import this,將這段The Zen of Python呈現出來,他是python開發的核心精神,句句珠璣,實際編寫python程式時,遵守zen of python的原則,可以讓你的程式更好。即使你不使用python,細細地拜讀這段也會很有幫助。

因此,我選擇在Python新手教學的最後一節放上這段,並依我個人見解以及相關資料逐條解釋。

import this

The Zen of Python, by Tim Peters

Beautiful is better than ugly.

Explicit is better than implicit.

Simple is better than complex.

Complex is better than complicated.

Flat is better than nested.

Sparse is better than dense.

Readability counts.

Special cases aren't special enough to break the rules.

Although practicality beats purity.

Errors should never pass silently.

Unless explicitly silenced.

In the face of ambiguity, refuse the temptation to guess.

There should be one-- and preferably only one --obvious way to do it.

Although that way may not be obvious at first unless you're Dutch.

Now is better than never.

Although never is often better than *right* now.

If the implementation is hard to explain, it's a bad idea.

If the implementation is easy to explain, it may be a good idea.

Namespaces are one honking great idea -- let's do more of those!

中文翻譯:

Python之禪 by Tim Peters

美麗優於醜陋。明確勝過含糊。

簡單優於複雜。複雜優於錯雜。

平舖勝過層疊。勻散勝過密集。

可讀性很重要。

特例不應該特殊到違反原則。

雖然實用勝過純粹。

錯誤絕對不該安靜的通過。

除非被明確地緘默 。

在面對含糊時,拒絕猜測的誘惑。

應該只有一種最明顯的處理方法。

雖然一開始可能不是很明顯,除非你是荷蘭人。

現在做勝過永遠不做。

雖然永遠不做又勝過"立刻馬上"做。

如果實作很難解釋,這是個壞點子。

如果實作很容易解釋,這有可能是個好點子。

命名空間是很好的點子,我們應該多加使用!

Beautiful is better than ugly.

美麗優於醜陋。

python的語法寫起來要優美不要醜陋,因此,不使用%或、| ,而是使用直接使用and、 or等,明確好理解的符號,程式看起來要一致,因此強制規定了縮排等,讓python的語法寫起來優美,當然,優美是很主觀的定義,但是基本的整齊、容易理解的程式碼,看起來比較優美。

因為python強制要求一致性的縮排,醜不到哪去,因此我貼上手邊一段優美和醜陋的c++程式碼,讓你們比較看看,何謂醜陋和優美。

醜陋版:

void UTypeWriterRTextBlock::UpdateString()
{	FString StringToShow = MyTextWrapper(C_WrapAt, 
UsePerCharacter).ToString();
if (bFinishedNow) {
SubStringIndex = StringToShow.Len();
	} FString SubString = StringToShow.Mid(0, SubStringIndex);SetText(FText::FromString(SubString));

if (SubStringIndex < 
  StringToShow.Len()) {
		  SubStringIndex++;}
else {bFinished = true;
bFinishedNow = true;GetWorld()->GetTimerManager().ClearTimer(MyTimerHandle);}}

優美版:

void UTypeWriterRTextBlock::UpdateString()
{
	FString StringToShow = MyTextWrapper(C_WrapAt, UsePerCharacter).ToString();

	if (bFinishedNow) {
		SubStringIndex = StringToShow.Len();
	}

	FString SubString = StringToShow.Mid(0, SubStringIndex);

	SetText(FText::FromString(SubString));

	if (SubStringIndex < StringToShow.Len()) {
		SubStringIndex++;
	}
	else {
		bFinished = true;
		bFinishedNow = true;
		GetWorld()->GetTimerManager().ClearTimer(MyTimerHandle);
	}
}

一致的間距、一致的斷行、相同程度的縮排、好理解的命名、較勻散的程式等,能讓你的程式碼更好看,這兩段C++語法的功能一模一樣,而且都能compile。如果改以python來編寫,由於強制一致縮排的特性,上面那段醜陋版,是完全不能執行的。

當你開始熟悉python之後,如果想要編寫出美麗的python代碼,python社群在Python Enhance Protocol第8條(PEP8)底下有列出編寫原則,符合這些原則去編寫,可以讓你的python更加美麗。

PEP8的中文參考資料: https://cflin.com/wordpress/603/pep8-python%E7%B7%A8%E7%A2%BC%E8%A6%8F%E7%AF%84%E6%89%8B%E5%86%8A

另外,在現今優秀的IDE輔助下,通常各程式語言都會有auto-beautify的功能,python也有auto PEP8這樣的功能,快速地讓python程式碼符合PEP8的原則。

Explicit is better than implicit.

明確勝過含糊。

所有的變數、函數、類別、套件等的名稱,都應該明確的定義,讓人一看就猜得出來他的功能為何,以及來自哪裡,如下兩段功能一致的python的程式,前者比後者好。

import random
random.randint(1,3)
from random import *
randint(1,3)

使用from random import *是非常不推薦的作法,也因此我在import的教學章節里,直接跳過這個使用方法。雖然能讓你少打幾個字,但是你就搞不清楚randint來自哪裡。

另外,random.randint這個名字下的很不錯,你一看就猜得出來他大概是用來生產隨機整數的一個函數。

我們在實際寫程式時,要為一大堆的變數命名,你必須有一套好理解,一致的命名規則,如此才能讓整個程式的可讀性提高。

Simple is better than complex.
Complex is better than complicated.

簡單優於複雜,複雜優於錯雜。

越簡單越好,所有的程式碼,都是由很簡單的小元件構成,但是要將每個小元件組合起來,事情開始複雜起來,甚至完全攪再一起,變成錯雜。讓事情盡可能簡單,於此同時,也必須認清,事情不可能完完全全的簡單,無法避免以複雜的解法來處理事情。

Flat is better than nested.

平舖勝過層疊

為了方便管理,我們通常會將事物分類,但是過多的分層歸類,會讓事情更加複雜,難以去取用。在python建立list或是建立module時,請注意這個原則,如以下程式碼:

nested_list = [1, [[2, 3,], 4], 5]
print(nested_list[1][0][1])

flat_list = [1, 2, 3, 4, 5]
print(flat_list[2])

3
3

你可能有些特殊的理由會使用nested_list,請盡量避免,自找麻煩。

此外,我們在整理檔案時,也經常會新增很多資料夾,以及資料夾底下的資料夾等階層結構,雖然可能有實際的需求,但是老話一句,盡量避免。

Sparse is better than dense.

勻散勝過密集。

很多程式猿(包含我)會習慣將程式碼寫的短一點,因此會把很多事情擠在一行程式碼里去處理,這會讓程式碼變得不好理解。

dense:

import pandas as pd

pd.DataFrame(dict(a=[1,2,3],b=[4,5,6])).to_excel('demo.xlsx')

sparse:

import pandas as pd

my_dict = dict(a = [1, 2, 3], b = [4, 5, 6])
df = pd.DataFrame(my_dict)
df.to_excel('demo.xlsx')

這兩段執行一模一樣的事情,依序進行: import pandas套件、產生一個字典、產生一個dataframe、輸出成excel檔。前者將全部塞到一行內處理,後者分成各行處理。後者是比較建議的用法,比較容易看的懂,不是嗎?

有些人認為好的程式碼就是寫得很精簡,吹捧說只要XXX行就能寫出某某程式,我並不認同這個觀點,簡短的程式並沒有比較厲害,容易看懂的程式才比較厲害,你們覺得呢? 當然,有些神人寫得出又精簡又易懂的程式,但是通常較短的程式碼會降低可讀性,兩者在取捨時,應以可讀性為優先考量。

Readability counts.

可讀性很重要。

可讀性很重要,可讀性很重要,可讀性很重要,因為可讀性真的很重要,所以我一再重複。你寫的程式碼,不僅要能使用,也要能看的懂,讓半年後的自己看的懂,讓你的同事看的懂,讓每個人都看得懂。一個沒有人看的懂的程式碼,是沒什麼價值的,因為一旦某些因素而必須更新原始碼時,沒有人能夠處理。The Zen of Python前面每一段其實都是為了讓可讀性提高,Beautiful、Explicit、Simple、Flat、Sparse等原則,都是為了可讀性。也因為python重視這些原則,而使得python成為非常容易理解的程式語言。

Special cases aren't special enough to break the rules.
Although practicality beats purity.

特例不應該特殊到違反這些原則。

雖然實用勝過純粹。

這兩段是互相衝突的,不論多特殊都要符合規範,又說實用性勝過忠於符合規範。在可讀性面前,我們應該盡可能的提高可讀性,即使會讓程式碼變長,要多打很多碼。但是,為了實用,必要時候還是要犧牲可讀性,讓程式能夠加快開發、使功能正常運作,但你要知道你在打破規範。

做事情都遵照規範很好,但缺點就是缺乏彈性,以至於死板,有時候甚至會導致更大的問題。知道何時該打破規範,這是一門學問。

Errors should never pass silently.
Unless explicitly silenced.

錯誤絕對不該安靜的通過。

除非被明確地緘默 。

錯誤絕對不該被放過,如果碰到錯誤,就應該立刻斷掉。

想像以下兩個例子:

(1) 你用一個很大的迴圈重複計算某個複雜的式子,預計要跑上幾十個小時,於是你等隔天上班再來查看結果。隔天早上一檢查時,發現程式碼在你下班後沒多久,就碰到嚴重錯誤而導致完全中斷了,你浪費好幾個小時的運算時間,導致報告無法準時交出。

(2) 你用一個很大的迴圈重複計算某個複雜的式子,預計要跑上幾十個小時,於是你等隔天上班再來查看結果。隔天早上一檢查時,程式碼結果都跑出來了,你興高采烈地將運算結果寫到報告書裡面,然而,運算結果裡面充滿了所多錯誤,你交出了一份錯誤連篇的報告。

這兩個例子都是程式編寫不妥當造成的,但是很明顯,後者的情況會出大事。

你必須知道有那些例外狀況可能會讓程式執行失敗,而且要一一編寫出妥當的處置辦法將錯誤給緘默,如此才能寫出健全的程式。

有關python詳細的錯誤處置,可以查看我上一章的教學,python的嘗試try與錯誤error處理

In the face of ambiguity, refuse the temptation to guess.

在面對含糊時,拒絕猜測的誘惑。

當你跟朋友約好5點碰面,你很直覺的會猜測是下午5點,你不需要再多問一次是早上5點還是下午5點。但是,當你在寫程式時,如果都讓程式去猜測你的設定,那就等同於為自己將來留下除不完的bug。經常碰到的狀況是文件語系編碼encoding的問題,雖然現今大部分都會使用UTF8來編碼,而導致你在讀取、儲存文件時,沒有一一的指定,轉換成UTF8編碼。這時,當某個檔案是CP950時,你就會遇到亂碼,你的程式就會有錯誤,如果你又讓這個bug靜靜地通過的話,就出大事了。

Bonus Tip: 檢查文件的語系編碼可以使用python的chardet套件,字串要長一點比較容易偵測正確。

There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.

應該只有一種最明顯地處理方法。

雖然一開始可能不是很明顯,除非你是荷蘭人。

Python的語法會盡量採用一致的規則,例如python的list、tuple、range、dict都適用相同一套的index slice方法,這使得學習更加容易,也更容易看懂。第二段除非你是荷蘭人可以自動無視,因為python的作者 Guido van Rossum 是荷蘭人,這算是python語言的小小幽默吧?

PS: python的格式化字串違反此一原則,有三種不同的方法,但很明顯,應該使用最新的方法進行格式化字串,最簡便直觀。

這一段讓我聯想到JavaScript有多種不同的自訂函數的方法,光是codecademy的JavaScript 新手教學就有4種不同的方法...

JavaScript:

function squareNum(num){
  return num * num;
}

const squareNum = function(num){
  return num * num;
}

const squareNum = (num) => {
  return num * num;
};

const squareNum = num => num * num;

我完全無法理解這個程式語言為何要這樣設計,連老牌的c++也只有1種定義方式。這種設計方式真的會讓新手(包括我)馬上放棄。

c++:

int squareNum(int num){
    return num * num;
}

c++是strong typed的程式語言,所以都要定義類型。

python的語法:

def squareNum(num):
    return num * num

或許JavaScript的語法設計上有他的獨特的考量在,但我真的比較認同python的設計理念,簡單,單一,大家好學習,好理解。

Now is better than never.
Although never is often better than *right* now.

現在做勝過永遠不做。

雖然永遠不做又勝過"立刻馬上"做。

決定要做,就趕快做,但是要等規劃好怎麼做再做。沒有規劃好就做不如不做。或許這張圖可以很快地告訴你為什麼一定要等規劃好再做:

圖片來源: 截圖自http://themetapicture.com/the-life-of-a-software-engineer/

或許你要寫的程式很簡單,不需要經過縝密的規劃,即使如此,還是建議先寫出虛擬程式碼(pseudocode),再來開始寫程式。

pseudocode不是程式碼,而是以人類最容易讀懂的方式將程式執行的概念寫下來,明確的列出各步驟,是在寫程式前最簡單的規劃。我個人會習慣先用中文或英文,條列式的將每個步驟列出來,如以下是一段剪刀石頭布的pseudocode。

'''
這是剪刀石頭布的pseudocode

目的: 寫出剪刀石頭布的小遊戲

1. 讓玩家選擇輸入,剪刀或石頭或布
    1-1. 如何接收玩家的輸入?
    1-2. 如何儲存玩家的輸入?
    1-3. 檢查玩家的輸入是否合法,如果不合法重新輸入
2. 讓程式碼隨機產生剪刀石頭布
3. 比較玩家輸入和電腦產生的結果
    使用很多個if來比較,如果玩家出石頭,此時如果電腦出布,則...,將所有的比較寫出來
4. 呈現雙方的選項,並呈現結果
5. 結束程式
'''

If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.

如果實作很難解釋,這是個壞點子。

如果實作很容易解釋,這有可能是個好點子。

這兩點可以很快速幫我們判斷一個點子可不可行,如果連pseudocode都寫不出來,那麼請立刻放棄這個方法。同時,儘管你有辦法很清楚解釋如何解決問題,也只是有可能是個好點子,在實作過程中還是會碰到很多窒礙難行的地方。處理任何問題時都可以套用這個概念,如果你難以跟旁人解釋你的解決手段,那麼請立刻換個方法。

Namespaces are one honking great idea -- let's do more of those!

命名空間是很好的點子,我們應該多加使用!

命名空間是一套系統,讓所有python物件的名字都是獨特的,不會重複。在python的程式里,在同一空間底下,重複的命名會覆蓋掉上一個物件,這牽扯到另一個問題,什麼是"同一個空間(scope)"。每一個.py檔是互相隔開的空間,而每個.py檔底下的各個函數或自訂類別,也是互相區隔開來的空間,如下,同個.py檔底下,函數內的變數位在local scope,而函數外的位在global scope,彼此不會互相干擾。

x = 10 # 這是global scope的變數

def test():
    x = 5 #這是local scope的變數
    print(x)

test()
print(x)

5
10

如果沒有命名空間的概念,會覺得這裡應該會出現兩次5,因為執行test()時將x改成5了,但由於這兩個變數位在不同空間底下,所以不會互相干擾。而如果要在local scope中使用global scope的變數時,則要使用global這個關鍵字,如下:

x = 10 # 這是global scope的變數

def test():
    global x #使用global scope的變數
    x = 5 
    print(x)

test()
print(x)

5
5

執行test時將global變數x改成了5,因此出現了兩次5。

另一個關於命名空間的尺度是位在不同.py檔,例如: numpy和random這兩個套件底下都有randint這個function,名字一模一樣,因此在引用套件進來時,盡可能把套件的名字保留下來,如下:

import numpy.random as npn 
import random as ran

print(npn.randint)
print(ran.randint)

<built-in method randint of mtrand.RandomState object at 0x000001AC61AADE58>
<bound method Random.randint of <random.Random object at 0x000001AC5FA97388>>

在使用上可以很清楚知道是在用哪一個randint,而如果你採用from xxx import *這種引用套件的方法,你將套件的命名空間完全移除,這時候你會讓後引用套件的randint,覆蓋掉前一個套件的randint,如下:

from numpy.random import *
from random import *

print(randint)
print(randint)

<bound method Random.randint of <random.Random object at 0x000001AC5FA97388>>
<bound method Random.randint of <random.Random object at 0x000001AC5FA97388>>

你為自己引入了潛在的bug,因此這是非常不建議的寫法。

以上就是python的命名空間,請多加善用。

結語

我花了很大的篇幅逐一介紹Python之禪,希望你們會對你們有所幫助。這段文字曾經給我很大的啟發,也讓我忠於python這個語言,成為python忠誠的傳教士。來吧!來吧!,來學python吧!

這是我的python新手教學系列的最後一篇文章,如果你一路跟隨到了這裡,那你應該已經掌握了python語言的基本操作,接下來依你的需求,再去學習各種套件的使用、各個領域的基本知識,就可以設著寫出一些簡單的程式。最好的進步方法就是不斷的練習,如果不練習,很快就會忘記。而最好的練習題目,就是去做專案,你可以試著將手邊的文書作業改成以python處理,一開始可能會十分痛苦,需要一直google,但是堅持下去,沒有幾天你就會有著顯著的進步,開始能夠寫出一些簡單的小程式,幫助你快速地處理掉手邊的文書作業。

當你能夠熟練地寫出小程式,想要更進一步的學習開發大型專案時,建議你必須再花時間學習python的進階課程。除了這篇新手教學提到的內容外,還有許多較艱深的概念沒有介紹到,如自訂類別(class)、多重繼承(multiple inheritance)、裝飾器(decorator)等,才有辦法更進一步地寫出有彈性、具有高度擴充性的程式碼。

最後,一樣感謝各位耐心地閱讀最後,祝您一切一帆風順,再會~

python新手教學
Why python
Hello Python!
This is a python - 基本數值運算與邏輯判斷
[Python, 的, list, 教學 ]
"python的字串教學"
python的迴圈與流程控制
python的(tuple)與{dict}
def 一個python的自訂函數:
import套件到你的python
python的嘗試try與錯誤error處理
Python之禪

相關文章:

>