python入口函数的作用_python之函数中参数的作用域
學編程究竟學的是什么呢?在寫文章的這幾天也一直在思考這個問題——恐怕這也是接下來的幾年一直會去思考的問題。這個問題的答案也會指導我的方法論,所以索性整頓一下。
現階段我的回答是,發現需求,然后解決。
最大的需求無非是完成一個項目,為了做到這一點,還有很多需求是完成模塊化的功能,再細分下去則是要實現每一個具體的函數。這些都做到了,可能整個功能就跑通了。但是,需求其實沒有止步:代碼效率能更高么?
魯棒性能優化么?
可讀性能改善么?
整體功能的架構完善么?
甚至工程細節,debug效率能更高么?
logger輸出能更合理么?
這些需求則是關系到積累,可不只是一次項目就能簡單回答的,需要更深的思考和總結。
所以,在學習中怎么貫徹這一點呢?將學到的知識和項目盡量建立聯系,以后的知識點的例子我也會盡量從這方面去構建。
查看python源碼。很多需求提不出來的原因是見得不夠多,但是源碼里,很多經驗豐富的前輩不僅預見了這些需求,還封裝了這個需求的解決辦法。所以不僅對現階段有幫助,也能是下一階段——如何把問題解決的更高效的預熱。
下面,我們開始討論面向過程抽象的最基本也是最重要的單位——函數。
環境與作用域
在SICP(第三章)中對環境有著如下定義:一個環境是框架(frame)的一個序列,每個框架是包含著一些約束的一個表格(可能為空),這些約束將一些變量的名字關聯于對應的值(在一個框架里,任何變量至多只能有一個約束)。每一個框架還包含了一個指針,指向這一框架的外部環境......
當時走馬觀花的看到了這個定義,但是在python中debug的時候才發現理解了環境是多么的好用:我們可以通過選取不同的frame,觀看到不同的變量在各個環境中是如何變化的。
但是被動的使用肯定是不足夠的,我們還是要主動的洞察變量定義和frame的關系,更好的預見我們寫出的代碼真實的效果是什么。(SICP大法好。。自學者還是要夯實基礎。雖然第一次看的時候似懂非懂,不過這就是積累哦,第二次看到的時候就有機會融會貫通了)
變量名解析原則LEGB
其實上述一大段話,說的是這個意思:如果當前frame里有這個變量,直接引用,否則去上層frame中查找是否有同名變量,直到找到最高層;若都沒有就報錯咯~
看個例子
x = 1
def fun():
print(x)
fun()
# 1
引用好說,但當牽扯到賦值的時候,就不那么顯然了
x = 1
def fun():
x = 1
print(x)
fun()
# 2
這個還好,比較符合大家直觀感受,但是下面就不一樣了
x = 1
def fun():
x += 1
print(x)
fun()
# UnboundLocalError: local variable 'x' referenced before assignment
既然引用沒錯,這咋不能引用加賦值呢??原來,在函數內部,一旦牽扯到賦值語句,變量就會變成局部變量,像第二個例子一樣屏蔽掉全局變量x(x=1)。如果想改變全局變量,那么就在函數內部事先聲明
def fun():
global x
...
這樣,我們就有一個比較直觀的感覺:牽扯到在函數內部賦值時,如果是內部變量沒什么關系,但如果改變外部變量的話,一定要像一個辦法將其引入內部空間(一般不是global);相反,如果僅僅是普通引用的話則十分方便,無需過度擔心。
但是如果函數嵌套的話會發生什么情況呢?顯見,最內層函數的外一層就不再是global環境。具體的層次就是所謂的LEGB。Scope Resolution in Python | LEGB Rule - GeeksforGeeks?www.geeksforgeeks.org
文中小圖清晰的展現了frame的嵌套關系。留一個問題:如果import了其他py文件,frame結構又是什么樣子的呢?
frame的存在時間
最后舉一個特別精巧的例子,在這個例子中,我們把函數作為另一個函數的返回值。熟悉數學的朋友們知道這個在數學里叫做泛函,是一種強有力的抽象手段。如果有機會會在SICP中好好討論一下這種所謂的過程的抽象。
def counter():
c = [0]
def inc():
c[0] += 1
return c[0]
return inc
f = counter()
f()
# 1
f()
# 2
f()
# 3
從這個例子不難看出,局部變量c一直存在在f所代表的frame當中。如果g=counter(),則g的計數與f毫無關聯。
為什么可以c[0] += 1?這是因為,我們賦值的是變量c所代表的列表中的元素,即,本質上,我們對c是引用,所以在local frame中找不到c時,我們能從enclosed frame中找到c拿來引用。
(這個例子給了我們做計數器的巨大的啟發。
默認值的本質
def fun(x=[1]):
x.append([2])
print(x)
# 比較下面兩個輸出
for _ in range(2):
fun()
# [1, 2]
# [1, 2, 2]
for _ in range(2):
fun([1])
# [1, 2]
# [1, 2]
原來,函數中有這兩個屬性收集默認值:fun.__defaults__ 收集默認位置參數
fun.__kwdefaults_ 收集默認關鍵字參數
如果我們不明確的賦值,則調用默認值。但是!!因為我們這里的默認值可變(雖然列表的內存地址沒有改變,但是列表的內容變了),所以我們的行為也許會修改默認值!
只要函數不被銷毀,作為屬性的默認值就會一直記錄所有的改變。
首先,我們要意識到,這種做法有時有利,有時有害,不可一概而論;下面我們展示兩種方法:如果一旦我們不想讓默認值改變,該采取什么做法。
def fun(x=[]):
x = x[:] # shadow copy
....
第一方面,如果傳入了x,立即對xshadow copy,沒問題;如果沒傳入,我們操作的是默認值x的shadow copy,而不是x本身,所以。。。?
這里比較討巧,用了一個空列表,如果一個默認值很復雜(其實不推薦復雜的默認值吧。。),那么shadow copy也是會有shadow copy的問題的對吧?
所以這種做法須謹慎。
def fun(x=None):
if x is None:
x = []
....
哇!這個方法還是厲害的咧。也是推薦大家使用的。每次調用,對x重新賦值,肯定能避免這次的改變泄漏到下次操作中。
我們也可以這么理解:
函數的屬性,和函數定義本身綁定在一起,存在于global frame(假設是最外層函數);而每次調用函數,就會自動生成一個新的local frame。所以,方法二中的賦值方法是沒有問題的。
總結
以上是生活随笔為你收集整理的python入口函数的作用_python之函数中参数的作用域的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Xmind 2022精彩体验---什么叫
- 下一篇: python装饰器解析_Python 装