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

繁華似錦Fighting發表於2022-01-05

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裡討論這個確實是沒有意義。

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

相關文章