Python基礎之裝飾器

裝飾器

此文多是有史以來最全的關於Python裝飾器的Blog了...面試

函數名的運用

關於函數名

函數名是⼀個變量,但它是⼀個特殊的變量。與括號配合能夠執⾏函數的變量。緩存

查看函數名的內存地址:閉包

def func():
    print('呵呵')

print(func)  # <function func at 0x10983c048>

作變量

def func():
    print('呵呵')


a = func  # 把函數當成變量賦值給另一個變量
a()  # 經過變量a調用函數

作容器的元素

def func1():
    print('func1')
    

def func2():
    print('func2')
    
    
def func3():
    print('func3')
    
    
def func4():
    print('func4')


list1 = [func1, func2, func3, func4]
for i in list1:
    i()

作參數

def func1():
    print('func1')


def func2(arg):
    print('start')
    arg()  # 執行傳遞進來的arg
    print('end')

func2(func1)  # 把func1當成參數傳遞給func2

作返回值

def func1():
    print('這裏是func1')

    def func2():
        print('這裏是func2')
    return func2  # 把func2當成返回值返回


ret = func1()  # 調用func1,把返回值賦值給ret
ret()  # 調用ret

 

閉包

靈魂三問

首先咱們來看一個例子:函數

def func1():
    name = '張三'

    def func2(arg):
        print(arg)
    func2(name)

func1()

理解了上面的例子,咱們再看一個例子:工具

def func1():
    name = '張三'

    def func2():
        print(name)  # 可以訪問到外層做用域的變量
    func2()

func1()

最後再看一個例子:spa

def func1(name):

    def func2():
        print(name)  # 可以訪問到外層做用域的變量
    func2()

func1('張三')

閉包的定義

一個內層函數中,引用了外層函數(非全局)的變量,這個內層函數就能夠成爲閉包。設計

在Python中,咱們可使用__closure__來檢測函數是不是閉包。code

def func1():
    name = '張三'

    def func2():
        print(name)  # 可以訪問到外層做用域的變量
    func2()
    print(func2.__closure__)  # (<cell at 0x1036c7438: str object at 0x10389d088>,)

func1()
print(func1.__closure__)  # None

問題來了,咱們如何在函數外邊調用函數內部的函數呢?orm

固然是把內部函數當成返回值返回了。blog

def func1():
    name = '張三'

    def func2():
        print(name)

    return func2  # 把內部函數當成是返回值返回


ret = func1()  # 把返回值賦值給變量ret
ret()  # 調用內部函數

內部函數固然還可包含其餘的函數,多層嵌套的道理都是同樣的。

def func1():
    def func2():
        def func3():
            print('func3')
        return func3
    return func2


ret1 = func1()  # func2
ret2 = ret1()  # func3
ret2()

接下來咱們看下面這個例子,來更深入的理解一下閉包的含義:

def print_msg(msg):
# 這是外層函數

    def printer():
    # 這是內層函數
        print(msg)

    return printer  # 返回內層函數


func = print_msg("Hello")
func()

 如今咱們進行以下操做:

>>> del print_msg
>>> func()
Hello
>>> print_msg("Hello")
Traceback (most recent call last):
...
NameError: name 'print_msg' is not defined

 

咱們知道若是⼀個函數執⾏完畢,則這個函數中的變量以及局部命名空間中的內容都將會被銷燬。在閉包中內部函數會引用外層函數的變量,並且這個變量將不會隨着外層函數的結束而銷燬,它會在內存中保留。

也就是說,閉包函數能夠保留其用到的變量的引用。

閉包面試題

# 編寫代碼實現func函數,使其實現如下效果:
foo = func(8)
print(foo(8))  # 輸出64
print(foo(-1))  # 輸出-8

裝飾器

裝飾器來歷

在說裝飾器以前,咱們先說⼀個軟件設計的原則: 開閉原則, ⼜被成爲開放封閉原則。

開放封閉原則是指對擴展代碼的功能是開放的,可是對修改源代碼是封閉的。這樣的軟件設計思路能夠保證咱們更好的開發和維護咱們的代碼。

咱們先來寫一個例子,模擬一下女媧造人:

def create_people():
    print('女媧真厲害,捏個泥吹口氣就成了人!')
    

create_people()

好吧,如今問題來了。上古時期啊,天氣很不穩定,這個時候忽然大旱三年。女媧再去捏人啊,由於太乾了就捏不到一起去了,須要在捏人以前灑點水才行。

def create_people():
    print('灑點水')
    print('女媧真厲害,捏個泥吹口氣就成了人!')


create_people()

這不就搞定了麼?可是呢,咱們是否是違背了開放封閉原則呢?咱們是添加了新的功能,可是咱們是直接修改了源代碼。在軟件開發中咱們應該對直接修改源代碼是謹慎的。

好比,女媧爲了防止浪費,想用剩下點泥巴捏個雞、鴨、鵝什麼的,也須要灑點水。那咱們能在每一個造雞、造鴨、造鵝函數的源代碼中都手動添加代碼麼?確定是不現實的。

怎麼辦?再寫一個函數不就OK了麼?

def create_people():
    print('女媧真厲害,捏個泥吹口氣就成了人!')


def create_people_with_water():
    print('灑點水')
    create_people()


create_people_with_water()

不讓我直接修改源代碼,那我從新寫一個函數不就能夠了嗎?

可是,你有沒有想過一個問題,女媧造人也很累的,她後來開了不少分店,每家分店都是調用了以前的create_people函數造人,那麼你修改了以後,是否是全部調用原來函數的人都須要修改調用函數的名稱呢?很麻煩啊!!!

總結一句話就是如何在不改變函數的結構和調用方式的基礎上,動態的給函數添加功能?

def create_people():
    print('女媧真厲害,捏個泥吹口氣就成了人!')


def a(func):
    def b():
        print('灑點水')
        func()
    return b

ret = a(create_people)
ret()

利用閉包函數不就能夠了麼?

可是,你這最後調用的是ret啊,不仍是改變了調用方式麼?

再往下看:

def create_people():
    print('女媧真厲害,捏個泥吹口氣就成了人!')


def a(func):
    def b():
        print('灑點水')
        func()
    return b

create_people = a(create_people)
create_people()

上面這段代碼是否是完美解決了咱們的問題呢?

看一下它的執行過程吧:

  1. 首先訪問a(create_people)
  2. 把create_people函數賦值給了a函數的形參func,記住後續執行func的話其實是執行了最開始傳入的create_people函數。
  3. a函數執行過程就是一句話,返回了b函數。這個時候把b函數賦值給了create_people這個變量
  4. 執行create_people的時候,至關於執行了b函數,先打印灑點水再執行func,也就是咱們最開始傳入的create_people函數

咱們巧妙的使用閉包實現了,把一個函數包裝了一下,而後再賦值給原來的函數名。

裝飾器語法糖

上面的代碼就是一個裝飾器的雛形,Python中針對於上面的功能提供了一個快捷的寫法,俗稱裝飾器語法糖。

使用裝飾器語法糖的寫法,實現一樣功能的代碼以下:

def a(func):
    def b():
        print('灑點水')
        func()
    return b


@a  # 裝飾器語法糖
def create_people():
    print('女媧真厲害,捏個泥吹口氣就成了人!')

create_people()

裝飾器進階

裝飾帶返回值的函數

若是被裝飾的函數有返回值,咱們應該怎麼處理呢?

請看下面的示例:

def foo(func):  # 接收的參數是一個函數名
    def bar():  # 定義一個內層函數
        print("這裏是新功能...")  # 新功能
        r = func()  # 在內存函數中拿到被裝飾函數的結果
        return r  # 返回被裝飾函數的執行結果
    return bar


# 定義一個有返回值的函數
@foo
def f1():
    return '嘿嘿嘿'


# 調用被裝飾函數
ret = f1()  # 調用被裝飾函數並拿到結果
print(ret)

 

裝飾帶參數的函數

def foo(func):  # 接收的參數是一個函數名
    def bar(x, y):  # 這裏須要定義和被裝飾函數相同的參數
        print("這裏是新功能...")  # 新功能
        func(x, y)  # 被裝飾函數名和參數都有了,就能執行被裝飾函數了
    return bar


# 定義一個須要兩個參數的函數
@foo
def f1(x, y):
    print("{}+{}={}".format(x, y, x+y))


# 調用被裝飾函數
f1(100, 200)

帶參數的裝飾器

被裝飾的函數能夠帶參數,裝飾器一樣也能夠帶參數。

回頭看咱們上面寫得那些裝飾器,它們默認把被裝飾的函數當成惟一的參數。可是呢,有時候咱們須要爲咱們的裝飾器傳遞參數,這種狀況下應該怎麼辦呢?

接下來,咱們就一步步實現帶參數的裝飾器:

首先咱們來回顧下上面的代碼:

def f1(func):  # f1是咱們定義的裝飾器函數,func是被裝飾的函數
    def f2(*arg, **kwargs):  # *args和**kwargs是被裝飾函數的參數
        func(*arg, **kwargs)
    return f2

從上面的代碼,咱們發現了什麼?

個人裝飾器若是有參數的話,沒地方寫了…怎麼辦呢?

仍是要使用閉包函數!

咱們須要知道,函數除了能夠嵌套兩層,還能嵌套更多層:

# 三層嵌套的函數
def f1():    
    def f2():
        name = "張三"       
        def f3():
            print(name)
        return f3    
    return f2

嵌套三層以後的函數調用:

f = f1()  # f --> f2
ff = f()  # ff --> f3
ff()  # ff()  --> f3()  --> print(name)  --> 張三

注意:在內部函數f3中可以訪問到它外層函數f2中定義的變量,固然也能夠訪問到它最外層函數f1中定義的變量。

# 三層嵌套的函數2
def f1():
    name = '張三'
    def f2():
        def f3():
            print(name)
        return f3
    return f2

調用:

f = f1()  # f --> f2
ff = f()  # ff --> f3
ff()  # ff()  --> f3()  --> print(name)  --> 張三

好了,如今咱們就能夠實現咱們的帶參數的裝飾器函數了:

# 帶參數的裝飾器須要定義一個三層的嵌套函數
def d(name):  # d是新添加的最外層函數,爲咱們原來的裝飾器傳遞參數,name就是咱們要傳遞的函數
    def f1(func):  # f1是咱們原來的裝飾器函數,func是被裝飾的函數
        def f2(*arg, **kwargs):  # f2是內部函數,*args和**kwargs是被裝飾函數的參數
            print(name)  # 使用裝飾器函數的參數
            func(*arg, **kwargs)  # 調用被裝飾的函數
        return f2
    return f1

上面就是一個帶參裝飾器的代碼示例,如今咱們來寫一個完整的應用:

def d(a=None):  # 定義一個外層函數,給裝飾器傳參數--role
    def foo(func):  # foo是咱們原來的裝飾器函數,func是被裝飾的函數
        def bar(*args, **kwargs):  # args和kwargs是被裝飾器函數的參數
            # 根據裝飾器的參數作一些邏輯判斷
            if a:
                print("歡迎來到{}頁面。".format(a))
            else:
                print("歡迎來到首頁。")
            # 調用被裝飾的函數,接收參數args和kwargs
            func(*args, **kwargs)
        return bar
    return foo


@d()  # 不給裝飾器傳參數,使用默認的'None'參數
def index(name):
    print("Hello {}.".format(name))


@d("電影")  # 給裝飾器傳一個'電影'參數
def movie(name):
    print("Hello {}.".format(name))

if __name__ == '__main__':
    index('張三')
    movie('張三')

裝飾器修復技術

被裝飾的函數最終都會失去原本的__doc__等信息, Python給咱們提供了一個修復被裝飾函數的工具。

def a(func):
    @wraps(func)
    def b():
        print('灑點水')
        func()
    return b


@a  # 裝飾器語法糖
def create_people():
    """這是一個女媧造人的功能函數"""
    print('女媧真厲害,捏個泥吹口氣就成了人!')

create_people()
print(create_people.__doc__)
print(create_people.__name__)

 

多個裝飾器裝飾同一函數

同一個函數能夠被多個裝飾器裝飾,此時須要注意裝飾器的執行順序。

def foo1(func):
    print("d1")

    def inner1():
        print("inner1")
        return "<i>{}</i>".format(func())

    return inner1


def foo2(func):
    print("d2")

    def inner2():
        print("inner2")
        return "<b>{}</b>".format(func())

    return inner2


@foo1
@foo2
def f1():
    return "Hello Andy"

# f1 = foo2(f1)  ==> print("d2") ==> f1 = inner2
# f1 = foo1(f1)  ==> print("d1") ==> f1 = foo1(inner2) ==> inner1

ret = f1()  # 調用f1() ==> inner1()  ==> <i>inner2()</i>  ==> <i><b>inner1()</b></i> ==> <i><b>Hello Andy</b></i>
print(ret)

 

裝飾器終極進階

類裝飾器

咱們除了可使用函數裝飾函數外,還能夠用類裝飾函數。

class D(object):
    def __init__(self, a=None):
        self.a = a
        self.mode = "裝飾"

    def __call__(self, *args, **kwargs):
        if self.mode == "裝飾":
            self.func = args[0]  # 默認第一個參數是被裝飾的函數
            self.mode = "調用"
            return self
        # 當self.mode == "調用"時,執行下面的代碼(也就是調用使用類裝飾的函數時執行)
        if self.a:
            print("歡迎來到{}頁面。".format(self.a))
        else:
            print("歡迎來到首頁。")
        self.func(*args, **kwargs)


@D()
def index(name):
    print("Hello {}.".format(name))


@D("電影")
def movie(name):
    print("Hello {}.".format(name))

if __name__ == '__main__':
    index('張三')
    movie('張三')

 

裝飾類

咱們上面全部的例子都是裝飾一個函數,返回一個可執行函數。Python中的裝飾器除了能裝飾函數外,還能裝飾類。

可使用裝飾器,來批量修改被裝飾類的某些方法:

# 定義一個類裝飾器
class D(object):
    def __call__(self, cls):
        class Inner(cls):
            # 重寫被裝飾類的f方法
            def f(self):
                print('Hello 張三.')
        return Inner


@D()
class C(object):  # 被裝飾的類
    # 有一個實例方法
    def f(self):
        print("Hello world.")


if __name__ == '__main__':
    c = C()
    c.f()

舉個實際的應用示例:

咱們把類中的一個只讀屬性定義爲property屬性方法,只有在訪問它時才參與計算,一旦訪問了該屬性,咱們就把這個值緩存起來,下次再訪問的時候無需從新計算。

class lazyproperty:
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            value = self.func(instance)
            setattr(instance, self.func.__name__, value)
            return value


import math


class Circle:
    def __init__(self, radius):
        self.radius = radius

    @lazyproperty
    def area(self):
        print('計算面積')
        return math.pi * self.radius ** 2


c1 = Circle(10)
print(c1.area)
print(c1.area)
相關文章
相關標籤/搜索