讓你的 Python 代碼優雅又地道

2021-02-20 Python開發者

(點擊上方藍字,快速關注我們)

編譯:0xFEE1C001 

www.lightxue.com/transforming-code-into-beautiful-idiomatic-python

如有好文章投稿,請點擊 → 這裡了解詳情

譯序

如果說優雅也有缺點的話,那就是你需要艱巨的工作才能得到它,需要良好的教育才能欣賞它。

—— Edsger Wybe Dijkstra

在Python社區文化的澆灌下,演化出了一種獨特的代碼風格,去指導如何正確地使用Python,這就是常說的pythonic。一般說地道(idiomatic)的python代碼,就是指這份代碼很pythonic。Python的語法和標準庫設計,處處契合著pythonic的思想。而且Python社區十分注重編碼風格一的一致性,他們極力推行和處處實踐著pythonic。所以經常能看到基於某份代碼P vs NP (pythonic vs non-pythonic)的討論。pythonic的代碼簡練,明確,優雅,絕大部分時候執行效率高。閱讀pythonic的代碼能體會到「代碼是寫給人看的,只是順便讓機器能運行」暢快。

然而什麼是pythonic,就像什麼是地道的漢語一樣,切實存在但標準模糊。import this可以看到Tim Peters提出的Python之禪,它提供了指導思想。許多初學者都看過它,深深贊同它的理念,但是實踐起來又無從下手。PEP 8給出的不過是編碼規範,對於實踐pythonic還遠遠不夠。如果你正被如何寫出pythonic的代碼而困擾,或許這份筆記能給你幫助。

Raymond Hettinger是Python核心開發者,本文提到的許多特性都是他開發的。同時他也是Python社區熱忱的布道師,不遺餘力地傳授pythonic之道。這篇文章是網友Jeff Paine整理的他在2013年美國的PyCon的演講的筆記。

術語澄清:本文所說的集合全都指collection,而不是set。

以下是正文。

本文是Raymond Hettinger在2013年美國PyCon演講的筆記(視頻, 幻燈片)。

示例代碼和引用的語錄都來自Raymond的演講。這是我按我的理解整理出來的,希望你們理解起來跟我一樣順暢!

遍歷一個範圍內的數字

for i in [0, 1, 2, 3, 4, 5]:

    print i ** 2

 

for i in range(6):

    print i ** 2

更好的方法

for i in xrange(6):

    print i ** 2

xrange會返回一個迭代器,用來一次一個值地遍歷一個範圍。這種方式會比range更省內存。xrange在Python 3中已經改名為range。

遍歷一個集合

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

 

for i in range(len(colors)):

    print colors[i]

更好的方法

for color in colors:

    print color

反向遍歷

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

 

for i in range(len(colors)-1, -1, -1):

    print colors[i]

更好的方法

for color in reversed(colors):

    print color

遍歷一個集合及其下標

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

 

for i in range(len(colors)):

    print i, '--->', colors[i]

更好的方法

for i, color in enumerate(colors):

    print i, '--->', color

這種寫法效率高,優雅,而且幫你省去親自創建和自增下標。

當你發現你在操作集合的下標時,你很有可能在做錯事。

遍歷兩個集合

names = ['raymond', 'rachel', 'matthew']

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

 

n = min(len(names), len(colors))

for i in range(n):

    print names[i], '--->', colors[i]

 

for name, color in zip(names, colors):

    print name, '--->', color

更好的方法

for name, color in izip(names, colors):

    print name, '--->', color

zip在內存中生成一個新的列表,需要更多的內存。izip比zip效率更高。

注意:在Python 3中,izip改名為zip,並替換了原來的zip成為內置函數。

有序地遍歷

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

 

# 正序

for color in sorted(colors):

    print colors

 

# 倒序

for color in sorted(colors, reverse=True):

    print colors

自定義排序順序

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

 

def compare_length(c1, c2):

    if len(c1) < len(c2): return -1

    if len(c1) > len(c2): return 1

    return 0

 

print sorted(colors, cmp=compare_length)

更好的方法

print sorted(colors, key=len)

第一種方法效率低而且寫起來很不爽。另外,Python 3已經不支持比較函數了。

調用一個函數直到遇到標記值

blocks = []

while True:

    block = f.read(32)

    if block == '':

        break

    blocks.append(block)

更好的方法

blocks = []

for block in iter(partial(f.read, 32), ''):

    blocks.append(block)

iter接受兩個參數。第一個是你反覆調用的函數,第二個是標記值。

譯註:這個例子裡不太能看出來方法二的優勢,甚至覺得partial讓代碼可讀性更差了。方法二的優勢在於iter的返回值是個迭代器,迭代器能用在各種地方,set,sorted,min,max,heapq,sum……

在循環內識別多個退出點

def find(seq, target):

    found = False

    for i, value in enumerate(seq):

        if value == target:

            found = True

            break

    if not found:

        return -1

    return i

更好的方法

def find(seq, target):

    for i, value in enumerate(seq):

        if value == target:

            break

    else:

        return -1

    return i

for執行完所有的循環後就會執行else。

譯註:剛了解for-else語法時會困惑,什麼情況下會執行到else裡。有兩種方法去理解else。傳統的方法是把for看作if,當for後面的條件為False時執行else。其實條件為False時,就是for循環沒被break出去,把所有循環都跑完的時候。所以另一種方法就是把else記成nobreak,當for沒有被break,那麼循環結束時會進入到else。

遍歷字典的key

d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}

 

for k in d:

    print k

 

for k in d.keys():

    if k.startswith('r'):

        del d[k]

什麼時候應該使用第二種而不是第一種方法?當你需要修改字典的時候。

如果你在迭代一個東西的時候修改它,那就是在冒天下之大不韙,接下來發生什麼都活該。

d.keys()把字典裡所有的key都複製到一個列表裡。然後你就可以修改字典了。

注意:如果在Python 3裡迭代一個字典你得顯示地寫:list(d.keys()),因為d.keys()返回的是一個「字典視圖」(一個提供字典key的動態視圖的迭代器)。詳情請看文檔。

遍歷一個字典的key和value

# 並不快,每次必須要重新哈希並做一次查找

for k in d:

    print k, '--->', d[k]

 

# 產生一個很大的列表

for k, v in d.items():

    print k, '--->', v

更好的方法

for k, v in d.iteritems():

    print k, '--->', v

iteritems()更好是因為它返回了一個迭代器。

注意:Python 3已經沒有iteritems()了,items()的行為和iteritems()很接近。詳情請看文檔。

用key-value對構建字典

names = ['raymond', 'rachel', 'matthew']

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

 

d = dict(izip(names, colors))

# {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}

Python 3: d = dict(zip(names, colors))

用字典計數

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

 

# 簡單,基本的計數方法。適合初學者起步時學習。

d = {}

for color in colors:

    if color not in d:

        d[color] = 0

    d[color] += 1

 

# {'blue': 1, 'green': 2, 'red': 3}

更好的方法

d = {}

for color in colors:

    d[color] = d.get(color, 0) + 1

 

# 稍微潮點的方法,但有些坑需要注意,適合熟練的老手。

d = defaultdict(int)

for color in colors:

    d[color] += 1

用字典分組 — 第I部分和第II部分

names = ['raymond', 'rachel', 'matthew', 'roger',

         'betty', 'melissa', 'judith', 'charlie']

 

# 在這個例子,我們按name的長度分組

d = {}

for name in names:

    key = len(name)

    if key not in d:

        d[key] = []

    d[key].append(name)

 

# {5: ['roger', 'betty'], 6: ['rachel', 'judith'], 7: ['raymond', 'matthew', 'melissa', 'charlie']}

 

d = {}

for name in names:

    key = len(name)

    d.setdefault(key, []).append(name)

更好的方法

d = defaultdict(list)

for name in names:

    key = len(name)

    d[key].append(name)

字典的popitem()是原子的嗎?

d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}

 

while d:

    key, value = d.popitem()

    print key, '-->', value

popitem是原子的,所以多線程的時候沒必要用鎖包著它。

連接字典

defaults = {'color': 'red', 'user': 'guest'}

parser = argparse.ArgumentParser()

parser.add_argument('-u', '--user')

parser.add_argument('-c', '--color')

namespace = parser.parse_args([])

command_line_args = {k: v for k, v in vars(namespace).items() if v}

 

# 下面是通常的作法,默認使用第一個字典,接著用環境變量覆蓋它,最後用命令行參數覆蓋它。

# 然而不幸的是,這種方法拷貝數據太瘋狂。

d = defaults.copy()

d.update(os.environ)

d.update(command_line_args)

更好的方法

d = ChainMap(command_line_args, os.environ, defaults)

ChainMap在Python 3中加入。高效而優雅。

提高可讀性

位置參數和下標很漂亮

但關鍵字和名稱更好

第一種方法對計算機來說很便利

第二種方法和人類思考方式一致

用關鍵字參數提高函數調用的可讀性

twitter_search('@obama', False, 20, True)

更好的方法

twitter_search('@obama', retweets=False, numtweets=20, popular=True)

第二種方法稍微(微秒級)慢一點,但為了代碼的可讀性和開發時間,值得。

用namedtuple提高多個返回值的可讀性

# 老的testmod返回值

doctest.testmod()

# (0, 4)

# 測試結果是好是壞?你看不出來,因為返回值不清晰。

更好的方法

# 新的testmod返回值, 一個namedtuple

doctest.testmod()

# TestResults(failed=0, attempted=4)

namedtuple是tuple的子類,所以仍適用正常的元組操作,但它更友好。

創建一個nametuple

TestResults = namedTuple('TestResults', ['failed', 'attempted'])

unpack序列

p = 'Raymond', 'Hettinger', 0x30, 'python@example.com'

 

# 其它語言的常用方法/習慣

fname = p[0]

lname = p[1]

age = p[2]

email = p[3]

更好的方法

fname, lname, age, email = p

第二種方法用了unpack元組,更快,可讀性更好。

更新多個變量的狀態

def fibonacci(n):

    x = 0

    y = 1

    for i in range(n):

        print x

        t = y

        y = x + y

        x = t

更好的方法

def fibonacci(n):

    x, y = 0, 1

    for i in range(n):

        print x

        x, y = y, x + y

第一種方法的問題

第二種方法抽象層級更高,沒有操作順序出錯的風險而且更效率更高。

同時狀態更新

tmp_x = x + dx * t

tmp_y = y + dy * t

tmp_dx = influence(m, x, y, dx, dy, partial='x')

tmp_dy = influence(m, x, y, dx, dy, partial='y')

x = tmp_x

y = tmp_y

dx = tmp_dx

dy = tmp_dy

更好的方法

x, y, dx, dy = (x + dx * t,

                y + dy * t,

                influence(m, x, y, dx, dy, partial='x'),

                influence(m, x, y, dx, dy, partial='y'))

效率

優化的基本原則

除非必要,別無故移動數據

稍微注意一下用線性的操作取代O(n**2)的操作

總的來說,不要無故移動數據

連接字符串

names = ['raymond', 'rachel', 'matthew', 'roger',

         'betty', 'melissa', 'judith', 'charlie']

 

s = names[0]

for name in names[1:]:

    s += ', ' + name

print s

更好的方法

print ', '.join(names)

更新序列

names = ['raymond', 'rachel', 'matthew', 'roger',

         'betty', 'melissa', 'judith', 'charlie']

 

del names[0]

# 下面的代碼標誌著你用錯了數據結構

names.pop(0)

names.insert(0, 'mark')

更好的方法

names = deque(['raymond', 'rachel', 'matthew', 'roger',

               'betty', 'melissa', 'judith', 'charlie'])

 

# 用deque更有效率

del names[0]

names.popleft()

names.appendleft('mark')

裝飾器和上下文管理

用於把業務和管理的邏輯分開

分解代碼和提高代碼重用性的乾淨優雅的好工具

起個好名字很關鍵

記住蜘蛛俠的格言:能力越大,責任越大

使用裝飾器分離出管理邏輯

# 混著業務和管理邏輯,無法重用

def web_lookup(url, saved={}):

    if url in saved:

        return saved[url]

    page = urllib.urlopen(url).read()

    saved[url] = page

    return page

更好的方法

@cache

def web_lookup(url):

    return urllib.urlopen(url).read()

注意:Python 3.2開始加入了functools.lru_cache解決這個問題。

分離臨時上下文

# 保存舊的,創建新的

old_context = getcontext().copy()

getcontext().prec = 50

print Decimal(355) / Decimal(113)

setcontext(old_context)

更好的方法

with localcontext(Context(prec=50)):

    print Decimal(355) / Decimal(113)

譯註:示例代碼在使用標準庫decimal,這個庫已經實現好了localcontext。

如何打開關閉文件

f = open('data.txt')

try:

    data = f.read()

finally:

    f.close()

更好的方法

with open('data.txt') as f:

    data = f.read()

如何使用鎖

# 創建鎖

lock = threading.Lock()

 

# 使用鎖的老方法

lock.acquire()

try:

    print 'Critical section 1'

    print 'Critical section 2'

finally:

    lock.release()

更好的方法

# 使用鎖的新方法

with lock:

    print 'Critical section 1'

    print 'Critical section 2'

分離出臨時的上下文

try:

    os.remove('somefile.tmp')

except OSError:

    pass

更好的方法

with ignored(OSError):

    os.remove('somefile.tmp')

ignored是Python 3.4加入的, 文檔。

注意:ignored 實際上在標準庫叫suppress(譯註:contextlib.supress).

試試創建你自己的ignored上下文管理器。

@contextmanager

def ignored(*exceptions):

    try:

        yield

    except exceptions:

        pass

把它放在你的工具目錄,你也可以忽略異常

譯註:contextmanager在標準庫contextlib中,通過裝飾生成器函數,省去用__enter__和__exit__寫上下文管理器。詳情請看文檔。

分離臨時上下文

# 臨時把標準輸出重定向到一個文件,然後再恢復正常

with open('help.txt', 'w') as f:

    oldstdout = sys.stdout

    sys.stdout = f

    try:

        help(pow)

    finally:

        sys.stdout = oldstdout

更好的寫法

with open('help.txt', 'w') as f:

    with redirect_stdout(f):

        help(pow)

redirect_stdout在Python 3.4加入(譯註:contextlib.redirect_stdout), bug反饋。

實現你自己的redirect_stdout上下文管理器。

@contextmanager

def redirect_stdout(fileobj):

    oldstdout = sys.stdout

    sys.stdout = fileobj

    try:

        yield fieldobj

    finally:

        sys.stdout = oldstdout

簡潔的單句表達

兩個衝突的原則:

Raymond的原則:

列表解析和生成器

result = []

for i in range(10):

s = i ** 2

    result.append(s)

print sum(result)

更好的方法

print sum(i**2 for i in xrange(10))

第一種方法說的是你在做什麼,第二種方法說的是你想要什麼。

看完本文有收穫?請轉發分享給更多人

關注「Python開發者」,提升Python技能

相關焦點

  • 代碼這樣寫更優雅(Python版)
    題圖:unsplash.comPython 這門語言最大的優點之一就是語法簡潔,好的代碼就像偽代碼一樣,乾淨、整潔、一目了然。但有時候我們寫代碼,特別是 Python 初學者,往往還是按照其它語言的思維習慣來寫,那樣的寫法不僅運行速度慢,代碼讀起來也費盡,給人一種拖泥帶水的感覺,過段時間連自己也讀不懂。
  • 5種非常棒的 Python 技巧,使你的代碼更優雅
    今天教大家一些 Python 技巧,它可以使你的代碼更優雅和高效。列表只需一行代碼,讓你的代碼看起來更簡潔,並且看起來更專業。a = [2,5,7,9]b = [a[i]+1 for i in range(len(a))] 你可以看到代碼之間的差別。現在這只是一個簡單的例子,後續你會發現有更多的應用案例,並可以大幅幫你減少代碼長度。
  • 給你Python程序構建一個優雅的終端CLI界面
    我們知道在Linux下有優雅的shell終端命令行界面,shell腳本都可以優雅用命令行的方式來運行。而且shell也再帶優化命令行參數解析的bash內部命令getopts和大多數發行版附帶的外部命令getops。Perl語言也有Getopt::XX系列模塊來實現類似功能;Golang也有flag標準庫以及更加強大的第三庫cobra。
  • 寫出漂亮 Python 代碼的 20條準則
    https://www.python.org/dev/peps/pep-0008/瀏覽完 PEP8 後,看看下面這些文章,其中展示了一些亮點和應用:如何參照 PEP 8 編寫漂亮的 Python 代碼https://realpython.com/python-pep8/優雅的 Python 與 PEP8https:/
  • Python編寫代碼規範-幫你寫出優雅的代碼
    在工作或實習中,掌握規範的代碼編寫能力是對代碼編寫人員的基本要求。
  • 讓你的代碼更「地道」:提高Python編碼水平的小技巧
    雖然程式設計師依然可以通過不同的方式實現同樣的功能,編寫出優秀的代碼,只要代碼能夠滿足預期目的就OK。編寫非慣用Python程序也沒有問題。但就像我們不斷練習英文的口音一樣,也有一些人也想讓自己的Python代碼變得更地道。本文中,我將分享自己在過去幾年中積累的一些習慣用法,希望對提高你的Python編碼水平有所幫助。
  • Python代碼技巧,你值得擁有!
    如何在命令行查看python文檔如何將python代碼打包成獨立的二進位文件需要編譯的python代碼如下:#!/usr/bin/env python# -*- coding: utf-8 -*-print 'hello, world!'將python代碼打包成獨立的二進位文件步驟:
  • Python代碼如何升級為Pythonic 代碼
    符合這樣要求的代碼也被python社區稱為pythonic的代碼。正文共:7841 字預計閱讀時間:20 分鐘Python是當今最流行的語言之一。相對較新的領域如數據科學、人工智慧、機器人和數據分析,以及傳統的專業如Web開發和科學研究等,都在擁抱Python。對於用Python這樣的動態語言編寫代碼的程式設計師來說,確保代碼的高質量和無錯誤變得越來越重要。
  • python發布代碼教程
    def tria(d,h):''' 計算三角形的面積 '''s=d*h/2return ssetup.py文件包含有關發布的元數據,代碼如下。sdist,如圖所示:回車後出現如圖所示:就說明你完成了構建發布文件。
  • 5種方法,加密你的Python代碼
    其中一個缺點,讓不少開發者頭疼不已,由於Python解釋器開源的關係,導致Python代碼無法加密,代碼的安全性得不到保障。當然,想要加密Python代碼,也並非無解。最常見的加密方式有4種,還有1種獨特的加密方式。
  • 符合語言習慣的Python優雅編程技巧
    Python最大的優點之一就是語法簡潔,好的代碼就像偽代碼一樣,乾淨、整潔、一目了然。要寫出 Pythonic(優雅的、地道的、整潔的)代碼,需要多看多學大牛們寫的代碼,github 上有很多非常優秀的原始碼值得閱讀,比如:requests、flask、tornado,下面列舉一些常見的Pythonic寫法。0. 程序必須先讓人讀懂,然後才能讓計算機執行。
  • 代碼跑得慢甩鍋Python?手把手教你如何給代碼提速30%
    其實某個特定程序(無論使用何種程式語言)的運行速度是快還是慢,在很大程度上取決於編寫該程序的開發人員自身素質,以及他們編寫優化而高效代碼的能力。Medium上一位小哥就詳細講了講如何讓python提速30%,以此證明代碼跑得慢不是python的問題,而是代碼本身的問題。
  • Python代碼可以加密碼?Python字節碼告訴你!
    眾所周知,執行Python程序可以直接使用python.exe命令,如下所示:看到python直接執行了abc.py,可能很多同學認為python是解釋執行abc.py的,其實不然。如果要真是解釋執行,那效率慢的就沒法用了。實際上,Python與Java一樣,也是玩字節碼出身。Java的字節碼叫Java ByteCode,Python的字節碼叫Python ByteCode。
  • 這些Python代碼技巧,你肯定還不知道
    但是,當你可以編寫這樣的代碼時,很難去反駁這種言論:x = [True, True, False]if any(x): print("At least one True")if all(x):inspect 模塊:https://docs.python.org/3/library/inspect.html下面的代碼示例使用 inspect.getsource() 列印自己的原始碼
  • 慢步python,說說import,引用功能代碼(功能庫、py文件代碼)
    今天慢步休息,繼續學習python語言。其實慢步已經將基礎和必要的python語言知識點寫了。今天想說說importimport是python語言的保留字,它能實現引用當前程序之外已有的功能代碼。python語言像積木,你可以根據你想要的功能,編寫一系列的代碼。比如筆者之前編寫的《word文檔標題置換》。
  • 好用的PYTHON IDE和代碼編輯器| TOP10推薦
    回顧一開始python的介紹,有一節直接就有一個定論:新手就入pycharm吧。正所謂磨刀不誤砍柴工,那就來過一遍吧(儘可能的漢化了如果翻譯的不夠專業,請指正):什麼是IDE你可以簡單理解是一個可以進行編寫、調試、釋放代碼的圖形化界面(軟體)。最簡單的IDE會包括原始碼編輯器、構建工具和debug模塊,以滿足開發和測試。
  • Python 入門系列 —— 3. 代碼縮進和注釋
    C:\Users\Your Name>python myfile.pyPython 縮進 縮進 指的是代碼行開頭處的空格,在其他程式語言中使用的 縮進 僅僅是為了提高可讀性,而在 python 中這個縮進卻是非常重要的,它決定了你的語法是否正確。
  • 在Rust 代碼中編寫 Python 是種怎樣的體驗?
    預覽如果不熟悉inline-python類庫,你可以執行以下操作:fn main() {let who = "world";let n = 5; python!{for i in range('n): print(i, "Hello", 'who)print("Goodbye") }}它允許你將Python代碼直接嵌入Rust代碼行之間,甚至直接在Python代碼中使用Rust
  • 六個步驟,封裝你最愛的Python代碼包!
    全文共7134字,預計學習時長14分鐘假設你很喜歡用同一段Python代碼,裡面有幾個相關的小型函數,或者是含有幾百行代碼的中型模塊。程式設計師可能會把它複製到不同的項目或存儲庫中,或者從特別設置的實用工具代碼文件夾中導入這段代碼。 這很正常。程式設計師在編寫代碼的過程中都會不斷積累這些個性化的小工具。
  • 雙劍合璧,Python調用C代碼
    在本文中,我們將用C語言實現一個Python模塊,並在Python代碼中對其進行調用。首先我們創建一個.c文件,並且加入#include <Python.h>其中包含了必要的用C語言實現的Python對象setup.py接下來,我們利用Python中提供方法將C代碼作為擴展模塊加入到