讓Python更優雅更易讀(第一集)

2022-08-13 15:01:14
變數和註釋

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具名元祖

元組經常用來存放結構化資料,但只能通過數位來存取元組成員其實特別不方便

  1. 初始化具名函數
  2. 可以通過數位索引來存取
  3. 也可以通過名稱來存取
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,函數就會儲存每一個執行結果,不再剔除任何舊快取。這時如果被快取的內容太多,就會有佔用過多記憶體的風險。