1. 程式人生 > >Python 例項詳解:銀行 ATM 等待時間分析

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 語句,分別進行這些操作:

  1. 將排第一的客戶移除排隊列表去操作 ATM ;

  2. 當沒人排隊並且還有客戶庫存時提取客戶至列表;

  3. 當列表中排最後的客戶到達時間與預計 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)

【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)所示。

在這裡插入圖片描述

(圖 2)

【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)所示。

在這裡插入圖片描述

(圖 3)

本次預設總客戶數為 5 人,即再提取一位客戶 E 庫存清零。新增完畢後,情況如(圖 3)所示。

for 迴圈:排隊列表有 C、D、E 這 3 位 客戶。此時時間 cur_time 與 next_time 為綠色標識。

  1. C 客戶到達時間滿足 elif 語句,設 C 等候時間 wait_time 為 C1,等於 next_time - C 到達時間。

  2. D 客戶同理,設 D 等候時間 wait_time 為 D1,等於 next_time - D 到達時間。

  3. 而 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 為橙色標識。

  1. D 客戶到達時間滿足 if 語句,設 D 等候時間 wait_time 為 D2,等於 next_time - cur_time。

  2. E 客戶到達時間滿足 elif 語句,設 E 等候時間 wait_time 為 E1,等於 next_time - E 到達時間。

更新當前時間,當前 next_time 即為 cur_time。

在這裡插入圖片描述

(圖 4)

【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操作時間時長範圍擴大後,客戶平均等候時間更久。可以拷貝程式碼,修改引數執行,看看還能總結出什麼規律呢?