茴香豆的“茴”有四種寫法,Python的格式化字串也有

deuk發表於2022-04-18

茴香豆的“茴”有四種寫法,Python的格式化字串也有

最近正在閱讀《Python Tricks: The Book》這本書,想要通過掌握書中提及的知識點提高自己的Python程式設計能力。本文主要記錄在學習該書第二章“Patterns for Cleaner Python”過程中的一些心得體會。

被低估的斷言

斷言是程式語言提供的一種除錯工具,是讓程式在執行時進行自檢的程式碼,可以輔助程式設計師進行交流和除錯。程式設計師可以通過斷言瞭解程式碼正確執行所依賴的假設,此外,當斷言為假時,程式設計師可以快速排查出由於輸入(輸入引數的取值範圍不符合預期)輸出(返回的結果是沒有意義的)不符合介面假設而導致的錯誤。

斷言常用於驗證以下兩類條件是否滿足:

  • 前置條件,呼叫方(Caller)在呼叫函式或類時必須滿足的條件,比如平方根運算要求被開方數必須大於0;
  • 後置條件,被呼叫方(Callee)的執行結果需要滿足的條件,比如商品打折後的價格不能高於原價或者低於0。

在Python中通過“assert expression ["," expression]”的方式宣告斷言,例如:

def apply_discount(product, discount):
    price = int(product['price'] * (1.0 - discount))
    # 驗證後置條件:打折後的價格應該介於0到原價之間
    assert 0 <= price <= product['price']
    return price

注意:斷言主要用於處理程式碼中不應發生的錯誤,而那些在預期中的可能發生的錯誤建議使用異常進行處理。

多一個逗號,少一點糟心事

在定義列表、元組、字典以及集合時,在最後一個元素後面追加一個額外的逗號非常有用:

  • 增刪元素或調整元素的順序將變得容易,不會因為忘了逗號而導致錯誤;
  • 使得Git這樣的軟體配置管理工具可以準確追蹤到程式碼的更改(git diff,改了哪一行就顯示哪一行,新增元素時不會因為在上一行的末尾加了個逗號,把上一行也標綠)。
# 新增元素'Jane',由於忘了在'Dilbert'後面加逗號,names變成了['Alice', 'Bob', 'DilbertJane']
names = [
    'Alice',
    'Bob',
    'Dilbert'
    'Jane'
]

上下文管理器和with語句

with語句解構了try/finally語句的標準用法:with語句開始執行時,會呼叫上下文管理器物件的“__enter__”方法,這對應try/finally語句之前申請系統資源的過程(比如開啟檔案);with語句執行結束後,會呼叫上下文管理器物件的“__exit__”方法,這對應finally子句中釋放系統資源的過程(比如關閉檔案控制程式碼)。

可以通過兩種方式定義上下文管理器:

  1. 通過定義類的方式,實現“__enter__”和“__exit__”方法;
  2. 使用contextlib模組的contextmanager裝飾器,利用yield語句解構try/finally。
class Indenter:
    """縮排管理器"""
    def __init__(self):
        self.level = 0
    
    def __enter__(self):
        self.level += 1
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.level -= 1

    def print(self, text: str):
        print('    ' * self.level + text)

with Indenter() as indent:
    indent.print('風在吼')
    indent.print('馬在叫')
    with indent:
        indent.print('黃河在咆哮')
        indent.print('黃河在咆哮')
        with indent:
            indent.print('河西山岡萬丈高')
            indent.print('河東河北高粱熟了')
    indent.print('...')
    indent.print('保衛家鄉!保衛黃河!')
    indent.print('保衛華北!保衛全中國!')

#    風在吼
#    馬在叫
#        黃河在咆哮
#        黃河在咆哮
#            河西山岡萬丈高
#            河東河北高粱熟了
#    ...
#    保衛家鄉!保衛黃河!
#    保衛華北!保衛全中國!

(有沒有更好的寫法,感覺使用全域性變數不太優雅?)

import contextlib

level = 0

@contextlib.contextmanager
def indenter():
    global level
    level += 1
    yield
    level -= 1

def cprint(text: str):
    global level
    print('    ' * level + text)

with indenter():
    cprint('風在吼')
    cprint('馬在叫')
    with indenter():
        cprint('黃河在咆哮')
        cprint('黃河在咆哮')
        with indenter():
            cprint('河西山岡萬丈高')
            cprint('河東河北高粱熟了')
    cprint('...')
    cprint('保衛家鄉!保衛黃河!')
    cprint('保衛華北!保衛全中國!')

作為字首和字尾的下劃線

在Python的變數名或方法名的前面或後面使用單個下劃線或兩個下劃線,有著不同的含義:

  1. 單個下劃線作為字首(_var)表示類或模組的私有成員,嘗試通過萬用字元匯入模組的所有函式和變數時(from module import *),私有成員不會被匯入;

  2. 單個下劃線作為字尾(var_)用於與Python關鍵字進行區分,比如“def make_object(name, class_)”中的形參”class_”;

  3. 兩個下劃線作為字首(__var)用於避免與子類相同名稱的類變數之間的衝突,變數名會被Python直譯器重寫為“_類名__var”;

    class Test:
        """父類"""
        def __init__(self):
            self.foo = 0
            self.__bar = 1
    
    test = Test()
    dir(test)  # ['_Test__bar', 'foo', ...]
    
    class ExtendedTest(Test):
        """子類"""
        def __init__(self):
            super().__init__()
            self.foo = 3
            self.__bar = 4
    
    test = ExtendedTest()
    dir(test)  # ['_ExtendedTest__bar', '_Test__bar', 'foo', ...]
    
  4. 兩個下劃線同時作為字首和字尾(__var__)表示特殊方法;

  5. 單獨的下劃線表示臨時變數的名稱(主要用於拆包時進行佔位),也表示REPL最近一個表示式的結果。

茴香豆的“茴”有四種寫法,格式化字串也有

在Python中有四種常用的格式化字串的方法:

  1. printf風格的格式化,比如print('逐夢演藝%s' % '圈圈圈圈圈');

  2. str.format,比如print('逐夢演藝{0}{0}{0}{0}{0}'.format('圈'))

  3. f-string字串插值:

    echoes = '圈'
    print(f'逐夢演藝{echoes * 5}')
    
  4. string.Template:

    import string
    
    template = string.Template('逐夢演藝${echoes}')
    print(template.substitute(echoes='圈圈圈圈圈'))
    

當需要對使用者輸入的字串進行格式化時,推薦使用string.Template而非其他方案,因為惡意使用者可能通過類似SQL隱碼攻擊的方式獲取系統的敏感資訊:

import string

SECRET = '這是私鑰'

class Error:
    def __init__(self):
        pass

err = Error()
user_input = '{error.__init__.__globals__[SECRET]}'
user_input.format(error=err)  # '這是私鑰',糟糕,私鑰被洩露了!

user_input = '${error.__init__.__globals__[SECRET]}'
string.Template(user_input).substitute(error=err)  # ValueError

Python之禪

Beautiful is better than ugly.
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.
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.
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!

參考資料

  1. Python Tricks: The Book
  2. 什麼時候用異常,什麼時候用斷言?
  3. 《重構:改善既有程式碼的設計》,9.8節“引入斷言”
  4. 《程式碼大全-第二版》,8.2節“斷言”
  5. Why are trailing commas allowed in a list?
  6. 《流暢的Python》,15.2節“上下文管理器和with塊”

相關文章