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