爲什麼Python中的函數會修改全局的列表和字典

Python中的函數(內置函數和我們自己編寫的自定義函數)是處理數據的關鍵工具。但是他們對我們的數據做了什麼可能有點令人困惑,如果我們不知道發生了什麼,它可能會在我們的分析中造成嚴重的錯誤。

在本教程中,我們將詳細研究Python在函數中處理不同數據類型時是如何對它們進行操作的,並學習如何確保只有在希望更改數據時才更改數據。

函數中的內存隔離

爲了理解Python如何處理函數內部的全局變量,我們來做一個小實驗。我們將創建兩個全局變量number_1和number_2,併爲它們賦值整數 5 和 10。然後,我們將使用這些全局變量作爲函數的參數來執行一些簡單的數學運算。我們還將使用變量名作爲函數的參數名。然後,我們將查看函數中所有變量的使用是否影響了這些變量的全局值。

1240

正如我們在上面看到的,函數正常運行,全局變量number_1和number_2的值沒有變化,儘管我們在函數中使用它們作爲形參和實參名。這是因爲Python將函數中的變量存儲在與全局變量不同的內存位置。它們是被隔離的。因此,變量number_1可以在全局中有一個值(5),而在函數內部有一個不同的值(50),在這個函數中它是獨立的。

(順便說一句,如果你對parameters(形參) 和 arguments(實參)之間的區別感到困惑,Python文檔中關於這個主題的內容非常有用。)

那麼列表和字典呢?

列表

我們已經看到,我們在函數內部對上面的number_1這樣的變量所做的操作並不影響它的全局值。但是number_1是一個整數,這是一個非常基本的數據類型。如果我們用不同的數據類型(比如列表)嘗試相同的實驗,會發生什麼?下面,我們將創建一個名爲duplicate_last() 的函數,它將複製我們作爲參數傳遞的任何列表中的最終條目。

1240

正如我們所看到的,這裏 initial_list 的全局值被更新了,即使它的值只在函數內部更改!

字典

現在,我們來編寫一個函數,該函數以一個字典作爲參數來查看全局字典變量在函數中被操作時是否也會被修改。

爲了看起來更直觀一點,我們將使用Python基礎課程中使用的 AppleStore.csv 數據集中的數據(數據可以從這裏下載)。

在下面的代碼片段中,我們從一個字典開始,它包含了數據集中各個年齡級別的應用程序的數量(因此有4433個應用程序的級別爲“4+”,987個應用程序的級別爲“9+”,等等)。假設我們想計算每個年齡等級的百分比,這樣我們就可以得到在App Store中哪個年齡等級是最常見的。

爲此,我們將編寫一個名爲 make_percentages() 的函數,該函數以一個字典作爲參數並將計數轉換爲百分比。我們需要從0開始計數,然後遍歷字典中的每個值,將它們添加到計數中,這樣就得到了評級的總數。然後我們將再次遍歷字典,並對每個值做一些數學運算來計算百分比。

1240

在查看輸出之前,讓我們快速回顧一下上面發生的事情。在將我們的app 年齡評級字典分配給變量content_ratings之後,我們創建了一個名爲make_percentages()的新函數,它只接受一個參數: a_dictionary。

爲了計算每個年齡等級的應用程序所佔比例,我們需要知道應用程序的總數,因此我們首先將一個名爲total的新變量設置爲0,然後在a_dictionary中循環遍歷每個鍵值,並將其添加到total中。

完成之後,我們需要做的就是再次遍歷a_dictionary,將每個條目除以總數,然後將結果乘以100。這將給我們返回一個包含百分比的詞典。

但是,當我們使用全局content_ratings變量作爲這個新函數的參數時發生了什麼呢?

1240

正如我們在列表中看到的,我們的全局content_ratings變量已經更改,儘管它只是在我們創建的make_percentages()函數中進行了修改。

這裏到底發生了什麼?我們遇到了可變不可變數據類型之間的差異。

可變和不可變的數據類型

在Python中,數據類型可以是可變的(可更改的),也可以是不可變的(不可更改的)。雖然我們在介紹Python時使用的大多數數據類型都是不可變的(包括整數、浮點數、字符串、布爾值和元組),但是列表和字典是可變的。這意味着全局列表或字典即使在函數內部使用時也可以更改,就像我們在上面的示例中看到的那樣。

要理解可變(可更改)和不可變(不可更改)之間的區別,瞭解Python如何處理這些變量是很有幫助的。

讓我們從考慮一個簡單的變量賦值開始:

1240

變量名a的作用類似於一個指向5的指針,它可以幫助我們隨時檢索5。

5是一個整數,整數是不可變的數據類型。如果數據類型是不可變的,這意味着一旦創建,就不能更新它。如果我們執行a += 1,我們實際上並沒有更新5到6。在下面的動畫中,我們可以看到這一點:

a 初始指向 5.

執行a += 1 後, 將指針從 5指向 6, 它並沒有實際改變 5.

可變數據類型(如列表和字典)的行爲有所不同。它們可以更新。舉個例子,我們來創建一個非常簡單的列表:

1240

如果我們在列表末尾添加一個3,我們不是簡單地將list_1指向另一個列表,而是直接更新現有列表:

即使我們創建多個列表變量,只要它們指向同一個列表,當列表被更改時,它們都會被更新,如下面的代碼所示:

1240

下面是上面代碼中實際發生的動態可視化:

這就解釋了爲什麼我們之前在試驗列表和字典時我們的全局變量被改變了。因爲列表和字典是可變的,所以更改它們(即使是在函數中)也會更改列表或字典本身,這與不可變數據類型不同。

保持可變數據類型不變

一般來說,我們不希望函數更改全局變量,即使它們包含列表或字典之類的可變數據類型。這是因爲在更復雜的分析和程序中,我們可能會經常使用許多不同的函數。如果所有函數都更改它們正在調用的列表和字典,那麼要跟蹤什麼在更改什麼就會變得非常困難。

幸運的是,有一種簡單的方法可以繞過這個問題:我們可以使用內建的Python方法.copy()複製列表或字典。

如果你還沒有學習過方法,請不要擔心。它們包含在我們的中級Python課程中,但是在本教程中,你只需要知道.copy()的工作原理類似於.append():

1240

讓我們再看一下我們爲列表寫的函數,對它進行更新,這樣函數內部執行的操作就不會更改initial_list。我們只需要將傳遞給函數的參數從initial_list更改爲initial_list.copy()

1240

正如我們所看到的,這已經解決了我們的問題。原因如下:使用.copy()創建一個列表的獨立副本,這樣a_list就不會指向initial_list本身,而是指向一個以initial_list副本開始的新列表。在此之後對a_list所做的任何更改都將只對該獨立列表生效,而不是initial_list本身,因此initial_list的全局值將保持不變。

不過,這個解決方案仍然不完美,因爲每次向函數傳遞參數時都必須記得添加.copy(),否則可能會意外更改initial_list的全局值。如果我們不想操心這個,我們可以在函數內部創建列表拷貝:

1240

使用這種方法,我們可以安全地將一個可變的全局變量(如initial_list)傳遞給我們的函數,全局值不會被改變,因爲函數本身會複製一個副本,然後對該副本執行操作。

.copy()方法也適用於字典。與列表一樣,我們可以簡單地將.copy()添加到傳遞給函數的參數中,創建一個用於函數的拷貝,而不會改變原始變量:

1240

但是,再次說明,使用這種方法意味着在每次將字典傳遞給make_percentages()函數時,都要記得添加.copy()。如果我們要頻繁地使用這個函數,最好在函數內部實現複製,這樣我們就不需要記住了。

下面,我們將在函數內部使用.copy()。這樣,就可以確保我們在將全局變量作爲參數傳遞給函數時不會被更改,而且我們也不需要記得爲傳遞的每個參數添加.copy()。

1240

正如我們所看到的,修改我們的函數來創建字典的副本,然後只在副本中將計數更改爲百分比,這樣我們就可以在不更改content_ratings的情況下執行我們想要的操作。

結論

在本教程中,我們研究了可變數據類型(可以更改)和不可變數據類型(不能更改)之間的區別。我們學習瞭如何使用.copy()方法複製列表和字典等可變數據類型,這樣我們就可以在不更改其全局值的情況下在函數中使用它們。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章