Python線程池的實現(xiàn)淺析
雷猴啊,兄弟們!今天來展示一下如何用Python快速實現(xiàn)一個線程池。
一、序言
當(dāng)有多個 IO 密集型的任務(wù)要被處理時,我們自然而然會想到多線程。但如果任務(wù)非常多,我們不可能每一個任務(wù)都啟動一個線程去處理,這個時候最好的辦法就是實現(xiàn)一個線程池,至于池子里面的線程數(shù)量可以根據(jù)業(yè)務(wù)場景進(jìn)行設(shè)置。
比如我們實現(xiàn)一個有 10 個線程的線程池,這樣可以并發(fā)地處理 10 個任務(wù),每個線程將任務(wù)執(zhí)行完之后,便去執(zhí)行下一個任務(wù)。通過使用線程池,可以避免因線程創(chuàng)建過多而導(dǎo)致資源耗盡,而且任務(wù)在執(zhí)行時的生命周期也可以很好地把控。
而線程池的實現(xiàn)方式也很簡單,但這里我們不打算手動實現(xiàn),因為 Python 提供了一個標(biāo)準(zhǔn)庫 concurrent.futures,已經(jīng)內(nèi)置了對線程池的支持。所以本篇文章,我們就來詳細(xì)介紹一下該模塊的用法。
二、正文
1、Future 對象
當(dāng)我們往線程池里面提交一個函數(shù)時,會分配一個線程去執(zhí)行,同時立即返回一個 Future 對象。通過 Future 對象可以監(jiān)控函數(shù)的執(zhí)行狀態(tài),有沒有出現(xiàn)異常,以及有沒有執(zhí)行完畢等等。如果函數(shù)執(zhí)行完畢,內(nèi)部便會調(diào)用 future.set_result 將返回值設(shè)置到 future 里面,然后外界便可調(diào)用 future.result 拿到返回值。
除此之外 future 還可以綁定回調(diào),一旦函數(shù)執(zhí)行完畢,就會以 future 為參數(shù),自動觸發(fā)回調(diào)。所以 future 被稱為未來對象,可以把它理解為函數(shù)的一個容器,當(dāng)我們往線程池提交一個函數(shù)時,會立即創(chuàng)建相應(yīng)的 future 然后返回。函數(shù)的執(zhí)行狀態(tài)什么的,都通過 future 來查看,當(dāng)然也可以給它綁定一個回調(diào),在函數(shù)執(zhí)行完畢時自動觸發(fā)。
那么下面我們就來看一下 future 的用法,文字的話理解起來可能有點枯燥。
將函數(shù)提交到線程池里面運行時,會立即返回一個對象
這個對象就叫做 Future 對象,里面包含了函數(shù)的執(zhí)行狀態(tài)等等
當(dāng)然我們也可以手動創(chuàng)建一個Future對象。
from concurrent.futures import Future # 創(chuàng)建 Future 對象 future future = Future() # 給 future 綁定回調(diào) def callback(f: Future): print("當(dāng)set_result的時候會執(zhí)行回調(diào),result:", f.result()) future.add_done_callback(callback) # 通過 add_done_callback 方法即可給 future 綁定回調(diào) # 調(diào)用的時候會自動將 future 作為參數(shù) # 如果需要多個參數(shù),那么就使用偏函數(shù) # 回調(diào)函數(shù)什么時候執(zhí)行呢? # 顯然是當(dāng) future 執(zhí)行 set_result 的時候 # 如果 future 是向線程池提交函數(shù)時返回的 # 那么當(dāng)函數(shù)執(zhí)行完畢時會自動執(zhí)行 future.set_result(xx) # 并將自身的返回設(shè)置進(jìn)去 # 而這里的 future 是我們手動創(chuàng)建的,因此需要手動執(zhí)行 future.set_result("嘿嘿")
當(dāng)set_result的時候會執(zhí)行回調(diào),result: 嘿嘿
需要注意的是:只能執(zhí)行一次 set_result,但是可以多次調(diào)用 result 獲取結(jié)果。
from concurrent.futures import Future future = Future() future.set_result("哼哼") print(future.result()) # 哼哼 print(future.result()) # 哼哼 print(future.result()) # 哼哼
執(zhí)行 future.result() 之前一定要先 set_result,否則會一直處于阻塞狀態(tài)。當(dāng)然 result 方法還可以接收一個 timeout 參數(shù),表示超時時間,如果在指定時間內(nèi)沒有獲取到值就會拋出異常。
2、提交函數(shù)自動創(chuàng)建 Future 對象
我們上面是手動創(chuàng)建的 Future 對象,但工作中很少會手動創(chuàng)建。我們將函數(shù)提交到線程池里面運行的時候,會自動創(chuàng)建 Future 對象并返回。這個 Future 對象里面就包含了函數(shù)的執(zhí)行狀態(tài),比如此時是處于暫停、運行中還是完成等等,并且函數(shù)在執(zhí)行完畢之后,還會調(diào)用 future.set_result 將自身的返回值設(shè)置進(jìn)去。
from concurrent.futures import ThreadPoolExecutor import time def task(name, n): time.sleep(n) return f"{name} 睡了 {n} 秒" # 創(chuàng)建一個線程池 # 里面還可以指定 max_workers 參數(shù),表示最多創(chuàng)建多少個線程 # Python學(xué)習(xí)交流裙279199867 # 如果不指定,那么每提交一個函數(shù),都會為其創(chuàng)建一個線程 executor = ThreadPoolExecutor() # 通過 submit 即可將函數(shù)提交到線程池,一旦提交,就會立刻運行 # 因為開啟了一個新的線程,主線程會繼續(xù)往下執(zhí)行 # 至于 submit 的參數(shù),按照函數(shù)名,對應(yīng)參數(shù)提交即可 # 切記不可寫成task("古明地覺", 3),這樣就變成調(diào)用了 future = executor.submit(task, "屏幕前的你", 3) # 由于函數(shù)里面出現(xiàn)了 time.sleep,并且指定的 n 是 3 # 所以函數(shù)內(nèi)部會休眠 3 秒,顯然此時處于運行狀態(tài) print(future) """ <Future at 0x7fbf701726d0 state=running> """ # 我們說 future 相當(dāng)于一個容器,包含了內(nèi)部函數(shù)的執(zhí)行狀態(tài) # 函數(shù)是否正在運行中 print(future.running()) """ True """ # 函數(shù)是否執(zhí)行完畢 print(future.done()) """ False """ # 主程序也 sleep 3 秒 time.sleep(3) # 顯然此時函數(shù)已經(jīng)執(zhí)行完畢了 # 并且打印結(jié)果還告訴我們返回值類型是 str print(future) """ <Future at 0x7fbf701726d0 state=finished returned str> """ print(future.running()) """ False """ print(future.done()) """ True """ # 函數(shù)執(zhí)行完畢時,會將返回值設(shè)置在 future 里 # 也就是說一旦執(zhí)行了 future.set_result # 那么就表示函數(shù)執(zhí)行完畢了,然后外界可以調(diào)用 result 拿到返回值 print(future.result()) """ 屏幕前的你 睡了 3 秒 """
這里再強(qiáng)調(diào)一下 future.result(),這一步是會阻塞的,舉個例子:
# 提交函數(shù) future = executor.submit(task, "屏幕前的你", 3) start = time.perf_counter() future.result() end = time.perf_counter() print(end - start) # 3.00331525
可以看到,future.result() 這一步花了將近 3s。其實也不難理解,future.result() 是干嘛的?就是為了獲取函數(shù)的返回值,可函數(shù)都還沒有執(zhí)行完畢,它又從哪里獲取呢?所以只能先等待函數(shù)執(zhí)行完畢,將返回值通過 set_result 設(shè)置到 future 里面之后,外界才能調(diào)用 future.result() 獲取到值。
如果不想一直等待的話,那么在獲取值的時候可以傳入一個超時時間。
from concurrent.futures import ( ThreadPoolExecutor, TimeoutError ) import time def task(name, n): time.sleep(n) return f"{name} 睡了 {n} 秒" executor = ThreadPoolExecutor() future = executor.submit(task, "屏幕前的你", 3) try: # 1 秒之內(nèi)獲取不到值,拋出 TimeoutError res = future.result(1) except TimeoutError: pass # 再 sleep 2 秒,顯然函數(shù)執(zhí)行完畢了 time.sleep(2) # 獲取返回值 print(future.result()) """ 屏幕前的你 睡了 3 秒 """
當(dāng)然啦,這么做其實還不夠智能,因為我們不知道函數(shù)什么時候執(zhí)行完畢。所以最好的辦法還是綁定一個回調(diào),當(dāng)函數(shù)執(zhí)行完畢時,自動觸發(fā)回調(diào)。
from concurrent.futures import ThreadPoolExecutor import time def task(name, n): time.sleep(n) return f"{name} 睡了 {n} 秒" def callback(f): print(f.result()) executor = ThreadPoolExecutor() future = executor.submit(task, "屏幕前的你", 3) # 綁定回調(diào),3 秒之后自動調(diào)用 future.add_done_callback(callback) """ 屏幕前的你 睡了 3 秒 """
需要注意的是,在調(diào)用 submit 方法之后,提交到線程池的函數(shù)就已經(jīng)開始執(zhí)行了。而不管函數(shù)有沒有執(zhí)行完畢,我們都可以給對應(yīng)的 future 綁定回調(diào)。
如果函數(shù)完成之前添加回調(diào),那么會在函數(shù)完成后觸發(fā)回調(diào)。如果函數(shù)完成之后添加回調(diào),由于函數(shù)已經(jīng)完成,代表此時的 future 已經(jīng)有值了,或者說已經(jīng) set_result 了,那么會立即觸發(fā)回調(diào)。
3、future.set_result 到底干了什么事情
當(dāng)函數(shù)執(zhí)行完畢之后,會執(zhí)行 set_result,那么這個方法到底干了什么事情呢?
我們看到 future 有兩個被保護(hù)的屬性,分別是 _result 和 _state。顯然 _result 用于保存函數(shù)的返回值,而 future.result() 本質(zhì)上也是返回 _result 屬性的值。而 _state 屬性則用于表示函數(shù)的執(zhí)行狀態(tài),初始為 PENDING,執(zhí)行中為 RUNING,執(zhí)行完畢時被設(shè)置為 FINISHED。
調(diào)用 future.result() 的時候,會判斷 _state 的屬性,如果還在執(zhí)行中就一直等待。當(dāng) _state 為 FINISHED 的時候,就返回 _result 屬性的值。
4、提交多個函數(shù)
我們上面每次只提交了一個函數(shù),但其實可以提交任意多個,我們來看一下:
from concurrent.futures import ThreadPoolExecutor import time def task(name, n): time.sleep(n) return f"{name} 睡了 {n} 秒" executor = ThreadPoolExecutor() futures = [executor.submit(task, "屏幕前的你", 3), executor.submit(task, "屏幕前的你", 4), executor.submit(task, "屏幕前的你", 1)] # 此時都處于running print(futures) """ [<Future at 0x1b5ff622550 state=running>, <Future at 0x1b5ff63ca60 state=running>, <Future at 0x1b5ff63cdf0 state=running>] """ time.sleep(3) # 主程序 sleep 3s 后 # futures[0]和futures[2]處于 finished # futures[1]仍處于 running print(futures) """ [<Future at 0x1b5ff622550 state=running>, <Future at 0x1b5ff63ca60 state=running>, <Future at 0x1b5ff63cdf0 state=finished returned str>] """
如果是多個函數(shù),要如何拿到返回值呢?很簡單,遍歷 futures 即可。
executor = ThreadPoolExecutor() futures = [executor.submit(task, "屏幕前的你", 5), executor.submit(task, "屏幕前的你", 2), executor.submit(task, "屏幕前的你", 4), executor.submit(task, "屏幕前的你", 3), executor.submit(task, "屏幕前的你", 6)] for future in futures: print(future.result()) """ 屏幕前的你 睡了 5 秒 屏幕前的你 睡了 2 秒 屏幕前的你 睡了 4 秒 屏幕前的你 睡了 3 秒 屏幕前的你 睡了 6 秒 """
這里面有一些值得說一說的地方,首先 futures 里面有 5 個 future,記做 future1, future2, future3, future4, future5。
當(dāng)使用 for 循環(huán)遍歷的時候,實際上會依次遍歷這 5 個 future,所以返回值的順序就是我們添加的函數(shù)的順序。由于 future1 對應(yīng)的函數(shù)休眠了 5s,那么必須等到 5s 后,future1 里面才會有值。
但這五個函數(shù)是并發(fā)執(zhí)行的,future2, future3, future4 由于只休眠了 2s, 4s, 3s,所以肯定會先執(zhí)行完畢,然后執(zhí)行 set_result,將返回值設(shè)置到對應(yīng)的 future 里。
但 Python 的 for 循環(huán)不可能在第一次迭代還沒有結(jié)束,就去執(zhí)行第二次迭代。因為 futures 里面的幾個 future 的順序已經(jīng)一開始就被定好了,只有當(dāng)?shù)谝粋€ future.result() 執(zhí)行完成之后,才會執(zhí)行第二個 future.result(),以及第三個、第四個。
因此即便后面的函數(shù)已經(jīng)執(zhí)行完畢,但由于 for 循環(huán)的順序,也只能等著,直到前面的 future.result() 執(zhí)行完畢。所以當(dāng)?shù)谝粋€ future.result() 結(jié)束時,后面三個 future.result() 會立刻輸出,因為它們內(nèi)部的函數(shù)已經(jīng)執(zhí)行結(jié)束了。
而最后一個 future,由于內(nèi)部函數(shù) sleep 了 6 秒,因此要再等待 1 秒,才會打印 future.result()。
5、使用 map 來提交多個函數(shù)
使用 submit 提交函數(shù)會返回一個 future,并且還可以給 future 綁定一個回調(diào)。但如果不關(guān)心回調(diào)的話,那么還可以使用 map 進(jìn)行提交。
executor = ThreadPoolExecutor() # map 內(nèi)部也是使用了 submit results = executor.map(task, ["屏幕前的你"] * 3, [3, 1, 2]) # 并且返回的是迭代器 print(results) """ <generator object ... at 0x0000022D78EFA970> """ # 此時遍歷得到的是不再是 future # 而是 future.result() for result in results: print(result) """ 屏幕前的你 睡了 3 秒 屏幕前的你 睡了 1 秒 屏幕前的你 睡了 2 秒 """
可以看到,當(dāng)使用for循環(huán)的時候,map 執(zhí)行的邏輯和 submit 是一樣的。唯一的區(qū)別是,此時不需要再調(diào)用 result 了,因為返回的就是函數(shù)的返回值。
或者我們直接調(diào)用 list 也行。
executor = ThreadPoolExecutor() results = executor.map(task, ["屏幕前的你"] * 3, [3, 1, 2]) print(list(results)) """ ['屏幕前的你 睡了 3 秒', '屏幕前的你 睡了 1 秒', '屏幕前的你 睡了 2 秒'] """
results 是一個生成器,調(diào)用 list 的時候會將里面的值全部產(chǎn)出。由于 map 內(nèi)部還是使用的 submit,然后通過 future.result() 拿到返回值,而耗時最長的函數(shù)需要 3 秒,因此這一步會阻塞 3 秒。3 秒過后,會打印所有函數(shù)的返回值。
6、按照順序等待執(zhí)行
上面在獲取返回值的時候,是按照函數(shù)的提交順序獲取的。如果我希望哪個函數(shù)先執(zhí)行完畢,就先獲取哪個函數(shù)的返回值,該怎么做呢?
from concurrent.futures import ( ThreadPoolExecutor, as_completed ) import time def task(name, n): time.sleep(n) return f"{name} 睡了 {n} 秒" executor = ThreadPoolExecutor() futures = [executor.submit(task, "屏幕前的你", 5), executor.submit(task, "屏幕前的你", 2), executor.submit(task, "屏幕前的你", 1), executor.submit(task, "屏幕前的你", 3), executor.submit(task, "屏幕前的你", 4)] for future in as_completed(futures): print(future.result()) """ 屏幕前的你 睡了 1 秒 屏幕前的你 睡了 2 秒 屏幕前的你 睡了 3 秒 屏幕前的你 睡了 4 秒 屏幕前的你 睡了 5 秒 """
此時誰先完成,誰先返回。
7、取消一個函數(shù)的執(zhí)行
我們通過 submit 可以將函數(shù)提交到線程池中執(zhí)行,但如果我們想取消該怎么辦呢?
executor = ThreadPoolExecutor() future1 = executor.submit(task, "屏幕前的你", 1) future2 = executor.submit(task, "屏幕前的你", 2) future3 = executor.submit(task, "屏幕前的你", 3) # 取消函數(shù)的執(zhí)行 # 會將 future 的 _state 屬性設(shè)置為 CANCELLED future3.cancel() # 查看是否被取消 print(future3.cancelled()) # False
問題來了,調(diào)用 cancelled 方法的時候,返回的是False,這是為什么?很簡單,因為函數(shù)已經(jīng)被提交到線程池里面了,函數(shù)已經(jīng)運行了。而只有在還沒有運行時,取消才會成功。
可這不矛盾了嗎?函數(shù)一旦提交就會運行,只有不運行才會取消成功,這怎么辦?還記得線程池的一個叫做 max_workers 的參數(shù)嗎?用來控制線程池內(nèi)的線程數(shù)量,我們可以將最大的線程數(shù)設(shè)置為2,那么當(dāng)?shù)谌齻€函數(shù)進(jìn)去的時候,就不會執(zhí)行了,而是處于暫停狀態(tài)。
executor = ThreadPoolExecutor(max_workers=2) future1 = executor.submit(task, "屏幕前的你", 1) future2 = executor.submit(task, "屏幕前的你", 2) future3 = executor.submit(task, "屏幕前的你", 3) # 如果池子里可以創(chuàng)建空閑線程 # 那么函數(shù)一旦提交就會運行,狀態(tài)為 RUNNING print(future1._state) # RUNNING print(future2._state) # RUNNING # 但 future3 內(nèi)部的函數(shù)還沒有運行 # 因為池子里無法創(chuàng)建新的空閑線程了,所以狀態(tài)為 PENDING print(future3._state) # PENDING # 取消函數(shù)的執(zhí)行,前提是函數(shù)沒有運行 # 會將 future 的 _state 屬性設(shè)置為 CANCELLED future3.cancel() # 查看是否被取消 print(future3.cancelled()) # True print(future3._state) # CANCELLED
在啟動線程池的時候,肯定是需要設(shè)置容量的,不然處理幾千個函數(shù)要開啟幾千個線程嗎。另外當(dāng)函數(shù)被取消了,就不可以再調(diào)用 future.result() 了,否則的話會拋出 CancelledError。
8、函數(shù)執(zhí)行時出現(xiàn)異常
我們前面的邏輯都是函數(shù)正常執(zhí)行的前提下,但天有不測風(fēng)云,如果函數(shù)執(zhí)行時出現(xiàn)異常了該怎么辦?
from concurrent.futures import ThreadPoolExecutor def task1(): 1 / 0 def task2(): pass executor = ThreadPoolExecutor(max_workers=2) future1 = executor.submit(task1) future2 = executor.submit(task2) print(future1) print(future2) """ <Future at 0x7fe3e00f9e50 state=finished raised ZeroDivisionError> <Future at 0x7fe3e00f9eb0 state=finished returned NoneType> """ # 結(jié)果顯示 task1 函數(shù)執(zhí)行出現(xiàn)異常了 # 那么這個異常要怎么獲取呢? print(future1.exception()) print(future1.exception().__class__) """ division by zero <class 'ZeroDivisionError'> """ # 如果執(zhí)行沒有出現(xiàn)異常,那么 exception 方法返回 None print(future2.exception()) # None # 注意:如果函數(shù)執(zhí)行出現(xiàn)異常了 # 那么調(diào)用 result 方法會將異常拋出來 future1.result() """ Traceback (most recent call last): File "...", line 4, in task1 1 / 0 ZeroDivisionError: division by zero """
出現(xiàn)異常時,調(diào)用 future.set_exception 將異常設(shè)置到 future 里面,而 future 有一個 _exception 屬性,專門保存設(shè)置的異常。當(dāng)調(diào)用 future.exception() 時,也會直接返回 _exception 屬性的值。
9、等待所有函數(shù)執(zhí)行完畢
假設(shè)我們往線程池提交了很多個函數(shù),如果希望提交的函數(shù)都執(zhí)行完畢之后,主程序才能往下執(zhí)行,該怎么辦呢?其實方案有很多:
第一種:
from concurrent.futures import ThreadPoolExecutor import time def task(n): time.sleep(n) return f"sleep {n}" executor = ThreadPoolExecutor() future1 = executor.submit(task, 5) future2 = executor.submit(task, 2) future3 = executor.submit(task, 4) # 這里是不會阻塞的 print("start") # 遍歷所有的 future,并調(diào)用其 result 方法 # 這樣就會等到所有的函數(shù)都執(zhí)行完畢之后才會往下走 for future in [future1, future2, future3]: print(future.result()) print("end") """ start sleep 5 sleep 2 sleep 4 end """
第二種:
from concurrent.futures import ( ThreadPoolExecutor, wait ) import time def task(n): time.sleep(n) return f"sleep {n}" executor = ThreadPoolExecutor() future1 = executor.submit(task, 5) future2 = executor.submit(task, 2) future3 = executor.submit(task, 4) # return_when 有三個可選參數(shù) # FIRST_COMPLETED:當(dāng)任意一個任務(wù)完成或者取消 # FIRST_EXCEPTION:當(dāng)任意一個任務(wù)出現(xiàn)異常 # 如果都沒出現(xiàn)異常等同于ALL_COMPLETED # ALL_COMPLETED:所有任務(wù)都完成,默認(rèn)是這個值 fs = wait([future1, future2, future3], return_when="ALL_COMPLETED") # 此時返回的fs是DoneAndNotDoneFutures類型的namedtuple # 里面有兩個值,一個是done,一個是not_done print(fs.done) """ {<Future at 0x1df1400 state=finished returned str>, <Future at 0x2f08e48 state=finished returned str>, <Future at 0x9f7bf60 state=finished returned str>} """ print(fs.not_done) """ set() """ for f in fs.done: print(f.result()) """ start sleep 5 sleep 2 sleep 4 end """
第三種:
# 使用上下文管理 with ThreadPoolExecutor() as executor: future1 = executor.submit(task, 5) future2 = executor.submit(task, 2) future3 = executor.submit(task, 4) # 所有函數(shù)執(zhí)行完畢(with語句結(jié)束)后才會往下執(zhí)行
第四種:
executor = ThreadPoolExecutor() future1 = executor.submit(task, 5) future2 = executor.submit(task, 2) future3 = executor.submit(task, 4) # 所有函數(shù)執(zhí)行結(jié)束后,才會往下執(zhí)行 executor.shutdown()
三、小結(jié)
如果我們需要啟動多線程來執(zhí)行函數(shù)的話,那么不妨使用線程池。每調(diào)用一個函數(shù)就從池子里面取出一個線程,函數(shù)執(zhí)行完畢就將線程放回到池子里以便其它函數(shù)執(zhí)行。如果池子里面空了,或者說無法創(chuàng)建新的空閑線程,那么接下來的函數(shù)就只能處于等待狀態(tài)了。
最后,concurrent.futures 不僅可以用于實現(xiàn)線程池,還可以用于實現(xiàn)進(jìn)程池。兩者的 API 是一樣的:
from concurrent.futures import ProcessPoolExecutor import time def task(n): time.sleep(n) return f"sleep {n}" executor = ProcessPoolExecutor() # Windows 上需要加上這一行 if __name__ == '__main__': future1 = executor.submit(task, 5) future2 = executor.submit(task, 2) future3 = executor.submit(task, 4) executor.shutdown() print(future1.result()) print(future2.result()) print(future3.result()) """ sleep 5 sleep 2 sleep 4 """
線程池和進(jìn)程池的 API 是一致的,但工作中很少會創(chuàng)建進(jìn)程池。
到此這篇關(guān)于Python線程池的實現(xiàn)淺析的文章就介紹到這了,更多相關(guān)Python線程池內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python3中map(),reduce(),filter()的詳細(xì)用法
這篇文章主要介紹了Python3中map(),reduce(),filter()的詳細(xì)用法,Python3中的map()、reduce()、filter()?這3個一般是用于對序列進(jìn)行操作的內(nèi)置函數(shù),它們經(jīng)常需要與?匿名函數(shù)?lambda?聯(lián)合起來使用2022-08-08使用python tkinter開發(fā)一個爬取B站直播彈幕工具的實現(xiàn)代碼
這篇文章主要介紹了使用python tkinter開發(fā)一個爬取B站直播彈幕的工具,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-02-02python實現(xiàn)的DES加密算法和3DES加密算法實例
這篇文章主要介紹了python實現(xiàn)的DES加密算法和3DES加密算法,以實例形式較為詳細(xì)的分析了DES加密算法和3DES加密算法的原理與實現(xiàn)技巧,需要的朋友可以參考下2015-06-06python3結(jié)合openpyxl庫實現(xiàn)excel操作的實例代碼
這篇文章主要介紹了python3結(jié)合openpyxl庫實現(xiàn)excel操作的實例代碼,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2018-09-09python數(shù)據(jù)庫開發(fā)之MongoDB安裝及Python3操作MongoDB數(shù)據(jù)庫詳細(xì)方法與實例
這篇文章主要介紹了python數(shù)據(jù)庫開發(fā)之MongoDB安裝及Python3操作MongoDB數(shù)據(jù)庫詳細(xì)方法與實例,需要的朋友可以參考下2020-03-03