首頁 > 後端開發 > Python教學 > 歸納總結Python中的裝飾器知識點

歸納總結Python中的裝飾器知識點

WBOY
發布: 2022-06-17 13:50:40
轉載
2418 人瀏覽過

本篇文章為大家帶來了關於python的相關知識,其中主要介紹了關於裝飾器的相關問題,包括了閉包、裝飾器、使用多個裝飾器、帶參數的裝飾器等等內容,下面一起來看一下,希望對大家有幫助。

歸納總結Python中的裝飾器知識點

推薦學習:python影片教學

#一、閉包

要了解什麼是裝飾器(decorator),我們首先需要知道閉包(closure)的概念。

閉包,又稱閉包函數或閉合函數,通俗一點來講,當某個函數被當成物件返回時還夾帶了外部變量,就形成了一個閉包。

以列印Hello World為例,我們先來看看巢狀函數的結構應該是什麼樣的:

def print_msg(msg):

    def printer():
        print(msg)

    printer()print_msg('Hello World')# Hello World
登入後複製

執行print_msg('Hello World')相當於執行了printer(),也就是執行print(msg),所以會輸出Hello World

我們再來看一下如果是閉包,該是什麼樣的結構:

def print_msg(msg):

    def printer():
        print(msg)

    return printer


my_msg = print_msg('Hello World')my_msg()# Hello World
登入後複製

本例中的 printer 函數就是閉包。

執行print_msg('Hello World') 實際上是傳回如下這樣一個函數,它夾帶了外部變數'Hello World'

def printer():
    print('Hello World')
登入後複製

於是呼叫my_msg 就相當於執行printer()


那麼要如何判斷函數是否是閉包函數呢?閉包函數的 __closure__ 屬性裡面定義了一個元組用於存放所有的cell對象,每個cell對象保存了這個閉包中所有的外部變數。而普通函數的 __closure__ 屬性為 None

def outer(content):

    def inner():
        print(content)

    return innerprint(outer.__closure__)
    # Noneinner = outer('Hello World')print(inner.__closure__)
    # (<cell at 0x0000023FB1FD0B80: str object at 0x0000023FB1DC84F0>,)
登入後複製

由此可見 outer 函數不是閉包,而 inner 函數是閉包。

我們還可以查看閉包所攜帶的外部變數:

print(inner.__closure__[0].cell_contents)# Hello World
登入後複製

說了那麼多,那麼閉包究竟有什麼用呢?閉包存在的意義就是它夾帶了外部變數(私貨),如果它不夾帶私貨,那麼就和普通的函數沒有任何區別。

閉包的優點如下:

  • 局部變數無法共享且長久的保存,而全域變數可能造成變數污染,閉包既可以長久的保存變數又不會造成全域污染。
  • 閉包使得函數內局部變數的值始終保持在記憶體中,不會在外部函數呼叫後自動清除。

二、裝飾器

我們先考慮這樣一個場景,假設先前寫的一個函數已經實現了4個功能,為簡單起見,我們用print 語句來代表每一個具體的功能:

def module():
    print('功能1')
    print('功能2')
    print('功能3')
    print('功能4')
登入後複製

現在,由於某種原因,你需要為module 這個函數新增一個功能5,你完全可以這樣修改:

def module():
    print('功能1')
    print('功能2')
    print('功能3')
    print('功能4')
    print('功能5')
登入後複製

但在現實業務中,直接做出這樣的修改往往是比較危險的(會變得不容易維護)。那麼如何在不修改原函數的基礎上去為它新添一個功能呢?

你可能已經想到了使用之前的閉包知識:

def func_5(original_module):

    def wrapper():
        original_module()
        print('功能5')

    return wrapper
登入後複製

#func_5 代表該函數主要用於實作功能5 ,我們接下來將module 傳入進去觀察效果:

new_module = func_5(module)new_module()# 功能1# 功能2# 功能3# 功能4# 功能5
登入後複製

可以看出,我們的新模組:new_module 已經實作了功能5

在上面的範例中,函數 func_5 就是一個裝飾器,它裝飾了原來的模組(為它新添了一個功能)。

#當然,Python有更簡潔的寫法(稱之為語法糖),我們可以將@符號與裝飾器函數的名稱一起使用,並將其放置在要裝飾的函數的定義上方:

def func_5(original_module):

    def wrapper():
        original_module()
        print('功能5')

    return wrapper@func_5def module():
    print('功能1')
    print('功能2')
    print('功能3')
    print('功能4')module()# 功能1# 功能2# 功能3# 功能4# 功能5
登入後複製

基於此,我們可以在不修改原始函數的基礎上完成計時任務(計算原始函數的運行時間),如下:

def timer(func):

    def wrapper():
        import time
        tic = time.time()
        func()
        toc = time.time()
        print('程序用时: {}s'.format(toc - tic))

    return wrapper@timerdef make_list():
    return [i * i for i in range(10**7)]my_list = make_list()# 程序用时: 0.8369960784912109s
登入後複製

事實上,my_list 並不是列表,直接列印會顯示None,這是因為我們的wrapper 函數沒有設定回傳值。如果需要取得make_list 的回傳值,可以這樣修改wrapper 函數:

def wrapper():
    import time
    tic = time.time()
    a = func()
    toc = time.time()
    print('程序用时: {}s'.format(toc - tic))
    return a
登入後複製

三、使用多個裝飾器

假如我們要為module 新增功能5功能6(以數字順序),那該如何做呢?

好在Python允許同時使用多個裝飾器:

def func_5(original_module):
    def wrapper():
        original_module()
        print('功能5')
    return wrapperdef func_6(original_module):
    def wrapper():
        original_module()
        print('功能6')
    return wrapper@func_6@func_5def module():
    print('功能1')
    print('功能2')
    print('功能3')
    print('功能4')module()# 功能1# 功能2# 功能3# 功能4# 功能5# 功能6
登入後複製

上述過程實際上等價於:

def module():
    print('功能1')
    print('功能2')
    print('功能3')
    print('功能4')new_module = func_6(func_5(module))new_module()
登入後複製

此外,需要注意的是,在使用多個裝飾器時,最靠近函數定義的裝飾器會先裝飾該函數,如果我們改變裝飾順序,則輸出結果也會改變:

@func_5@func_6def module():
    print('功能1')
    print('功能2')
    print('功能3')
    print('功能4')module()# 功能1# 功能2# 功能3# 功能4# 功能6# 功能5
登入後複製

四、被裝飾的函數帶有參數

如果被裝飾的函數帶有參數,那該如何去建構裝飾器呢?

考慮這樣一個函數:

def pide(a, b):
    return a / b
登入後複製

b=0 时会出现 ZeropisionError。如何在避免修改该函数的基础上给出一个更加人性化的提醒呢?

因为我们的 pide 函数接收两个参数,所以我们的 wrapper 函数也应当接收两个参数:

def smart_pide(func):
    def wrapper(a, b):
        if b == 0:
            return '被除数不能为0!'
        else:
            return func(a, b)
    return wrapper
登入後複製

使用该装饰器进行装饰:

@smart_pidedef pide(a, b):
    return a / bprint(pide(3, 0))# 被除数不能为0!print(pide(3, 1))# 3.0
登入後複製

如果不知道要被装饰的函数有多少个参数,我们可以使用下面更为通用的模板:

def decorator(func):
    def wrapper(*args, **kwargs):
        # ...
        res = func(*args, **kwargs)
        # ...
        return res  # 也可以不return
    return wrapper
登入後複製

五、带参数的装饰器

我们之前提到的装饰器都没有带参数,即语法糖 @decorator 中没有参数,那么该如何写一个带参数的装饰器呢?

前面实现的装饰器都是两层嵌套函数,而带参数的装饰器是一个三层嵌套函数。

考虑这样一个场景。假如我们在为 module 添加新功能时,希望能够加上实现该功能的开发人员的花名,则可以这样构造装饰器(以 功能5 为例):

def func_5_with_name(name=None):
    def func_5(original_module):
        def wrapper():
            original_module()
            print('功能5由{}实现'.format(name))
        return wrapper    return func_5
登入後複製

效果如下:

@func_5_with_name(name='若水')def module():
    print('功能1')
    print('功能2')
    print('功能3')
    print('功能4')module()# 功能1# 功能2# 功能3# 功能4# 功能5由若水实现
登入後複製

对于这种三层嵌套函数,我们可以这样理解:当为 func_5_with_name 指定了参数后,func_5_with_name(name='若水') 实际上返回了一个 decorator,于是 @func_5_with_name(name='若水') 就相当于 @decorator

六、使用类作为装饰器

将类作为装饰器,我们需要实现 __init__ 方法和 __call__ 方法。

以计时器为例,具体实现如下:

class Timer:

    def __init__(self, func):
        self.func = func    def __call__(self):
        import time
        tic = time.time()
        self.func()
        toc = time.time()
        print('用时: {}s'.format(toc - tic))@Timerdef make_list():
    return [i**2 for i in range(10**7)]make_list()# 用时: 2.928966999053955s
登入後複製

如果想要自定义生成列表的长度并获得列表(即被装饰的函数带有参数情形),我们就需要在 __call__ 方法中传入相应的参数,具体如下:

class Timer:

    def __init__(self, func):
        self.func = func    def __call__(self, num):

        import time
        tic = time.time()
        res = self.func(num)
        toc = time.time()
        print('用时: {}s'.format(toc - tic))

        return res@Timerdef make_list(num):
    return [i**2 for i in range(num)]my_list = make_list(10**7)# 用时: 2.8219943046569824sprint(len(my_list))# 10000000
登入後複製

如果要构建带参数的类装饰器,则不能把 func 传入 __init__ 中,而是传入到 __call__ 中,同时 __init__ 用来初始化类装饰器的参数。

接下来我们使用类装饰器来复现第五章节中的效果:

class Func_5:

    def __init__(self, name=None):
        self.name = name    def __call__(self, func):

        def wrapper():
            func()
            print('功能5由{}实现'.format(self.name))

        return wrapper@Func_5('若水')def module():
    print('功能1')
    print('功能2')
    print('功能3')
    print('功能4')module()# 功能1# 功能2# 功能3# 功能4# 功能5由若水实现
登入後複製

七、内置装饰器

Python中有许多内置装饰器,这里仅介绍最常见的三种:@classmethod@staticmethod@property

7.1 @classmethod

@classmethod 用于装饰类中的函数,使用它装饰的函数不需要进行实例化也可调用。需要注意的是,被装饰的函数不需要 self 参数,但第一个参数需要是表示自身类的 cls 参数,它可以来调用类的属性,类的方法,实例化对象等。

cls 代表类本身,self 代表实例本身。

具体请看下例:

class A:

    num = 100

    def func1(self):
        print('功能1')

    @classmethod
    def func2(cls):
        print('功能2')
        print(cls.num)
        cls().func1()A.func2()# 功能2# 100# 功能1
登入後複製

7.2 @staticmethod

@staticmethod 同样用来修饰类中的方法,使用它装饰的函数的参数没有任何限制(即无需传入 self 参数),并且可以不用实例化调用该方法。当然,实例化后调用该方法也是允许的。

具体如下:

class A:

    @staticmethod
    def add(a, b):
        return a + bprint(A.add(2, 3))# 5print(A().add(2, 3))# 5
登入後複製

7.3 @property

使用 @property 装饰器,我们可以直接通过方法名来访问类方法,不需要在方法名后添加一对 () 小括号。

class A:

    @property
    def printer(self):
        print('Hello World')a = A()a.printer# Hello World
登入後複製

除此之外,@property 还可以用来防止类的属性被修改。考虑如下场景

class A:

    def __init__(self):
        self.name = 'ABC'a = A()print(a.name)# ABCa.name = 1print(a.name)# 1
登入後複製

可以看出类中的属性 name 可以被随意修改。如果要防止修改,则可以这样做

class A:

    def __init__(self):
        self.name_ = 'ABC'

    @property
    def name(self):
        return self.name_
    
    
a = A()print(a.name)# ABCa.name = 1print(a.name)# AttributeError: can't set attribute
登入後複製

推荐学习:python视频教程

以上是歸納總結Python中的裝飾器知識點的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:csdn.net
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板