1. 程式人生 > >如何像Python高手(Pythonista)一樣程式設計

如何像Python高手(Pythonista)一樣程式設計

最近在網上看到一篇介紹Pythonic程式設計的文章:Code Like a Pythonista: Idiomatic Python,其實作者在2006的PyCon會議後就寫了這篇文章,寫這篇文章的主要原因是作者發現很多有經驗的Pythoner寫出的程式碼不夠Pythonic。我覺得這篇文章很不錯,所以將它用中文寫了下來(不是逐字的翻譯,中間加了一些自己的理解),分享給大家。另:由於本人平時時間有限,這篇文章翻譯了比較長的時間,如果你發現了什麼不對的地方,歡迎指出。。

  一、Python之禪(The Zen of Python) 

  The Zen of Python是Python語言的指導原則,遵循這些基本原則,你就可以像個Pythonista一樣

程式設計。具體內容你可以在Python命令列輸入import this看到:

The Zen of Python, by Tim Peters

Beautiful is better than ugly.
# 優美勝於醜陋(Python以編寫優美的程式碼為目標)

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.
# 不要包容所有錯誤,除非你確定需要這樣做(精準地捕獲異常,不寫except:pass風格的程式碼)

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.
# 雖然這並不容易,因為你不是 Python 之父(這裡的Dutch是指Guido)
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背後的哲學真理。Python之禪已經正式成文PEP 20,具體內容見:PEP 20

  二、PEP8: Python編碼規範(PEP8: Style Guide for Python Code) 

  Abelson & Sussman在《計算機程式的構造和解釋》一書中說道:程式是寫來給人讀的,只是順帶讓機器執行。所以,我們在編碼時應該儘量讓它更易讀懂。PEP8是Python的編碼規範,官方文件見:PEP 8,PEP是Python Enhancement Proposal的縮寫。PEP8包括很多編碼的規範,下面主要介紹一下縮排和命名等內容。

  空格和縮排(WhiteSpace and Indentation)

  空格和縮排在Python語言中非常重要,它替代了其他語言中{}的作用,用來區分程式碼塊和作用域。在這方面PEP8有以下的建議:

1、每次縮排使用4個空格
2、不要使用Tab,更不要Tab和空格混用
3、兩個方法之間使用一個空行,兩個Class之間使用兩個空行
4、新增一個空格在字典、列表、序列、引數列表中的“,“後,以及在字典中的”:“之後,而不是之前
5、在賦值和比較兩邊放置一個空格(引數列表中除外)
6、緊隨括號後面或者引數列表前一個字元不要存在空格

  Python命名

  命名規範是程式設計語言的基礎,而且大部分的規範對於高階語言來說都是一樣的,Python的基本規範如下:

1、方法 & 屬性:joined_lower
2、常量:joined_lower or ALL_CAPS
3、類:StudlyCaps
4、類屬性:interface, _internal, __private
5、camelCase only to conform to pre-existing conventions

  以上內容只是對PEP8做了非常簡單的介紹,由於今天的主題不在於此,所以就不在這裡多講。想要更加深入的瞭解Python編碼規範,可以閱讀PEP8官方文件Google Python編碼規範等內容。

  三、交換變數值(Swap Values)

  在其他語言中,交換兩個變數值的時候,可以這樣寫:

temp = a
a = b
b = temp

  在Python中,我們可以簡單的這樣寫:

b, a = a, b

  可能你已經在其他地方見過這種寫法,但是你知道Python是如何實現這種語法的嗎?首先,逗號(,)是Python中tuple資料結構的語法;上面的語法會執行一下的操作:

  1、Python會先將右邊的a, b生成一個tuple(元組),存放在記憶體中;

  2、之後會執行賦值操作,這時候會將tuple拆開;

  3、然後將tuple的第一個元素賦值給左邊的第一個變數,第二個元素賦值給左邊第二個變數。

  再舉個tuple拆分的例子:

In [1]: people = ['David', 'Pythonista', '15145551234']

In [2]: name, title, phone = people

In [3]: name
Out[3]: 'David'

In [4]: title
Out[4]: 'Pythonista'

In [5]: phone
Out[5]: '15145551234'

  這種語法在For迴圈中非常實用:

In [6]: people = [['David', 'Pythonista', '15145551234'], ['Wu', 'Student', '15101365547']]

In [7]: for name, title, phone in people:
   ...:     print name,  phone
   ...:     
David 15145551234
Wu 15101365547

  PS:在使用這種語法時,需要確保左邊的變數個數和右邊tuple的個數一致,否則,Python會丟擲ValueError異常。

  更多tuple的例子:

>>> 1,
(1,)
>>> (1,)
(1,)
>>> (1)
1
>>> value = 1,
>>> value
(1,)

  我們知道:逗號(,)在Python中是建立tuple的構造器,所以我們可以按照上面的方式很方便的建立一個tuple;需要注意的是:如果宣告只有一個元素的tuple,末尾必須要帶上逗號,兩個以上的元素則不需要。宣告tuple的語法很簡單,但同時它也比較坑:如果你發現Python中的變數不可思議的變成了tuple,那很可能是因為你多寫了一個逗號。。

  四、Python控制檯的"_"(Interactive "_")

  這是Python中比較有用的一個功能,不過有很多人不知道(我也是接觸Python很久之後才知道的)。。在Python的互動式控制檯中,當你計算一個表示式或者呼叫一個方法的時候,運算的結果都會放在一個臨時的變數 _ 裡面。_(下劃線)用來儲存上一次的列印結果,比如:

>>> import math
>>> math.pi / 3
1.0471975511965976
>>> angle = _
>>> math.cos(angle)
0.50000000000000011
>>> _
0.50000000000000011

  PS:當返回結果為None的時候,控制檯不會列印,_ 裡面儲存的值也就不會改變。

  五、合併字串(Building Strings from Sub strings)

  假如現在有一個list,裡面是一些字串,你現在需要將它們合併成一個字串,最簡單的方法,你可以按照下面的方式去處理:

colors = ['red', 'blue', 'green', 'yellow']

result = ''
for s in colors:
    result += s

  但是,很快你會發現:這種方法非常低效,尤其當list非常大的時候。Python中的字串物件是不可改變的,因此對任何字串的操作如拼接,修改等都將產生一個新的字串物件,而不是基於原字串。所以,上面的方法會消耗很大的記憶體:它需要計算,儲存,同時扔掉中間的計算結果。正確的方法是使用Python中的join方法:

result = ','.join(colors)

  當合並元素比較少的時候,使用join方法看不出太大的效果;但是當元素多的時候,你會發現join的效率還是非常明顯的。不過,在使用的時候請注意:join只能用於元素是字串的list,它不會進行任何的強制型別轉換。連線一個存在一個或多個非字串元素的list時將丟擲異常。

  六、使用關鍵字in(Use in where possible)

  當你需要判斷一個KEY是否在dict中或者要遍歷dict的KEY時,最好的方法是使用關鍵字in:

d = {'a': 1, 'b': 2}
if 'c' in d:
    print True
# DO NOT USE
if d.has_key('c'):
    print True

for key in d:
    print key
# DO NOT USE
for key in d.keys():
    print key

  Python的dict物件是對KEY做過hash的,而keys()方法會將dict中所有的KEY作為一個list物件;所以,直接使用in的時候執行效率會比較快,程式碼也更簡潔。

  七、字典(Dictionary)

  dict是Python內建的資料結構,在寫Python程式時會經常用到。這裡介紹一下它的get方法和defaultdict方法。

  1、get

  在獲取dict中的資料時,我們一般使用index的方式,但是如果KEY不存在的時候會丟擲KeyError。這時候你可以使用get方法,使用方法:dict.get(key, default=None),可以避免異常。例如:

d = {'a': 1, 'b': 2}
print d.get('c')        # None
print d.get('c', 14)    # 14

  2、fromkeys

  dict本身有個fromkeys方法,可以通過一個list生成一個dict,不過得提供預設的value,例如:

# ⽤序列做 key,並提供預設value
>>> dict.fromkeys(['a', 'b', 'c'], 1)
# {'a': 1, 'c': 1, 'b': 1}

  3、setdefault

  有些情況下,我們需要給dict的KEY一個預設值,你可以這樣寫:

equities = {}
for (portfolio, equity) in data:
    if portfolio in equities:
        equities[portfolio].append(equity)
    else:
        equities[portfolio] = [equity]

  上面的實現方式很麻煩,使用dict的setdefault(key, default)方法會更簡潔,更效率。

equities = {}
for (portfolio, equity) in data:
    equities.setdefault(portfolio, []).append(equity)

  setdefault方法相當於"get, or set & get",或者相當於"set if necessary, then get"

  八、defaultdict

  defaultdict是Python2.5之後引入的功能,具體的用法我已經在另外一篇文章中詳細介紹:Python的defaultdict模組和namedtuple模組

  九、字典的組裝和拆分(Building & Splitting Dictionaries)

  在Python中,你可以使用zip方法將兩個list組裝成一個dict,其中一個list的值作為KEY,另外一個list的值作為VALUE:

>>> given = ['John', 'Eric', 'Terry', 'Michael']
>>> family = ['Cleese', 'Idle', 'Gilliam', 'Palin']
>>> pythons = dict(zip(given, family))
>>> print pythons
{'John': 'Cleese', 'Michael': 'Palin', 'Eric': 'Idle', 'Terry': 'Gilliam'}

  相反的,你可以使用dict的keys()和values()方法來獲取KEY和VALUE的列表:

>>> s.keys()
['John', 'Michael', 'Eric', 'Terry']
>>> pythons.values()
['Cleese', 'Palin', 'Idle', 'Gilliam']

  需要注意的是:由於dict本身是無序的,所以通過keys()和values()方法獲得的list的順序已經和原始的list不一樣了。。

  十、Python的True值(Truth Values)

  在Python中,判斷一個變數是否為True的時候,你可以這樣做:

# 這樣寫
if x:
    pass
# !不要這樣寫
if x == True:
    pass

# 對於list,要這樣寫
if items:
    pass
# !不要這樣寫
if len(items) == 0:
    pass

  Python中的真值物件有以下幾個:

False True
False (== 0) True (== 1)
"" (空字串) 除 "" 之外的字串(" ", "anything")
0, 0.0 除 0 之外的數字(1, 0.1, -1, 3.14)
[], (), {}, set() 非空的list,tuple,set和dict ([0], (None,), [''])
None 大部分的物件,除了明確指定為False的物件

  對於自己宣告的class,如果你想明確地指定它的例項是True或False,你可以自己實現class的__nonzero__或__len__方法。當你的class是一個container時,你可以實現__len__方法,如下:

class MyContainer(object):

    def __init__(self, data):
        self.data = data

    def __len__(self):
        """ Return my length. """
        return len(self.data)

  如果你的class不是container,你可以實現__nonzero__方法,如下:

class MyClass(object):

    def __init__(self, value):
        self.value = value

    def __nonzero__(self):
        """ Return my truth value (True or False). """
        # This could be arbitrarily complex:
        return bool(self.value)

  在Python 3.x中,__nonzero__方法被__bool__方法替代。考慮到相容性,你可以在class定義中加上以下的程式碼:

__bool__ = __nonzero__

  十一、enumerate:索引和元素(Index & Item: enumerate)

  在Python中,我們在遍歷列表的時候,可以通過enumerate方法來獲取遍歷時的index,比如:

>>> items = 'zero one two three'.split()
>>> print list(enumerate(items))
[(0, 'zero'), (1, 'one'), (2, 'two'), (3, 'three')]
>>> for (index, item) in enumerate(items):
    print index, item

  enumerate方法是惰性方法,所以它只會在需要的時候生成一項,也因此在上述程式碼print的時候需要包裝一個list。enumerate其實是一個生成器(generator),這個下面會講到。使用enumerate之後,for迴圈變得很簡單:

for (index, item) in enumerate(items):
    print index, item

# compare:
index = 0
for item in items:
    print index, item
    index += 1

# compare:
for i in range(len(items)):
    print i, items[i]

  使用enumerate的程式碼比其他兩個都短,而且更簡單,更容易讀懂。下面的例子可以說明一下enumerate實際返回的資料:一個迭代器,

>>> enumerate(items)
<enumerate object at 0x011EA1C0>
>>> e = enumerate(items)
>>> e.next()
(0, 'zero')
>>> e.next()
(1, 'one')
>>> e.next()
(2, 'two')
>>> e.next()
(3, 'three')
>>> e.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
StopIteration

  十二、Python中的變數 & 引用(variables & names)

  在很多其他高階語言中,給一個變數賦值時會將"value"放在一個"盒子"裡:

int a = 1;

  如圖:

a1box.png

    現在,盒子"a"中包含了一個整數 1;將另外一個"value"賦值給同一個變數時,會將"盒子"中的內容替換掉:

a = 2;

    如圖:

a2box.png

    現在,盒子"a"中包含了一個整數 2;將變數賦值給其他一個變數時,會將"value"拷貝一份放在一個新的"盒子"中:

int b = a;

    如圖:

b2box.png  a2box.png

  盒子"b"是第二個"盒子",裡面是整數 2的一個拷貝,盒子"a"中是另外一個拷貝。

  在Python中,變數沒有資料型別,是附屬於物件的標示符名稱,如下圖:實際,這段表明了像,PHP這類動態指令碼語言中“變數”包含了兩個內容:1 識別符號名稱 2 識別符號所對應(引用)的值(物件),也就是說“變數”不在是一個容器。

a = 1

a1tag.png

    這裡,整數 1 物件有一個名字為 "a" 的變數(tag)。如果我們給變數 "a" 重新賦值,對Python來說,只是將變數(tag) "a" 指向另外一個物件:

a = 2

a2tag.png    1.png

  現在,變數 "a" 是附屬在整數物件 2 上面。最初的整數物件 1 已經沒有指向它的變數 "a",它可能還存在,但是我們已經不能通過變數 "a"獲得。當一個物件沒有了指向它的引用的時候,它將會被從記憶體中刪除(垃圾回收)。如果我們將存在的變數賦值給一個新的變數,Python會在已經存在的物件上加上一個指向自己的變數(tag)。

b = a

ab2tag.png

  變數 "a"和"b" 是指向同一個整數物件的。

  PS:Python中的變數,引用等設計和其他語言不同,這裡只是將原文翻譯說明了一下,更多的介紹可以參看:Python中的變數、引用、拷貝和作用域

  十三、Python方法中引數的預設值(Default Parameter Values)

  對於Python初學者來說,Python的方法預設引數有一個很容易犯錯的地方:在預設引數中使用可變物件,甚至有不少Python老鳥也可能會在這個問題上掉坑裡,如果他們不能理解Python的物件引用。。問題如下:

def bad_append(new_item, a_list=[]):
    a_list.append(new_item)
    return a_list

>>> print bad_append('one')
['one']
>>> print bad_append('two')
['one', 'two']

  這個問題的主要原因是:a_list引數的預設值是一個空的list,它在函式定義的時候已經被建立。所以,之後每次呼叫該函式的時候,a_list的預設值都是這個list物件。List,dict和set是可變物件,如果想在函式中獲取一個預設的list(dict or set)物件,正確的做法是在函式中建立:

def good_append(new_item, a_list=None):
    if a_list is None:
        a_list = []
    a_list.append(new_item)
    return a_list

  十四、字串格式化(String Formatting)

    在許多程式設計語言中都包含有格式化字串的功能,比如C語言中的格式化輸入輸出。Python中內建有對字串進行格式化的操作符 "%" 以及str.format()方法。

  1、操作符 "%"

  Python中的 "%" 操作符和C語言中的sprintf類似。簡單來說,使用 "%" 來格式化字串的時候,你需要提供一個字串模板和用來插入的值。模板中有格式符,這些格式符為真實值預留位置,並說明真實數值應該呈現的格式。Python用一個tuple將多個值傳遞給模板,每個值對應一個格式符。注意:給定的值一定要和模板中的格式符一一對應!

name = 'xianglong'
messages = 3
text = ('Hello %s, you have %i messages' % (name, messages))
print text

# Output: Hello xianglong, you have 3 messages

  在上面的例子中,"Hello %s, you have %i messages" 是字串模板。%s為第一個格式符,表示一個字串。%i為第二個格式符,表示一個十進位制整數。(name, messages)的兩個元素為替換%s和%i的真實值。在模板和tuple之間,有一個%號分隔,它代表了格式化操作。

  常用的格式符如下:

格式 描述
%% 百分號 % 標記
%s 字串 (採用str()的顯示)
%r 字串 (採用repr()的顯示)
%c 字元及其ASCII碼
%b 二進位制整數
%d 十進位制整數 (有符號整數)
%u 十進位制整數 (無符號整數)
%i 十進位制整數 (有符號整數)
%o 八進位制整數 (無符號整數)
%x 十六進位制整數 (無符號整數)
%X 十六進位制整數 (無符號整數)
%e 指數 (基底寫為e)
%E 指數 (基底寫為E)
%f 浮點數
%F 浮點數,與上相同
%g 指數(e)或浮點數 (根據顯示長度)
%G 指數(E)或浮點數 (根據顯示長度)
%p 指標(用十六進位制列印值的記憶體地址)
%n 儲存輸出字元的數量放進引數列表的下一個變數中

  使用操作符 "%" 也可以通過字典格式化字串:

values = {'name': name, 'messages': messages}
print ('Hello %(name)s, you have %(messages)i messages' % values)

# Output: Hello xianglong, you have 3 messages

  上面的程式碼中,我們指定了用來格式化的值的名字,然後可以根據name在字典中查詢相應的value。其實,上面的"name"和"messages"已經在local名稱空間中定義,所以,我們可以利用這一點:

print ('Hello %(name)s, you have %(messages)i messages' % locals())

  locals()方法返回一個包含所有本地變數的字典。這個功能非常強大,你可以不必擔心提供的values是否和模板匹配;但是同時這個也是非常危險的:你將會暴露整個本地名稱空間給呼叫者,這一點需要你注意。

  在Python中,物件有一個__dict__屬性,你可以在格式化字串的時候使用;

print ("We found %(error_count)d errors" % self.__dict__)

# 等同於
print ("We found %d errors" % self.error_count)

  另外,我們還可以用如下的方式,對字串格式化進一步的控制:%[(name)][flags][width].[precision]typecode,其中:

  (name)為命名

  flags可以有+,-,' '或0。+表示右對齊。-表示左對齊。' '為一個空格,表示在正數的左側填充一個空格,從而與負數對齊。0表示使用0填充。

  width表示顯示寬度

  precision表示小數點後精度

  比如:

print("%+10x" % 10)    # +a
print("%04d" % 5)    # 0005
print("%6.3f" % 2.3)    # 2.300

  上面的width, precision為兩個整數。我們可以利用*,來動態代入這兩個量。比如:

print("%.*f" % (4, 1.2))    # 1.2000

  Python實際上用4來替換*。所以實際的模板為"%.4f"。

  2、str.format()方法

  str.format()方法是在Python 2.6中引入的,它通過 {} 和 : 來代替 % ,功能非常強大。具體的用法見下面的例子:

In [1]: name = 'xianglong'
In [2]: messages = 4

# 通過位置
In [3]: 'Hello {0}, you have {1} messages'.format(name, messages)
Out[3]: 'Hello xianglong, you have 4 messages'

# 通過關鍵字引數
In [4]: 'Hello {name}, you have {messages} messages'.format(name=name, messages=messages)
Out[4]: 'Hello xianglong, you have 4 messages'

# 通過下標
In [5]: 'Hello {0[0]}, you have {0[1]} messages'.format([name, messages])
Out[5]: 'Hello xianglong, you have 4 messages'

# 格式限定符:填充與對齊
# ^、<、>分別是居中、左對齊、右對齊,後面頻寬度
# :號後面帶填充的字元,只能是一個字元,不指定的話預設是用空格填充
In [6]: 'Hello {0:>14}, you have {1:>14} messages'.format(name, messages)
Out[6]: 'Hello      xianglong, you have              4 messages'

# 格式限定符:精度與型別f
In [7]: '{:.2f}'.format(321.33345)
Out[7]: '321.33'

# 格式限定符:b、d、o、x分別是二進位制、十進位制、八進位制、十六進位制
In [8]: '{:b}'.format(14)
Out[8]: '1110'

In [9]: '{:d}'.format(14)
Out[9]: '14'

In [10]: '{:o}'.format(14)
Out[10]: '16'

In [11]: '{:x}'.format(14)
Out[11]: 'e'

# 格式限定符:千位分隔符
In [12]: '{:,}'.format(1234567890)
Out[12]: '1,234,567,890'

  十五、迭代器(List comprehensions)

  List Comprehensions即迭代器(列表生成式),是Python內建的非常簡單卻強大的可以用來建立list的生成式。在不使用迭代器的時候,建立一個新列表可以使用for和if來實現:

new_list = []
for item in a_list:
    if condition(item):
        new_list.append(fn(item))

  使用迭代器的話:

new_list = [fn(item) for item in a_list if condition(item)]

  列表生成式非常簡潔的,不過是在某種程度上。你可以在列表生成式中使用多個for迴圈和多個if語句,但是兩個以上的for和if語句會讓列表生成式非常複雜,這時候建議直接用for迴圈。根據Zen of Python,選擇更容易讀的方式。下面是一些例子:

>>> [n ** 2 for n in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

>>> [n ** 2 for n in range(10) if n % 2]
[1, 9, 25, 49, 81]

  十六、生成器(Generator & Generator expressions)

  先出一個題:計算1 ~ 100的平方和。最簡單的方法就是使用一個for迴圈:

total = 0
for num in range(1, 101):
    total += num * num

  其實,我們可以使用Python內建的sum方法計算:

# 迭代器(列表生成式)
total = sum([num * num for num in range(1, 101)])

# 生成器
total = sum(num * num for num in xrange(1, 101))

  生成器和上面提到的迭代器差不多,可以說:生成器是一種特殊的迭代器;但是它們之間有一個很大