『無為則無心』Python函數 — 29、Python變數和參數傳遞

1、Python的變數

(1)Python變數不能獨立存在

  • 比如在C++等語言中,變數的聲明和賦值是可以分開的。
    int a;
    a=343;
    
  • 而在Python中卻不行,在聲明Python變數的同時必須進行賦值操作,否則會報錯。
    Python Console: starting.
    Python 3.7.7 
    >>> a
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
    NameError: name 'a' is not defined
    >>> a = 343
    
    >>> a
    343
    

    如果你直接使用一個不存在的變數,就會發生錯誤:NameError: name 'a' is not defined

(2)變數是記憶體中數據的引用

a = 343這樣程式碼被執行時,首先要在記憶體中創建出343這個對象,然後讓a指向它,這便是引用

此後,我們在程式中使用變數a時,其實都是在使用343,Python可以通過a找到343這個值,這是對引用最通俗的解釋。

如下圖所示:

image

(3)注意點

賦值語句執行過程中,有一點極容易被忽略掉,那就是這個過程中,在記憶體中創建了新的數據的問題。

a = [1]
b = [1]

print(a == b)
True

print(a is b)
False

兩行賦值語句,分別將列表[1]賦值給ab,表達式a==b的結果是Ture,因為他們的內容的確相同,但表達式a is b的結果是False,因為這兩行賦值語句執行過程中,一共創建了兩個列表,他們只是內容相同而已,但記憶體地址絕對不相同,下圖是這兩個變數的記憶體描述示意圖。

image

2、了解變數的引用

在Python中,變數的值是靠引用來傳遞來的。

我們可以用id()函數來判斷兩個變數是否引用的同一個值。

id()函數返回對象的唯一標識符,標識符是一個整數,是對象的記憶體地址。如果兩個變數的記憶體地址相同,說明兩個變數引用的是同一個地址。

看下面綜合示例:

# 1. int類型
"""
聲明變數保存整型數據,把這個數據賦值到另一個變數; 
id()檢測兩個變數的id值(記憶體的十進位值)
"""
a = 1
b = a

print(b)  # 列印變數b的值為1

# id(a):返回a變數在記憶體中的十進位地址
# 變數a和變數b的記憶體地址一樣,說明ab引用的是同一數據。
print(id(a))  # 140708464157520
print(id(b))  # 140708464157520

# 將變數a重新賦值
a = 2
print(b)  # 列印變數b的值為1

# 因為修改了a的數據,記憶體要開闢另外一份記憶體取存儲2,
# id檢測a和b的地址不同
print(id(a))  # 140708464157552,此時得到是的數據2的記憶體地址
print(id(b))  # 140708464157520


# 2. 列表(可變數據類型)
aa = [10, 20]
bb = aa

# 發現a和b的id值相同的,說明引用的是同一個記憶體地址
print(id(aa))  # 2325297783432
print(id(bb))  # 2325297783432

# 變數aa添加數據
aa.append(30)
print(bb)  # 變數bb的值為[10, 20, 30], 列表為可變類型

# 列印結果
print(id(aa))  # 2325297783432
print(id(bb))  # 2325297783432

# 繼續操作
bb = bb + [666]
print(aa) # [10, 20, 30]
print(bb) # [10, 20, 30, 666]

print(id(aa)) # 30233096
print(id(bb)) # 40054280
# 這時我們會發現可變類型變數的引用也會發生改變,
# 這是為什麼呢?這裡就不解釋了,下面一點進行詳細說明。

3、Python的參數傳遞(重點)

Python的參數傳遞也就是Python變數的引用。

在Python中,所有的變數都是指向某一個地址,變數本身不保存數據,而數據是保存在記憶體中的一個地址上。

通俗的來說,在 Python 中,變數的名字類似於把便簽紙貼在數據上。

我看網上很多文章說:

  • Python基本的參數傳遞機制有兩種:值傳遞和引用傳遞。
  • 不可變參數是值傳遞,可變參數是引用傳遞。

這樣理解也是可以的,但我認為都是引用的傳遞,下面我用示例說明一下。

先來說明一個知識點,在Python中,不可變數據類型可變數據類型是如何分配記憶體地址的。

如下所示:

inta = 10086
intb = 10086
lista = [10,20,30]
listb = [10,20,30]

print('inta的記憶體地址',id(inta))
print('intb的記憶體地址',id(intb))
print('lista的記憶體地址',id(lista))
print('listb的記憶體地址',id(listb))

"""
運行結果如下:
inta的記憶體地址 37930032
intb的記憶體地址 37930032
lista的記憶體地址 30233096
listb的記憶體地址 30233608
"""

我們可以看到:

  • 對於不可變數據類型變數,相同的值是共用記憶體地址的。(這一點很重要)
  • 對於可變數據類型變數,如上面的listalistb,即使內容一樣,Python也會給它們分配不同的記憶體地址。

(1)示例

下面來看一下示例:

提示:對不可變數據類型,++=都會創建新對象,對可變數據類型來說,+=不會創建新對象。

1)可變數據類型變數示例

我們通過示例來看看可變數據類型變數的引用是如何傳遞的。

def ChangeParam(paramList):
    paramList.append([1, 2, 3, 4])
    print("函數內paramList狀態1,取值: ", paramList)
    print('函數內paramList狀態1的記憶體地址:', id(paramList))

    paramList += [888]
    print("函數內paramList狀態2,取值: ", paramList)
    print('函數內paramList狀態2的記憶體地址:', id(paramList))

    paramList = paramList + [888]
    print("函數內paramList狀態3,取值: ", paramList)
    print('函數內paramList狀態3的記憶體地址:', id(paramList))
    return


mylist = [10, 20, 30]
print('mylist函數外的記憶體地址(前):', id(mylist))

ChangeParam(mylist)
print("函數外取值: ", mylist)
print('mylist函數外的記憶體地址(後):', id(mylist))

"""
mylist函數外的記憶體地址(前): 32264712
函數內paramList狀態1,取值:  [10, 20, 30, [1, 2, 3, 4]]
函數內paramList狀態1的記憶體地址: 32264712
函數內paramList狀態2,取值:  [10, 20, 30, [1, 2, 3, 4], 888]
函數內paramList狀態2的記憶體地址: 32264712
函數內paramList狀態3,取值:  [10, 20, 30, [1, 2, 3, 4], 888, 888]
函數內paramList狀態3的記憶體地址: 42905160
函數外取值:  [10, 20, 30, [1, 2, 3, 4], 888]
mylist函數外的記憶體地址(後): 32264712
"""

2)不可變數據類型變數示例

我們通過示例來看看不可變數據類型變數的引用是否是值傳遞。

示例如下:

def NoChangeParam(prarmInt):

    print('函數中變數prarmInt的初始狀態,prarmInt變數的值', prarmInt)
    print('函數中變數prarmInt的初始狀態,prarmInt的記憶體地址', id(prarmInt))

    prarmInt += prarmInt
    print('函數中狀態1,此時prarmInt的值:', prarmInt)
    print('函數中狀態1,prarmInt的記憶體地址:', id(prarmInt))

# 1.定義變數a
a = 1000
print('執行函數前,變數a的記憶體地址:', id(a))

# 2.調用函數
NoChangeParam(a)

# 3.列印執行函數後,a變數的值和指向記憶體地址
print('執行函數後,a變數的值。a =', a)
print('執行函數後,a的記憶體地址:', id(a))

"""
執行函數前,變數a的記憶體地址: 32817968
函數中變數prarmInt的初始狀態,prarmInt變數的值 1000
函數中變數prarmInt的初始狀態,prarmInt的記憶體地址 32817968
函數中狀態1,此時prarmInt的值: 2000
函數中狀態1,prarmInt的記憶體地址: 32818224
執行函數後,a變數的值。a = 1000
執行函數後,a的記憶體地址: 32817968
"""

(2)結論

通過上面示例我們可以看到:

  • 在函數執行前後,不可變數據類型變數和可變數據類型變數的所指向的地址都沒有發生改變。只不過不可變數據類型變數的值沒有改變,而可變數據類型變數的值發生了改變。
  • 不可變數據類型變數和可變數據類型變數,在傳入函數的最開始的狀態,都和原變數一致,說明函數的參數傳遞是地址傳遞
  • 不可變數據類型變數和可變數據類型變數,在函數中只要產生了新對象,記憶體引用地址都會發生改變。
    也就是說:

    • 對於不可變數據類型變數來說,只有改變了變數的值,就會產生一個新對象,記憶體地址的引用就會發生改變。(因為前邊的結論,對於不可變數據類型變數,相同的值是共用記憶體地址的)
    • 對於可變數據類型變數來說,因為是可變的,所以改變變數的值,記憶體地址的引用不會發生改變。只有產生了新對象,如mylist = mylist + [888],記憶體地址的引用才會發生改變。

(3)總結

通過上面的內容,我們可以知道:

  • 對於不可變類型變數而言:因為不可變類型變數特性,修改變數需要新創建一個對象,形參的標籤轉而指向新對象,而實參沒有變。
  • 對於可變類型變數而言,因為可變類型變數特性,直接在原對象上修改,因為此時形參和實參都是指向同一個對象,所以實參指向的對象自然就被修改了。而如果可變類型變數在函數內的操作創建了新的對象,記憶體地址的引用也會發生改變,但僅限於在函數內。

(4)補充(重點)

感覺以上的話很啰嗦,在最後整理一下。

看下面例子:

# 交換函數
def swap(a, b):
    # 下面程式碼實現a、b變數的值交換
    a, b = b, a
    print("swap函數里,a的值是", a, ";b的值是", b)

    a = 777
    b = 999
    print("swap函數里第二次列印,a的值是", a, ";b的值是", b)


a = 666
b = 888
swap(a, b)
print("函數交換結束後,變數a的值是", a, ";變數b的值是", b)


"""
swap函數里,a的值是 888 ;b的值是 666
swap函數里第二次列印,a的值是 777 ;b的值是 999
函數交換結束後,變數a的值是 666 ;變數b的值是 888
"""

1)第一步,執行swap(a, b)函數

  • 變數a把自己指向的記憶體地址傳遞給了函數的形參a,變數b把自己指向的記憶體地址傳遞給了函數的形參b。
  • 這樣變數a和swap函數的形參a,都指向了同一個記憶體地址。變數b和形參b同理。
  • 這也說明了上面(1)示例中,變數進入函數的初始索引地址沒有變化的原因。

如下圖所示:

image

2)第二步,swap(a, b)函數內進行了形參a和形參b的值交換。

也就時執行了a, b = b, a命令。

  • 形參a和形參b的值進行了交換,因為記憶體中就有這兩個值,所以只是記憶體地址的引用交互了一下。
  • 之後就執行了列印命令,顯示”swap函數里,a的值是 888 ;b的值是 666″

如下圖所示:

image

提示:形參a和b就時給函數內的變數起一個名,用於區分。這裡說明一下,因為我這樣的描述不是很準確。

3)第三步,繼續給形參a和b賦予新的值。

  • 也就是模擬產生新的對象,並指引到新對象的記憶體地址上。
  • 執行了a = 777b = 999,列印結果為「swap函數里第二次列印,a的值是 777 ;b的值是 999」。

如下圖所示:

image

4)第四步,swap(a, b)函數執行完畢。

  • swap(a, b)函數執行完畢,形參a和b的生命周期也就結束了。
  • 所以變數a和b在函數結束後的列印結果還是初始的狀態,「函數交換結束後,變數a的值是 666 ;變數b的值是 888」。

如下圖所示:

image

5)總結:

所以對於不可變數據類型變數的參數傳遞,執行外表上看,好像只傳遞了數值,其實通過上面的例子彈道,也進行了引用地址的傳遞。

上面使用了不可變數據類型變數進行了示例,可變數據類型變數是一樣的,只不過修改變數的內容,地址是不發生改變的。但產生了新的對象,記憶體地址的引用會到新的對象上,和不可變數據類型變數是一樣的。

最後我覺得到現在再來討論Python中參數的傳遞是值傳遞還是引用傳遞,就會發現在Python里討論這個確實是沒有意義。

參考://c.biancheng.net/view/2258.html