怎樣才能寫出 Pythonic 的代碼?
C++ 程序員苦惱中,最近在做 Python,總被人說寫的代碼不夠 Pythonic。
本來我覺得這個問題很簡單,沒有回答的必要,但是,到目前我也沒有看到一個很好的解釋Pythonic的答案。所以,我準備擼袖子自己上。
首先,我們要回答什麼是Pythonic?
我們先來看一下,Python程序員每天津津樂道的the zen of 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!
我簡單翻譯幾句:
優美勝於醜陋
明了勝於晦澀
簡介勝於複雜
……
可讀性很重要
……
難道只有我一個人覺得這是正確的廢話嗎?難道只有我一個人覺得這是正確的廢話嗎?難道只有我一個人覺得這是正確的廢話嗎?
它只告訴你什麼是好,什麼是不好,但是,卻沒有告訴你通往成功彼岸的方法。從這個角度來說,我更推薦:
Pythonic一直以來都是只能意會,不能言傳的東西,然而,為了幫助新同學理解,我準備給出確切的定義:Pythonic就是以Python的方式寫出簡潔優美的代碼!
首先,不管用什麼語言,你都應該努力寫出簡潔優美的代碼,如果不能,那我推薦你看看《重構》和《代碼整潔之道》,雖然這兩本書使用的是java語言,但是,並不影響作者要傳遞的思想。
比如,我們知道,Python裡面的字元串、列表和元組,都稱之為序列,序列支持索引操作和切片操作,切片操作可以指定起點、終點和步長,步長也可以為負數。我們看下面的切片:L = [1, 2, 3, 4, 5, 6, 7, 8, 9]
L[-2::-2]
L[-2:2:-2]
L[2:2:-2]
誰能快速的回答我上面幾個切片的答案?沒有人!所以,Python也有很多語法,但是,並不能亂用。就這個例子來說:
- 在同一個切片操作中,不要同時使用start、end和stride
- 盡量使用stride為正數,且不要帶start和end索引的切割操作
在你已經能夠寫出簡潔優美的代碼的前提下,要寫出Pythonic的代碼,還需要對Python的語言有比較好的了解。我舉幾個在Python裡面與在C、C++和Java裡面有顯著差別的例子。
1. 交換兩個數字在其他語言裡面t = a
a = b
b = t
在Python語言裡面
a, b = b, a
2. 列表推導
列表推導是C、C++、Java裡面沒有的語法,但是,是Python裡面使用非常廣泛,是特別推薦的用法。
與列表推導對應的,還有集合推導和字典推導。我們來演示一下。- 列表:30~40 所有偶數的平方
[ i*i for i in range(30, 41) if i% 2 == 0 ]
- 集合:1~20所有奇數的平方的集合
{ i*i for i in range(1, 21) if i % 2 != 0 }
- 字典:30~40 所有奇數的平方
{ i:i*i for i in range(30, 40) if i% 2 != 0 }
- 當前用戶home目錄下所有的文件列表
[ item for item in os.listdir(os.path.expanduser("~")) if os.path.isfile(item) ]
- 當前用戶home目錄下所有的目錄列表
[ item for item in os.listdir(os.path.expanduser("~")) if os.path.isdir(item) ]
- 當前用戶home目錄下所有目錄的目錄名到絕對路徑之間的字典
{ item: os.path.realpath(item) for item in os.listdir(os.path.expanduser("~")) if os.path.isdir(item) }
3. 上下文管理器
我們要打開文件進行處理,在處理文件過程中可能會出錯,但是,我們需要在處理文件出錯的情況下,也順利關閉文件。
Java風格/C++風格的Python代碼:myfile= open(r"C:miscdata.txt")
try:
for line in myfile:
...use line here...
finally:
myfile.close()
with open(r"C:miscdata.txt") as myfile:
for line in myfile:
...use line here...
這裡要說的是,上下文管理器是Python裡面比較推薦的方式,如果用try...finally而不用with,就會被認為不夠Pythonic。此外,上下文管理器還可以應用於鎖和其他很多類似必須需要關閉的地方。
4. 裝飾器
裝飾器並不是Python特有的,只是,在Python裡面應用非常廣泛,我們來看一個例子。
考慮這樣一組函數,它們在被調用時需要對某些參數進行檢查,在本例中,需要對用戶名進行檢查,以判斷用戶是否有相應的許可權進行某些操作。class Store(object):
def get_food(self, username, food):
if username != "admin":
raise Exception("This user is not allowed to get food")
return self.storage.get(food)
def put_food(self, username, food):
if username != "admin":
raise Exception("This user is not allowed to put food")
self.storage.put(food)
顯然,代碼有重複,作為一個有追求的工程師,我們嚴格遵守DRY(Don』t repeat yourself)原則,於是,代碼被改寫成了這樣:
def check_is_admin(username):
if username != "admin":
raise Exception("This user is not allowed to get food")
class Store(object):
def get_food(self, username, food):
check_is_admin(username)
return self.storage.get(food)
def put_food(self, username, food):
check_is_admin(username)
return self.storage.put(food)
現在代碼整潔一點了,但是,有裝飾器能夠做的更好:
def check_is_admin(f):
def wrapper(*args, **kwargs):
if kwargs.get("username") != "admin":
raise Exception("This user is not allowed to get food")
return f(*arg, **kargs)
return wrapper
class Storage(object):
@check_is_admin
def get_food(self, username, food):
return self.storage.get(food)
@check_is_admin
def put_food(self, username, food):
return storage.put(food)
在這裡,我們使用裝飾器,就可以把參數檢查和業務邏輯完全分離開來,讓代碼顯得更加清晰。這也是比較Pythonic的代碼。
5. 動態類型語言
我們再來看一個例子,該例子充分演示了動態類型語言與靜態類型語言編程之間的差異。
在這個例子中,我們會收到很多不同的請求,對於不同的請求,調用不同的請求處理函數,這個需求如此常見,相信大家應該見過這樣的代碼:if (cmd == "a")
processA()
else if (cmd == "b")
processB()
else if (cmd == "c")
processC()
else if (cmd == "d")
processD()
……
else
raise NotImplementException
在Python裡面,我們可以先判斷一個類,有沒有這個函數,如果有,則獲取這個函數,然後再調用。所以,我們的代碼可以寫成這樣:
class A:
def fetch_func(self, action_name):
func= getattr(self, action_name, None)
return func
def execute(self, action, msg):
func= self.fetch_func(action)
if func is None:
return False, "Action not found"
return func(action, msg)
結論:所謂的Pythonic,其實並沒有大家想的那麼神秘,最終目的都是寫出簡潔優美的代碼。寫出簡潔優美代碼的思想在各個語言中都是一樣的。如果你用其他編程語言寫不出簡潔優美的代碼,那麼,你也沒辦法用Python寫出簡介優美的代碼。如果你能用其他語言寫出很好的代碼,那麼,還是需要了解Python這門語言特有的一些語法和語言特性,充分利用Python裡面比較好語言特性。這樣,就能夠寫出Pythonic的代碼了。
如果你喜歡我這篇回答(那就點贊以示鼓勵),可能對我這篇回答也會感興趣:
怎麼樣才算是精通 Python? - 知乎用戶的回答Python 有哪些優雅的代碼實現?讓自己的代碼更pythonic - 知乎用戶的回答希望對大家有幫助,謝謝。這個問題很好,看似簡單卻很難系統地總結回答,我嘗試用自己現有的理解講一講。
首先,已有各種答案中,提到的所謂——
- 怎樣交換兩個數
- 怎樣寫列表推導
- 用 Context manager
- 用 decorator
- ……
都是「術」,是「授人以魚」中的「魚」,你吃完了,就沒有了。當你用上這些術之後,還怎樣寫「 Pythonic 」的代碼呢?
在我理解,Pythonic 指的是「很 Python 」的 Python 代碼。類比於——
- 「這很知乎」一般用來表示「知乎社區特有的現象」
- 「這很百度」表示「百度公司特有的行為」
「很 Python 」的代碼就是 「 Pythonic 」,也就是說,Python 很明顯區別於其它語言的(&優雅&)寫法。
就如同每個人都不同,你無法見到一個人第一面就特別了解一個人的愛好、性格,但是當你們特別熟悉之後,自然就了解了。初學者(這裡指初學 Python 的人,可能會有或沒有多種其他語言使用經歷)在一開始,應該要接受自己可以不寫 Pythonic 的代碼的設定,先學會基本的使用。
開始 Pythonic 的第一步熟悉 Python 特有語法,可以參考以下官方文檔(對應中文版網上也有):
- The Python2 Language Reference
- The Python3 Language Reference
上面的文檔裡面有大部分各位答主提到的「魚」。
然後是閱讀 PEP 8 -- Style Guide for Python Code ,並在寫代碼的過程中遵守。
接著是不要嘗試用自己的代碼實現已有的標準庫已經實現的功能。比如拼接字元串時,你不應該嘗試自己 for 循環一段一段拼,而是用 str.join 。所以,你需要熟悉標準庫有哪些內容,並對常用的有點大概的印象。這樣至少在需要用到某個東西的時候,大腦中有個關鍵詞供你在 Google/百度 搜索。
- Python2 標準庫
- Python3 標準庫
網上很容易可以找到對應的中文翻譯版文檔,大概地過一遍,只需要有個印象就好,不用一字一句背下來。然後對你認為自己可能會用到的內容,再認真過一遍。
Python 官方有一個「 Python HOWTOs 」文檔,每節文檔覆蓋了一個特定的主題,比上面提到的文檔更詳細、具有參考性。
- Python HOWTOs - Python 2.7.14 documentation
- Python HOWTOs - Python 3.5.4 documentation
這裡面介紹了「兩門 Python 語言之間的遷移」、descriptor、Python 中常用推薦的寫法、日誌、正則、命令行參數等主題,認真學習一遍,對所謂的 Pythonic 是很有幫助的。
最後你就會發現,所謂寫出 Pythonic 的代碼,不是你代碼很炫酷,而是開始對 Python 有一定了解了。
- 《Code Like a Pythonista: Idiomatic Python》
- 《Write Idiom Python》
- 《編碼之前碎碎念(工程實踐) - python-web-guide 0.1 文檔》
- 《分享書籍[writing idiomatic python ebook]》
- 《Python 3 Patterns, Recipes and Idioms》
- 《30個有關Python的小技巧》
- 《Hidden features of Python》
- 《Python程序員的10個常見錯誤》
- 《Python高級編程slide》
- 《Effective Python》
- 《編寫高質量代碼:改善Python程序的91個建議》
- 《The Little Book of Python Anti-Patterns》
1. 如果是寫業務不是寫框架,一般不太提倡使用很多奇淫技巧,維護起來比較費勁。上邊列舉了不少參考資料,如果覺得多可以只看前三個,基本就差不多了,尤其是第一個鏈接,很全面了。(我的理解 pythonic 就是『地道』,用 python 的慣用法而不是其他語言的方式寫 python)
2. 多看看經典代碼倉庫 requests/flask/django 等。
3. 遵守 《PEP 8 – Style Guide for Python Code》, 使用 flake8,pylint 等檢測代碼,很多不符合規範的地方會提示。知乎有些後端代碼倉庫把 pylint 加到了 CI 上,寫糙了不讓通過。
所謂的pythonic,說白了就是python的設計中強調簡潔、方便、內置,相比起其他語言有很多優勢,python程序員應該盡量使用這些簡潔好用的新功能,而非以C++/Java思路來寫Python,反而會造成維護困難、性能下降。Pythonic也只是一些推薦方案,並非強求,挑選自己喜歡的來用就行了。比如:
b, a = a, b #交換
", ".join(items) #join功能
for i, item in enumerate(mylist): #內置遍歷器
[x*2 for x in x_list if x &> 5] #列表生成器
這裡有一些:http://chrisarndt.de/talks/rupy/2008/output/slides.html
0. 使用 in/not in 檢查 key 是否存在於字典
判斷某個 key 是否存在於字典中時,一般初學者想到的方法是,先以列表的形式把字典所有鍵返回,再判斷該key是否存在於鍵列表中:
dictionary = {}
keys = dictionary.keys()
for k in keys:
if key == k:
print True
break
更具Pythonic的用法是使用in關鍵字來判斷 key 是否 存在於字典中:
if key in dictionary:
print True
else:
print False
使用字典的時候經常會遇到這樣一種應用場景:動態更新字典,像如下代碼,如果 key 不在 dictionary 中那麼就添加它並把它對應的值初始為空列表 [] ,然後把元素 append 到空列表中:
dictionary = {}
if "key" not in dictionary:
dictionary["key"] = []
dictionary["key"].append("list_item")
儘管這段代碼沒有任何邏輯錯誤,但是我們可以使用setdefault來實現更Pyhonic的寫法:
dictionary = {}
dictionary.setdefault("key", []).append("list_item")
字典調用 setdefault 方法時,首先檢查 key 是否存在,如果存在該方法什麼也不做,如果不存在 setdefault 就會創建該 key,並把第二個參數[]作為 key 對應的值。
2. 使用 defaultdict() 初始化字典初始化一個字典時,如果初始情況下希望所有 key 對應的值都是某個默認的初始值,比如有一批用戶的信用積分都初始為100,現在想給 a 用戶增加10分
d = {}
if "a" not in d:
d["a"] = 100
d["a"] += 10
同樣這段代碼也沒任何問題,換成更pyhtonic的寫法是:
from collections import defaultdict
d = defaultdict(lambda: 100)
d["a"] += 10
defaultdict 是位於 collections 模塊下,它是 dict 類的子類,語法結構是:
class collections.defaultdict([default_factory[, ...]])
第一個參數default_factory是一個工廠方法,每次初始化某個鍵的時候將會被調用,value就是default_factory返回的值,剩下的參數和dict()函數接收的參數一樣
3. 使用 iteritems() 迭代大數據迭代大數據字典時,如果是使用 items() 方法,那麼在迭代之前,迭代器迭代前需要把數據完整地載入到內存,這種方式不僅處理非常慢而且浪費內存,下面代碼約佔1.6G內存(為什麼是1.6G?可以參考:Python"s underlying hash data structure for dictionaries)
d = {i: i * 2 for i in xrange(10000000)}
for key, value in d.items():
print("{0} = {1}".format(key, value))
而使用 iteritem() 方法替換 items() ,最終實現的效果一樣,但是消耗的內存降低50%,為什麼差距那麼大呢?因為 items() 返回的是一個 list,list 在迭代的時候會預先把所有的元素載入到內存,而 iteritem() 返回的一個迭代器(iterators),迭代器在迭代的時候,迭代元素逐個的生成。
d = {i: i * 2 for i in xrange(10000000)}
for key, value in d.iteritem():
print("{0} = {1}".format(key, value))
大量閱讀他人的高質量的Python代碼,然後模仿
這就像寫毛筆字的描紅,總是從模仿開始的
這也像你最開始會說「我能幫你嗎?」,學了英文一開始說成了「i can help you?」,可是後來你發現其他人都說的都是「can i help you?」,於是你慢慢模仿他們, 自己後來也就說成「can i help you?」了沒讀過這篇http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html (Code Like a Pythonista: Idiomatic Python)顯然你就不pythonic
作者是:
My credentials: I am
- a resident of Montreal,
- father of two great kids, husband of one special woman,
- a full-time Python programmer,
- author of the Docutils project and reStructuredText,
- an editor of the Python Enhancement Proposals (or PEPs),
- an organizer of PyCon 2007, and chair of PyCon 2008,
- a member of the Python Software Foundation,
- a Director of the Foundation for the past year, and its Secretary.
使用類,具體可見 MATLAB 的 plot 和 matplotlib 2.0 的區別。
另外強烈不推薦 [expr for for expr if condition] 的寫法;個人建議 filter map
的確這兩個效率慢,但是 expr for if 和 for if expr 之間的次序不太一致,有時候不是特別好讀另外,個人覺得以 python 的語法特性來說還很難說什麼特別 pythonic 的事情。推薦本書《Python Cookbook》3rd 中文版
這個是國內朋友翻譯的這份文檔也是之前在知乎看到的自己正在看 感覺很值得學習鏈接在這裡:在線閱讀地址:
http://python3-cookbook.readthedocs.org/zh_CN/latest/
中文簡體版PDF下載地址:https://pan.baidu.com/s/1pL1cI9d
謝邀
只要願意,任何人都可以設計一個解釋器/編譯器來支持自己覺得「酷」的語法。所以說到底語法和代碼風格是很膚淺的東西
關注演算法和架構這些決定程序動態行為的更本質的要素,不要太拘泥於Pythonic比如你有一個
lst = [1,2,3,5,6]
現在你想要每個數都加1那怎麼辦呢?
newList = []
for x in lst:
newList.append(x+1)
或者你可以寫的更有簡潔一點
[x+1 for x in lst]
恩,還可以更有biger一點:
map(lambda x: x+1, lst)
比如你突然又想求個和了,機智的你肯定想到了不會再一個一個去加了:
reduce(lambda x, y: x+y, lst, 0)
是不是biger滿滿?
但是其實明明可以直接sum(lst)
的吖!
遵守 PEP 8 -- Style Guide for Python Code
盡量用 Python 標準庫和語法支持的簡單方法解決。[1][1] 意思是,打倒 Java 風格的 OO 垃圾堆。我用專欄里的一篇文章來答題。
專欄鏈接:給妹子講python,歡迎大家關注,提意見!
比如說在迭代循環時使用range、zip和enumerate等常用技巧。
內置函數range:返回一系列連續增加的整數
這個函數產生的連續增加的整數序列,可以作為for循環迭代的索引
for x in range(5):
print(x, end=",")
0,1,2,3,4,
range也可以用在任何需要整數列表的地方。直接列印range函數的返回值是不能直接返回一個整數列表的,如果將其作為一個參數傳給list函數,則可以一次性顯示全部結果。
print(range(5))
range(0, 5)
print(list(range(-5,5)))
[-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]
內置函數range在for循環中是最常用的,它提供了一種簡單的方法,重複特定次數的動作。
當然還可以做一些定製工作,比如在遍歷的過程中跳過一些元素。例如每隔一個元素對字元串進行遍歷:
S = "abcdefghijklmn"
for i in range(0,len(S),2):
print(S[i], end=",")
a,c,e,g,i,k,m,
當然,這只是一個示例,我們之前提到過的分片表達式才是實現該功能的最簡單的方法,這個我們之前是介紹過的:
S = "abcdefghijklmn"
for c in S[::2]:
print(c, end=",")
a,c,e,g,i,k,m,
和文件迭代一樣,這裡range函數的優點也是明顯的,它沒有複製字元串,不會在python中再創建一個字元串列表,這對於很大的字元串來說,會節約不少空間。
zip:並行迭代多個序列
內置zip函數允許我們使用for循環來並行迭代多個序列。zip使用多個序列作為參數,然後返回元組的列表,將這些序列中的並排元素一一配對。
L1 = [1,2,3,4,5]
L2 = ["A","B","C","D","E"]
for t in zip(L1,L2):
print(t, end=",")
(1, "A"),(2, "B"),(3, "C"),(4, "D"),(5, "E"),
和range一樣,zip在遍歷時也是依次按需產生結果,而不是一次性顯示所有結果
L1 = [1,2,3,4,5]
L2 = ["A","B","C","D","E"]
print(zip(L1,L2)) 同樣的,如果想一次性顯示所有結果,則必須將其包含在一個list調用中,以便一次性顯示所有結果 [(1, "A"), (2, "B"), (3, "C"), (4, "D"), (5, "E")] 最後只說明一點,當zip的多個參數長度不同時,zip會以最短序列的長度為準來截斷所得到的元組 [(1, "A"), (2, "B"), (3, "C")] 回顧一下,之前我們談到過,當字典的鍵和值必須在運行時計算產生時,zip函數可以用於產生這樣的字典 {"C": 3, "A": 1, "B": 2} enumerate:同時產生偏移和元素 有時我們在遍歷的時候,既需要偏移值,又需要對應元素,那麼內置函數enumerate就可以實現這個功能。 他在for循環的條件下每輪迭代返回一個包含偏移值和偏移元素的元組:(index,value) (0, "s") (1, "p") (2, "a") (3, "m") 同樣,他也是按需產生,而非一次性產生所有元素的列表 & 就像這樣,多使用便捷的方法,也許就更pythonic了
&
L1 = [1,2,3,4,5]
L2 = ["A","B","C","D","E"]
print(list(zip(L1,L2)))
L1 = [1,2,3,4,5]
L2 = ["A","B","C"]
print(list(zip(L1,L2)))
keys = ["A", "B", "C"]
vals = [1, 2, 3]
D = dict(zip(keys,vals))
print(D)
S = "spam"
for t in enumerate(S):
print(t,end=" ")
S = "spam"
print(enumerate(S))
如果有其他語言基礎,那把其他語言的語法習慣帶過來是很正常的。
我的辦法很簡單,哪裡有最規範的py代碼?自然是標準庫。
嗯,有空就看看吧!- 用你的思路保證你的代碼足夠清楚;
- 向說你不 pythonic 的人請教;
- 對比,擇其善者而從之。
pep8
流暢的python和think python這兩本書,深入了解python特性,了解標準庫
閱讀優秀的源碼,比較python與其他語言。已經寫了接近5年 python 了,來回答一發。很多細節性的東西不做具體描述,看推薦連接的內容比我說來的管用。
首先之前不用 python 的人會把大量的其他語言的習慣帶入 python,第一步要做的就是請記住 python 裡面任何東西都是可以當做對象傳遞的,包括方法包括類。這個特性極為重要。
然後請閱讀下面的知識點,也是我畢業時的 mentor 推薦的一個非常小的 ppt,視頻需要梯子,看不了的話給出了 github 的 ppt。其實視頻完全就解決的題主的問題。
JeffPaine/beautiful_idiomatic_python
寫代碼請遵循 pep8,提交前用 pylint 驗證下自己的代碼
PEP 8 -- Style Guide for Python Code
請使用 ipython,沒事兒就打方法名字加兩個英文問號去看源碼,尤其是 github 上 star 數極多的項目,這個不用可以去看,看自己用到的庫即可。
每隔一段時間,請在 ipython 裡面輸入 import this,用心體會。這真的是 python 聖經,不同時間段閱讀會有新的體驗,而且請看看他的源碼,你會有更奇妙的認識。
最後就寫一段代碼,描述我寫在最上面的 python 所有東西都能傳值的特性。消除大量的 if else
def func_dosomething1(a, b, c=1):
return a
def func_dosomething2(a, b, c):
return b
def func_dosomething3(a, b=5, c=2):
return c
mapper = {
"frontend_want_a_boy": func_dosomething1,
"frontend_want_a_girl": func_dosomething2,
"frontend_want_nothing": func_dosomething3,
}
def handle(frontend_wantsomething, *args, **kwargs):
a = b = c = "for fun"
func = mapper.get(frontend_wantsomething, "frontend_want_nothing")
return func(a=a, b=b, c=c)
終極答案是:看cpython源碼,模仿它來寫。
Python之禪中有一句:
There should be one-- and preferably only one --obvious way to do it. - 找到一種或唯一的一種解決方案去解決問題
這個時候寫的就是所謂的pythonic代碼。
在這之前無非多寫、多看(別人好的代碼)、多想(為什麼別人是寫的代碼更簡練性能更好,我寫的代碼是否可以再優化?)
import this
不必特意pythonic,可以先土點,KISS,工程上也方便後人接手。
以後了解原因後再pythonic,例如:xrange替代range,因為背後的yield generator能節省內存;字元串連接多用join,因為性能較直接+要高,等等。沒必要盲目背誦。推薦閱讀:
※優化 Python 性能:PyPy、Numba 與 Cython,誰才是目前最優秀的 Python 運算解決方案?
※Python 所謂的「閉包」是不是本著故意把人搞暈的態度發明出來的?
※學習python為什麼要在linux下?怎麼學?
※NumPy和MATLAB哪個強大,Numpy能替代MATLAB嗎?
※關於python遞歸的邏輯困惑?