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來開發,那感覺就如同一個經驗豐富的大師,手把手盯著你編程 ^^


  1. 先說個新手最容易犯的錯誤,寫代碼時,不要把文件名保存為跟系統(第三方)模塊一樣的名字,比如,寫個 random.py 文件,然後在該文件中導入random模塊時,老是報錯,還一臉莫名其妙
  2. 比較值不用 is,而用 ==
  3. 摒棄C/C++、Java那套編碼思維習慣,Pythonic 的代碼比如:優雅的Python代碼(一),優雅的Python代碼(二)
  4. 按照PEP8規範編寫Python代碼
  5. 使用virtulenv虛擬環境獨立不同應用系統
  6. 記得寫單元測試,大型系統沒有單元測試,寫代碼就跟踩地雷似的
  7. 換個快點的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版) (豆瓣)的第二章。

三、優美的代碼實現

在這一部分,我們會依次討論一些美的代碼。由於內容較多,所以,我進行了簡單地分類,包括:

  1. 內置函數

  2. Python中的一些小細節

  3. 充分使用數據結構的便利性

  4. 合理使用Python的高級並發工具

  5. 巧妙使用裝飾器簡化代碼

  6. 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
")

ConfigParser

上面兩個例子知道的人可能比較多,這個例子知道的人可能就不多了。在大部分服務中,會將如資料庫連接參數這樣的配置,寫到配置文件中,然後使用ConfigParser來管理。連接資料庫的時候,我們可以讀取配置參數,然後生成連接字元串。其實,ConfigParser本身就提供了生成連接字元串的功能,如下所示:

$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么?
看書自學編程書後的習題多數都敲不出來怎麼辦?

TAG:程序員 | Python | 編程 | Python入門 |