讓Python更優雅更易讀(第一集)
變數和注釋
1.變數
在編寫變數盡量要讓其清晰只給,讓人清除搞清楚程式碼的意圖
下方兩段程式碼作用完全一樣,但第二段程式碼是不是更容易讓人理解
value = s.strip()
username = input_string.strip()
1.1變數的基礎知識
1.1.1變數的交換
作為一門動態語言,我們不僅可以無需預先聲明變數類型直接賦值
同時還可以在一行內操作多個變數,比如交換
parent, child = "father", "son" parent, child = child, parent print(parent)#son
1.1.2變數解包
fruit = ["red apple","green apple","orange"] *apple, orange =fruit print(apple) #['red apple', 'green apple']
1.1.3變數用單下劃線命名
它常作為一個無意義的佔位符出現在賦值語句中,例如你想在解包時候忽略某些變數
fruit = ["red apple","green apple","orange"] *_, orange =fruit #這裡的_就意味著這個變數後續不太會使用的,可以忽略的 print(orange) #orange
1.1.4給變數註明類型
雖然Pyhton無需聲明變數,聲明了變數也無法起到校驗的作用
但我們還是需要去註明類型來提高我們程式碼的可讀性
最常見的做法是寫函數文檔,把參數類型與說明寫在函數文檔內
def hello_world(items): """ 這是進入編程大門的入口 :param items: 待入門的對象 :type items: 包含字元串的列表,[string,...] """ pass
或者添加類型註解
from typing import List def hello_world(items: List[str]): """ 這是進入編程大門的入口 """
2.數值和字元串
2.1數值
2.1.1浮點數的精度問題
print(0.1+0.2)#0.30000000000000004 為了解決上述的精度問題我們可以用decimal這個內置函數 from decimal import Decimal print(Decimal('0.1')+Decimal('0.2'))#必須用字元串表示數字
2.2字元串
2.2.1拼接多個字元串
最常見的做法是創建一個空列表,然後把需要拼接的字元串都放進列表中
最後使用str.join來拼接
除此之外也可以通過+號來拼接
3容器類型
最常見的內置容器類型有四種:列表、元組、字典、集合
3.1具名元祖
元組經常用來存放結構化數據,但只能通過數字來訪問元組成員其實特別不方便
- 初始化具名函數
- 可以通過數字索引來訪問
- 也可以通過名稱來訪問
from collections import namedtuple FruitColor = namedtuple('FruitColor','apple,orange') fruitColor = FruitColor("RED","GREEN") print(fruitColor[0])#RED print(fruitColor.apple)#RED
Python3.6以後我們還可以用類型註解語法和typing.NameTuple來定義具名函數
from typing import NamedTuple class FruitColor(NamedTuple): apple: str orange: str fruitColor = FruitColor("RED","GREEN") print(fruitColor[0]) print(fruitColor.apple)
3.2訪問不存在的字典鍵
當用不存在的鍵訪問字典內容時,程式會拋出KeyError異常
通常的做法:讀取內容前先做一次條件判斷,只有判斷通過的情況下才繼續執行其他操作
if "first" in example: num = example["first"] else: num = 0
或者
try: num = example["first"] except KeyError: num = 0
如果只是「提供默認值的讀取操作」,其實可以直接使用字典的.get()方法
#dict.get(key, default)方法接收一個default參數 example.get("first",0)
3.3使用setdefault取值並修改
比如我們有一個字典,這個字典內我們不知道有沒有這個鍵
example = {"first":[1],"second":[2]}
try: example["third"].append(3) except KeyError: example["third"] = [3]
除了上述寫法還有一個更合適的寫法
調用dict.setdefault(key, default)會產生兩種結果:
當key不存在時,該方法會把default值寫入字典的key位置,並返回該值;
假如key已經存在,該方法就會直接返回它在字典中的對應值
example.setdefault("third",[]).append(3)
3.4認識字典的有序性
在Python 3.6版本以前,幾乎所有開發者都遵從一條常識:「Python的字典是無序的。
」這裡的無序指的是:當你按照某種順序把內容存進字典後,就永遠沒法按照原順序把它取出來了。
這種無序現象,是由字典的底層實現所決定的
Python里的字典在底層使用了哈希表(hash table)數據結構。當你往字典里存放一對key: value時,Python會先通過哈希演算法計算出key的哈希值——一個整型數字;然後根據這個哈希值,決定數據在表裡的具體位置
因此,最初的內容插入順序,在這個哈希過程中被自然丟掉了,字典里的內容順序變得僅與哈希值相關,與寫入順序無關
字典變為有序只是作為3.6版本的「隱藏特性」存在。但到了3.7版本,它已經徹底成了語言規範的一部分
4.條件分支
4.1分支基礎注意事項
4.1.1不要顯式地和布爾值做比較
#不推薦的寫法 if example.is_deleted() == True: # 推薦寫法 if example.is_deleted():
4.1.2省略0值判斷
在if分支里時,解釋器會主動對它進行「真值測試」,也就是調用bool()函數獲取它的布爾值
if containers_count == 0: pass if fruits_list != []: pass
所以我們可以把程式碼改成如下:
if not containers_count: pass if fruits_list: pass
4.1.3三元表達式
一種濃縮版的條件分支——三元表達式
#語法: # true_value if <expression> else false_value
4.1.4修改對象的布爾值
from typing import List class Length: def __init__(self,items: List[str]): self.items = items lengthList = Length(["2","3"]) if len(lengthList.items) > 0 : pass
只要給UserCollection類實現__len__魔法方法,實際上就是為它實現了Python世界的長度協議
from typing import List class Length: def __init__(self,items: List[str]): self.items = items def __len__(self): return len(self.items)
或者可以在類中實現__bool__
from typing import List class Length: def __init__(self,items: List[str]): self.items = items def __bool__(self): return len(self.items)>2 lengthList = Length(["2","3"]) print(bool(lengthList)) #Fales
註:
假如一個類同時定義了__len__和__bool__兩個方法,解釋器會優先使用__bool__方法的執行結果
4.2優化分支程式碼
4.2.1優化枚舉程式碼
class Movie: """電影對象數據類""" @property def rank(self): """按照評分對電影分級: - S: 8.5 分及以上 - A:8 ~ 8.5 分 - B:7 ~ 8 分 - C:6 ~ 7 分 - D:6 分以下 """ rating_num = float(self.rating) if rating_num >= 8.5: return 'S' elif rating_num >= 8: return 'A' elif rating_num >= 7: return 'B' elif rating_num >= 6: return 'C' else: return 'D'
這就是一個普通的枚舉程式碼,根據電影評分給予不同的分級,但是程式碼冗餘
使用二分法模組進行優化
import bisect @property def rank(self): # 已經排好序的評級分界點 breakpoints = (6, 7, 8, 8.5) # 各評分區間級別名 grades = ('D', 'C', 'B', 'A', 'S') index = bisect.bisect(breakpoints, float(self.rating)) return grades[index]
4.2.2 使用字典優化分支
def get_sorted_movies(movies, sorting_type): if sorting_type == 'name': sorted_movies = sorted(movies, key=lambda movie: movie.name.lower()) elif sorting_type == 'rating': sorted_movies = sorted( movies, key=lambda movie: float(movie.rating), reverse=True ) elif sorting_type == 'year': sorted_movies = sorted( movies, key=lambda movie: movie.year, reverse=True ) elif sorting_type == 'random': sorted_movies = sorted(movies, key=lambda movie: random.random()) else: raise RuntimeError(f'Unknown sorting type: {sorting_type}') return sorted_movies
我們發現每一個分支都基本一樣:
都是對sorting_type做等值判斷(sorting_type == ‘name’)
邏輯也大同小異——都是調用sorted()函數,只是key和reverse參數略有不同
所以我們考慮用字典去優化:
sorting_algos = { # sorting_type: (key_func, reverse) 'name': (lambda movie: movie.name.lower(), False), 'rating': (lambda movie: float(movie.rating), True), 'year': (lambda movie: movie.year, True), 'random': (lambda movie: random.random(), False), }
4.3建議
4.3.1盡量避免多層嵌套
這些多層嵌套可以用一個簡單的技巧來優化——「提前返回」。
「提前返回」指的是:當你在編寫分支時,首先找到那些會中斷執行的條件,把它們移到函數的最前面,然後在分支里直接使用return或raise結束執行。
4.3.2別寫太複雜的表達式
如果表達式很長很複雜:
我們需要對條件表達式進行簡化,把它們封裝成函數或者對應的類方法,這樣才能提升分支程式碼的可讀性
4.3.3使用德摩根定律
簡單來說,「德摩根定律」告訴了我們這麼一件事:not A or not B等價於not (A and B)。
if not A or not B: pass #可以改寫成 if not (A and B): pass
這樣寫少了一個not變成更容易理解
4.3.4使用all() any()函數構建條件表達式
· all(iterable):僅當iterable中所有成員的布爾值都為真時返回True,否則返回False。
· any(iterable):只要iterable中任何一個成員的布爾值為真就返回True,否則返回False。
def all_numbers_gt_10(numbers): """僅當序列中所有數字都大於10 時,返回 True""" if not numbers: return False for n in numbers: if n <= 10: return False return True #改寫後 def all_numbers_gt_10_2(numbers): return bool(numbers) and all(n > 10 for n in numbers)
4.3.5 or的短路特性
#or最有趣的地方是它的「短路求值」特性。比如在下面的例子里,1 / 0永遠不會被執行,也就意味著不會拋出ZeroDivisionError異常 True or (1 / 0)
所以我們利用這個特性可以簡化一些分支
context = {} # 僅當 extra_context 不為 None 時,將其追加進 context 中 if extra_context: context.update(extra_context) #優化後 context.update(extra_context or {})
5異常處理
5.1獲取原諒比許可更簡單
在Python世界裡,EAFP指不做任何事前檢查,直接執行操作,但在外層用try來捕獲可能發生的異常。
def changeInt(value): """Try to convert the input to an integer""" try: return int(value) except TypeError: print(f'type error:{type(value)} is invalid') except ValueError: print(f'value error:{value} is invalid') finally: print('function completed')
5.1.1把最小的報錯更精確的except放在最前面
如果把最大的報錯放在最前面會導致所有的報錯都報的同一個異常,其他都不會被觸發
5.1.2使用else注意點
try: oneBranch() except Exception as e: print("error") else: print("branch succeeded")
- 異常捕獲語句里的else表示:僅當try語句塊里沒拋出任何異常時,才執行else分支下的內容,效果就像在try最後增加一個標記變數一樣
- 和finally語句不同,假如程式在執行try程式碼塊時碰到了return或break等跳轉語句,中斷了本次異常捕獲,那麼即便程式碼沒拋出任何異常,else分支內的邏輯也不會被執行。
5.1.3使用空raise語句
當一個空raise語句出現在except塊里時,它會原封不動地重新拋出當前異常
try: oneBranch() except Exception as e: print("error") raise else: print("branch succeeded")
5.1.4使用上下文管理器
有一個關鍵字和異常處理也有著密切的關係,它就是with
with是一個神奇的關鍵字,它可以在程式碼中開闢一段由它管理的上下文,並控制程式在進入和退出這段上下文時的行為。
比如在上面的程式碼里,這段上下文所附加的主要行為就是:進入時打開某個文件並返迴文件對象,退出時關閉該文件對象。
class DummyContext: def __init__(self, name): self.name = name def __enter__(self): #enter會在進入管理器被調用 #返回介面 return f'{self.name}' def __exit__(self, exc_type, exc_val, exc_tb): #退出會被調用 print('Exiting DummyContext') return False
with DummyContext('HelloWorld') as name: print(f'Name:{name}') #Name:HelloWorld #Exiting DummyContext
上下文管理器功能強大、用處很多,其中最常見的用處之一,就是簡化異常處理工作
正如上方5.1的例子我們用with來簡化finally
def changeInt(value): """Try to convert the input to an integer""" with DummyContext(): try: return int(value) except TypeError: print(f'type error:{type(value)} is invalid') except ValueError: print(f'value error:{value} is invalid') class DummyContext: def __enter__(self): #enter會在進入管理器被調用 #返回介面 return True def __exit__(self, exc_type, exc_val, exc_tb): #退出會被調用 print('function completed') return False print(changeInt(3)) #function completed #3
5.1.5使用with用於忽略異常
try: func() except : pass
雖然這樣的程式碼很簡單,但沒法復用。當項目中有很多地方要忽略這類異常時,這些try/except語句就會分布在各個角落,看上去非常凌亂。
class DummyContext: def __enter__(self): #enter會在進入管理器被調用 #返回介面 pass def __exit__(self, exc_type, exc_val, exc_tb): #退出會被調用 if exc_type == NameError: return True return False with DummyContext() as c: func()
當你想忽略NameError異常時,只要把程式碼用with語句包裹起來即可
在程式碼執行時,假如with管轄的上下文內沒有拋出任何異常,那麼當解釋器觸發__exit__方法時,上面的三個參數值都是None;
但如果有異常拋出,這三個參數就會變成該異常的具體內容。
(1) exc_type:異常的類型。
(2) exc_value:異常對象。
(3) traceback:錯誤的堆棧對象。
此時,程式的行為取決於__exit__方法的返回值。如果__exit__返回了True,那麼這個異常就會被當前的with語句壓制住,不再繼續拋出,達到「忽略異常」的效果;如果__exit__返回了False,那這個異常就會被正常拋出,交由調用方處理。
5.1.6使用contextmanager裝飾器
雖然上下文管理器很好用,但定義一個符合協議的管理器對象其實挺麻煩的——得首先創建一個類,然後實現好幾個魔法方法。
為了簡化這部分工作,Python提供了一個非常好用的工具:@contextmanager裝飾器
from contextlib import contextmanager @contextmanager def create_conn_obj(host, port, timeout=None): """創建連接對象,並在退出上下文時自動關閉""" conn = create_conn() try: yield conn finally: conn.close()
以yield關鍵字為界,yield前的邏輯會在進入管理器時執行(類似於__enter__),yield後的邏輯會在退出管理器時執行(類似於__exit__)
如果要在上下文管理器內處理異常,必須用try語句塊包裹yield語句在日常工作中,我們用到的大多數上下文管理器,可以直接通過「生成器函數+@contextmanager」的方式來定義,這比創建一個符合協議的類要簡單得多。
6循環和可迭代對象
在編寫for循環時,不是所有對象都可以用作循環主體——只有那些可迭代(iterable)對象才行
說到可迭代對象,你最先想到的肯定是那些內置類型,比如字元串、生成器以及第3章介紹的所有容器類型,等等。
6.1iter()與next()的內置函數
當你使用for循環遍歷某個可迭代對象時,其實是先調用了iter()拿到它的迭代器,然後不斷地用next()從迭代器中獲取值。
所以我們可以自己實現迭代器起到循環效果
intList = [1,2,3,4] num = iter(intList) while True: try: _int = next(num) print(_int) except StopIteration: break
6.2自定義迭代器
class Range7: #生產一個包含7或者可以被7整除 def __init__(self,start,end): self.start = start self.end = end #當前位置 self.current = start #__iter__:調用iter()時觸發,迭代器對象總是返回自身。 def __iter__(self): return self # __next__:調用next()時觸發,通過return來返回結果 # 沒有更多內容就拋出StopIteration異常,會在迭代過程中多次觸發 def __next__(self): while True: if self.current >= self.end: raise StopIteration if self.num_is_vaild(self.current): ret = self.current self.current += 1 return ret self.current += 1 def num_is_vaild(self,num): #判斷數字是否滿足 if not num : return False return num % 7 ==0 or '7' in str(num) r = Range7(0,20) for num in r: print(num)
6.3區分迭代器與可迭代對象
一個合法的迭代器,必須同時實現__iter__和__next__兩個魔法方法。
可迭代對象只需要實現__iter__方法,不一定得實現__next__方法。
class Range7: def __init__(self,start,end): self.start = start self.end = end def __iter__(self): #返回一個新的迭代器對象 return Range7Iterator(self) class Range7Iterator: #生產一個包含7或者可以被7整除 def __init__(self,range_obj): self.end = range_obj.end self.start = range_obj.start #當前位置 self.current = range_obj.start #__iter__:調用iter()時觸發,迭代器對象總是返回自身。 def __iter__(self): return self # __next__:調用next()時觸發,通過return來返回結果 # 沒有更多內容就拋出StopIteration異常,會在迭代過程中多次觸發 def __next__(self): while True: if self.current >= self.end: raise StopIteration if self.num_is_vaild(self.current): ret = self.current self.current += 1 return ret self.current += 1 def num_is_vaild(self,num): #判斷數字是否滿足 if not num : return False return num % 7 ==0 or '7' in str(num) r = Range7(0,20) print(tuple(r),1) print(tuple(r),2)
6.4生成器是迭代器
生成器還是一種簡化的迭代器實現,使用它可以大大降低實現傳統迭代器的編碼成本。
因此在平時,我們基本不需要通過__iter__和__next__來實現迭代器,只要寫上幾個yield就行。
還是用上面的例子。我們用生成器來簡化程式碼
def isRang7(num: int): return True if num !=0 and (num % 7 ==0 or '7' in str(num)) else False def rang7(start: int, end: int): num = start while num < end : if isRang7(num): yield num num += 1
6.5使用itertools模組
看下面這個例子我們如何簡化
def find_twelve(num_list1, num_list2, num_list3): """從3 個數字列表中,尋找是否存在和為 12 的3 個數""" for num1 in num_list1: for num2 in num_list2: for num3 in num_list3: if num1 + num2 + num3 == 12: return num1, num2, num3
我們可以使用product()函數來優化它。product()接收多個可迭代對象作為參數,然後根據它們的笛卡兒積不斷生成結果
from itertools import product print(list(product([1,2],[3,4])))#[(1, 3), (1, 4), (2, 3), (2, 4)]
from itertools import product def find_twelve_v2(num_list1, num_list2, num_list3): for num1, num2, num3 in product(num_list1, num_list2, num_list3): if num1 + num2 + num3 == 12: return num1, num2, num3
相比之前,新函數只用了一層for循環就完成了任務,程式碼變得更精練了。
7.函數
7.1常用函數模組
7.1.1functools.partial
functools是一個專門用來處理函數的內置模組,其中有十幾個和函數相關的有用工具
def multiply(x, y): return x * y #假設我們有很多地方需要調用上面這個函數 #result = multiply(2, value) #val = multiply(2, number) #這些程式碼有一個共同的特點,這些程式碼有一個共同的特點 #為了簡化程式碼 def double(value): # 返回 multiply 函數調用結果 return multiply(2, value) # 調用程式碼變得更簡單 # result = double(value) # val = double(number)
針對這類場景,我們其實不需要像前面一樣,用def去完全定義一個新函數——直接使用functools模組提供的高階函數partial()就行。
def multiply(x, y): return x * y import functools double = functools.partial(multiply,2) print(double(3))#6
7.1.2functools.lru_cache()
為了提高效率,給這類慢函數加上快取是比較常見的做法。
lru即「最近最少使用」(least recently used,LRU)演算法丟掉舊快取,釋放記憶體
下面模擬一個慢函數
import time from functools import lru_cache @lru_cache(maxsize=None) def slow_func(): time.sleep(10) return 1
第一次快取沒有命中,耗時比較長
第二個調用相同函數,就不會觸發函數內部邏輯,結果直接返回
在使用lru_cache()裝飾器時,可以傳入一個可選的maxsize參數,該參數代表當前函數最多可以保存多少個快取結果。
默認情況下,maxsize的值為128。如果你把maxsize設置為None,函數就會保存每一個執行結果,不再剔除任何舊快取。這時如果被快取的內容太多,就會有佔用過多記憶體的風險。