Python編程思想(25):方法詳解

李寧老師已經在「極客起源」 微信公衆號推出《Python編程思想》電子書,囊括了Python的核心技術,以及Python的主要函數庫的使用方法。讀者可以在「極客起源」 公衆號中輸入 160442 開始學習。

《Python編程思想》總目錄

《Python編程思想》專欄

 

目錄

1. 在類中調用實例方法

2. 類方法與靜態方法

3. 函數裝飾器


方法是類或對象行爲的抽象,但 Python的方法本章上也是函數,其定義方式、調用方式和函數都非常相似,因此 Python的方法並不僅僅是單純的方法,它與函數也有莫大的關係。

1. 在類中調用實例方法

在前面的文章講過,在 Python的類體中定義的方法默認都是實例方法,前面也示範了通過對象來調用實例方法。但要提醒大家的是,Python的類在很大程度上是一個命名空間。當程序在類體中定義變量、定義方法時,與前面介紹的定義變量、定義函數其實並沒有太大的不同。對比如下代碼。

示例代碼:class_demo1.py

# 定義全局空間的test函數
def test ():
    print("全局空間的test方法")
# 全局空間的name變量
name = 'Bill'
class Dog:
    # 定義Dog空間的run函數
    def run():
        print("Dog空間的run方法")
    # 定義Bird空間的bar變量
    value = 123
# 調用全局空間的函數和變量
test()
print(name)
# 調用Bird空間的函數和變量
Dog.run()
print(Dog.value)

上面代碼在全局空間和Dog類(Dog空間)中分別定義了test函數和name變量,從定義它們的代碼來看,幾乎沒有任何區別,只是在Dog類中定義它們時需要縮進。

接下來程序在調用Dog空間內的value變量和run函數(方法)時,只要添加Dog.前綴即可,這說明完全可以通過Dog類來調用run函數(方法)。

現在問題來了,如果使用類調用實例方法,那麼該方法的第一個參數(self)怎麼自動綁定呢?

例如如下程序:

示例代碼:class_demo2.py

class Person:
    def run (self):
        print(self, '正在跑步...')
# 通過類調用實例方法
Person.run()

運行這段代碼,程序會拋出如下異常:

TypeError: run() missing 1 required positional argument: 'self'

在這段代碼中,run方法缺少傳入的self參數,所以導致程序出錯。這說明在使用類調用實例方法時, Python不會自動爲第1個參數綁定調用者。實際上也沒法自動綁定,因此實例方法的調用者是類本身,而不是對象。

如果程序依然希望使用類來調用實例方法,則必須手動爲方法的第1個參數傳入參數值。例如,使用下面的代碼:

class Person:
    def run (self):
        print(self, '正在跑步...')
# 通過類調用實例方法
# Person.run()
person = Person()
# 顯式爲方法的第一個參數綁定參數值
Person.run(person)

這段代碼顯式地爲 run方法的第1個參數綁定了參數值,這樣的調用效果完全等同於執行 person.run()方法。實際上,當通過Person類調用run實例方法時, Python只要求手動爲第1個參數綁定參數值,並不要求必須綁定Person對象,因此也可使用如下代碼進行調用。

# 顯式地爲方法的第一個參數綁定Python字符串參數值
Person.run('Python')

如果按上面方式進行綁定,那麼Python字符串就會被傳給run()方法的第1個參數self。因此,運行上面代碼,將會看到如下輸出結果:

Python 正在跑步...

Python的類可以調用實例方法,但使用類調用實例方法時,Python不會自動爲方法的第1個參數self綁定參數值。程序必須顯式地爲第1個參數self傳入方法調用者。這種調用方式被稱爲“未綁定調用”。

2. 類方法與靜態方法

實際上, Python完全支持定義類方法,甚至支持定義靜態方法。 Python的類方法和靜態方法類似,它們都推薦使用類來調用(其實也可使用對象來調用)。類方法和靜態方法的區別:Python會自動綁定類方法的第1個參數,類方法的第1個參數(通常建議參數名爲cls)會自動綁定到類本身。但對於靜態方法則不會自動綁定。

使用@ classmethod修飾的方法就是類方法,使用@ staticmethod修飾的方法就是靜態方法。

下面代碼演示了定義類方法和靜態方法。

示例代碼:class_static_method.py

class Pandas:
    # classmethod修飾的方法是類方法
    @classmethod
    def run (cls):
        print('類方法run: ', cls)
    # staticmethod修飾的方法是靜態方法
    @staticmethod
    def printName (p):
        print('靜態方法info: ', p)
# 調用類方法,Dog類會自動綁定到第一個參數
Pandas.run()
# 調用靜態方法,不會自動綁定,因此程序必須手動綁定第1個參數
Pandas.printName('小糰子')
# 創建Bird對象
p = Pandas()
# 使用對象調用run()類方法,其實依然還是使用類調用,
# 因此第1個參數依然被自動綁定到Pandas類
p.run()
# 使用對象調用printName靜態方法,其實依然還是使用類調用,
# 因此程序必須爲第一個參數執行綁定
p.printName('小糰子')

從這段代碼可以看出,使用@classmethod修飾的方法是類方法,該類方法定義了一個cls參數,該參數會被自動綁定到Pandas類本身,不管程序是使用類還是對象調用該方法,Python始終都會將類方法的第1個參數綁定到類本身。

這段代碼還使用 @staticmethod定義了一個靜態方法,程序同樣既可使用類調用靜態方法,也可使用對象調用靜態方法,不管用哪種方式調用,Python都不會爲靜態方法執行自動綁定。

在使用 Python編程時,一般不需要使用類方法或靜態方法,程序完全可以使用函數來代替類方法或靜態方法。但是在特殊的場景(例如,使用工廠模式)下,類方法或靜態方法也是不錯的選擇。

3. 函數裝飾器

前面介紹的@staticmethod和@classmethod的本質就是函數裝飾器,其中 staticmethod和classmethod都是 Python內置的函數。

使用@符號引用已有的函數(比如@staticmethod和@classmethod)後,可用於修飾其他函數,裝飾被修飾的函數。那麼我們是否可以開發自定義的函數裝飾器呢?答案是肯定的。

當程序使用“@函數”(比如函數X)裝飾另一個函數(比如函數Y)時,實際上完成如下兩步:

(1) 將被修飾的函數(函數Y)作爲參數傳給@符號引用的函數(函數A);

(2)將函數Y替換(裝飾)成第(1)步的返回值;

從上面介紹不難看出,被“@函數”修飾的函數不再是原來的函數,而是被替換成一個新的東西。

爲了讓大家更清楚函數裝飾器的作用,下面看一個非常簡單的示例。

示例代碼:decorator_demo.py

def funX(fn):
    print('X')
    fn() # 執行傳入的fn參數
    return 'Python'
'''
下面裝飾效果相當於:funX(funY),
funY將會替換(裝飾)成該語句的返回值;
由於funX()函數返回Python,因此funB就是Python
'''
@funX
def funY():
    print('funY')
print(funY) # Python

上面程序使用@funX修飾funY,這意味着程序要完成如下兩步操作:

(1)將funY作爲 funX的參數,也就是相當於執行funX(funY);

(2)將funY替換成第(1)步執行的結果,funX()執行完成後返回Python,因此funY就不再是函數,而是被替換成一個字符串;

運行這段代碼,可以看到如下輸出結果:

X
funY
Python

通過這個例子,相信讀者對函數裝飾器的執行關係已經有了一個較爲清晰的認識,但讀者可能會產生另一個疑問:這個函數裝飾器導致被修飾的函數變成了字符串,那麼函數裝飾器有什麼用?

別忘記了,被修飾的函數總是被替換成@符號所引用的函數的返回值,因此被修飾的函數會變成什麼,完全由@符號所引用的函數的返回值決定。如果@符號所引用的函數的返回值是函數,那麼被修飾的函數在替換之後還是函數。

下面程序演示了更復雜的函數裝飾器(接前面的程序)。

def process(fn):
    # 定義一個嵌套函數
    def print_info(*args):
        print('-------1-------', args)
        n = args[0]
        print('-------2-------', n ** 3)
        # 查看傳給process函數的fn函數
        print(fn.__name__)
        fn(n * (n + 1))
        print("*" * 20)
        return fn(n * (n - 1))
    return print_info

'''
下面裝飾效果相當於:process(my_value),
my_value將會替換(裝飾)成該語句的返回值;
由於process()函數返回print_info函數,因此funY就是print_info

'''
@process
def my_value(a):
    print("-----my_value函數------", a)
# 打印my_value函數,將看到實際上是bar函數
print(my_value) #
# 下面代碼看上去是調用my_value(),其實是調用print_info()函數
my_value(10)
my_value(6, 5)

上面程序定義了一個裝飾器函數process,該函數執行完成後並不是返回普通值,而是返回print_info函數(這是關鍵),這意味着被該@process修飾的函數最終都會被替換成print_info函數。

上面程序使用@process修飾 my_value()函數,因此程序同樣會執行process(my_value),並將 my_value替換成process函數的返回值print_info函數。所以,在這段代碼中打印 my_value函數時,實際上輸出的是print_info函數,這說明my_value已經被替換成print_info函數。接下來程序兩次調用 my_value函數,實際上就是調用print_info函數。

運行上面程序,可以看到如下輸出結果:

<function process.<locals>.print_info at 0x7fb4880a55f0>
-------1------- (10,)
-------2------- 1000
my_value
-----my_value函數------ 110
********************
-----my_value函數------ 90
-------1------- (6, 5)
-------2------- 216
my_value
-----my_value函數------ 42
********************
-----my_value函數------ 30

通過@符號來修飾函數是 Python的一個非常實用的功能,它既可以在被修飾函數的前面添加些額外的處理邏輯(比如權限檢查),也可以在被修飾函數的後面添加一些額外的處理邏輯(比如記錄日誌),還可以在目標方法拋出異常時進行一些修復操作。這種改變不需要修改被修飾函數的代碼,只要增加一個修飾即可。

其實前面介紹的這種在被修飾函數之前、之後、拋出異常後增加某種處理邏輯的方式,就是其他編程語言中的AOP( Aspect Orient Programming,面向切面編程)。

下面例子示範瞭如何通過函數裝飾器爲函數添加權限檢查的功能。程序代碼如下:

示例代碼:auth_demo.py

def auth(fn):
    def verify_auth(*args):
        # 用一條語句模擬執行權限檢查
        print("----模擬執行權限檢查----")
        # 回調要裝飾的目標函數
        fn(*args)
    return verify_auth
@auth
def test(a, b,c,d):
    print(f"執行test函數,參數a: {a}, 參數b: {b}, 參數d: {c},參數d: {d}" )
# 調用test()函數,其實是調用裝飾後返回的verify_auth函數
test(123, 55, 135,66)

上面程序使用@auth修飾了test()函數,這會使得 test()函數被替換成 auth函數所返回的 verify_auth函數,而 verify_auth函數的執行流程如下:

(1)先執行權限檢查;

(2)回調被修飾的目標函數;

也就是說,verify_auth函數就爲被修飾函數添加了一個權限檢查的功能。運行該程序,,可以看到如下輸出結果:

----模擬執行權限檢查---- 執行test函數,參數a: 123, 參數b: 55, 參數d: 135,參數d: 66

-----------------支持作者請轉發本文,也可以加李寧老師微信:unitymarvel,或掃描下面二維碼加微信--------

歡迎關注  極客起源  微信公衆號,更多精彩視頻和文章等着你哦!

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章