Python 編程,應該養成哪些好的習慣?
我以為編程習慣很重要的,一開始就養成這些習慣,不僅可以提高編程速度,還可以減少 bug 出現的概率。希望各位分享好的編程習慣。
我在模塊還沒認全,元類、類方法和靜態方法的區別、裝飾器寫的還不溜等還不懂的時候,就開始注意讓寫的代碼盡量Pythonic和符合PEP8標準。
到現在,基本手寫的都是符合標準的Python代碼。我寫代碼力求如下幾點:
1. 簡單,調用鏈簡單,不用寫注釋,同事看我代碼,除了我的命名比較簡陋不高大上以外,應該沒有難度。
2. 不炫技。力求粗暴實現功能,只在合適的地方用合適的方法。
3. 不給其他同事機會吐槽。除了變數命名偶爾會被其他同事鄙視以外,一般很少有人對我的代碼抽象能力,性能提供評價意見。
4. 時間允許情況下盡量把代碼寫完美。一般我的代碼都是merge後就不動了,因為沒啥可重構的機會... 除非有BUG。哦,我的BUG數量應該是很少的。
這不是一個自吹自擂的答案。
我和我們組在控制代碼質量上面做了很多工作,上段時間我寫了篇專欄 保證代碼質量二三事 - Python之美 - 知乎專欄 ,BTW,dennis還打賞了5塊錢(òωó?)。
作為一個過來人,我一定要給關注了和看這個問題的同學提點經驗。
首先看一下我剛到豆瓣第二周提的一個PR來給大家樂呵一下:
這個頁面來著豆瓣自己造的團隊協作工具 CODE(Douban CODE)。這個PR包含9百多行代碼,但是其中包含了700多行thirft自動生成的。 看一下評論數 「164」,感受下一個覺得自己代碼寫的不錯的人,被團隊其他人這麼吐,我的心理陰影。
為什麼會造成這麼多呢:
1. 豆瓣有一些工程上的習慣甚至黑魔法,團隊有自己的規範。
2. 代碼寫的不好。
3. 來豆瓣前我是一路自學的野路子,我還算對自己寫的代碼要求高,但是好多細節上既有理解問題也有代碼實現的問題。
那麼,假如你現在對自己很放縱,但是還挺有追求希望去更好的團隊,到更高的平台,防止遇到我們這些人被噴的第二天不想去上班。如下編程習慣一定要有:
1. PEP8。 這個大家都知道了,在我們組,PEP8不過你的PR都基本沒人理,吐都懶得吐。但是PEP8里的要求還挺多,怎麼辦呢?沒辦法,熟能生巧爾。推薦看 PEP8: The Style Guide for Python Code , requests作者做的,更友好和直觀。不熟的時候除了在終端執行下pycodestyle(也就是原來的pep8),還可以給自己的編輯器配上一些自動做PEP8檢查的插件。
2. Pythonic。寫程序最大的特點就是你可以用多種方法實現同一個功能,但是我能分辨出來那個更Pythonic,那個明顯是XX語言轉型過來的程序員寫的。一般只有一種是最優解,代碼最簡練執行效率也最高,這個也沒啥辦法,就是要靠你日常的搜集和自己的不斷提高。我現在翻看2年前的代碼覺得不忍直視,希望2年後看今天自己寫的代碼也有這種感覺 。
3. 搜索能力。你沒有能力和精力完成所有的事情,有些時候需要去找答案,至少是找個靈感。所以學習從Google、Github、Stackoverflow等網站找到自己需要的東西是一個很好的習慣,以後你會感謝自己這個好習慣。
4. 善用Python標準庫。很多功能的最佳實踐其實在標準庫中,不要一味的自己吭哧吭哧造,造多了你會發現很多都無意義,浪費了時間甚至把你帶偏了。
5. 找到一整套完成工作的最佳流程。這個有點大,其實就是配置一個趁手的編輯器,俺強推spacemacs,多花時間用好它;找到一個調試bug的方式,出現問題你要有一系列定位問題,跟蹤問題,解決問題的方案,讓你非常快的完成工作,而不是漫無目的的每次看心情去debug,必須有「套路」。
6. 盡能力熟悉工作中常用的工具和項目。比如用Flask,有空讀讀它的源碼,這樣出了問題,會對你找到原因很有幫助,我現在經常受益於這條。沒看過源碼就敢在產品線用的人都是「銀才」。
歡迎關注本人的微信公眾號獲取更多Python相關的內容(也可以直接搜索「Python之美」):
http://weixin.qq.com/r/D0zH35LE_s_Frda89xkd (二維碼自動識別)
有人提到要嚴格遵照PEP 8規定的Python Coding Style來寫,要多讀英文文檔,多看看一些優秀的庫(比如requests)
其實這一切的一切,用Jetbrain的 PyCharm IDE就好啦:
(Python IDE amp Django IDE for Web developers : JetBrains PyCharm)- 免費,全平台(Win、MacOS、Linux),為Python帶來完整的IDE體驗
- 體驗不輸同門的IntelliJ (IntelliJ是什麼水平呢?對我來說,IntelliJ在寫Java上面的體驗是吊打Eclipse的)
- 自動提示Coding Style:函數之間空兩行,4空格縮進,函數名稱寫法 etc,統統會提示你!比某個PEP8檢查script來的更加方便
- 可以隨意點擊任何函數、某個lib,對所有python內置lib、和已經安裝了的第三方lib都有效,可以迅速打開該第三方函數的實現,方便查看其文檔實現細節(這就不用手動去python lib目錄下一個個去翻了,對於學習優秀的第三方lib特別有幫助)
- 包括有人提到的Unit Test,PyCharm也提供one-click生成test case
總之,我推薦剛入門Python的大家,儘早的使用PyCharm來開發,那感覺就如同一個經驗豐富的大師,手把手盯著你編程 ^^- 先說個新手最容易犯的錯誤,寫代碼時,不要把文件名保存為跟系統(第三方)模塊一樣的名字,比如,寫個 random.py 文件,然後在該文件中導入random模塊時,老是報錯,還一臉莫名其妙
- 比較值不用 is,而用 ==
- 摒棄C/C++、Java那套編碼思維習慣,Pythonic 的代碼比如:優雅的Python代碼(一),優雅的Python代碼(二)
- 按照PEP8規範編寫Python代碼
- 使用virtulenv虛擬環境獨立不同應用系統
- 記得寫單元測試,大型系統沒有單元測試,寫代碼就跟踩地雷似的
- 換個快點的pip源,官方的慢死了
1. 養成看英文文檔、材料的習慣2. 看完 requests 庫作者寫的這份 Python Guide: The Hitchhiker』s Guide to Python!3. 按照這上面的做
忘記Java!忘記Java!忘記Java!!!
&> 每個公共函數有unit tests&> 公共函數和類的名稱準確概括其目的
&> 寫docstring
&> 不寫代碼內注釋(inline comment)&> release代碼里不用print&> 每個函數不超過10行(不包括docstring)虛擬環境、解包、列表推導、迭代器、生成器、裝飾器、抽象基類、靜態方法、類方法、
我這個回答下有很多比較好的技巧,討論如何寫好的代碼,大家可以看一看。
鏈接:Python 有哪些優雅的代碼實現?讓自己的代碼更pythonic - 知乎用戶的回答
一、前言
我前兩天回答了兩個Python相關的問題,收到了很多贊,從答案被收藏的情況來看,確實對不少人都很有幫助,所以我也很開心。我今天準備把這個問題認真回答一下。我會先討論什麼是優美的代碼;然後,我會給出一些我壓箱底的好東西;最後,我會討論怎麼寫出優美的代碼。
二、什麼是優美(優雅)的代碼
什麼是優美或優雅的代碼實現呢?在Python裡面,我們一般稱之為Pythonic。Pythonic並沒有一個確切的定義,一直以來都是只能意會,不能言傳的東西。為了幫助新同學理解,我對Pythonic給出了明確的定義:所謂Pythonic,就是用Python的方式寫出簡潔優美的代碼。關於Pythonic,大家可以看我個問題下的回答怎樣才能寫出pythonic的代碼? - 知乎用戶的回答
有了Pythonic以後,不同的工程師之間,也依然無法對優美的代碼達成一致的意見。因為,美本身是一個主觀感受,每個人對美的感受是不一樣的。比如,有些人覺得湯唯更美,有些人覺得范冰冰最漂亮,還有些人居然喜歡AngelaBaby(一把年紀了叫baby你們不覺得怪嗎?)。而我,依然最喜歡劉濤。我在這篇文章中,會給出很多具體的例子,來說明怎樣寫代碼是"美"的,由於美是一種主觀感受,所以,這裡的回答可能會引起大家的爭議。
另外,在這篇回答中,我們只討論優美的Python代碼實現,並不討論Python中存在的坑。我估計Python裡面有很多坑大家都沒有注意到,比如: &>&>&> a = 4.2
&>&>&> b = 2.1
&>&>&> print a+b == 6.3
False
對於這個坑,如果有特別感興趣的同學,可以看深入理解計算機系統(原書第2版) (豆瓣)的第二章。
三、優美的代碼實現
在這一部分,我們會依次討論一些美的代碼。由於內容較多,所以,我進行了簡單地分類,包括:- 內置函數
- Python中的一些小細節
- 充分使用數據結構的便利性
- 合理使用Python的高級並發工具
- 巧妙使用裝飾器簡化代碼
- Python中的設計模式
3.1 善用內置函數
enumerate類
enumerate是一個類,但是用起來卻跟函數一樣方便,為了表述方便,我們後面統稱為函數。不使用enumerate可能是Python新手最容易被吐槽的地方了。enumerate其實非常簡單,接收一個可迭代對象,返回index和可迭代對象中的元素的組合。對於Python新手,推薦使用ipython(還有bpython和ptpython,感興趣的同學也可以了解一下)互動式地測試各個函數的效果,並且,我們可以在函數後面輸入一個問號,然後回車,就能夠獲得這個函數的幫助文檔了。如下所示: In [1]: enumerate?
Type: type
String Form:&
Namespace: Python builtin
Docstring:
enumerate(iterable[, start]) -&> iterator for index, value of iterable
Return an enumerate object. iterable must be another object that supports
iteration. The enumerate object yields pairs containing a count (from
start, which defaults to zero) and a value yielded by the iterable argument.
enumerate is useful for obtaining an indexed list:
(0, seq[0]), (1, seq[1]), (2, seq[2]), ...
關於enumerate的效果,我們一起來看一下,你就知道為什麼不使用enumerate會被吐槽了。這是不使用enumerate的時候,列印列表中的元素和元素在列表中的位置代碼:
from __future__ import print_function
L = [ i*i for i in range(5) ]
index = 0
for data in L:
index += 1
print(index, ":", data)
這是使用enumerate的Python代碼:
from __future__ import print_function
L = [ i*i for i in range(5) ]
for index, data in enumerate(L):
print(index + 1, ":", data)
這是正確使用enumerate的姿勢:
from __future__ import print_function
L = [ i*i for i in range(5) ]
for index, data in enumerate(L, 1):
print(index, ":", data)
去除import語句和列表的定義,實現同樣的功能,不使用enumerate需要4行代碼,使用enumerate只需要2行代碼。如果想把代碼寫得簡潔優美,那麼,大家要時刻記住:在保證代碼可讀性的前提下,代碼越少越好。顯然,使用enumerate效果就好很多。
reversed對Python熟悉的同學知道,Python中的列表支持切片操作,可以像L[::-1]這樣去reverse列表。如下所示: [1, 2, 3, 4]
&>&>&> for item in L[::-1]:
... print(item)
...
4
3
2
1
與此同時,我們也可以使用內置的reversed函數,如下所示:
&>&>&> for item in reversed(L):
... print(item)
...
4
3
2
1
我的觀點是,L[::-1]不如使用reversed好,因為,L[::-1]是一個切片操作。我們看到這個代碼的第一反應是序列切片,然後才是切片的效果是reverse列表。對於reversed函數,即使是剛接觸Python的同學,也能夠一眼看出來這個函數是要做什麼事情。也就是說,實現同樣的功能,L[::-1]比reversed多繞了一個彎。我們這個問題是如何寫出優美的代碼,而我認為,優美的代碼就應該簡潔、直接、少繞彎。
讀者如果對我這裡的解釋表示懷疑的話,我表示理解。但是,我還是想勸你認可我的說法。因為我認為,不管我們使用代碼還是文字,都是在表達某些東西。而我的表達能力,也是讀研究生以後寫論文鍛鍊出來的。就我目前比大多數人強的表達能力來說,我以我母校的榮譽保證,reversed確實比L[::-1]好。
any
在內置函數中,sort、sum、min和max是大家用的比較多的,也比較熟悉的。像any和all這種函數,是大家都知道,並且覺得很簡單,但是使用的時候就想不起來的。我們來看一個具體的例子。我們現在的需求是判斷MySQL中的一張表是否存在主鍵,有主鍵的情況,如下所示: mysql&> show index from t;
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| t | 0 | PRIMARY | 1 | id | A | 0 | NULL | NULL | | BTREE | | |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
1 row in set (0.00 sec)
我們再來看一個沒有主鍵的例子,如下所示:
mysql&> show index from t;
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| t | 0 | id | 1 | id | A | 0 | NULL | NULL | | BTREE | | |
| t | 1 | idx_age | 1 | age | A | 0 | NULL | NULL | YES | BTREE | | |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
2 rows in set (0.00 sec)
在這個沒有主鍵的例子中,雖然沒有顯示定義主鍵,但是,它有一個非空的唯一索引。在InnoDB中,如果存在非空的唯一約束,那麼,這一列將會被當作主鍵。綜合前面兩種情況的輸出,我們知道,我們要判斷一張表是否存在主鍵,我們不能通過是否存在一個key_name名為PRIMARY的索引來判斷,而應該通過Non_unique為0和Null列不為YES來判斷。說完了需求,我們來看一下具體的實現。使用pymysql連接資料庫,資料庫中的每一行,將會以元組的形式返回,如下所示:
(("t", 0, "PRIMARY", 1, "id", "A", 0, None, None, "", "BTREE", "", ""),)
也就是說,我們現在要遍歷一個二維的元組,然後判斷是否存在Non_unique為0,Null列不為YES的記錄。詳細了解了具體實現以後,我們寫下了下面的代碼:
def has_primary_key():
for row in rows:
if row[1] == 0 and row[9] != "YES":
return True
return False
非常的簡單,但是,如果我們使用any函數的話,代碼將會更短。如下所示:
def has_primary_key():
return any(row[1] == 0 and row[9] != "YES" for row in rows):
從這一節大家可以看到,即使內置函數這麼簡單的知識,我們也要充分掌握,靈活使用,才能夠寫出優美的代碼。
3.2 Python中的小細節
這一節我們來看3個很小的知識點。
raise SystemExit假設你現在要實現一個需求,在程序檢測到某種錯誤的時候,列印錯誤信息,並退出程序。在Python中,我們可以是SystemExit,如下所示: import sys
sys.stderr.write("It failed!
")
raise SystemExit(1)
但是,你其實可以直接這麼用的:
raise SystemExit("It failed!")
後面的這個操作會直接將信息列印到標準錯誤輸出,然後使用退出碼為1來退出程序,以表示程序沒有正常退出。
文件的x模式大家應該知道,如果我們以w模式打開一個文件進行寫入的話,文件的內容將會被我們覆蓋掉。假設你現在有這樣一個需求:寫一個文件,如果該文件已經存在,則不寫。實現方式也很簡單,我們先判斷一下文件是否存在,如果已經存在,則列印提示信息並跳過,否則,我們就以w模式打開文件,然後寫入內容。如下所示: &>&>&> import os
&>&>&> if not os.path.exists("somefile"):
... with open("somefile", "wt") as f:
... f.write("Hello
")
... else:
... print("File already exists!")
...
File already exists!
如果我們使用x模式的話,代碼能夠好看很多,如下所示:
&>&>&> with open("somefile", "xt") as f:
... f.write("Hello
")
$cat db.conf
[DEFAULT]
conn_str = %(dbn)s://(%user)s:%(pw)s@%(host)s:%(port)s/%(db)s
dbn = mysql
user = root
pw = root
host = localhost
port = 3306
db = test
這裡給出了幾個Python中的小細節,可能很多人會覺得沒啥用,又或者大家其實已經知道了。但是,我還是把這一節放上來了,只要對一個人有用,那麼,這就是有意義的。
3.3 合理使用數據結構
這一節中,我們會討論Python中的部分數據結構的用法,可能是對大家最有用的章節。
字典的get可以傳遞默認值很多人是這麼給參數賦默認值的: port = kwargs.get("port")
if port is None:
port = 3306
其實,我們完全不用這麼麻煩,因為,字典的get方法支持提供默認參數,在字典沒有值的情況下,將會返回用戶提供的默認參數,所以,優美的代碼應該是這樣的:
port = kwargs.get("port", 3306)
我們再來看一個類似的例子,獲取元素且刪除:
L = [1, 2, 3, 4]
last = L[-1]
L.pop()
這裡,我們又多此一舉了。因此,在調用L.pop()函數的時候,本身就會返回給我們需要pop的數據。也就是說,我們可以這樣:
last = L.pop()
defaultdict Counter
我們來看兩個不是Python內置的數據類型,而是Python標準庫裡面的例子。假設我們的字典中的value是一個list。大家知道,list也是元素的集合。所以,我們會見到很多下面這樣的代碼,先判斷key是否已經存在,如果不存在,則新建一個list並賦值給key,如果已經存在,則調用list的append方法,將值添加進去。 d = {}
for key, value in pairs:
if key not in d:
d[key] = []
d[key].append(value)
如果我們知道defaultdict,那就不用這麼麻煩了,如下所示:
d = defaultdict(list)
for key, value in pairs:
d[key].append(value)
我們接著上面的defaultdit的例子來討論。現在有一個需求,需要統計一個文件中,每個單詞出現的次數。很多新同學拿到這個題目,首先想到的是使用字典,所以,寫出來下面這樣的代碼:
d = {}
with open("/etc/passwd") as f:
for line in f:
for word in line.strip().split(":"):
if word not in d:
d[word] = 1
else:
d[word] += 1
如果我們使用前面介紹的defaultdict,代碼能夠減少3行,會被認為更加Pythonic。如下所示:
d = defaultdict(int)
with open("/etc/passwd") as f:
for line in f:
for word in line.strip().split(":"):
d[word] += 1
對於這個問題,其實還有一個更好的解決辦法,使用collections中的Counter。如下:
word_counts = Counter()
with open("/etc/passwd") as f:
for line in f:
word_counts.update(line.strip().split(":"))
可以看到,使用Counter以後,我們的代碼更加短小了。我們先把代碼重8行重構到5行,然後又重構到4行。記住我前面給出的實踐方法:要想把代碼寫得優美,在保證可讀性的前提下,代碼越短越好。對於這個問題,使用Counter還有其他的一些理由,那就是其他相關的需求。比如,現在還有第二個需求,列印出現次數最多的三個單詞。如果我們使用字典,那麼,我們需要這樣:
result = sorted(zip(d.values(), d.keys()), reverse=True)[:3]
for val, key in result:
print(key, ":", val)
使用Counter就簡單了,因為Counter直接就為我們提供了相應的函數,如下所示:
for key, val in (word_counts.most_common(3)):
print(key, ":", val)
是不是代碼更短,看起來更加清晰呢。而且,統計每個單詞出現的次數和出現次數最多的單詞,這兩個需求相關性實在是太強了,幾乎會同時出現。所以,我們使用了Counter模塊和該模塊的most_common方法。如果Counter沒有提供這個方法,那才是要被吐槽的!
這個例子就充分說明了,善用標準庫的重要性。我們再來看一個標準庫的例子。
nametuple我們要寫一個監控系統,該系統要監控主機的方方面面。當然,也包括磁碟相關的監控。我們可以從/proc/diskstats中獲取磁碟的詳細信息。/proc/diskstats 的內容像下面這樣 """
https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats
What: /proc/diskstats
The /proc/diskstats file displays the I/O statistics
of block devices. Each line contains the following 14
fields:
1 - major number
2 - minor mumber
3 - device name
4 - reads completed successfully
5 - reads merged
6 - sectors read
7 - time spent reading (ms)
8 - writes completed
9 - writes merged
10 - sectors written
11 - time spent writing (ms)
12 - I/Os currently in progress
13 - time spent doing I/Os (ms)
14 - weighted time spent doing I/Os (ms)
"""
$ cat /proc/diskstats
254 0 vda 24471 251 818318 33200 37026 64382 2289256 394516 0 24868 427704
254 1 vda1 24143 229 815530 33156 36072 64382 2289256 394428 0 24748 427572
254 32 vdc 614 0 22180 408 51203 2822 1857922 1051716 0 40792 1052064
/proc/diskstats文件有很多列,如果我們使用下標訪問的話,肯定需要藉助我們的手指頭。並且,也不一定能數清楚。就算你算術特別厲害,能夠輕易地數清楚,如果下一個人要來修改你寫的代碼,對他來說,將會是一個噩夢。
作為一個有追求的程序員,我們當然要尋找更好的辦法。對於這個問題,我們可以使用Python中的命名元組,也就是collections中的namedtuple。我們定義命名元組: DiskDevice = collections.namedtuple("DiskDevice", "major_number minor_number device_name read_count read_merged_count"
" read_sections time_spent_reading write_count write_merged_count "
"write_sections time_spent_write io_requests time_spent_doing_io"
" weighted_time_spent_doing_io")
有了命名元組以後,如果我們要返回某個磁碟的請求數據,就返回一個命名元組。調用者通過該命名元組,就能夠通過屬性的方式,而不是下標的方式訪問各個欄位。獲取磁碟監控的代碼如下:
def get_disk_info(disk_name):
with open("/proc/diskstats") as f:
for line in f:
if line.split()[2] == disk_name:
#返回給調用者的是一個命名元祖(namedtuple)
return DiskDevice(*(line.split()))
3.4 使用高級並發工具
數據結構先講到這邊,我們來看一個並發的例子。即生產者和消費者模型。分別創建生產者和消費者,生產者向隊列中放東西,消費者從隊列中取東西。創建一個鎖來保證線程間操作的互斥性,當隊列滿的時候,生產者進入等待狀態,當隊列空的時候,消費者進入等待狀態。下面是一個簡單的實現Producer-consumer problem in Python: from threading import Thread, Condition
import time
import random
queue = []
MAX_NUM = 10
condition = Condition()
class ProducerThread(Thread):
def run(self):
nums = range(5)
global queue
while True:
condition.acquire()
if len(queue) == MAX_NUM:
print "Queue full, producer is waiting"
condition.wait()
print "Space in queue, Consumer notified the producer"
num = random.choice(nums)
queue.append(num)
print "Produced", num
condition.notify()
condition.release()
time.sleep(random.random())
class ConsumerThread(Thread):
def run(self):
global queue
while True:
condition.acquire()
if not queue:
print "Nothing in queue, consumer is waiting"
condition.wait()
print "Producer added something to queue and notified the consumer"
num = queue.pop(0)
print "Consumed", num
condition.notify()
condition.release()
time.sleep(random.random())
ProducerThread().start()
ConsumerThread().start()
對於這一類同步問題,其實,我們應該直接使用Queue。Queue提供了線程安全的隊列,特別適合用來解決生產者和消費者問題。這裡我再強調一下,Queue本身是線程安全的,而且支持阻塞讀、阻塞寫。如下所示:
from threading import Thread
import time
import random
from Queue import Queue
queue = Queue(10)
class ProducerThread(Thread):
def run(self):
nums = range(5)
global queue
while True:
num = random.choice(nums)
queue.put(num)
print "Produced", num
time.sleep(random.random())
class ConsumerThread(Thread):
def run(self):
global queue
while True:
num = queue.get()
queue.task_done()
print "Consumed", num
time.sleep(random.random())
ProducerThread().start()
ConsumerThread().start()
可以看到,使用Queue以後,代碼量少了很多,可維護性也強了不少。當然了,我這裡只是舉了一個特別簡單的例子,只是想說明,我們應該使用高級的並發工具。
我們再來看一個並發的例子。很多時候,我們要用並發編程,並不用自己手動啟動一個線程或進程,完全可以使用Python提供的並發工具,如下所示:
內置的map是單線程運行的,如果涉及到網路請求或者大量的cpu計算,則速度相對會慢很多,因此, 出現了並發的map,如下所示: import requests
from multiprocessing import Pool
def get_website_data(url):
r = requests.get(url)
return r.url
def main():
urls = ["http://www.google.com",
"http://www.baidu.com",
"http://www.163.com"]
pool = Pool(2)
print pool.map(get_website_data, urls)
main()
為了與線程兼容,該模塊還提供了multiprocessing.dummy,用以提供線程池的實現,如下所示:
from multiprocessing.dummy import Pool
使用Pool,我們可以快速的在線程池和進程池之間來回切換,最重要的是,使用高級的並發工具不那麼容易出錯。
3.5 使用裝飾器
裝飾器可能是Python面試中被問的最多的知識了。之所以如此頻繁的出現,是因為,裝飾器確實是個好東西。舉個例子,我們有兩個模塊,A模塊要發消息給B模塊,B模塊檢查A模塊發送過來的參數,沒有問題就進行處理,對於檢查參數這個操作,如果我們使用裝飾器的話,代碼將會是下面這樣: import inspect
import functools
def check_args(parameters):
"""check parameters of action"""
def decorated(f):
"""decorator"""
@functools.wraps
def wrapper(*args, **kwargs):
"""wrapper"""
func_args = inspect.getcallargs(f, *args, **kwargs)
msg = func_args.get("msg")
for item in parameters:
if msg.body_dict.get(item) is None:
return False, "check failed, %s is not found" % item
return f(*args, **kwargs)
return wrapper
return decorated
使用的時候:
class AsyncMsgHandler(MsgHandler):
@check.check_args(["ContainerIdentifier", "MonitorSecretKey", "InstanceID", "UUID"])
def init_container(self, msg):
pass
這樣很多好處,例如,我們不用為不同的消息,編寫不同的參數檢查函數。我們將參數檢查和業務邏輯完全分離,使得代碼更加簡潔明了。當然,裝飾器還有各種用處,我就不多說了,感興趣的同學可以看我以前的一篇博客python裝飾器入門與提高。
3.6 Python中的設計模式
設計模式本來是與編程語言不相關的,屬於程序架構的範疇。但是,有些設計模式,在Python中,完全可以使用Python的一些特性,實現得更加簡潔優美。
單例模式模式是最簡單也比較常用的一種模式,所謂單例模式,就是要保證系統中一個類只有一個實例。我們來看一個典型的單例實現: public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上面這段java代碼大家可以輕易地轉換成Python的實現,然而,並不Pythonic。Python Cookbook 中文版,第 3 版 (豆瓣)中給了一種思路,提供了一個Borg類,只要繼承該類,就會成為單例。
class Borg:
_shared_state = {}
def __init__(self):
self.__dict__ = self.__shared_state
這個實現的思路是"實例的唯一性並不是重要的,我們應該關注的是實例的狀態,只要所有的實例共享狀態,行為一致,那就達到了單例的目的"。通過Borg模式,可以創建任意數量的實例,但因為它們共享狀態,從而保證了行為一致。
對Python的模塊熟悉的人會知道,在Python中,模塊只初始化一次,所有變數歸屬於某個模塊,import機制是線程安全的。所以,模塊本身是天然的單例實現。因此,我們可以藉助模塊來實現單例模式。
下面這個例子創建了一個函數,令其返回含有貨幣匯率的字典,這個函數可以被多次調用,但是,大部分情況下,匯率數據只獲取一次就夠了,無須每次調用時都獲取一遍。 def get():
if not get.rates:
_URL = "http://www.bankofcanada.ca/stats/assets/csv/fx-seven-day.csv"
with urllib.request.urlopen(_URL) as file:
for line in file:
# get.rates[key] = float(data)
pass
return get.rates
get.rates = {}
在這段代碼中,我們創建了名為rates的字典,用於保存私有數據,並將該字典設置成Rates.get()函數的屬性。第一次執行公開的get()函數時,會下載全新的匯率數據;其他時候,只需要把最近下載的那份數據返回就行了。儘管沒有引入類,但我們依然把匯率數據做成了"單例數據值"。大家可以看到,在Python中,單例可以直接使用模塊來實現,非常的簡單。
工廠模式我們再來看另外一個非常常用的設計模式,工廠模式。下面這段代碼是C++編程思想 (豆瓣)中的工廠模式: class Shape
class Circle: public Shape
class Square: public Shape
Shape* Shape::factory(const string type)
{
if (type == "Circle")
{return new Circle;}
if (type == "Square")
{return new Square;}
}
說實話,實現得非常好,可讀性很強。我們再看一下,同樣的需求,在Python中如何實現:
class Shape:pass
class Circle(Shape):pass
class Square(Shape):pass
for name in ["Circle", "Square"]:
cls = globals()[name]
obj = cls()
要理解Python中的工廠模式,關鍵是要知道,Python中的類也是可調用的對象。而且,我們import以後,存在於當前的命名空間中,所以,我們可以先通過名字獲取到"類",再用類構造出對象。比較這裡的C++實現和Python實現可以發現,當我們需要新增一個圖形的時候,C++的實現需要去修改Shape::factory函數,Python就不需要這麼麻煩,也就是說,Python版本的工廠模式比C++版本的工廠模式少了一個需要維護的函數。從這個角度來說,Python程序員是幸福的。
四、如何寫出優美(優雅)的代碼
關於這個問題,我想說的還有很多,然而,我編輯了好幾天也沒有編輯完。總是有個結束的時候,那就到此為止吧。關於如何能夠寫出Pythonic的代碼,我的觀點是:不管用什麼語言,你都應該努力寫出簡潔優美的代碼。如果不能,那我推薦你看看《重構》和《代碼整潔之道》。雖然這兩本書使用的是java語言,但是,並不影響作者要傳遞的思想。
此外,我也有一些經驗傳授給大家,希望能夠幫助新同學快速地寫出還不錯的代碼。
像寫報紙一樣寫代碼
準確無歧義 完整無廢話 注意排版以引導讀者 注意標點符號以幫助讀者 保證可讀性的前提下,代碼儘可能短小
其中,"保證代碼可讀性的前提下,代碼儘可能短小",這個我已經反覆強調,相信大家已經有點感覺了。我們來看一個"注意標點符號以幫助讀者",在Python裡面,空行是會被忽略的,也就說,有沒有空行,有多少空行,對Python來說都是一樣的,但是,對程序員來說可就不一樣了,我們來看一個例子。隨機生成1000個0~999之間的整數,然後求他們的和。
這裡是沒有空行的例子: import random
def sum_num(num, min_num, max_num):
i = 0
data = []
for i in range(num):
data.append(random.randint(min_num, max_num))
total = 0
for item in data:
total += item
return total
if __name__ == "__main__":
print sum_num(1000, 0, 1000)
import random
def sum_num(num, min_num, max_num):
i = 0
data = []
for i in range(num):
data.append(random.randint(min_num, max_num))
total = 0
for item in data:
total += item
return total
if __name__ == "__main__":
print sum_num(1000, 0, 1000)
可以看到,有空行的代碼,明顯看起來更加舒適愉悅。在大家用漢語寫作文的時候,老師一直教導我們要合理的使用標點符號,合理的分段。其實,寫代碼和寫文章是一樣的。在代碼中,大家可以這樣想像:換行是逗號,空一行是句號,空兩行是分段。至於逗號,由於我們總是在一行中寫一條語句,所以,逗號是可以忽略的,如果你在一行中寫了多條語句,就好比在寫作文的時候沒有正確的使用逗號,也讓人難以理解。如果你從來不空行,所有代碼糾纏在一起,就好比沒有句號,讓人讀起來很累,同理,不分段也不是 好習慣。
在這篇回答中,我給出了很多Python優雅實現的例子,然後,討論了如何才能寫出優美的代碼,希望對大家有幫助。
如果你喜歡我這篇回答(那就點贊以示鼓勵),可能對我這篇回答也會感興趣:1. 怎麼樣才算是精通 Python? - 知乎用戶的回答2. 怎樣才能寫出pythonic的代碼? - 知乎用戶的回答import this
不光是上面一些大牛們說的那些基本的縮進,自動化測試之類的
更重要的還是寫出Python的特色來。PyConChina2014 杭州場 @施遠敏 分享的主題
idiomatic.py —— 如何寫一個具有高B格的Python代碼原文(牆外):
https://docs.google.com/presentation/d/1Mer-SFLtELLtmS_QxLWbW1aEDX997JSN6eD3mCyV81k/edit#slide=id.g475844c86_0333譯文(個人翻譯):
轉載: idiomatic.py都是一些小的細節, 但是正是這些細節體現了Py的與眾不同謹慎使用鴨子類型,對於作為函數參數傳進來的對象(尤其是自定義的類的實例),使用之前最好用type/isinstance方法做一下類型檢查,或者Python3的 Function Annotations 功能做一下注釋。
這樣既增加了代碼的可讀性,也使得IDE(比如Pycharm)更容易理解你的代碼,提供更準確的錯誤提示,代碼跳轉和自動補全。1,隨時開著一個解釋器待用。忘了api直接dir+help,忘了用法直接在解釋器裡面進行試驗。
充分利用python強大的自省能力,不僅能讓初學者儘快入門,對日後使用也很有幫助。2,import大法好。遇到用python處理某一類比較通用且複雜的問題,先去搜一下有沒有現成的輪子:安裝包自帶的有很多,github上也活躍著很多項目。
講的雖然都不是具體的編程習慣,但都是我的切身體會。上面幾點都是我喜歡python的原因。(^_^)過來人告訴你,養成寫測試代碼的習慣絕對是一個聰明的選擇。
牢記pep8,牢記the zen of python。
多寫注釋!多寫文檔!
(?_?)There should be one-- and preferably only one --obvious way to do it.所以良好習慣就是,在StackOverflow上搜索遇到的問題,然後把被採納的答案背下來,以後遇到這種問題都用它。
最好先看完PEP8 code style再開始編碼。不然老員工code reviews的時候看到新人奇葩的風格就想撕逼了噠
縮進
隨身攜帶遊標卡尺
web掏糞新手。只有兩點給題主:1.用pycharm編輯器2.《代碼大全》這本書,能擼多少次擼多少次以上~
推薦閱讀:
※為什麼玩C++的都喜歡調用別人的庫?
※寫代碼很厲害是怎樣一種體驗?
※當碼農難道不需要情商嗎?
※單片機編程能用MAC么?
※看書自學編程書後的習題多數都敲不出來怎麼辦?