首頁»Python»改善 Python 程序的 91 個建議(一)

                    改善 Python 程序的 91 個建議(一)

                    來源:馭風者 發布時間:2017-05-17 閱讀次數:

                    學習筆記一:《編寫高質量代碼 改善 Python 程序的 91 個建議》

                    第 1 章 引論

                    建議 1:理解 Pythonic 概念

                    Pythonic

                    Tim Peters 的 《The Zen of Python》相信學過 Python 的都耳熟能詳,在交互式環境中輸入import this可以查看,其實有意思的是這段 Python 之禪的源碼:

                    d = {}
                    for c in (65, 97):
                        for i in range(26):
                            d[chr(i+c)] = chr((i+13) % 26 + c)
                     
                    print "".join([d.get(c, c) for c in s])
                    

                    哈哈哈,相信這是大佬在跟我們舉反例吧。

                    書中還舉了一個快排的例子:

                    def quicksort(array):
                        less = []
                        greater = []
                        if len(array) <= 1:
                            return array
                        pivot =array.pop()
                        for x in array:
                            if x <= pivot:
                                less.append(x)
                            else:
                                greater.append(x)
                        return quicksort(less) + [pivot] + quicksort(greater)
                    

                    代碼風格

                    通過對語法、庫和應用程序的理解來編寫代碼,充分體現 Python 自身的特色:

                    # 變量交換
                    a, b = b, a
                    # 上下文管理
                    with open(path, 'r') as f:
                        do_sth_with(f)
                    # 不應當過分地追求奇技淫巧
                    a = [1, 2, 3, 4]
                    a[::-1] # 不推薦。好吧,自從學了切片我一直用的這個
                    list(reversed(a))   # 推薦
                    

                    然后表揚了 Flask 框架,提到了 generator 之類的特性尤為 Pythonic,有個包和模塊的約束:

                    • 包和模塊的命名采用小寫、單數形式,而且短小

                    • 包通常僅作為命名空間,如只含空的__init__.py文件

                    建議 2:編寫 Pythonic 代碼

                    命名的規范:

                    def find_num(searchList, num):
                        for listValue in searchList:
                            if num == listValue:
                                return True
                            else:
                                pass
                    

                    嘗試去通讀官方手冊,掌握不斷發展的新特性,這將使你編寫代碼的執行效率更高,推薦深入學習 Flask、gevent 和 requests。

                    建議 3:理解 Python 與 C 語言的不同之處

                    提到了三點:

                    • Python 使用代碼縮進的方式來分割代碼塊,不要混用 Tab 鍵和空格

                    • Python 中單、雙引號的使用

                    • 三元操作符:x if bool else y

                    建議 4:在代碼中適當添加注釋

                    這一點已經受教了,現在編寫代碼都會合理地加入塊注釋、行注釋和文檔注釋,可以使用__doc__輸出。

                    建議 5:通過適當添加空行使代碼布局更為優雅、合理

                    建議 6:編寫函數的 4 個原則

                    1. 函數設計要盡量短小,嵌套層次不宜過深

                    2. 函數申明應該做到合理、簡單、易于使用

                    3. 函數參數設計應該考慮向下兼容

                    4. 一個函數只做一件事,盡量保證函數語句粒度的一致性

                    Python 中函數設計的好習慣還包括:不要在函數中定義可變對象作為默認值,使用異常替換返回錯誤,保證通過單元測試等。

                    # 關于函數設計的向下兼容
                    def readfile(filename):         # 第一版本
                        pass
                    def readfile(filename, log):    # 第二版本
                        pass
                    def readfile(filename, logger=logger.info):     # 合理的設計
                        pass
                    

                    最后還有個函數可讀性良好的例子:

                    def GetContent(ServerAdr, PagePath):
                        http = httplib.HTTP(ServerAdr)
                        http.putrequest('GET', PagePath)
                        http.putheader('Accept', 'text/html')
                        http.putheader('Accept', 'text/plain')
                        http.endheaders()
                        httpcode, httpmsg, headers = http.getreply()
                        if httpcode != 200:
                            raise "Could not get document: Check URL and Path."
                        doc = http.getfile()
                        data = doc.read()       # 此處是不是應該使用 with ?
                        doc.close
                        return data
                    
                    def ExtractData(inputstring, start_line, end_line):
                        lstr = inputstring.splitlines()             # split
                        j = 0
                        for i in lstr:
                            j += 1
                            if i.strip() == start_line: slice_start = j
                            elif i.strip() == end_line: slice_end = j
                        return lstr[slice_start:slice_end]
                    
                    def SendEmail(sender, receiver, smtpserver, username, password, content):
                        subject = "Contented get from the web"
                        msg = MIMEText(content, 'plain', 'utf-8')
                        msg['Subject'] = Header(subject, 'utf-8')
                        smtp = smtplib.SMTP()
                        smtp.connect(smtpserver)
                        smtp.login(username, password)
                        smtp.sendmail(sender, receiver, msg.as_string())
                        smtp.quit()
                    

                    建議 7:將常量集中到一個文件

                    在 Python 中應當如何使用常量:

                    • 通過命名風格提醒使用者該變量代表常量,如常量名全部大寫

                    • 通過自定義類實現常量功能:將存放常量的文件命名為constant.py,并在其中定義一系列常量

                    class _const:
                        class ConstError(TypeError): pass
                        class ConstCaseError(ConstError): pass
                        
                        def __setattr__(self, name, value):
                            if self.__dict__.has_key(name):
                                raise self.ConstError, "Can't change const.%s" % name
                            if not name.isupper():
                                raise self.ConstCaseError, \
                                        'const name "%s" is not all uppercase' % name
                            self.__dict__[name] = value
                    
                    import sys
                    sys.modules[__name__] = _const()
                    import const
                    const.MY_CONSTANT = 1
                    const.MY_SECOND_CONSTANT = 2
                    const.MY_THIRD_CONSTANT = 'a'
                    const.MY_FORTH_CONSTANT = 'b'
                    

                    其他模塊中引用這些常量時,按照如下方式進行即可:

                    from constant import const
                    print(const.MY_CONSTANT)
                    

                    第 2 章 編程慣用法

                    建議 8:利用 assert 語句來發現問題

                    >>> y = 2
                    >>> assert x == y, "not equals"
                    Traceback (most recent call last):
                      File "<stdin>", line 1, in <module>
                    AssertionError: not equals
                    >>> x = 1
                    >>> y = 2
                    # 以上代碼相當于
                    >>> if __debug__ and not x == y:
                    ...     raise AssertionError("not equals")
                    ... 
                    Traceback (most recent call last):
                      File "<stdin>", line 2, in <module>
                    AssertionError: not equals
                    

                    運行是加入-O參數可以禁用斷言。

                    建議 9:數據交換的時候不推薦使用中間變量

                    >>> Timer('temp = x; x = y; y = temp;', 'x = 2; y = 3').timeit()
                    0.059251302998745814
                    >>> Timer('x, y = y, x', 'x = 2; y = 3').timeit()
                    0.05007316499904846
                    

                    對于表達式x, y = y, x,在內存中執行的順序如下:

                    1. 先計算右邊的表達式y, x,因此先在內存中創建元組(y, x),其標識符和值分別為y, x及其對應的值,其中y和x是在初始化已經存在于內存中的對象

                    2. 計算表達式左邊的值并進行賦值,元組被依次分配給左邊的標識符,通過解壓縮,元組第一標識符y分配給左邊第一個元素x,元組第二標識符x分配給左邊第一個元素y,從而達到交換的目的

                    下面是通過字節碼的分析:

                    >>> import dis
                    >>> def swap1():
                    ...     x = 2
                    ...     y = 3
                    ...     x, y = y, x
                    ... 
                    >>> def swap2():
                    ...     x = 2
                    ...     y = 3
                    ...     temp = x
                    ...     x = y
                    ...     y = temp
                    ... 
                    >>> dis.dis(swap1)
                      2           0 LOAD_CONST               1 (2)
                                  3 STORE_FAST               0 (x)
                    
                      3           6 LOAD_CONST               2 (3)
                                  9 STORE_FAST               1 (y)
                    
                      4          12 LOAD_FAST                1 (y)
                                 15 LOAD_FAST                0 (x)
                                 18 ROT_TWO                             # 交換兩個棧的最頂層元素
                                 19 STORE_FAST               0 (x)
                                 22 STORE_FAST               1 (y)
                                 25 LOAD_CONST               0 (None)
                                 28 RETURN_VALUE
                    >>> dis.dis(swap2)                                                                                                                                    
                      2           0 LOAD_CONST               1 (2)
                                  3 STORE_FAST               0 (x)
                    
                      3           6 LOAD_CONST               2 (3)
                                  9 STORE_FAST               1 (y)
                    
                      4          12 LOAD_FAST                0 (x)
                                 15 STORE_FAST               2 (temp)
                    
                      5          18 LOAD_FAST                1 (y)
                                 21 STORE_FAST               0 (x)
                    
                      6          24 LOAD_FAST                2 (temp)
                                 27 STORE_FAST               1 (y)
                                 30 LOAD_CONST               0 (None)
                                 33 RETURN_VALUE
                    

                    建議 10:充分利用 Lazy evaluation 的特性

                    def fib():
                        a, b = 0, 1
                        while True:
                            yield a
                            a, b = b, a + b
                    

                    哈哈哈,我猜到肯定是生成器實現菲波拉契序列的例子,不過對比我寫的版本,唉。。。

                    建議 11:理解枚舉替代實現的缺陷

                    利用 Python 的動態特征,可以實現枚舉:

                    # 方式一
                    class Seasons:
                        Spring, Summer, Autumn, Winter = range(4)
                    # 方式二
                    def enum(*posarg, **keysarg):
                        return type("Enum", (object,), dict(zip(posarg, range(len(posarg))), **keysarg))
                    Seasons = enum("Spring", "Summer", "Autumn", Winter=1)
                    Seasons.Spring
                    # 方式三
                    >>> from collections import namedtuple
                    >>> Seasons = namedtuple('Seasons', 'Spring Summer Autumn Winter')._make(range(4))
                    >>> Seasons.Spring
                    0
                    # 但通過以上方式實現枚舉都有不合理的地方
                    >>> Seasons._replace(Spring=2)                                             │
                    Seasons(Spring=2, Summer=1, Autumn=2, Winter=3)  
                    # Python3.4 中加入了枚舉,僅在父類沒有任何枚舉成員的時候才允許繼承
                    

                    建議 12:不推薦使用 type 來進行類型檢查

                    作為動態語言,Python 解釋器會在運行時自動進行類型檢查并根據需要進行隱式類型轉換,當變量類型不同而兩者之間又不能進行隱式類型轉換時便拋出TypeError異常。

                    >>> def add(a, b):
                    ...     return a + b
                    ... 
                    >>> add(1, 2j)
                    (1+2j)
                    >>> add('a', 'b')
                    'ab'
                    >>> add(1, 2)
                    3
                    >>> add(1.0, 2.3)
                    3.3
                    >>> add([1, 2], [3, 4])
                    [1, 2, 3, 4]
                    >>> add(1, 'a')
                    Traceback (most recent call last):
                      File "<stdin>", line 1, in <module>
                      File "<stdin>", line 2, in add
                    TypeError: unsupported operand type(s) for +: 'int' and 'str'
                    

                    所以實際應用中,我們常常需要進行類型檢查,但是不推薦使用type(),因為基于內建類型擴展的用戶自定義類型,type()并不能準確返回結果:

                    class UserInt(int):
                        def __init__(self, val=0):
                            self._val = int(val)
                        def __add__(self, val):
                            if isinstance(val, UserInt):
                                return UserInt(self._val + val._val)
                            return self._val + val
                        def __iadd__(self, val):
                            raise NotImplementedError("not support operation")
                        def __str__(self):
                            return str(self._val)
                        def __repr__(self):
                            return "Integer %s" % self._val
                    >>> n = UserInt()
                    >>> n
                    Integer 0
                    >>> print(n)
                    0
                    >>> m = UserInt(2)
                    >>> print(m)
                    2
                    >>> type(n) is int
                    False                   # 顯然不合理
                    >>> isinstance(n, int)
                    True
                    

                    我們可以使用isinstance來檢查:isinstance(object, classinfo)

                    建議 13:盡量轉換為浮點類型后再做除法

                    # 計算平均成績績點
                    >>> gpa = ((4*96+3*85+5*98+2*70)*4) / ((4+3+5+2)*100)
                    >>> gpa
                    3.625714285714286   # 終于知道自己的績點是咋算的了
                    

                    建議 14:警惕 eval() 的安全漏洞

                    eval(expression[, globals[, locals]])將字符串 str 當成有效的表達式來求值并返回計算結果,globas為字典形式,locals為任何映射對象,它們分別表示全局和局部命名空間,兩者都省略表達式將在調用的環境中執行,為什么需要警惕eval()呢:

                    # 合理正確地使用
                    >>> eval("1+1==2")
                    True
                    >>> eval('"a"+"b"')
                    'ab'
                    # 壞心眼的geek
                    >>> eval('__import__("os").system("dir")')
                    Desktop  Documents  Downloads  examples.desktop  Music  Pictures  Public  __pycache__  Templates  Videos
                    0
                    >>> eval('__import__("os").system("del * /Q")')     # 嘿嘿嘿
                    

                    如果確實需要使用eval,建議使用安全性更好的ast.literal_eval。

                    建議 15:使用 enumerate() 獲取序列迭代的索引和值

                    >>> li = ['a', 'b', 'c', 'd', 'e']
                    >>> for i, e in enumerate(li):
                    ...     print('index: ', i, 'element: ', e)
                    ... 
                    index:  0 element:  a
                    index:  1 element:  b
                    index:  2 element:  c
                    index:  3 element:  d
                    index:  4 element:  e
                    # enumerate(squence, start=0) 內部實現
                    def enumerate(squence, start=0):
                        n = start
                        for elem in sequence:
                            yield n, elem   # 666
                            n += 1
                    # 明白了原理我們自己也來實現一個反序的
                    def reversed_enumerate(squence):
                        n = -1
                        for elem in reversed(sequence):
                            yield len(sequence) + n, elem
                            n -= 1
                    

                    建議 16:分清 == 與 is 的適用場景

                    操作符意義isobject identity==equal

                    is的作用是用來檢查對象的標示符是否一致,也就是比較兩個對象在內存中是否擁有同一塊內存空間,相當于id(x) == id(y),它并不適用于判斷兩個字符串是否相等。==才是用來判斷兩個對象的值是否相等,實際是調用了內部的__eq__,所以a==b相當于a.__eq__(b),也就是說==是可以被重載的,而is不能被重載。

                    >>> s1 = 'hello world'
                    >>> s2 = 'hello world'
                    >>> s1 == s2
                    True
                    >>> s1 is s2
                    False
                    >>> s1.__eq__(s2)
                    True
                    >>> a = 'Hi'
                    >>> b = 'Hi'
                    >>> a == b
                    True
                    >>> a is b
                    True
                    

                    咦~怎么上例中的a, b又是“同一對象”了?這跟 Python 的 string interning 機制有關,為了提高系統性能,對于較小的字符串會保留其值的一個副本,當創建新的字符串時直接指向該副本,所以a和b的 id 值是一樣的,同樣對于小整數[-5, 257)也是如此:

                    >>> id(a)
                    140709793837832
                    >>> id(b)
                    140709793837832
                    >>> x = -5
                    >>> y = -5
                    >>> x is y
                    True
                    >>> id(x) == id(y)
                    True
                    

                    建議 17:考慮兼容性,盡可能使用 Unicode

                    我之前也總結過編碼的問題。由于最早的編碼是 ASCII 碼,只能表示 128 個字符,顯然這對其它語言編碼并不適用,Unicode就是為了不同的文字分配一套統一的編碼。

                    建議 18:構建合理的包層次來管理 module

                    本質上每一個 Python 文件都是一個模塊,使用模塊可以增強代碼的可維護性和可重用性,在較大的項目中,我們需要合理地組織項目層次來管理模塊,這就是包(Package)的作用。

                    一句話說包:一個包含__init__.py 文件的目錄。包中的模塊可以通過.進行訪問,即包名.模塊名。那么這個__init__.py文件有什么用呢?最明顯的作用就是它區分了包和普通目錄,在該文件中申明模塊級別的 import 語句從而變成了包級別可見,另外在該文件中定義__all__變量,可以控制需要導入的子包或模塊。

                    這里給出一個較為合理的包組織方式,是FlaskWeb 開發:基于Python的Web應用開發實戰一書中推薦而來的:

                    |-flasky
                        |-app/                      # Flask 程序
                            |-templates/            # 存放模板
                            |-static/               # 靜態文件資源
                            |-main/
                                |-__init__.py
                                |-errors.py         # 藍本中的錯誤處理程序
                                |-forms.py          # 表單對象
                                |-views.py          # 藍本中定義的程序路由
                            |-__init__.py
                            |-email.py              # 電子郵件支持
                            |-models.py             # 數據庫模型
                        |-migrations/               # 數據庫遷移腳本
                        |-tests/                    # 單元測試
                            |-__init__.py
                            |-test*.py
                        |-venv/                     # 虛擬環境
                        |-requirements/
                            |-dev.txt               # 開發過程中的依賴包
                            |-prod.txt              # 生產過程中的依賴包
                        |-config.py                 # 儲存程序配置
                        |-manage.py                 # 啟動程序以及其他的程序任務
                    

                    第 3 章:基礎語法

                    建議 19:有節制地使用 from...import 語句

                    Python 提供三種方式來引入外部模塊:import語句、from...import語句以及__import__函數,其中__import__函數顯式地將模塊的名稱作為字符串傳遞并賦值給命名空間的變量。

                    使用import需要注意以下幾點:

                    • 優先使用import a的形式

                    • 有節制地使用from a import A

                    • 盡量避免使用from a import *

                    為什么呢?我們來看看 Python 的 import 機制,Python 在初始化運行環境的時候會預先加載一批內建模塊到內存中,同時將相關信息存放在sys.modules中,我們可以通過sys.modules.items()查看預加載的模塊信息,當加載一個模塊時,解釋器實際上完成了如下動作:

                    1. 在sys.modules中搜索該模塊是否存在,如果存在就導入到當前局部命名空間,如果不存在就為其創建一個字典對象,插入到sys.modules中

                    2. 加載前確認是否需要對模塊對應的文件進行編譯,如果需要則先進行編譯

                    3. 執行動態加載,在當前命名空間中執行編譯后的字節碼,并將其中所有的對象放入模塊對應的字典中

                    >>> dir()
                    ['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__']
                    >>> import test
                    testing module import
                    >>> dir()
                    ['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'test']
                    >>> import sys
                    >>> 'test' in sys.modules.keys()
                    True
                    >>> id(test)
                    140367239464744
                    >>> id(sys.modules['test'])
                    140367239464744
                    >>> dir(test)
                    ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b']
                    >>> sys.modules['test'].__dict__.keys()
                    dict_keys(['__file__', '__builtins__', '__doc__', '__loader__', '__package__', '__spec__', '__name__', 'b', 'a', '__cached__'])
                    

                    從上可以看出,對于用戶自定義的模塊,import 機制會創建一個新的 module 將其加入當前的局部命名空間中,同時在 sys.modules 也加入該模塊的信息,但本質上是在引用同一個對象,通過test.py所在的目錄會多一個字節碼文件。

                    建議 20:優先使用 absolute import 來導入模塊

                    建議 21: i+=1 不等于 ++i

                    首先++i或--i在 Python 語法上是合法,但并不是我們通常理解的自增或自減操作:

                    >>> ++1     # +(+1)
                    1
                    >>> --1     # -(-1)
                    1
                    >>> +++2
                    2
                    >>> ---2
                    -2
                    

                    原來+或-只表示正負數符號。

                    建議 22:使用 with 自動關閉資源

                    對于打開的資源我們記得關閉它,如文件、數據庫連接等,Python 提供了一種簡單優雅的解決方案:with。

                    先來看with實現的原理吧。

                    with的實現得益于一個稱為上下文管理器(context manager)的東西,它定義程序運行時需要建立的上下文,處理程序的進入和退出,實現了上下文管理協議,即對象中定義了__enter__()和__exit__(),任何實現了上下文協議的對象都可以稱為一個上下文管理器:

                    • __enter__():返回運行時上下文相關的對象

                    • __exit__(exception_type, exception_value, traceback):退出運行時的上下文,處理異常、清理現場等

                    包含with語句的代碼塊執行過程如下:

                    with 表達式 [as 目標]:
                        代碼塊
                    # 例
                    >>> with open('test.txt', 'w') as f:
                    ...     f.write('test')
                    ... 
                    4
                    >>> f.__enter__
                    <built-in method __enter__ of _io.TextIOWrapper object at 0x7f1b967aaa68>
                    >>> f.__exit__
                    <built-in method __exit__ of _io.TextIOWrapper object at 0x7f1b967aaa68>
                    
                    1. 計算表達式的值,返回一個上下文管理器對象

                    2. 加載上下文管理器對象的__exit__()以備后用

                    3. 調用上下文管理器對象的__enter__()

                    4. 將__enter__()的返回值賦給目標對象

                    5. 執行代碼塊,正常結束調用__exit__(),其返回值直接忽略,如果發生異常,會調用__exit__()并將異常類型、值及 traceback 作為參數傳遞給__exit__(),__exit__()返回值為 false 異常將會重新拋出,返回值為 true 異常將被掛起,程序繼續執行

                    于此,我們可以自定義一個上下文管理器:

                    >>> class MyContextManager(object):
                    ...     def __enter__(self):
                    ...         print('entering...')
                    ...     def __exit__(self, exception_type, exception_value, traceback):
                    ...         print('leaving...')
                    ...         if exception_type is None:
                    ...             print('no exceptions!')
                    ...             return False
                    ...         elif exception_type is ValueError:
                    ...             print('value error!')
                    ...             return True
                    ...         else:
                    ...             print('other error')
                    ...             return True
                    ... 
                    >>> with MyContextManager():
                    ...     print('Testing...')
                    ... 
                    entering...
                    Testing...
                    leaving...
                    no exceptions!
                    >>> with MyContextManager():
                    ...     print('Testing...')
                    ...     raise(ValueError)
                    ... 
                    entering...
                    Testing...
                    leaving...
                    value error!
                    

                    Python 還提供contextlib模塊,通過 Generator 實現,其中的 contextmanager 作為裝飾器來提供一種針對函數級別上的上下文管理器,可以直接作用于函數/對象而不必關心__enter__()和__exit__()的實現。

                    推薦文章

                    建議 23:使用 else 子句簡化循環(異常處理)

                    Python 的 else 子句提供了隱含的對循環是否由 break 語句引發循環結束的判斷,有點繞哈,來看例子:

                    >>> def print_prime(n):
                    ...     for i in range(2, n):
                    ...         for j in range(2, i):
                    ...             if i % j == 0:
                    ...                 break
                    ...         else:
                    ...             print('{} is a prime number'.format(i))
                    ... 
                    >>> print_prime(7)
                    2 is a prime number
                    3 is a prime number
                    5 is a prime number
                    

                    可以看出,else 子句在循環正常結束和循環條件不成立時被執行,由 break 語句中斷時不執行,同樣,我們可以利用這顆語法糖作用在 while 和 try...except 中。

                    QQ群:WEB開發者官方群(515171538),驗證消息:10000
                    微信群:加小編微信 849023636 邀請您加入,驗證消息:10000
                    提示:更多精彩內容關注微信公眾號:全棧開發者中心(fsder-com)
                    網友評論(共0條評論) 正在載入評論......
                    理智評論文明上網,拒絕惡意謾罵 發表評論 / 共0條評論
                    登錄會員中心
                    福彩试机号今天 免费大公开一码中特 腾讯分分彩专业版 幸运农场秘籍 广西快3计划精准 香港官方特码资料 4月26日体彩20选5 okooo澳客网pk 彩经网旧版 新疆时时彩84走势图 广东36选7综合走势图 爱彩人幸运赛车开奖 北京快3开奖app 宁夏十一选五爱乐彩 韩国二分彩是什么 标准篮球场尺寸