1. 程式人生 > >python-生成器、裝飾器

python-生成器、裝飾器

目錄

動態語言和靜態語言:

動態語言可以在執行的過程中修改程式碼,例如python在執行的過程中給已建立好的類新增屬性和方法。

靜態語言在編譯時已經確定好程式碼,在執行過程中不能修改程式碼

那麼問題來了,如果我們不想在python的執行中,或者讓別人在呼叫我們的模組時新增新的屬性,我們就需要使用python定義的一個特殊變數:__slots__

__slots__

在定義一個類的時候使用這個變數,可以限制類的例項新增屬性

class Person(object):
    __slots__ = ('name','age')
    
p = person()
p.name = 'laowang'
p.age = 20
p.heigt = 180
>> AttributeError: Person instance has no attribute 'score'

注意:

使用__slots__定義的屬性僅針對當前類的例項起作用,對繼承自該類的子類不起作用

生成器

首先思考一個問題,當某個函式隔一段時間需要去從一個列表中獲取一個數據,那假設這個函式要不斷獲取1百萬個數據時,需要先把這一百萬個數據全都放在列表裡嗎?答案是否定的,這會非常浪費記憶體空間。所以生成器產生了,在python中,一邊迴圈一邊計算的機制成為生成器:generator,我是這樣理解的:先定義一個按需求迴圈生產資料的物件,不需要一次全部生產出來,當每次另一邊需要時,從這個物件裡面取出一個值使用就好了,這樣可以節省記憶體空間,同時也方便多個物件來使用它。

建立生成器的方法1
G = ( x*2 for x in range(5))
>>G
>><generator object <genexpr> at 0x7f626c132db0>
>>next(G)
>>0
>>next(G)
>>2
建立生成器的方法2

除了用簡單的for方法,我們還可以用函式來實現生成器

def fib(times):
    n = 0
    a,b = 0,1
    while n<times:
        yield b
        a,b = b,a+b
        n+1
    retrue 'done'

F = fib(5)
next(F)
>>1
next(F)
>>1
next(F)
>>2

注意,這裡使用了yield,相當於執行到yield之後,這個函式會進入阻塞狀態。再次呼叫時,從yield下方開始,再到yield停止。當這個生成器的值被取完後,再次取值程式會報錯,返回done。所以一定要注意生成器的剩餘值情況。

生成器的send和__next__方法
def gen():
    i = 0
    while i<5:
        temp = yield i
        print(temp)
        i += 1 
>> f = gen()
>> f.__next__()
>> 0
>> f.__next__()
>> None
>> 1
>> f.send('laowang')
>> laowang
>> 2

從上兩塊程式碼可見,__next__()方法跟next(f)實現了相同的效果。

前面提到過,yield後,程式會進入阻塞狀態,另外賦值語句是先執行等號的右邊,再執行等號的左邊,所以send方法傳送的引數會傳遞給temp,這就意味著處理資料的函式和生成器可以“溝通”下一步該怎麼生產資料

迭代器

迭代是訪問集合的一種方式,迭代器是一個可以記住遍歷的位置的物件,迭代器物件從集合的第一個元素開始訪問,知道所有元素都被訪問完結束。

可迭代物件

以直接作用於 for 迴圈的資料型別有以下幾種:

一類是集合資料型別,如 list 、 tuple 、 dict 、 set 、 str 等;

一類是 generator ,包括生成器和帶 yield 的generator function。

這些可以直接作用於 for 迴圈的物件統稱為可迭代物件: Iterable 。

生成器都是 Iterator 物件,但 list 、 dict 、 str 雖然是 Iterable ,卻不是 Iterator 。

總結
  • 凡是可作用於 for 迴圈的物件都是 Iterable 型別;
  • 凡是可作用於 next() 函式的物件都是 Iterator 型別
  • 集合資料型別如 list 、 dict 、 str 等是 Iterable 但不是 Iterator ,不過可以通過 iter() 函式獲得一個 Iterator 物件。

閉包

首先舉個例子

def test():
    print('233')

x = test
print(id(x))
print(id(test))
x()
>> 140212571149040
>> 140212571149040
>> 233

x實際上是引用了test函式的地址,並不是直接建立了一個新的函式或者使用了一個新的地址。

什麼是閉包

在函式內部再定義一個函式,並且這個函式用到了外邊函式的變數,那麼將這個函式及用到的一些變數稱之為閉包

def test(num_out):
    def test_in(num_in):
        print(num_in)
        return num_out + num_in
    return test_in  # 注意返回的是內部函式地址

ret = test(20)  # 20給num_out,因為都在一個函式內部,所以變數是可以使用的
print(ret(100)) # 100給num_in
>> 100
>> 120

這裡需要注意ret = test(100)直接執行到return test_in 將內部函式的地址返回給ret,所以才能列印。

閉包再理解
def line_conf(a, b):
    def line(x):
        return a*x + b
    return line

line1 = line_conf(1, 1)
line2 = line_conf(4, 5)
print(line1(5))
print(line2(5))

在建立閉包的時候,我們通過line_conf的引數a,b說明了這兩個變數的取值,這樣,我們就確定了函式的最終形式(y = x + 1和y = 4x + 5)。我們只需要變換引數a,b,就可以獲得不同的直線表達函式。由此,我們可以看到,閉包也具有提高程式碼可複用性的作用。

如果沒有閉包,我們需要每次建立直線函式的時候同時說明a,b,x。這樣,我們就需要更多的引數傳遞,也減少了程式碼的可移植性。

閉包可以幫我們把相同規律的事情總結一下,初始化部分交給一個函式,後續操作交給另一個函式,讓事情變得層次分明,同時也更方便操作。

閉包思考
1.閉包似優化了變數,原來需要類物件完成的工作,閉包也可以完成
2.由於閉包引用了外部函式的區域性變數,則外部函式的區域性變數沒有及時釋放,消耗記憶體

裝飾器

作為python3的新特性,裝飾器可以提高開發效率,也是python面試中經常會問的問題。

首先對函式名要有這樣的理解,名字只是代表了對一個空間的引用,它是可以改變的

def foo():
    print('ffffooo')
foo = lambda x:x+1
foo()
>>TypeError: <lambda>() missing 1 required positional argument: 'x'
肯定會報錯,因為foo指向的引用地址改變了。
裝飾器(decorator)功能
  • 引入日誌
  • 函式執行時間統計
  • 執行函式前預備處理
  • 執行函式後清理功能
  • 許可權校驗等場景
  • 快取
引入

假設某公司讓一個開發部門開發了一些功能,讓四個部門分別使用自己的功能,所以要在四個部門使用前驗證一次身份,以保證安全性。

已實現的完整功能:
def f1():
    print('f1')
def f2():
    print('f2')
呼叫:
# A部門使用f1
# B部門使用f2
# 因為要驗證,所以某開發人員又將函式寫成了如下形式,
def check()
    驗證身份
def f1():
    check() 
    print('f1')
def f2():
    check()
    print('f2')
# 然後再讓A部門呼叫f1,B部門呼叫f2

雖然也完成了想要的功能,但違反了開放封閉原則,即已實現的功能程式碼塊不允許被修改,但可以擴充套件(修改複雜業務邏輯的程式碼容易出現bug,但可以在引數傳入前加工一下或資料處理後再進行修正)

  • 開放:對擴充套件開發
  • 封閉:已實現的功能程式碼塊

所以需要結合閉包將程式碼改成如下形式

def w1(func):
    def inner():
        print('check')
        func()
    print("i am inner", id(inner))
    return inner

@w1
def f1():
    print('i am f1', id(f1))
    print('f1')
f1()

>>
i am inner 2814185593032
check
i am f1 2814185593032
f1

此時,我們再不修改f1的情況下完成了驗證,那程式碼的執行邏輯是怎麼的呢?

最關鍵的一點是@w1 > f1 = w1(f1)

w1執行後會將f1的引用傳遞給func(以供inner呼叫它),然後列印inner函式的id,再將inner的引用返回給f1
f1():執行f1,則f1實際上的引用指向的是inner,所以會執行整個inner函式,而inner中又包含了f1的引用,這樣就可以再f1功能執行前加入一些需要的操作,例如之前提到的驗證身份

所以@w1就是所謂的裝飾器,@函式名 是python中的一種語法糖

再談裝飾器
# 定義函式:完成包裹資料
def makeBold(fn2):
    def wrapped2():
        return "<b>" + fn2() + "</b>"
    return wrapped2

# 定義函式:完成包裹資料
def makeItalic(fn1):
    def wrapped1():
        return "<i>" + fn1() + "</i>"
    return wrapped1

@makeBold   
@makeItalic 
def test():
    return "hello world"

print(test())
>> <b><i>hello world</i></b>

@makeBold

@makeItalic

這裡會先執行makeItalic,再執行makeBold,因為這個語法糖需要對函式操作,沒有函式就讓下面的先執行。

@makeItalic  # test = makeItalic(test) -> makeItalic-wrapped1, fn1 -> test(原始記憶體空間的引用)
@makeBold   # test(引用已改變) = makeBold(test) -> makeBold-wrapped2, fn2 -> makeItalic-wrapped1
裝飾器舉例
被裝飾的函式有不定長引數
from time import ctime, sleep
def timefun(func):
    def wrappedfunc(*args, **kwargs):
        print("%s called at %s"%(func.__name__, ctime()))
        func(*args, **kwargs)
    return wrappedfunc

@timefun
def foo(a, b, c):
    print(a+b+c)

foo(3, 5, 7)
sleep(2)
foo(1, 4, 6)
被裝飾的函式有return的處理
from time import ctime, sleep

def timefun(func):
    def wrappedfunc():
        print("%s called at %s"%(func.__name__, ctime()))
        func()
    return wrappedfunc

@timefun
def getInfo():
    return 'haha'
print(getInfo())
>> None
# getInfo被裝飾後,因為return的'haha'是給的func(),而不是getInfo,因此getInfo是沒有返回值的,所以列印的是None。
# 需要將func()修改成 return func(),才返回給getInfo,然後打印出'haha'

總結:為了裝飾器更通用,一般可以有return

裝飾器帶引數,在原有裝飾器的基礎上,設定變數
from time import ctime, sleep

def timefun_arg(pre="hello"):
    print('tag1, ', pre)
    def timefun(func):
        def wrappedfunc():
            print("%s called at %s %s"%(func.__name__, ctime(), pre))
            return func()
        return wrappedfunc
    return timefun

@timefun_arg("itcast")
def foo():
    print("I am foo")

print('tag2')
sleep(2)
foo()
>>輸出:
tag1, itcast
tag2
foo called at Sun Dec  9 21:43:36 2018 itcast
I am foo

可見,當裝飾器帶有引數後,它會先去執行裝飾器函式,而以前沒有帶入引數時它會直接裝飾下方定義的函式。這樣的話我們就可以給裝飾器新增一些變數或者常用量