Python 例項詳解:銀行 ATM 等待時間分析
這是一道 Python 面向物件程式設計的例項,包含面向物件程式設計、Class 類相關知識,請讀者先自行掌握。本文分析已有程式碼,主要理解其中邏輯,學習程式設計方法。程式碼乍看之下稍微複雜,拆解之後,弄通邏輯,還是很清晰明瞭的。
前部分為程式碼分析,可以重點看後面的程式執行部分哈,配合圖文,便於理解。
這是筆者第一次寫例項分析,內容詳細,篇幅較長,僅用於學習與交流。若有疏漏,請多包涵,歡迎各位指點和討論啊~
【需求分析】
假設銀行有 1 臺 ATM 機,共 n 位客戶來銀行操作 ATM 機器,求客戶平均的排隊等候時間。
【邏輯梳理】
ATM 機操作時間由客人選擇的辦理業務決定,符合隨機分佈。
客戶流到達銀行時間不定,符合隨機分佈。
累加每位客戶排隊等候的時間,再除以總客戶數,即可得出客戶的平均等候時間。
【前提假設】
實際應用中,ATM 機和客戶流時間均為隨機數,在本例項需做出一定條件限制以方便計算。
ATM 機操作時間:1-5 分鐘
下位客戶到達時間:1-10 分鐘
時間以整數為單位。
【程式碼總覽】
class ATM():
def __init__(self, maxtime = 5):
self.t_max = maxtime
def getServCompleteTime(self, start = 0):
return start + random.randint(1, self.t_max)
class Customers():
def __init__(self, n):
self.count = n
self.left = n
def getNextArrvTime(self, start = 0, arrvtime = 10):
if self.left != 0:
self.left -= 1
return start + random.randint(1, arrvtime)
else:
return 0
def isOver(self):
return True if self.left == 0 else False
c = Customers(100)
a = ATM()
wait_list = []
wait_time = 0
cur_time = 0
cur_time += c.getNextArrvTime()
wait_list.append(cur_time)
while len(wait_list) != 0 or not c.isOver():
if wait_list[0] <= cur_time:
next_time = a.getServCompleteTime(cur_time)
del wait_list[0]
else:
next_time = cur_time + 1
if not c.isOver() and len(wait_list) == 0:
next_arrv = c.getNextArrvTime(cur_time)
wait_list.append(next_arrv)
if not c.isOver() and wait_list[-1] < next_time:
next_arrv = c.getNextArrvTime(wait_list[-1])
wait_list.append(next_arrv)
while next_arrv < next_time and not c.isOver():
next_arrv = c.getNextArrvTime(next_arrv)
wait_list.append(next_arrv)
for i in wait_list:
if i <= cur_time:
wait_time += next_time - cur_time
elif cur_time < i < next_time:
wait_time += next_time - i
else:
pass
cur_time = next_time
print(wait_time/c.count)
程式碼可以分為 7 個部分:
設定 ATM 機物件,設定客戶流物件,設定初始資料,主程式(判斷客戶出場條件,統計客戶排隊等候時間,更新當前時間),輸出最終結果
【模組拆解】
設定ATM機物件
class ATM():
def __init__(self, maxtime = 5):
self.t_max = maxtime
def getServCompleteTime(self, start = 0):
return start + random.randint(1, self.t_max)
用類方法設定 ATM 物件,其中限定最長操作時間 maxtime 為 5 分鐘。
定義例項方法 getServCompleteTime(),作用是使用整數隨機函 random.randint(),返回在 ATM 當次操作結束的時間,累加下次 ATM 操作時長,範圍在 [1, maxtime] 之間。
設定客戶流物件
class Customers():
def __init__(self, n):
self.count = n
self.left = n
def getNextArrvTime(self, start = 0, arrvtime = 10):
if self.left != 0:
self.left -= 1
return start + random.randint(1, arrvtime)
else:
return 0
def isOver(self):
return True if self.left == 0 else False
用類方法設定 Customers 物件,代表客戶流,設定客戶庫中總數 self.count 為 n,初始時剩餘客戶數 self.left 相同。
定義例項方法 getNextArrvTime() ,作用是根據客戶庫存情況,提取客戶至銀行,返回從 某個時間點 start,累加下位客戶到達銀行需要的時間(可以理解為客戶使用步行、騎行或開車的方式從庫存轉移到銀行…),範圍在[1, arrvtime]之間。
定義例項方法 isOver(),作用是判斷客戶庫存是否清零。
設定初始資料
c = Customers(100)
a = ATM()
wait_list = []
wait_time = 0
cur_time = 0
cur_time += c.getNextArrvTime()
wait_list.append(cur_time)
設定 c 表示客戶,a 表示 ATM,wait_list 是等待列表,wait_time 是客戶總排隊等候時間,cur_time 為當前時間,時間初始時皆為 0。
提取使用者觸發程式,用 getNextArrvTime() 提取第一位來銀行的客戶,返回客戶到達時間,更新 cur_time。第一位客戶到達銀行後,預設自動加入排隊列表,實際等候時間為 0。
主程式
while len(wait_list) != 0 or not c.isOver():
當排隊列表不為空,或者客戶庫存未清零時執行操作。
初始時排隊列表有 1 位客人,而且有庫存。
*為何不只用庫存未清零做判斷條件呢?
當全部客戶提取值排隊列表後,庫存為零,可是排隊列表還有客戶,若只用庫存判斷,則不執行主程式,無法累計列表剩餘客戶的等候時間。
判斷客戶出場條件
這一塊包括 3 個 if 語句,分別進行這些操作:
-
將排第一的客戶移除排隊列表去操作 ATM ;
-
當沒人排隊並且還有客戶庫存時提取客戶至列表;
-
當列表中排最後的客戶到達時間與預計 ATM 操作完畢(閒置)時間之間有時間空檔,而這時客戶庫存仍有客戶,則提取客戶排隊,直至排最後的客戶到達時間比預計 ATM 操作完畢時間遲。(這一步就是避免 ATM 機處於閒置狀態,一直有人排隊…)
if wait_list[0] <= cur_time:
next_time = a.getServCompleteTime(cur_time) del wait_list[0]
else:
next_time = cur_time + 1
如果排第一的客戶到達時間比當前時間早,或者等於當前時間,就將客戶移除列表去操作 ATM。呼叫 a.getServCompleteTime(),返回在 cur_time 上累加操作 ATM 時長,即 ATM 在 next_time 才閒置。
如果排第一的客戶在當前時間之後到達,那就是客戶還在路上,ATM 閒置,時間逐步 +1 推進程序,直至客戶抵達。
if not c.isOver() and len(wait_list) == 0:
next_arrv = c.getNextArrvTime(cur_time)
wait_list.append(next_arrv)
經過上一步 if 操作,排隊列表可能清零,這時如果還有客戶庫存,則將客戶提取至排隊列表。
if not c.isOver() and wait_list[-1] < next_time:
next_arrv = c.getNextArrvTime(wait_list[-1])
wait_list.append(next_arrv)
while next_arrv < next_time and not c.isOver():
next_arrv = c.getNextArrvTime(next_arrv)
wait_list.append(next_arrv)
當列表排最後的客戶到達時間比當次 ATM 操作完畢的時間早,而且還有客戶庫存時,提取客戶至佇列,直至排最後的客戶到達時間比當次 ATM 操作完畢的時間遲。這個做法是在還有客戶庫存時,讓排隊列表不為空,也避免了 ATM 閒置的情況。
*為什麼選擇當次ATM操作完畢的時間 next_time 為參照?
筆者思考是因為在真實情況中,在 ATM 被佔用而且有人排隊的情況下,路過的人看到可能會選別的時間再來辦事,從而節省等候時間。選用 next_time 參照,可能是模擬這種情況,並且推進整個程序。
統計客戶排隊等候時間
for i in wait_list:
if i <= cur_time:
wait_time += next_time - cur_time
elif cur_time < i < next_time:
wait_time += next_time - i
else:
pass
用遍歷迴圈統計排隊列表中每位客戶的等待時間。分段來統計客戶等候時間,後面會畫圖說明,更加直觀。
更新當前時間
cur_time = next_time #將 ATM 機器下次執行完畢時間 賦給 當前時間
將當次ATM操作完畢時間賦值給當前時間,下一位客戶由當前時間開始進行 ATM 操作。
輸出最終結果
print(wait_time/c.count) #當庫存清零,計算平均等待時間
庫存清零後,用總等候時間處以客戶總數得出平均等待時間。
【程式執行】
接下來模擬程式執行過程,預設客戶總數為 5 人,分別記為 A-E 並按順序出場。
可以用兩條平行的時間軸分別代表客戶流和 ATM 使用情況。
【1】
初始時 cur_time 為 0,通過呼叫 c.getNextArrvTime() 從庫存提取出第一位客戶 A,新增至排隊列表。
第 1 個 if 語句:由於 A 到達時間即是當前時間,也無其他客戶,此時 A 無需排隊,直接使用 ATM 機器。用 a.getServCompleteTime(cur_time) 獲取 A 操作完 ATM 的時間,即 next_time。將 A 移除佇列。
第 2 個 if 語句:當前排隊列表為空,從庫存提取客戶 B,用 c.getNextArrvTime(cur_time) 獲取 B 到達時間,即 next_arrv,並增加至排隊列表。
第 3 個 if 語句:根據判斷條件 wait_list[-1] < next_time,而佇列中 B 到達時間比當前 next_time 遲,故不執行操作。
for 迴圈:此時排隊列表只有客戶 B,且是在當前 next_time 之後達到,不滿足條件,故不執行操作。
更新當前時間,當前 next_time 即為 cur_time。
【2】
還有客戶庫存,繼續執行主程式。
第 1 個 if 語句:此時客戶 B 仍未到達,ATM 機器閒置,在當前時間基礎上 +1 分鐘,更新 next_time 以驅動程序。
第 2 個 if 語句:由於排隊列表不為空,不滿足條件,故不執行操作。
第 3 個 if 語句:判斷條件 wait_list[-1] < next_time,就算客戶 B 下一分鐘就到達,也只滿足 wait_list[-1] = next_time,不滿足條件,故不執行操作。
for 迴圈:此時排隊列表只有客戶 B,且是在當前 next_time 之後達到,不滿足條件,故不執行操作。
更新當前時間,當前 next_time 即為 cur_time。
此時重複執行至客戶 B 到達。上述情況如(圖 1)所示。
【3】
排隊列表不為空,且還有客戶庫存,繼續執行主程式。
第 1 個 if 語句:客戶 B 到達後無需等候,直接使用 ATM。獲取 B 操作完 ATM 的時間,即 next_time。將 B 移除佇列。
第 2 個 if 語句:排隊列表為空,提取庫存中客戶 C,獲取 C 到達時間 next_arrv,並加入排隊列表。
第 3 個 if 語句:排隊列表中最後一位客戶為 C,由圖可知 C 到達時間早於 next_time,滿足條件,執行操作。從庫存提取客戶 D,加入排隊列表。若客戶 D 到達時間仍早於 next_time,繼續提取客戶,直至最後一位客戶到達時間晚於 next_time 或者客戶庫存清零為止。情況如(圖2)所示。
本次預設總客戶數為 5 人,即再提取一位客戶 E 庫存清零。新增完畢後,情況如(圖 3)所示。
for 迴圈:排隊列表有 C、D、E 這 3 位 客戶。此時時間 cur_time 與 next_time 為綠色標識。
-
C 客戶到達時間滿足 elif 語句,設 C 等候時間 wait_time 為 C1,等於 next_time - C 到達時間。
-
D 客戶同理,設 D 等候時間 wait_time 為 D1,等於 next_time - D 到達時間。
-
而 E 客戶到達時間在 next_time 之後,不滿足條件,無等候時間。
更新當前時間,當前 next_time 即為 cur_time。
【4】
此時庫存已清零,但排隊列表不為空,仍然執行主程式。
第 1 個 if 語句:排第一的客戶是 C,獲取 C 操作完 ATM 的時間,即 next_time。將 C 移除佇列。
第 2 個 if 語句:此時庫存清零,無法新增客戶,故不執行操作。
第 3 個 if 語句:此時庫存清零,無法新增客戶,故不執行操作。
for 迴圈:此時時間 cur_time 與 next_time 為橙色標識。
-
D 客戶到達時間滿足 if 語句,設 D 等候時間 wait_time 為 D2,等於 next_time - cur_time。
-
E 客戶到達時間滿足 elif 語句,設 E 等候時間 wait_time 為 E1,等於 next_time - E 到達時間。
更新當前時間,當前 next_time 即為 cur_time。
【5】
此時庫存已清零,但排隊列表不為空,仍然執行主程式。
第 1 個 if 語句:排第一的客戶是 D,獲取 D 操作完 ATM 的時間,即 next_time。將 D 移除佇列。
第 2 個 if 語句:此時庫存清零,無法新增客戶,故不執行操作。
第 3 個 if 語句:此時庫存清零,無法新增客戶,故不執行操作。
for 迴圈:此時時間 cur_time 與 next_time 為玫紅標識。
E 客戶到達時間滿足 if 語句,設 E 等候時間 wait_time 為 E2,等於 next_time - cur_time。
更新當前時間,當前 next_time 即為 cur_time。
【6】
此時庫存已清零,但排隊列表仍有客戶 E,仍然執行主程式。
第 1 個 if 語句:剩餘客戶 E,獲取 E 操作完 ATM 的時間,即 next_time。將 E 移除佇列。
第 2 個 if 語句:此時庫存清零,無法新增客戶,故不執行操作。
第 3 個 if 語句:此時庫存清零,無法新增客戶,故不執行操作。
for 迴圈:排隊列表清零,故不執行操作。
更新當前時間,當前 next_time 即為 cur_time。
【7】
此時庫存已清零,排隊列表已清零,故不執行主程式。
跳到輸出語句 print,此時如(圖4)所示,總等候時間 wait_time = C1 + D1 + D2 + E1 + E2,總客戶數為 5,由 wait_time/c.count 即可算出平均等候時間。
程式結束。
【程式結果】
ATM: maxtime = 5
Customers: n = 100, arrvtime = 10
執行結果:
0.43
0.44
0.91
…
ATM: maxtime = 10
Customers: n = 100, arrvtime = 10
執行結果:
62.16
36.1
33.93
…
可以發現當ATM操作時間時長範圍擴大後,客戶平均等候時間更久。可以拷貝程式碼,修改引數執行,看看還能總結出什麼規律呢?