函數基礎
一.函數的定義調用
函數的使用必須遵循先定義,後調用的原則
函數 定義 階段: 只檢測函數體的語法,不執行函數體代碼
函數 調用 階段: 執行函數體代碼
Ps:代碼從上而下執行,遇到def語句,函數體代碼不會執行.但在運行前會進行 預編譯.
def 函數名(參數1,參數2,...):
"""
函數功能描述消息
:param 參數1:描述
:param 參數2:描述
:return: 返回值
"""
pass
函數返回值
函數中不寫return或return不帶任何返回值,該函數返回值都是None
不寫return默認會在函數的最後一行添加 return None
二.函數參數傳遞
敲重點, 根據 python在作用域里對變量的賦值操作規則 , 函數的參數就是局部變量!!
既然是局部變量,就需要定義, 即函數的參數傳遞 可看作是一個賦值操作a = ?
重點看?
的指向是啥..
賦值操作
在理清函數參數傳遞之前,要先了解兩個重要的概念!! 非常重要! 知其然要知其所以然..
1> python在作用域里對變量的賦值操作規則:
若這個變量在該作用域存在(已經定義), 則對其綁定新的對象; 若不存在,則將這次賦值視為對這個變量的定義..
2> python對象的賦值都是引用(內存地址)傳遞 被賦值 = (右側的變量們)被引用
“=”賦值操作右側的變量需要在某一個作用域里被找到,不然會報該變量未定義..
Ps:變量的找尋遵循一個規則 依次從 局部 – 全局 – 內置 命名空間找.. 後面會有詳細闡述..
在此處,直接理解成 此變量在函數里沒有,就去函數外找..
在這裡,簡述 賦值即引用 帶來的坑.. 需求:循環去除列表中的3
"""
分析: 問題出在,B操作會同步改變A操作循環的值,i的取值 num[0]、num[1]、num[2]..這樣的順序來的
當循環到num[2]時,去除了第一個3,此時列表為[1,2,3,3,4];
繼續循環,輪到nums[3],可此時列表中nums[2]的值為3卻被跳過了..
"""
nums = [1, 2, 3, 3, 3, 4]
for i in nums: # A
if i == 3:
nums.remove(i) # B
print(nums, len(nums)) # [1, 2, 3, 4] 4
# --- 正解
nums = [1, 2, 3, 3, 3, 4]
for i in nums[:]:
if i == 3:
nums.remove(i)
print(nums, len(nums)) # [1, 2, 4] 3
值傳遞
函數參數 值傳遞(拷貝) 針對不可變對象(tuple\str\bool\數字\不可變集合)
"""-- 分析
1> 參數a接收到的是值的類型是數字,數字是一個不可變對象
2> So,會將接收到的值進行一份拷貝! -- 值傳遞(拷貝)
因為func函數內不存在變量a,所以會在func函數里新定義了一個局部變量a,將拷貝的值賦值給局部變量a..
注意: func函數新定義的局部變量a與全局變量a無任何瓜葛!!
"""
def func(a):
a = a + '3'
return a
a = '5'
res = func(a)
print(a,res) # '5' '53'
引用傳遞
函數參數 引用傳遞(內存地址) 針對可變對象(list\dict\set)
"""-- 初步分析
1> 參數a接收到的是值的類型是列表,列表是一個不可變對象
2> So,會對接收到的值進行引用 -- 引用傳遞(內存地址)
因為func函數內不存在變量a,所以會在func函數里新定義了一個局部變量a,將接收到的引用給局部變量a..
注意: func函數新定義的局部變量a與全局變量a維護/指向的是同一個內存地址!!
"""
def func(a):
a += [4] # -- 列表+=操作原地改變 等同於 a.extend([4])
return a
a = [1,2,3]
res = func(a)
print(a,res) # [1, 2, 3, 4] [1, 2, 3, 4]
# --- --- ---
def func(a):
a = a + [4] # -- 在參數傳遞的時候,已經定義了局部變量a. 此時[1,2,3]的引用計數為2
# 根據python在作用域里對變量的賦值操作規則 該賦值操作是在對局部變量a重新賦值
# 重新賦值後,局部變量a重新綁定了一個新的對象[1,2,3,4]. 導致[1,2,3]的引用計數減1
# id(左邊a) != id(右邊a) 它倆不是同一個對象
return a
a = [1,2,3]
res = func(a)
print(a,res) # [1, 2, 3] [1, 2, 3, 4]
UnboundLocalError
官方解釋: When a name is not found at all, a
NameError
exception is raised.
If the name refers to a local variable that has not been bound, aUnboundLocalError
exception is raised.UnboundLocalError
is a subclass ofNameError
.翻譯成人話: 若引用了某個變量,此變量在各個作用域里都找不到(詳見4.3.1LEGB),就會報錯
NameError
;
若引用的變量是局部變量,但還未完成綁定,就會報錯UnboundLocalError
..
UnboundLocalError
是NameError
的一個子類..
"""
代碼第二行報錯: local variable 'a' referenced before assignment
翻譯過來: 局部變量'a'在賦值前被引用
分析: func()函數沒有參數,即不存在函數傳遞過程中定義局部變量
函數體內有對a的賦值操作,根據python在作用域里對變量的賦值操作規則,因為func函數內沒有局部變量a,則該賦值操作是在定義局部變量a,而在賦值操作的右側引用了還未被定義完成的局部變量a
Ps:有很長一段時間我是這樣理解這個報錯的.開始回溯我的理解.理解出錯的地方在於(-- --)包含起來的這段文字.
(-- 解釋器發現右側的a變量在局部作用域里還未綁定好,就引用全局變量a-- ),而"="左側的a是局部變量,在一個賦值操作中,某一變量不可能既充當局部變量,又充當全局變量.. 進而產生了衝突.報錯提示不是這樣的,顯然這樣理解有偏差!!
正確的過程應該是,解釋器執行到a = a + 4,這個語句,發現它是賦值操作,準備將a變量名寫入局部命名空間里,到這一步a變量已經定性為一個局部變量啦,但是此a變量還未完成綁定,正準備將"="右側的內容與a變量進行綁定時,發現右側的內容中又引用了還未綁定好的局部變量a..
Python解釋器一看這騷操作,很生氣,大罵一聲擱這套娃呢?果斷扔出一張紅牌,直接報錯..
"""
def func():
a += 4 # a+=[4]報錯 a.extend([4])不會報錯
return a
a = 1 # a=[] 當a是一個列表時
res = func()
print(a,res)
三.函數形參和實參
在上文我們知道 函數的參數傳遞 可看作是一個賦值操作
a = ?
定義的是函數的局部變量
具體一點, 參數分為形參和實參:
形參 — 本質就是變量名; 實參 — 本質就是變量的值
定義函數時 — 行參
定義函數時 位置形參-默認形參-不定長位置形參-命名關鍵字形參-不定長關鍵字形參
形參 | 特性 |
---|---|
位置形參 | 必須被傳值,多一個不行少一個也不行!! |
默認形參 | 意味着調用階段可以不用為其賦值(可以不用指定對應的實參)!! |
不定長位置形參 | *args 會將溢出的位置實參全部接收,然後以元祖的形式賦值給* 後面的變量args |
命名關鍵字形參 | * (* 或者*args ) 後面的形參對應的實參必須按照key=value的形式進行傳值 |
不定長關鍵字形參 | **kwargs 會將溢出的關鍵字實參全部接收,然後以字典的形式賦值給 ** 後面的變量kwargs |
調用函數時 — 實參
調用函數時 位置實參 關鍵字實參
1> 兩種實參可以混用, 但位置實參必須在關鍵字實參前面!且不能對一個形參重複賦值!
2> 實參可以拆包!
實參中帶*
,*
會將該變量的值循環取出,打散成位置實參.
即以後但凡碰到實參中帶*
的,它就是位置實參,應該立刻打散成位置實參來看
實參中帶**
,**
會將該變量的值循環取出,打散成關鍵字實參.
即以後但凡碰到實參中帶**
的,它就是關鍵字實參,應該立刻打散成關鍵字實參去看
def func3(a1, a2, a3, a4=10, *args, a5=20, a6, **kwargs):
# 11 22 33 44 20 10 (55, 66, 77) {'a10': 123}
print(a1, a2, a3, a4, a5, a6, args, kwargs)
# func3(11, *[22, 33, 44, 55, 66, 77], **{'a6': 10, 'a10': 123})
func3(11, 22, 33, 44, 55, 66, 77, a6=10, a10=123)
def func(*p):
return sum(p)
func(1,2,3,4) # 10
def func(**p):
return ''.join(sorted(p))
func(x=1,z=2,y=3) # 'xyz'
默認形參陷阱
記住兩點:
除非程序結束,否則該共享空間不會被釋放; 類似於全局變量…
默認形參的值通常應該定義為不可變類型.
1> 若函數有默認形參,在函數初次調用時,會為其開闢一塊共享空間;
2> 往後每一次調用此函數創建的空間都會共享這一塊 「共享空間」 里的默認形參
3> 除非程序結束,否則該共享空間不會被釋放!!!
Ps: 也有人會說, “默認形參的值只在定義階段賦值一次,即默認參數的值在函數定義階段就已經固定死了”. 這種說法反覆揣摩後,是有一定的道理的, 但不好理解.. 說法不夠嚴謹,不必深究…
def func(a=[555]):
a.extend([1, 2, 3]) # -- 等同於 a+=[1,2,3]
return a,id(a)
# -- 函數第一次調用時共享空間為[555];第二次調用時沒有用共享空間;第三次調用時共享空間為[555,1,2,3];
# 可以觀察到,第一次id(a)和第三次id(a)是一樣的..
# 證明第二次調用雖然沒有用共享空間會導致[555]的引用計數為0,但該共享空間沒有被釋放!!
print(func())
print(func([]))
print(func())
"""結果
([555, 1, 2, 3], 140573741203776)
([1, 2, 3], 140692741938688)
([555, 1, 2, 3, 1, 2, 3], 140573741203776)
"""
# --- --- ---
# 依照python在作用域里對變量的賦值操作規則
# 注意哦,函數體內對此默認形參重新賦值,是為此變量綁定了一個新的對象!!!
def func(a=[555]):
print(a, id(a))
a = a + [1, 2, 3] # -- 局部變量a重新賦值,其id發生變化
return a,id(a)
# -- 可以觀察到函數第一次和第二次調用的共享空間都為[555],其id也沒有發生變化!
# 證明 a = a + [1, 2, 3] 該行代碼雖然導致[555]的引用計數為0,但該共享空間沒有被釋放!
print(func()) # [555, 1, 2, 3]
print(func()) # [555, 1, 2, 3]
"""結果
[555] 140263832453056
([555, 1, 2, 3], 140263832467520)
[555] 140263832453056
([555, 1, 2, 3], 140263832428928)
"""
# --- --- --- 正解
def func0(a,b=None):
if b is None:
b = []
# --- --- --- 換個思考角度理解 默認形參陷阱
"""
默認形參的共享空間可以簡單理解成 一個全局變量
"""
def func(a=[555]):pass func() func()
b = [555] def func(a=b):pass func() func()
b = [555] def func(a):pass func(b) func(b)
四.函數作用域
4.1 函數對象
4.1.1 函數是第一類對象
函數是第一類對象,可以當作數據進行傳遞!
1> 被引用;
2> 當作參數傳遞;
3> 返回值是函數;
4> 作為容器類型的元素
def foo():
print('foo')
def bar():
print('bar')
dic = {
'foo': foo,
'bar': bar,
}
while True:
choice = input('>>: ').strip()
if choice in dic:
dic[choice]()
4.1.2 函數嵌套定義
需求: 將圓相關的計算(面積、周長)集中在一起.
from math import pi
def circle(radius, action='area'):
def area():
return pi * (radius**2)
def perimeter():
return 2 * pi * radius
if action == 'area':
return area()
elif action == 'perimeter':
return perimeter()
print(circle(10, 'perimeter'))
# -- Ps: max(max(2,3),8)
4.1.3 打破層級限制
函數對象可以將定義在函數內的函數返回到全局中使用,從而打破函數的層級限制
def f1():
def inner():
print('from inner')
return inner
f = f1() # -- 拿到inner函數的內存地址.
4.2 命名空間/變量名
要改變慣性思維,從命名空間的角度去看待變量!!
4.2.1 命名空間概念
在一個複雜的程序中,會定義成千上萬個變量(函數名、類名都是變量), 為了便於追蹤這些變量,讓它們互不干擾,命名空間就應運而生啦!!
命名空間 namespaces: 這幾種說法都正確
存放名字與值綁定關係的地方, 一般簡說為存放變量名的地方.
命名空間是鍵值對的集合! 變量名與值是一一對應的關係..
命名空間是變量名到對象內存地址的映射.
eg:x=1
開闢一塊空間,1
存放在內存中; x:id(1)
放在命名空間中
從變量存儲的角度, 在定義變量時, 變量名與值內存地址的關聯關係 存放於棧區stack, 變量值 存放於堆區heap.
4.2.2 命名空間分類
各個命名空間是獨立的, 沒有任何關係的, 所以一個命名空間中不能有重名
但不同的命名空間是可以重名而沒有任何影響
就好比一個文件夾中可以包含多個文件夾, 每個文件夾不能重名, 但不同文件夾中的文件可以重名…
print(len) # <built-in function len>
x=1
if 3>2:
z=5
def func():
y=2
func()
# -- 內置命名空間中的對象可以通過 dir(__builtins__) 命令查看.
類別 | 解釋 |
---|---|
內置命名空間 built-in names | 存放python解釋器自帶的名字 len |
全局命名空間 global names | 存放模塊中定義的名字(該模塊中除開內置的和局部的,都是全局的) x func z |
局部命名空間 local names | 存放函數調用時函數中定義的名字 y |
Ps: 嚴謹一點, 解釋器還為程序使用 import
語句加載的任何模塊創建一個全局命名空間
注意: 全局和局部命名空間的實現是字典, 但內置命名空間不是!
4.2.3 命名空間生命周期
指的是命名空間中 變量名!! 不是內存中的變量值的生命周期!!
內置: 在解釋器啟動時直接創建加載,直到解釋器關閉時失效.
全局: 在文件執行時生效,在文件執行完畢時失效.
局部: 在文件執行過程中,如果調用了某個函數才會臨時生效,在函數執行完畢後失效.
局部命名空間的聲明周期是自其建立開始,到它們各自的函數執行完畢終止.. 當這些命名空間的函數終止時, Python可能不會立即回收分配給這些命名空間的內存, 但是對其中對象的所有引用都將失效..
4.2.4 變量加載查詢順序
變量加載進命名空間的順序: 內置 –> 全局 –> 局部
變量在命名空間里查找的順序: 從當前位置的局部命名空間 –> 全局 –> 內置
通俗點解釋下:
python是解釋型語言,邊翻譯邊執行,在代碼從而下的執行過程中,不斷有變量被加載進命名空間.. 值得一提的是,當遇到def語句,會先將函數名字加載進全局命名空間,但函數體代碼不會執行,會暫時跳過; 函數內部的變量要等到函數調用的時候才會加載進局部命名空間…
變量在命名空間里查找的順序(簡單理解就是作用域).. 舉個例子,函數里使用了x變量,但此變量不在該函數的局部命名空間里,就會去全局命名空間里找… 當然實際情況會更複雜.後文會詳細闡述作用域.
"""變量加載進命名空間
第幾行代碼 全局 內置
1 func
4 func x:1
5 func x:1 y:2
6 func x:10 y:2
記住一句話,作用域關係是在函數定義階段就固定死了,與函數的調用位置無關!!
"""
def func():
y = 2
print(x)
x = 1
func() # 1
x = 10
func() # 10 -- 在調用函數之前,全局命名空間里的x的值被改為10啦
# --- --- ---
x = 10
a = lambda y: x + y
x = 20
b = lambda y: x + y
print(a(10)) # 30
print(b(10)) # 30
4.3 作用域
說白了, 作用域就是變量在命名空間里查找的順序.. 作用域定義了命名空間里的變量的在多大的範圍起作用!!
作用域關係是在函數定義階段就固定死了,與函數的調用位置無關!!
但凡調用函數都需要跑到定義階段去找作用域關係!一層套一層的..在定義階段就套好啦.
實則說的就是變量加載進命名空間以及變量在命名空間查詢的順序.. 不是什麼新的知識,換了個說法而已.
小聲提醒下: 只有模塊module,類class以及函數def、lambda才會引入新的作用域(即開闢新的命名空間), 其它的代碼塊(如 if/elif/else/、try/except、for/while等)是不會引入新的作用域的..
4.3.1 LEGB
在表達式中引用變量時,Python解釋器將按照如下順序遍歷各作用域,以解析該引用:
局部作用域/局部命名空間 — 當前函數的範圍 L Local
內嵌作用域/內嵌命名空間 — 函數嵌套里外圍函數的範圍 E Enclosing
全局作用域/全局命名空間 — 當前py模塊的範圍(不包含import的py模塊) G Global
內置作用域/內置命名空間 — 包含len及str等函數的那個scope B Built-in
若在這些地方都找不到名稱相符的變量,就會拋出NameError異常..
Ps: LEGB規則在Python文獻中會經常看到, 但Python官方文檔中並沒有實際出現這個術語..Hhh
def f1():
x = 1 # -- 提一嘴,f1函數結束後,x變量還能被函數嵌套中的內部函數inner訪問到..Why?因為x是自由變量!
# 後續的閉包部分會詳細闡述!!
def inner():
print(x) # -- 作用域關係是在函數定義階段就固定死了,與函數的調用位置無關!!
return inner
f = f1()
def bar():
x = 111
f() # -- 與調用位置無關
bar() # 1
4.3.2 globals()、locals()
python提供了兩個內置函數globals()和locals(),前者返回全局命名空間的字典,後者返回局部命名空間字典..
>>> type(globals()),type(locals())
(<class 'dict'>, <class 'dict'>)
注意:globals()
返回全局命名空間的實際引用; 隨便怎麼折騰都是在操作全局命名空間, 類似於 a = b
"""
可以通過引用對象的變量名x,以常規的方式訪問該對象..亦可以通過全局命名空間字典間接訪問它
"""
>>> x = 'foo'
>>> 'x' in globals() # -- 注意,變量名是以字符串的形式作為鍵的
True
>>> x
'foo'
>>> globals()['x']
'foo'
>>> x is globals()['x'] # -- x的值與"x"鍵所對應vlaue值的內存地址相同
True
"""
可以使用globals()函數在全局命名空間中創建和修改全局變量
"""
>>> y
NameError: name 'z' is not defined # -- 全局命名空間中沒有變量y
>>> glo = globals() # -- 注意:glo是全局命名空間的引用
>>> glo = 100 # -- 創建 等同於 globals()['y'] = 100
>>> y
100
>>> glo['y'] = 123 # -- 修改
>>> y
123
"""
globals結合format的應用
"""
>>> x = 1
>>> y = 2
>>> "{x},{y}".format(**globals())
'1,2'
locals()
返回的不是對局部命名空間的引用!! 那到底返回的是什麼呢?
查閱資料,很多博客說是拷貝、副本.. 雲里霧裡的. 自己做了實驗後, 發現這樣的描述都不夠嚴謹..
先說結論: locals()函數返回的是對局部命名空間的一個“拷貝” (打了引號哦) ,但此拷貝有點特殊,它具備淺拷貝的一些特點,同時當我們再次調用locals()函數時,它會 **同步更新 **”拷貝”的值..
"""
什麼是拷貝、副本、視圖?
參考鏈接: //blog.csdn.net/Reborn214/article/details/124539097
簡單來說,副本、視圖是python numpy數組中的專業名詞,但其具備的特性跟拷貝差不多.
numpy中引用 = python中引用; numpy中視圖 = python中淺拷貝; numpy中副本 = python中深拷貝
值得一提的是,引用一般發生在賦值操作(python賦值都是引用"內存地址"傳遞)
還要注意一個坑!!
id():返回對象的「標識值」.該值是一個整數,在此對象的生命周期中保證是唯一且恆定的.
兩個生命期不重疊的對象可能具有相同的id()值..
舉個例子
分析 -- 沒有將a[:]賦值給變量進行引用,當執行完a[:]後,這個對象就釋放啦.但這塊內存可能並沒有來得及釋放.
當我們新執行a[:]的時候,用的將會是同一個內存,沒有申請新的內存.
這會讓我們覺得兩個a[:]與a[:]是同一個對象
>>> a = [1,2,3]
>>> id(a[:])
140417051683648
>>> id(a[:])
140417051684544
>>> id(a[:])
140417051684544
>>> id(a[:]) is id(a[:]) # -- 讓其處於同一生命周期,每次淺拷貝都會生成不同的對象,結果肯定為False
False
"""
def func():
# -- 驗證了id()的坑.
print(id(locals())) # 140204424022656
print(id(locals())) # 140204424022656
print(id(locals()) is id(locals())) # False
m = [1, 2, 3]
n = 66
loc = locals() # -- loc是局部命名空間的「拷貝」 loc是對此"拷貝"的引用!!
print(loc) # {'m': [1, 2, 3], 'n': 66}
"""來,簡化一下
import copy
scope = {'m': [1, 2, 3], 'n': 66} # -- 指代局部命名空間
loc = copy.copy(scope) # -- 指代局部命名空間的淺拷貝
loc['m'].append(4)
scope.append(5)
scope['n'] = 88
scope['x'] = 20
loc['n'] = 77
print(scope) # -- {'m': [1, 2, 3, 4, 5], 'n': 88, 'x': 20}
print(loc) # -- {'m': [1, 2, 3, 4, 5], 'n': 77}
"""
loc['m'].append(4) # -- 試圖通過"拷貝"過來的字典修改局部命名空間里的可變類型的變量m 成功
m.append(5) # -- 在局部命令空間里修改了m變量對應的值
n = 88 # -- 在局部命令空間里修改了n變量對應的值
x = 20 # -- 在局部命名空間里添加了x變量
# -- 可以發現此"拷貝",具備淺拷貝的特性
print(loc) # {'m': [1, 2, 3, 4, 5], 'n': 66}
loc['n'] = 77 # -- 試圖通過"拷貝"過來的字典修改局部命名空間里的可變類型的變量m 失敗
print(locals()) # {'m': [1, 2, 3, 4, 5], 'n': 88, 'loc': {...}, 'x': 20}
# -- 再次調用locals()後,"拷貝"同步更新了.
print(loc) # {'m': [1, 2, 3, 4, 5], 'n': 88, 'loc': {...}, 'x': 20}
func()
4.3.3 global、nonlocal
回顧下Python在作用域里對變量的賦值操作規則:
若這個變量在該作用域存在(已經定義), 則對其綁定新的對象; 若不存在,則將這次賦值視為對這個變量的定義..
global — 不管是在函數嵌套的哪一層,對x變量的賦值操作都是修改全局作用域里的那個x變量!!
nonlocal — 如果在閉包內給x變量賦值,那麼修改的其實是閉包外那個作用域里的x變量..
nonlocal的唯一限制在於,不能延伸到模塊級別,這是為了防止它污染全局變量..
Ps: 提一嘴閉包,後續會詳細闡述. 函數嵌套,內部函數引用外部函數的參數或變量,就構成了閉包..
"""
★ --global
在局部若想修改全局的不可變類型,需要藉助global聲明
在局部若想修改全局的可變類型,不需要藉助任何聲明,可以直接修改
若全局中沒有x變量,global語句和賦值的組合(不管處於嵌套的哪一層)可以間接在局部中創建x全局變量
"""
x = []
def func():
global m
m = 10
globals()['n'] = 20 # -- n變量在全局中存在的話,此賦值操作就是在修改;通常不會這麼做,完全沒必要
x.append(1)
func()
func()
print(x) # [1,1]
print(m, n) # 10,20
"""
★ --nonlocal
"""
x = 1
def f1(): # E f1的參數對f2而言
x = 111 # E 此處的x對f2而言
def f2(): # E f2的參數對f3而言
x = 222 # E 此處的x對f3而言
def f3():
nonlocal x # -- 若f1函數嵌套里沒有x 則報錯找不到x變量
x = 333 # -- 改的是最近的x = 222的值
f3()
print(x) # 333
# {'f3': <function f1.<locals>.f2.<locals>.f3 at 0x7fc60c71c9d0>, 'x': 333}
print(locals()) # f2的局部作用域
f2()
print(x) # 111
# {'x': 111, 'f2': <function f1.<locals>.f2 at 0x7fc60c71c790>}
print(locals()) # f1的局部作用域
f1()
print(x) # 1