官术网_书友最值得收藏!

6.3 我的地盤聽我的

視頻講解

這里談的其實(shí)是變量的作用域,作用域是程序運(yùn)行時(shí)變量可被訪問的范圍,這個(gè)知識(shí)點(diǎn)在Python中是一個(gè)很容易“掉坑”的地方,大家一定要認(rèn)真學(xué)習(xí)。

6.3.1 局部變量

定義在函數(shù)內(nèi)部的變量是局部變量,局部變量的作用范圍只能在函數(shù)的內(nèi)部生效,它不能在函數(shù)外被引用。請(qǐng)分析下面代碼,判斷哪些變量是局部變量:

程序執(zhí)行效果如下:

     >>>
     請(qǐng)輸入原價(jià):80
     請(qǐng)輸入折扣率:0.75
     打折后價(jià)格是:60.00

分析:在函數(shù)discounts(price, rate)中,兩個(gè)參數(shù)是price和rate,還有一個(gè)是final_price,它們都是discounts()函數(shù)中的局部變量。

為什么把它們稱為局部變量呢?不妨修改一下代碼:

程序運(yùn)行,像剛才一樣輸入之后程序便報(bào)錯(cuò)了:

錯(cuò)誤分析:Python提示沒有找到'final_price'的定義,也就是說,Python找不到final_price這個(gè)變量。這是因?yàn)閒inal_price只是一個(gè)局部變量,它的作用范圍只在它的地盤上(discount()函數(shù)的定義范圍內(nèi))有效,超出這個(gè)范圍,就不再屬于它的地盤了,它將什么都不是。

6.3.2 全局變量

與局部變量相對(duì)的是全局變量,上面代碼中old_price、new_price、rate都是在函數(shù)外面定義的,它們都是全局變量,全局變量擁有更大的作用域,因此在函數(shù)中可以訪問到它們:

程序執(zhí)行效果如下:

     >>>
     請(qǐng)輸入原價(jià):80
     請(qǐng)輸入折扣率:0.75
     試圖在函數(shù)內(nèi)部訪問全局變量old_price的值:80.00
     打折后價(jià)格是:60.00

在Python中,可以在函數(shù)中“肆無忌憚”地訪問一個(gè)全局變量,但如果試圖去修改它,就會(huì)有奇怪的事情會(huì)發(fā)生了。

請(qǐng)看下面例子:

程序執(zhí)行效果如下:

     >>>
     請(qǐng)輸入原價(jià):80
     請(qǐng)輸入折扣率:0.75
     在局部變量中修改后old_price的值是:50.00
     全局變量old_price現(xiàn)在的值是:80.00
     打折后價(jià)格是:60.00

分析:如果在函數(shù)內(nèi)部試圖修改全局變量的值,那么Python會(huì)創(chuàng)建一個(gè)新的局部變量替代(名字與全局變量相同),但真正的全局變量是“不為所動(dòng)”的,所以才有了上面的實(shí)現(xiàn)結(jié)果。

6.3.3 global關(guān)鍵字

全局變量的作用域是整個(gè)模塊,也就是代碼段內(nèi)所有的函數(shù)內(nèi)部都可以訪問到全局變量。但要注意的一點(diǎn)是,在函數(shù)內(nèi)部僅僅去訪問全局變量就好,不要試圖去修改它。如果隨意修改全局變量的值,很容易牽一發(fā)而動(dòng)全身。

因此,Python使用屏蔽(shadowing)的手段對(duì)全局變量進(jìn)行“保護(hù)”:一旦函數(shù)內(nèi)部試圖直接修改全局變量,Python就會(huì)在函數(shù)內(nèi)部創(chuàng)建一個(gè)名字一模一樣的局部變量代替,這樣修改的結(jié)果只會(huì)影響到局部變量,而全局變量則絲毫不變。

請(qǐng)看下面例子:

代碼是死的,但人是活的!假設(shè)你已經(jīng)完全了解在函數(shù)中修改全局變量可能會(huì)導(dǎo)致程序可讀性變差、出現(xiàn)莫名其妙的BUG、代碼的維護(hù)成本成倍提高,但還是堅(jiān)持“虛心接受,死不悔改”這八字原則,仍然覺得有必要在函數(shù)內(nèi)部去修改這個(gè)全局變量,那么可以使用global關(guān)鍵字來達(dá)到目的。

代碼修改如下:

6.3.4 內(nèi)嵌函數(shù)

視頻講解

Python的函數(shù)定義是支持嵌套的,也就是允許在函數(shù)內(nèi)部定義另一個(gè)函數(shù),這種函數(shù)稱為內(nèi)嵌函數(shù)或者內(nèi)部函數(shù)。

舉個(gè)例子:

這是函數(shù)嵌套的經(jīng)典例子,雖然看起來很簡單,不過麻雀雖小,五臟俱全。

關(guān)于內(nèi)部函數(shù)的使用,有一個(gè)比較值得注意的地方,就是內(nèi)部函數(shù)整個(gè)作用域都在外部函數(shù)之內(nèi)。就像上面例子中,fun2()整個(gè)函數(shù)的作用域都在fun1()里面,也就是只有在fun1()這個(gè)函數(shù)體里面,才可以隨意地調(diào)用fun2()這個(gè)內(nèi)部函數(shù)。如果在其他地方試圖調(diào)用內(nèi)部函數(shù),就會(huì)出錯(cuò):

在嵌套函數(shù)中,內(nèi)部函數(shù)可以引用外部函數(shù)的局部變量:

6.3.5 LEGB原則

那現(xiàn)在有一個(gè)問題,如果有一個(gè)全局變量x=520,fun2()函數(shù)內(nèi)部有一個(gè)局部變量x=11,那么程序還會(huì)打印88嗎?

答案是否定的,程序打印的值是11。

另一個(gè)問題,上面三個(gè)x變量是同一個(gè)對(duì)象嗎?不妨使用id()函數(shù)(獲取)來測試一下:

可以看到上面程序返回了三個(gè)不同的id值,也就證明了三個(gè)x并不是同一個(gè)對(duì)象,它們只是變量的名字一樣而已。像這種名字一樣、作用域不同的變量引用,Python引入了LEGB原則進(jìn)行規(guī)范。

LEGB含義解釋:

? L-Local:函數(shù)內(nèi)的名字空間。

? E-Enclosing function locals:嵌套函數(shù)中外部函數(shù)的名字空間。

? G-Global:函數(shù)定義所在模塊的名字空間。

? B-Builtin:Python內(nèi)置模塊的名字空間。

那么變量的查找順序依次就是L→E→G→B。

6.3.6 閉包

閉包是函數(shù)式編程的一個(gè)重要的語法結(jié)構(gòu),維基百科上對(duì)于閉包這個(gè)概念是這么解釋的:“在計(jì)算機(jī)科學(xué)中,閉包(closure)是詞法閉包(lexical closure)的簡稱,是引用了自由變量的函數(shù)。這個(gè)被引用的自由變量將和這個(gè)函數(shù)一同存在,即使已經(jīng)離開了創(chuàng)造它的環(huán)境也不例外。所以,有另一種說法認(rèn)為閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實(shí)體。閉包在運(yùn)行時(shí)可以有多個(gè)實(shí)例,不同的引用環(huán)境和相同的函數(shù)組合可以產(chǎn)生不同的實(shí)例。”

不同編程語言實(shí)現(xiàn)閉包的方式各不相同。Python中的閉包從表現(xiàn)形式上定義為:如果在一個(gè)內(nèi)部函數(shù)里,對(duì)在外部作用域但不是在全局作用域的變量進(jìn)行引用(簡言之:就是在嵌套函數(shù)的環(huán)境下,內(nèi)部函數(shù)引用了外部函數(shù)的局部變量),那么內(nèi)部函數(shù)就被認(rèn)為是閉包。

舉個(gè)例子:

通過上面的例子理解閉包的概念:如果在一個(gè)內(nèi)部函數(shù)里(funY()就是這個(gè)內(nèi)部函數(shù))對(duì)在外部作用域(但不是在全局作用域)的變量進(jìn)行引用(x就是被引用的變量,x在外部作用域funX()函數(shù)里面,但不在全局作用域里),則這個(gè)內(nèi)部函數(shù)就是一個(gè)閉包。

注意:

因?yàn)殚]包的概念是由內(nèi)部函數(shù)而來,所以不能在外部函數(shù)以外的地方對(duì)內(nèi)部函數(shù)進(jìn)行調(diào)用,下面的做法是錯(cuò)誤的:

在閉包中,外部函數(shù)的局部變量對(duì)應(yīng)內(nèi)部函數(shù)的局部變量,事實(shí)上相當(dāng)于之前講的全局變量與局部變量的對(duì)應(yīng)關(guān)系,在內(nèi)部函數(shù)中,只能對(duì)外部函數(shù)的局部變量進(jìn)行訪問,但不能進(jìn)行修改。

這個(gè)錯(cuò)誤提示與之前講解全局變量的時(shí)候基本一樣,Python認(rèn)為在內(nèi)部函數(shù)的x是局部變量的時(shí)候,外部函數(shù)的x就被屏蔽了起來,所以執(zhí)行x = x + 1的時(shí)候,在等號(hào)右邊根本就找不到局部變量x的值,因此報(bào)錯(cuò)。

在Python 3以前并沒有直接的解決方案,只能間接地通過容器類型來存放,因?yàn)槿萜黝愋筒皇欠旁跅@铮圆粫?huì)被“屏蔽”掉。容器類型這個(gè)詞大家是不是似曾相識(shí)?之前介紹的字符串、列表、元組,這些可以存放各種類型數(shù)據(jù)的“倉庫”就是容器類型。于是乎可以把代碼改造如下:

到了Python 3的世界里,有了不少的改進(jìn)。如果希望在內(nèi)部函數(shù)里可以修改外部函數(shù)里的局部變量的值,可以使用nonlocal關(guān)鍵字告訴Python這不是一個(gè)局部變量,使用方式與global一樣:

好了,那么閉包“是什么、怎么用”總算是講清楚了,那為什么要使用閉包呢?看起來閉包似乎是一種高級(jí)但是并沒什么用的技巧。其實(shí),閉包概念的引入是為了盡可能地避免使用全局變量,閉包允許將函數(shù)與其所操作的某些數(shù)據(jù)(環(huán)境)關(guān)聯(lián)起來,這樣外部函數(shù)就為內(nèi)部函數(shù)構(gòu)成了一個(gè)封閉的環(huán)境。這一點(diǎn)與面向?qū)ο缶幊痰母拍钍欠浅n愃频模诿嫦驅(qū)ο缶幊讨校瑢?duì)象允許將某些數(shù)據(jù)(對(duì)象的屬性)與一個(gè)或者多個(gè)方法相關(guān)聯(lián)(詳細(xì)內(nèi)容請(qǐng)學(xué)習(xí)第11章:類和對(duì)象)。

【擴(kuò)展閱讀】游戲中的移動(dòng)角色:閉包在實(shí)際開發(fā)中的應(yīng)用,可訪問http://bbs.fishc.com/thread-42656-1-1.html或掃描此處二維碼獲取。

擴(kuò)展閱讀

6.3.7 裝飾器

這個(gè)名字聽著可能比較新鮮,在Python中裝飾器(decorator)的功能是將被裝飾的函數(shù)當(dāng)作參數(shù)傳遞給與裝飾器對(duì)應(yīng)的函數(shù)(名稱相同的函數(shù)),并返回包裝后的被裝飾的函數(shù)。聽上去有點(diǎn)繞,沒關(guān)系,下面通過實(shí)例來講述“裝飾器是什么”以及“為什么會(huì)有裝飾器”。

先隨意定義一個(gè)函數(shù):

現(xiàn)在,有一個(gè)新的需求,需要在執(zhí)行該函數(shù)時(shí)加上日志:

     >>> print("開始調(diào)用eat()函數(shù)…")
     開始調(diào)用eat()函數(shù)…
     >>> eat()
     開始吃了
     >>> print("結(jié)束調(diào)用eat()函數(shù)…")
     結(jié)束調(diào)用eat()函數(shù)…

這是一種方法,但代碼顯然變得臃腫起來,感覺就像大夏天時(shí)裹一件貂皮大衣在沙灘上漫步……

或者直接將代碼封裝到函數(shù)中:

這樣功能也算是實(shí)現(xiàn)了,唯一的問題就是它需要侵入到了原來的代碼里面,使得原有的業(yè)務(wù)邏輯變復(fù)雜,這樣的代碼也不符合“一個(gè)函數(shù)只做一件事情”的原則。

那么有沒有可能在不修改函數(shù)代碼的提前下,實(shí)現(xiàn)功能呢?

有的,剛學(xué)過的閉包就可以助你一臂之力:

log(eat)將eat函數(shù)作為參數(shù)傳遞給log(),由于wrapper()是log()的閉包,所以它可以訪問log()的局部變量func,也就是剛剛傳遞進(jìn)來的eat,因此,執(zhí)行func()與執(zhí)行eat()是一個(gè)效果。這樣一來,問題就解決了!既沒有修改eat()函數(shù)里面的邏輯結(jié)構(gòu),也不會(huì)給主程序帶來太多的干擾項(xiàng)。不過這個(gè)eat = log(eat)看著總有些別扭,能不能改善一下呢?

可以,Python因此發(fā)明了“@語法糖”來解決這個(gè)問題。所謂語法糖(Syntactic sugar),就是在計(jì)算機(jī)語言中添加的某種語法,這種語法對(duì)語言的功能沒有影響,但是更方便程序員使用。語法糖讓程序更加簡潔,有更高的可讀性。

有了“@語法糖”,上面的代碼就可以這么寫:

這樣就省去了手動(dòng)將eat()函數(shù)傳遞給log()再將返回值重新賦值的步驟。

有讀者可能會(huì)問了:“如果eat()函數(shù)有參數(shù)怎么辦?”

好辦,可以將參數(shù)扔給內(nèi)部的wrapper()函數(shù):

但這樣的話就必須要時(shí)刻關(guān)注eat()函數(shù)的參數(shù)數(shù)量,如果修改了eat(),就必須一并修改裝飾器log(),不僅不方便也容易出錯(cuò)。防微杜漸,可以在設(shè)計(jì)的時(shí)候就不讓這種情況發(fā)生:

在定義的時(shí)候使用收集參數(shù),將多個(gè)參數(shù)打包到一個(gè)元組中,然后在調(diào)用的時(shí)候同樣使用星號(hào)(*)進(jìn)行解包,這樣無論eat()有多少個(gè)參數(shù),都不再是問題了。

最后,還有高階玩法,如果裝飾一層覺得不夠,還可以一層套一層地加裝飾器,像下面這樣:

調(diào)用eat()的時(shí)候,相當(dāng)于調(diào)用buffer(performance(log(eat)))。

主站蜘蛛池模板: 于都县| 内乡县| 平山县| 岢岚县| 犍为县| 高邑县| 建德市| 辰溪县| 布尔津县| 武陟县| 陇西县| 清河县| 微山县| 横山县| 邢台市| 通海县| 中超| 瓮安县| 志丹县| 蒙自县| 益阳市| 安义县| 台州市| 方山县| 深泽县| 乌鲁木齐市| 瓦房店市| 南投市| 孝义市| 军事| 四会市| 遂昌县| 剑川县| 庆云县| 河西区| 平远县| 吉安县| 汝城县| 乐山市| 西林县| 博客|