python 基礎學習第十天

一.閉包python

因爲閉包這個概念比較難以理解,尤爲是初學者來講,相對難以掌握,因此咱們經過示例去理解學習閉包。安全

給你們提個需求,而後用函數去實現:完成一個計算不斷增長的系列值的平均值的需求。微信

例如:整個歷史中的某個商品的平均收盤價。什麼叫平局收盤價呢?就是從這個商品一出現開始,天天記錄當天價格,而後計算他的平均值:平均值要考慮直至目前爲止全部的價格。閉包

好比大衆推出了一款新車:小白轎車。app

第一天價格爲:100000元,平均收盤價:100000元函數

次日價格爲:110000元,平均收盤價:(100000 + 110000)/2 元工具

第三天價格爲:120000元,平均收盤價:(100000 + 110000 + 120000)/3 元學習

........指針

series = []
def make_averager(new_value):
    series.append(new_value)
    total = sum(series)
    return total / len(series)

print(make_averager(100000))
print(make_averager(110000))
print(make_averager(120000))

從上面的例子能夠看出,基本上完成了咱們的要求,可是這個代碼相對來講是不安全的,由於你的這個series列表是一個全局變量,只要是全局做用域的任何地方,均可能對這個列表進行改變。code

series = []
def make_averager(new_value):
    series.append(new_value)
    total = sum(series)
    return total / len(series)

print(make_averager(100000))
print(make_averager(110000))
series.append(666)  # 若是對數據進行相應改變,那麼你的平均收盤價就會出現很大的問題。
print(make_averager(120000))

那麼怎麼辦呢?有人說,你把他放在函數中不就好了,這樣不就是局部變量了麼?數據不就相對安全了麼?

def make_averager(new_value):
    series = []
    series.append(new_value)
    total = sum(series)
    return total / len(series)


print(make_averager(100000))  # 100000.0
print(make_averager(110000))  # 110000.0
print(make_averager(120000))  # 120000.0

這樣計算的結果是不正確的,那是由於執行函數,會開啓一個臨時的名稱空間,隨着函數的結束而消失,因此你每次執行函數的時候,都是從新建立這個列表,那麼這怎麼作呢?這種狀況下,就須要用到咱們講的閉包了,咱們用閉包的思想改一下這個代碼。

def make_averager():

    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)

    return averager

avg = make_averager()
print(avg(100000))
print(avg(110000))
print(avg(120000))

你們仔細看一下這個代碼,我是在函數中嵌套了一個函數。那麼avg 這個變量接收的實際是averager函數名,也就是其對應的內存地址,我執行了三次avg 也就是執行了三次averager這個函數。那麼此時大家有什麼問題?

確定有學生就會問,那麼個人make_averager這個函數只是執行了一次,爲何series這個列表沒有消失?反而還能夠被調用三次呢?這個就是最關鍵的地方,也是閉包的精華所在。我給你們說一下這個原理,以圖爲證:

img

上面被紅色方框框起來的區域就是閉包,被藍色圈起來的那個變量應該是make_averager()函數的局部變量,它應該是隨着make_averager()函數的執行結束以後而消失。可是他沒有,是由於此區域造成了閉包,series變量就變成了一個叫自由變量的東西,averager函數的做用域會延伸到包含自由變量series的綁定。也就是說,每次我調用avg對應的averager函數 時,均可以引用到這個自用變量series,這個就是閉包。

閉包的定義:

  1. 閉包是嵌套在函數中的函數。
  2. 閉包必須是內層函數對外層函數的變量(非全局變量)的引用。

如何判斷判斷閉包?舉例讓同窗回答:

# 例一:
def wrapper():
    a = 1
    def inner():
        print(a)
    return inner
ret = wrapper()

# 例二:
a = 2
def wrapper():
    def inner():
        print(a)
    return inner
ret = wrapper()


# 例三:

def wrapper(a,b):
    def inner():
        print(a)
        print(b)
    return inner
a = 2
b = 3
ret = wrapper(a,b)

以上三個例子,最難判斷的是第三個,其實第三個也是閉包,若是咱們每次去研究代碼判斷其是否是閉包,有一些不科學,或者過於麻煩了,那麼有一些函數的屬性是能夠獲取到此函數是否擁有自由變量的,若是此函數擁有自由變量,那麼就能夠側面證實其是不是閉包函數了(瞭解):

def make_averager():

    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)

    return averager
avg = make_averager()
# 函數名.__code__.co_freevars 查看函數的自由變量
print(avg.__code__.co_freevars)  # ('series',)
固然還有一些參數,僅供瞭解:

# 函數名.__code__.co_freevars 查看函數的自由變量
print(avg.__code__.co_freevars)  # ('series',)
# 函數名.__code__.co_varnames 查看函數的局部變量
print(avg.__code__.co_varnames)  # ('new_value', 'total')
# 函數名.__closure__ 獲取具體的自由變量對象,也就是cell對象。
# (<cell at 0x0000020070CB7618: int object at 0x000000005CA08090>,)
# cell_contents 自由變量具體的值
print(avg.__closure__[0].cell_contents)  # []

閉包的做用:保存局部信息不被銷燬,保證數據的安全性。

閉包的應用

  1. 能夠保存一些非全局變量可是不易被銷燬、改變的數據。
  2. 裝飾器。

二. 推導式

本節咱們講列表推導式,生成器表達式以及其餘推導式,我認爲推導式就是構建比較有規律的列表,生成器,字典等一種簡便的方式。那麼他如何簡便呢?看下面的例題:

2.1列表推導式

這裏讓學生本身作一下,首先咱們先看一下這樣的代碼,給出一個列表,經過循環,想列表中添加1~10:

li = []

for i in range(10):

    li.append(i)

print(li)

那麼按照上面的要求咱們用列表推導式寫一下:

ls = [i for i in range(10)]

print(ls)

怎麼樣?一行搞定,上面這個代碼就是列表推導式,接下來咱們將列表推導式進行一個分類:

列表推導式分爲兩種模式:

1.循環模式:[變量(加工的變量) for 變量 in iterable]

2.篩選模式: [變量(加工的變量) for 變量 in iterable if 條件]

固然還有多層循環的,這個咱們一會就會講到,那麼咱們先來看循環模式。

2.1.1 循環模式

剛纔咱們看到的就是循環模式,那麼有同窗會問到,什麼叫' 加工的變量'? 這個也比較簡單,接下來咱們作幾道題:

  1. 將10之內全部整數的平方寫入列表。
l1 = [i*i for i in range(1,11)]
print(l1)
  1. 100之內全部的偶數寫入列表.
l1 = [i for i in range(2,101,2)]
print(l1)
  1. 從python1期到python24期寫入列表lst
lst = [f'python{i}' % i for i in range(1,25)]

print(lst)

上面那個格式化輸出的變量f'python{i}',就是加工的變量。

上面作的那三個就是循環模式,比較簡單,接下來咱們研究篩選模式。

2.1.2 篩選模式

篩選模式就是在上面的基礎上加上一個判斷條件,將知足條件的變量留到列表中。

帶着同窗們作一個題:

將這個列表中大於3的元素留下來。

l1 = [4, 3, 2, 6, 5, 5, 7, 8] 
print([i for i in l1 if i > 3])

經過我給你們的演示,你們作幾道題:

  1. 三十之內能夠被三整除的數。

    multiples = [i for i in range(30) if i % 3 is 0]
     print(multiples)
  2. 過濾掉長度小於3的字符串列表,並將剩下的轉換成大寫字母

    l = ['wusir', 'laonanhai', 'aa', 'b', 'taibai']
     # print([i.upper() for i in l if len(i) > 3])
  3. 找到嵌套列表中名字含有兩個‘e’的全部名字(有難度

    names = [['Tom', 'Billy', 'Jefferson', 'Andrew', 'Wesley', 'Steven', 'Joe'],
              ['Alice', 'Jill', 'Ana', 'Wendy', 'Jennifer', 'Sherry', 'Eva']]
    
     print([name for lst in names for name in lst if name.count('e') >= 2])  
     # 注意遍歷順序,這是實現的關鍵

列表推導式基本上講完了,固然今天會作一些有關列表推導式的題,讓你們更加深刻的瞭解。

2.1.3 生成器表達式

生成器表達式和列表推導式的語法上如出一轍,只是把[]換成()就好了。好比將十之內全部數的平方放到一個生成器表達式中

gen = (i**2 for i in range(10))
print(gen)
# 結果: <generator object <genexpr> at 0x0000026046CAEBF8>

生成器表達式也能夠進行篩選

# 獲取1-100內能被3整除的數
gen = (i for i in range(1,100) if i % 3 == 0)
for num in gen:
    print(num)

生成器表達式和列表推導式的區別:

  1. 列表推導式比較耗內存,全部數據一次性加載到內存。而生成器表達式遵循迭代器協議,逐個產生元素。
  2. 獲得的值不同,列表推導式獲得的是一個列表.生成器表達式獲取的是一個生成器
  3. 列表推導式一目瞭然,生成器表達式只是一個內存地址。

不管是生成器表達式,仍是列表推導式,他只是Python給你提供了一個相對簡單的構造方式,由於使用推導式很是簡單,因此大多數都會爲之着迷,這個必定要慎重,推導式只能構建相對複雜的而且有規律的對象,對於沒有什麼規律,並且嵌套層數比較多(for循環超過三層)這樣就不建議你們用推導式構建。

生成器的惰性機制: 生成器只有在訪問的時候才取值,說白了.你找他要纔給你值.不找他要.他是不會執行的.

2.1.4 其餘相關的推導式(瞭解)

字典推導式

根據名字應該也能猜到,推到出來的是字典

lst1 = ['jay','jj','meet']
lst2 = ['周杰倫','林俊杰','郭寶元']
dic = {lst1[i]:lst2[i] for i in range(len(lst1))}
print(dic)

集合推導式

集合推導式能夠幫咱們直接生成一個集合,集合的特色;無序,不重複 因此集合推導式自帶去重功能

lst = [1,2,3,-1,-3,-7,9]
s = {abs(i) for i in lst}
print(s)

三 .迭代器

3.1 可迭代對象

  3.1.1 可迭代對象定義

對於迭代器來講,咱們更熟悉的應該是可迭代對象,以前不管是源碼仍是講課中或多或少咱們提到過可迭代對象這個詞。以前爲了便於你們理解可迭代對象,可能解釋的不是很正確,因此今天咱們正式的聊一聊什麼是可迭代對象。從字面意思來講,咱們先對其進行拆解:什麼是對象?Python中一切皆對象,以前咱們講過的一個變量,一個列表,一個字符串,文件句柄,函數名等等均可稱做一個對象,其實一個對象就是一個實例,就是一個實實在在的東西。那麼什麼叫迭代?其實咱們在平常生活中常常遇到迭代這個詞兒,更新迭代等等,迭代就是一個重複的過程,可是不能是單純的重複(若是隻是單純的重複那麼他與循環沒有什麼區別)每次重複都是基於上一次的結果而來。好比你爹生你,你生你爹,哦不對,你生你兒子,你兒子生你孫子等等,每一代都是不同的;還有你使用過得app,微信,抖音等,隔一段時間就會基於上一次作一些更新,那麼這就是迭代。可迭代對象從字面意思來講就是一個能夠重複取值的實實在在的東西。

那麼剛纔咱們是從字面意思分析的什麼是可迭代對象,到目前爲止咱們接觸到的可迭代對象有哪些呢?

str list tuple dic set range 文件句柄等,那麼int,bool這些爲何不能稱爲可迭代對象呢?雖然在字面意思這些看着不符合,可是咱們要有必定的判斷標準或者規則去判斷該對象是否是可迭代對象。

在python中,但凡內部含有*iter*方法的對象,都是可迭代對象

  3.1.2 查看對象內部方法

該對象內部含有什麼方法除了看源碼還有什麼其餘的解決方式麼?固然有了, 能夠經過dir() 去判斷一個對象具備什麼方法

s1 = 'alex'
print(dir(s1))

dir()會返回一個列表,這個列表中含有該對象的以字符串的形式全部方法名。這樣咱們就能夠判斷python中的一個對象是否是可迭代對象了:

s1 = 'alex'
i = 100
print('__iter__' in dir(i))  # False
print('__iter__' in dir(s1))  # True

  3.1.3 小結

從字面意思來講:可迭代對象就是一個能夠重複取值的實實在在的東西。

從專業角度來講:但凡內部含有__iter__方法的對象,都是可迭代對象。

可迭代對象能夠經過判斷該對象是否有__iter__方法來判斷。

可迭代對象的優勢:

能夠直觀的查看裏面的數據。

可迭代對象的缺點:

1.佔用內存。

2.可迭代對象不能迭代取值(除去索引,key之外)。

那麼這個缺點有人就提出質疑了,即便拋去索引,key之外,這些我能夠經過for循環進行取值呀!對,他們均可以經過for循環進行取值,其實for循環在底層作了一個小小的轉化,就是先將可迭代對象轉化成迭代器,而後在進行取值的。那麼接下來,咱們就看看迭代器是個什麼鬼。

3.2 迭代器

  3.2.1 迭代器的定義

從字面意思來講迭代器,是一個能夠迭代取值的工具,器:在這裏當作工具比較合適。

從專業角度來講:迭代器是這樣的對象:實現了無參數的__next__方法,返回序列中的下一個元素,若是沒有元素了,那麼拋出StopIteration異常.python中的迭代器還實現了__iter__方法,所以迭代器也能夠迭代。 出自《流暢的python》

那麼對於上面的解釋有一些超前,和難以理解,不用過於糾結,咱們簡單來講:在python中,內部含有__Iter__方法而且含有__next__方法的對象就是迭代器。

  3.2.2 如何判斷該對象是不是迭代器

ok,那麼咱們有了這個定義,咱們就能夠判斷一些對象是否是迭代器或者可迭代對象了了,請判斷這些對象:str list tuple dict set range 文件句柄 哪一個是迭代器,哪一個是可迭代對象:

o1 = 'alex'
o2 = [1, 2, 3]
o3 = (1, 2, 3)
o4 = {'name': '寶元','age': 18}
o5 = {1, 2, 3}
f = open('file',encoding='utf-8', mode='w')
print('__iter__' in dir(o1))  # True
print('__iter__' in dir(o2))  # True
print('__iter__' in dir(o3))  # True
print('__iter__' in dir(o4))  # True
print('__iter__' in dir(o5))  # True
print('__iter__' in dir(f))  # True

print('__next__' in dir(o1))  # False
print('__next__' in dir(o2))  # False
print('__next__' in dir(o3))  # False
print('__next__' in dir(o4))  # False
print('__next__' in dir(o5))  # False
print('__next__' in dir(f))  # True
f.close()

經過以上代碼能夠驗證,以前咱們學過的這些對象,只有文件句柄是迭代器,剩下的那些數據類型都是可迭代對象。

  3.2.3 可迭代對象如何轉化成迭代器:

l1 = [1, 2, 3, 4, 5, 6]
obj = l1.__iter__()
# <list_iterator object at 0x000002057FE1A3C8>
# 或
obj = iter(l1)
print(obj)
# <list_iterator object at 0x102cc67f0>

 3.2.4 迭代器取值:

可迭代對象是不能夠一直迭代取值的(除去用索引,切片以及Key),可是轉化成迭代器就能夠了,迭代器是利用__next__()進行取值:

l1 = [1, 2, 3,]
obj = l1.__iter__()  # 或者 iter(l1)
# print(obj)  # <list_iterator object at 0x000002057FE1A3C8>
ret = obj.__next__()
print(ret)
ret = obj.__next__()
print(ret)
ret = obj.__next__()
print(ret)
ret = obj.__next__()  # StopIteration
print(ret)
# 迭代器利用next取值:一個next取對應的一個值,若是迭代器裏面的值取完了,還要next,
# 那麼就報StopIteration的錯誤。

  3.2.5 while模擬for的內部循環機制:

剛纔咱們提到了,for循環的循環對象必定要是可迭代對象,可是這不意味着可迭代對象就能夠取值,由於for循環的內部機制是:將可迭代對象轉換成迭代器,而後利用next進行取值,最後利用異常處理處理StopIteration拋出的異常。

l1 = [1, 2, 3, 4, 5, 6]
# 1 將可迭代對象轉化成迭代器
obj = iter(l1)
# 2,利用while循環,next進行取值
while 1:
    # 3,利用異常處理終止循環
    try:
        print(next(obj))
    except StopIteration:
        break

  3.2.6 小結:

從字面意思來講:迭代器就是能夠迭代取值的工具。

從專業角度來講:在python中,內部含有__Iter__方法而且含有__next__方法的對象就是迭代器。

迭代器的優勢:

節省內存。 迭代器在內存中至關於只佔一個數據的空間:由於每次取值都上一條數據會在內存釋放,加載當前的此條數據。

惰性機制。 next一次,取一個值,毫不過多取值。

有一個迭代器模式能夠很好的解釋上面這兩條:迭代是數據處理的基石。掃描內存中放不下的數據集時,咱們要找到一種惰性獲取數據項的方式,即按需一次獲取一個數據項。這就是迭代器模式。

迭代器的缺點:

不能直觀的查看裏面的數據。

取值時不走回頭路,只能一直向下取值。

l1 = [1, 2, 3, 4, 5, 6]
obj = iter(l1)

for i in range(2):
    print(next(obj))

for i in range(2):
    print(next(obj))

3.3 可迭代對象與迭代器對比

咱們今天比較深刻的瞭解了可迭代對象與迭代器,接下來咱們說一下這二者之間比較與應用:

可迭代對象:

是一個私有的方法比較多,操做靈活(好比列表,字典的增刪改查,字符串的經常使用操做方法等),比較直觀,可是佔用內存,並且不能直接經過循環迭代取值的這麼一個數據集。

應用:當你側重於對於數據能夠靈活處理,而且內存空間足夠,將數據集設置爲可迭代對象是明確的選擇。

迭代器:

是一個很是節省內存,能夠記錄取值位置,能夠直接經過循環+next方法取值,可是不直觀,操做方法比較單一的數據集。

應用:當你的數據量過大,大到足以撐爆你的內存或者你以節省內存爲首選因素時,將數據集設置爲迭代器是一個不錯的選擇。(可參考爲何python把文件句柄設置成迭代器)。

四.生成器

4.1 初識生成器

什麼是生成器?這個概念比較模糊,各類文獻都有不一樣的理解,可是核心基本相同。生成器的本質就是迭代器,在python社區中,大多數時候都把迭代器和生成器是作同一個概念。不是相同麼?爲何還要建立生成器?生成器和迭代器也有不一樣,惟一的不一樣就是:迭代器都是Python給你提供的已經寫好的工具或者經過數據轉化得來的,(好比文件句柄,iter([1,2,3])。生成器是須要咱們本身用python代碼構建的工具。最大的區別也就如此了。

4.2 生成器的構建方式

在python中有三種方式來建立生成器:

  1. 經過生成器函數
  2. 經過生成器推導式
  3. python內置函數或者模塊提供(其實1,3兩種本質上差很少,都是經過函數的形式生成,只不過1是本身寫的生成器函數,3是python提供的生成器函數而已)

4.3 生成器函數

咱們先來研究經過生成器函數構建生成器。

首先,咱們先看一個很簡單的函數:

def func():
    print(11)
    return 22
ret = func()
print(ret)
# 運行結果:
11
22

將函數中的return換成yield,這樣func就不是函數了,而是一個生成器函數

def func():
    print(11)
    yield 22

咱們這樣寫沒有任何的變化,這是爲何呢? 咱們來看看函數名加括號獲取到的是什麼?

def func():
    print(11)
    yield 22
ret = func()
print(ret)

# 運行結果:
<generator object func at 0x000001A575163888>

爲何在函數中添加了yield在調用函數的時候就發現結果不是咱們預想的結果呢,是由於當咱們調用函數的時候函數體裏的代碼會進行執行當執行到yield的關鍵字的時候,發現咱們是想聲明一個生成器.程序就會返回一個生成器給我們

那麼生成器對象如何取值呢?

以前咱們說了,生成器的本質就是迭代器.迭代器如何取值,生成器就如何取值。因此咱們能夠直接執行next()來執行如下生成器

def func():
     print("111")
     yield 222
gener = func() # 這個時候函數不會執⾏. ⽽是獲取到⽣成器
ret = gener.__next__() # 這個時候函數纔會執⾏
print(ret)  # 而且yield會將func生產出來的數據 222 給了 ret。  

結果:
111
222

而且個人生成器函數中能夠寫多個yield。

def func():

    print("111")

    yield 222

    print("333")

    yield 444

gener = func()

ret = gener.__next__()

print(ret)

ret2 = gener.__next__()

print(ret2)

ret3 = gener.__next__()

# 最後⼀個yield執⾏完畢. 再次__next__()程序報錯
print(ret3)

當程序運行完最後一個yield,那麼後面繼續運行next()程序會報錯,一個yield對應一個next,next超過yield數量,就會報錯,與迭代器同樣。

yield與return的區別:

return通常在函數中只設置一個,他的做用是終止函數,而且給函數的執行者返回值。

yield在生成器函數中可設置多個,他並不會終止函數,next會獲取對應yield生成的元素。

舉例:

咱們來看一下這個需求:老男孩向樓下賣包子的老闆訂購了10000個包子.包子鋪老闆很是實在,一下就所有都作出來了 

def eat():

    lst = []

    for i in range(1,10000):

        lst.append('包子'+str(i))

    return lst

e = eat()

print(e)

這樣作沒有問題,可是咱們因爲學生沒有那麼多,只吃了2000個左右,剩下的8000個,就只能佔着必定的空間,放在一邊了。若是包子鋪老闆效率夠高,我吃一個包子,你作一個包子,那麼這就不會佔用太多空間存儲了,完美。

def eat():

    for i in range(1,10000):

        yield '包子'+str(i)

e = eat()

for i in range(200):
    next(e)

這二者的區別:

第一種是直接把包子所有作出來,佔用內存。

第二種是吃一個生產一個,很是的節省內存,並且還能夠保留上次的位置。

def eat():

    for i in range(1,10000):

        yield '包子'+str(i)

e = eat()

for i in range(200):
    next(e)

for i in range(300):
    next(e)
# 屢次next包子的號碼是按照順序記錄的。

4.4 send 方法(瞭解,不講)

接下來咱們再來認識一個新的東西,send方法

# next只能獲取yield生成的值,可是不能傳遞值。
def gen(name):
    print(f'{name} ready to eat')
    while 1:
        food = yield
        print(f'{name} start to eat {food}')

dog = gen('alex')
next(dog)
next(dog)
next(dog)


# 而使用send這個方法是能夠的。
def gen(name):
    print(f'{name} ready to eat')
    while 1:
        food = yield 222
        print(f'{name} start to eat {food}')

dog = gen('alex')
next(dog)  # 第一次必須用next讓指針停留在第一個yield後面
# 與next同樣,能夠獲取到yield的值
ret = dog.send('骨頭')
print(ret)


def gen(name):
    print(f'{name} ready to eat')
    while 1:
        food = yield
        print(f'{name} start to eat {food}')

dog = gen('alex')
next(dog)
# 還能夠給上一個yield發送值
dog.send('骨頭')
dog.send('狗糧')
dog.send('香腸')

send和next()區別:

相同點:

send 和 next()均可以讓生成器對應的yield向下執行一次。

均可以獲取到yield生成的值。

不一樣點:

第一次獲取yield值只能用next不能用send(能夠用send(None))。

send能夠給上一個yield置傳遞值。

4.5 yield from

在python3中提供一種能夠直接把可迭代對象中的每個數據做爲生成器的結果進行返回

# 對比yield 與 yield from 
def func():
    lst = ['衛龍','老冰棍','北冰洋','牛羊配']
    yield lst
g = func()
print(g)
print(next(g))  # 只是返回一個列表

def func():
    lst = ['衛龍','老冰棍','北冰洋','牛羊配']
    yield from lst
g = func()
print(g)
# 他會將這個可迭代對象(列表)的每一個元素當成迭代器的每一個結果進行返回。
print(next(g))
print(next(g))
print(next(g))
print(next(g))
'''
yield from ['衛龍','老冰棍','北冰洋','牛羊配'] 
等同於:
    yield '衛龍'
    yield '老冰棍'
    yield '北冰洋'
    yield '牛羊配'

4.6 yield from 小坑

def func():
    lst1 = ['衛龍', '老冰棍', '北冰洋', '牛羊配']
    lst2 = ['饅頭', '花捲', '豆包', '大餅']
    yield from lst1
    yield from lst2


g = func()
for i in g:
    print(i)

返回的結果是將第一個列表的元素所有返回後,在返回第二個列表

相關文章
相關標籤/搜索