圖片來源:RitaE from Pixabay
除非你知道所有這些要點(diǎn),否則不要使用Python Pickle
Pickle序列化的優(yōu)點(diǎn)和缺點(diǎn),以及我們應(yīng)該何時(shí)使用它
與其他大多數(shù)流行的編程語言相比,Python可能擁有最靈活的對(duì)象序列化。在Python中,所有的東西都是一個(gè)對(duì)象,所以我們可以說,幾乎所有的東西都可以被序列化。是的,我說的這個(gè)模塊就是 Pickle。
然而,與其他 "常規(guī) "的序列化方法如JSON相比,Pickle有更多的方面需要我們?cè)谑褂脮r(shí)注意。這就是標(biāo)題所說的,除非你知道這些事實(shí),否則不要使用Pickle。
在這篇文章中,我將組織一些關(guān)于Pickle的重要注意事項(xiàng),希望它們能對(duì)你有所幫助。
- 基本用法
圖片:Photo Mix from Pixabay
通過使用Python的Pickle模塊,我們可以輕松地將幾乎所有類型的對(duì)象序列化到一個(gè)文件中。在我們使用它之前,需要先導(dǎo)入它。
導(dǎo)入 pickle
讓我們以一個(gè)字典為例。
import pickle
Let’s take a dictionary as an example.
my_dict = {
'name': 'Chris',
'age': 33
}
我們可以使用 pickle.dump() 方法來序列化字典并將其寫入文件。
with open('my_dict.pickle', 'wb') as f:
pickle.dump(my_dict, f)
然后,我們可以讀取該文件并將其加載到一個(gè)變量中。之后,我們就有了準(zhǔn)確的字典回來。它們?cè)趦?nèi)容上是 100% 相同的。
with open('my_dict.pickle', 'rb') as f:
my_dict_unpickled = pickle.load(f)
my_dict == my_dict_unpickled
- 為什么是Pickle?有什么優(yōu)點(diǎn)和缺點(diǎn)?
圖片來自Pixabay的Christine Sponchia
的確,如果我們?cè)谏厦娴睦又惺褂肑SON來序列化Python字典,會(huì)有更多的好處。一般來說,Pickle序列化有三個(gè)主要缺點(diǎn)。
缺點(diǎn)-1:Pickle是不安全的
不像JSON只是一個(gè)字符串,有可能構(gòu)建惡意的Pickle數(shù)據(jù),在解壓過程中執(zhí)行任意代碼。
大喵補(bǔ)充:pickle是一串二進(jìn)制的數(shù)字!因此,我們絕對(duì)不應(yīng)該解開那些可能來自不信任的源頭或可能被篡改的數(shù)據(jù)。
缺點(diǎn)2:Pickle是不可讀的
將Python字典序列化為JSON字符串的最大意義在于,其結(jié)果是人類可讀的。然而,對(duì)于Pickle文件來說,這并不是真的。這里是我們剛剛腌制的字典的 pickle 文件。如果我們?cè)噲D把它作為一個(gè)文本文件打開,我們會(huì)得到這樣的結(jié)果。
缺點(diǎn) 3:Python 中的 Pickle 是有限的
一個(gè) pickle 對(duì)象只能用 Python 加載。其他語言可能可以這樣做,但需要第三方庫的參與,而且可能仍然沒有得到完美的支持。
相比之下,JSON字符串在編程領(lǐng)域非常常用,并且被大多數(shù)編程語言所支持。
Pickle的優(yōu)點(diǎn)
Pickle通過調(diào)用任意的函數(shù)來構(gòu)造任意的Python對(duì)象,這就是為什么它不安全。然而,這使得它能夠序列化幾乎所有的Python對(duì)象,而JSON和其他序列化方法是做不到的。
解開一個(gè)對(duì)象通常不需要 "模板"。所以,它非常適用于快速和簡(jiǎn)單的序列化。例如,你可以把所有的變量轉(zhuǎn)儲(chǔ)到pickle文件中,然后終止你的程序。稍后,你可以啟動(dòng)另一個(gè)Python會(huì)話,并從序列化的文件中恢復(fù)一切。所以,這使我們能夠以一種更靈活的方式運(yùn)行一段程序。
另一個(gè)例子將是多線程。當(dāng)我們使用多進(jìn)程模塊在多線程中運(yùn)行一個(gè)程序時(shí),我們可以很容易地將任意的Python對(duì)象發(fā)送到其他進(jìn)程或計(jì)算節(jié)點(diǎn)。
在這些情況下,安全問題通常并不適用,人類也不會(huì)去讀這些對(duì)象。我們只需要快速、簡(jiǎn)單和兼容性。在這些情況下,Pickle可以完美地被利用。
- 還有什么可以被腌制 pickle?
好吧,我一直在談?wù)搸缀跛锌梢员籔ickle序列化的東西。現(xiàn)在,讓我給你看一些例子。

pickle 一個(gè)函數(shù)
第一個(gè)例子將是一個(gè)函數(shù)。是的,我們可以在 Python 中序列化一個(gè)函數(shù),因?yàn)楹瘮?shù)在 Python 中也是一個(gè)對(duì)象。
def my_func(num):
print(f'my function will add 1 to the number {num}')
return num + 1
只是為演示目的定義了一個(gè)簡(jiǎn)單的函數(shù)?,F(xiàn)在,讓我們把它提取出來并載入一個(gè)新的變量中。
with open('my_func.pickle', 'wb') as f:
pickle.dump(my_func, f)
with open('my_func.pickle', 'rb') as f:
my_func_unpickled = pickle.load(f)
my_func_unpickled(10)
新的變量可以作為一個(gè)函數(shù)使用,而且這個(gè)函數(shù)將與原來的函數(shù)完全相同。
Pickle一個(gè)Pandas數(shù)據(jù)框
另一個(gè)例子是一個(gè)Pandas數(shù)據(jù)框。讓我們來定義一個(gè)Pandas數(shù)據(jù)框。
import pandas as pd
my_df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Chris'],
'age': [25, 29, 33]
})
現(xiàn)在,我們可以腌制它并將其解壓到一個(gè)新的變量中,新的DataFrame將是相同的。
with open('my_df.pickle', 'wb') as f:
pickle.dump(my_df, f)
with open('my_df.pickle', 'rb') as f:
my_df_unpickled = pickle.load(f)
請(qǐng)注意,Pandas有內(nèi)置的方法,可以對(duì)數(shù)據(jù)幀進(jìn)行pickle和unpickle處理
請(qǐng)注意,Pandas有內(nèi)置的方法,可以對(duì)數(shù)據(jù)幀進(jìn)行糾錯(cuò)和解錯(cuò)。它們會(huì)做與上面相同的工作,但代碼會(huì)更干凈。其性能也是相同的。
那么,可能會(huì)有一個(gè)問題,為什么我們要對(duì)數(shù)據(jù)框使用Pickle而不是CSV?
第一個(gè)答案是速度。CSV是人類可讀的,但它幾乎是存儲(chǔ)Pandas數(shù)據(jù)幀的最慢的方式。
對(duì)序列化Pandas數(shù)據(jù)框架的不同方式的性能進(jìn)行了基準(zhǔn)測(cè)試。





Reference benchmark test:
What is the fastest way to upload a big csv file in notebook to work with python pandas? - Stack Overflow
Pickle pandas dataframe的第二個(gè)好處是數(shù)據(jù)類型。當(dāng)我們把數(shù)據(jù)框架寫到CSV文件時(shí),所有的東西都要轉(zhuǎn)換成文本。有時(shí),當(dāng)我們把它加載回來時(shí),這將造成一些不便或麻煩。例如,如果我們把一個(gè)日期時(shí)間列寫到CSV中,我們很可能需要在加載回來時(shí)指定格式字符串。
然而,這個(gè)問題對(duì)于一個(gè)pickle對(duì)象來說并不存在。你腌制的東西,當(dāng)你加載它時(shí),你保證會(huì)有完全相同的東西回來。不需要做任何其他事情。
- 腌制協(xié)議版本
像我在前面的例子中所做的那樣使用Pickle是很常見的。他們沒有錯(cuò),但如果我們能指定Pickle的協(xié)議版本(通常是最高的),那就更好了。簡(jiǎn)單地說,Pickle的序列化有不同的版本。隨著Python版本的不斷迭代,Pickle模塊也在不斷發(fā)展。
如果你對(duì)現(xiàn)有的版本和改進(jìn)的內(nèi)容感興趣,這里有一個(gè)官方文檔的列表。
協(xié)議版本0是原始的 "人類可讀 "協(xié)議,并且向后兼容早期的Python版本。
協(xié)議版本1是一種舊的二進(jìn)制格式,也與早期的Python版本兼容。
協(xié)議版本 2 是在 Python 2.3 中引入的。它提供了一個(gè)更有效的新式類的腌制方法。
協(xié)議版本 3 是在 Python 3.0 中加入的。它對(duì)字節(jié)對(duì)象有明確的支持,并且不能被Python 2.x解開。這是Python 3.0-3.7的默認(rèn)協(xié)議。
協(xié)議版本 4 是在 Python 3.4 中添加的。它增加了對(duì)非常大的對(duì)象的支持,腌制了更多種類的對(duì)象,以及一些數(shù)據(jù)格式的優(yōu)化。從Python 3.8開始,它是默認(rèn)的協(xié)議。
協(xié)議版本5是在Python 3.8中添加的。它增加了對(duì)帶外數(shù)據(jù)的支持和對(duì)帶內(nèi)數(shù)據(jù)的加速。
一般來說,在以下方面,高版本總是比低版本好一些
腌制對(duì)象的大小
解除pickling的性能
如果我們使用不同的版本對(duì)Pandas數(shù)據(jù)幀進(jìn)行pickle,我們可以看到其大小上的差異。
with open('my_df_p4.pickle', 'wb') as f:
pickle.dump(my_df, f, protocol=4)
with open('my_df_p3.pickle', 'wb') as f:
pickle.dump(my_df, f, protocol=3)
with open('my_df_p2.pickle', 'wb') as f:
pickle.dump(my_df, f, protocol=2)
with open('my_df_p1.pickle', 'wb') as f:
pickle.dump(my_df, f, protocol=1)
path.getsize
import os
print('P4:', os.path.getsize('my_df_p4.pickle'))
print('P3:', os.path.getsize('my_df_p3.pickle'))
print('P2:', os.path.getsize('my_df_p2.pickle'))
print('P1:', os.path.getsize('my_df_p1.pickle'))
為什么 Python 仍然保留舊版本而新版本總是更好?這是因?yàn)閰f(xié)議并不總是向后兼容。這意味著,如果我們想要更好的兼容性,就必須選擇一個(gè)較低的版本。
然而,如果我們使用pickle對(duì)象而不需要向后兼容,我們可以使用枚舉來保證我們的程序使用最新的版本(最好的版本)。例子如下。
pickle.dump(my_df, f, protocol=pickle.HIGHEST_PROTOCOL)
- 拾取一個(gè)自定義類
盡管Pickle支持Python中幾乎所有的對(duì)象,但當(dāng)我們拾取一個(gè)從自定義類中實(shí)例化出來的對(duì)象時(shí),我們?nèi)匀恍枰⌒?。?jiǎn)而言之,當(dāng)我們加載被拾取的對(duì)象時(shí),該類需要是存在的。
例如,讓我們定義一個(gè)有兩個(gè)屬性和一個(gè)方法的簡(jiǎn)單類 "Person"。
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def self_introduce(self):
print(f'My name is {self.name} and my age is {self.age}')
p = Person('Chris', 33)
p.self_introduce()
現(xiàn)在,讓我們用Pickle來序列化對(duì)象 "p"。
with open('person.pickle', 'wb') as f:
pickle.dump(p, f)
如果這個(gè)類不存在,問題就會(huì)發(fā)生。如果我們?cè)噲D在一個(gè)新的會(huì)話中加載pickle對(duì)象,而這個(gè)類并沒有被定義,這就會(huì)發(fā)生。我們可以通過刪除類的定義來模擬這種情況。
del Person
然后,如果我們?cè)噲D重新加載腌制對(duì)象,就會(huì)出現(xiàn)一個(gè)異常。
with open('person.pickle', 'rb') as f:
p_unpickled = pickle.load(f)

因此,我們需要確保當(dāng)我們加載對(duì)象回來時(shí),這個(gè)類是存在的。然而,如果類的定義稍有不同,可能不會(huì)造成問題,但對(duì)象的行為可能會(huì)根據(jù)新的類定義而改變。
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def self_introduce(self):
print(f'(Modified) My name is {self.name} and my age is {self.age}')
在新的類定義中,我修改了自我介紹方法的打印信息。然后,如果我們把腌制好的對(duì)象裝回去,就不會(huì)有任何錯(cuò)誤,但自我介紹方法會(huì)與原來的方法不同。
with open('person.pickle', 'rb') as f:
p_unpickled = pickle.load(f)
p_unpickled.self_introduce()

- 不是所有的對(duì)象都可以被pickled
在最后一節(jié)中,我不得不回到我最初的聲明 "幾乎所有的 Python 對(duì)象都可以被腌制"。我使用 "幾乎所有 "是因?yàn)槿匀挥幸恍╊愋偷膶?duì)象不能被Pickle序列化。
一個(gè)典型的不能被腌制的類型是實(shí)時(shí)連接對(duì)象,比如網(wǎng)絡(luò)或數(shù)據(jù)庫連接。這是有道理的,因?yàn)镻ickle在關(guān)閉后將無法建立連接。這些對(duì)象只能用適當(dāng)?shù)膽{證和其他資源重新創(chuàng)建。
另一個(gè)需要提到的類型將是一個(gè)模塊。一個(gè)重要的模塊也不能被腌制。請(qǐng)看下面的例子。
import datetime
with open('datetime.pickle', 'wb') as f:
pickle.dump(datetime, f)
這一點(diǎn)很重要,因?yàn)檫@意味著我們將不能在global()中腌制所有的東西,因?yàn)閷?dǎo)入的模塊會(huì)在里面。
總結(jié)
在這篇文章中,我介紹了Python中內(nèi)置的序列化方法 - Pickle。它可以用于快速而簡(jiǎn)單的序列化。它幾乎支持所有類型的Python對(duì)象,如函數(shù),甚至Pandas數(shù)據(jù)幀。當(dāng)對(duì)不同版本的Python使用Pickle時(shí),我們還需要記住,Pickle的版本也可能不同。