1. 程式人生 > >第044講:魔法方法:簡單定製

第044講:魔法方法:簡單定製

目錄

0. 請寫下這一節課你學習到的內容:格式不限,回憶並複述是加強記憶的好方式!

動動手

0. 按照課堂中的程式,如果開始計時的時間是(2022年2月22日16:30:30),停止時間是(2025年1月23日15:30:30),那按照我們用停止時間減開始時間的計算方式就會出現負數,你應該對此做一些轉換。

題目:改進我們課堂中的例子,這次使用 perf_counter() 和 process_time() 作為計時器。另外增加一個 set_timer() 方法,用於設定預設計時器(預設是 perf_counter(),可以通過此方法修改為 process_time())。

2. 既然咱都做到了這一步,那不如再深入一下。再次改進我們的程式碼,讓它能夠統計一個函式執行若干次的時間。


0. 請寫下這一節課你學習到的內容:格式不限,回憶並複述是加強記憶的好方式!

這節課我們一起來完成一個類的定製,

基本要求:

  • 定製一個計時器的類
  • start和stop方法代表啟動計時和停止計時
  • 假設計時器物件 t1,print(t1) 和直接呼叫 t1 均顯示結果
  • 當計時器未啟動或已經停止計時,呼叫stop方法會給予溫馨的提示
  • 兩個計時器物件可以進行相加:t1 + t2
  • 只能使用提供的有限資源完成

顯然,你需要這些資源:

  • 使用time模組的localtime方法獲取時間

Python擴充套件閱讀:time 模組詳解(時間獲取和轉換)

  • time.localtime 返回 struct_time 的時間格式

你可以用索引值直接去索引它。

  • 因為直接呼叫物件就要顯示結果,所以需要表現你的類:__str__ 和 __repr__,具體用法見:Python魔法方法詳解-基本的魔法方法

例如:

>>> class A:
	def __str__(self):
		return("你好,來自江南的你!")

	
>>> a = A()
>>> print(a)
你好,來自江南的你!
>>> a
<__main__.A object at 0x000002D0B1F65390>

__str__ 用在被列印的時候需要以字串的形式輸出的時候,就會找到 __str__魔法方法,然後把返回的值打印出來。

下面是__repr__的示例:

>>> class B:
	def __repr__(self):
		return "你好,來自江南的你!"

	
>>> b = B()
>>> b
你好,來自江南的你!

下面程式碼走起:

import time as t

class MyTimer:
    def __init__(self):
        self.unit = ['年', '月', '天', '小時', '分鐘', '秒']
        self.prompt = "未開始計時!"
        self.lasted = []
        self.begin = 0
        self.end = 0
    
    def __str__(self):
        return self.prompt

    __repr__ = __str__

    def __add__(self, other):
        prompt = "總共運行了"
        result = []
        for index in range(6):
            result.append(self.lasted[index] + other.lasted[index])
            if result[index]:
                prompt += (str(result[index]) + self.unit[index])
        return prompt
    
    # 開始計時
    def start(self):
        self.begin = t.localtime()
        self.prompt = "提示:請先呼叫 stop() 停止計時!"
        print("計時開始...")

    # 停止計時
    def stop(self):
        if not self.begin:
            print("提示:請先呼叫 start() 進行計時!")
        else:
            self.end = t.localtime()
            self._calc()
            print("計時結束!")

    # 內部方法,計算執行時間
    def _calc(self):
        self.lasted = []
        self.prompt = "總共運行了"
        for index in range(6):
            self.lasted.append(self.end[index] - self.begin[index])
            if self.lasted[index]:
                self.prompt += (str(self.lasted[index]) + self.unit[index])
        # 為下一輪計時初始化變數
        self.begin = 0
        self.end = 0

進階定製

  • 如果開始計時的時間是(2022年2月22日16:30:30),停止時間是(2025年1月23日15:30:30),那按照我們用停止時間減開始時間的計算方式就會出現負數(3年-1月1天-1小時),你應該對此做一些轉換。
  • 現在的計算機速度都非常快,而我們這個程式最小的計算單位卻只是秒,精度是遠遠不夠的。(看下面的課後作業)

動動手

0. 按照課堂中的程式,如果開始計時的時間是(2022年2月22日16:30:30),停止時間是(2025年1月23日15:30:30),那按照我們用停止時間減開始時間的計算方式就會出現負數,你應該對此做一些轉換。

 

import time as t

class MyTimer:
    def __init__(self):
        self.unit = ['年', '月', '天', '小時', '分鐘', '秒']
        self.borrow = [0, 12, 31, 24, 60, 60]
        self.prompt = "未開始計時!"
        self.lasted = []
        self.begin = 0
        self.end = 0
    
    def __str__(self):
        return self.prompt

    __repr__ = __str__

    def __add__(self, other):
        prompt = "總共運行了"
        result = []
        for index in range(6):
            result.append(self.lasted[index] + other.lasted[index])
            if result[index]:
                prompt += (str(result[index]) + self.unit[index])
        return prompt
    
    # 開始計時
    def start(self):
        self.begin = t.localtime()
        self.prompt = "提示:請先呼叫 stop() 停止計時!"
        print("計時開始...")

    # 停止計時
    def stop(self):
        if not self.begin:
            print("提示:請先呼叫 start() 進行計時!")
        else:
            self.end = t.localtime()
            self._calc()
            print("計時結束!")

    # 內部方法,計算執行時間
    def _calc(self):
        self.lasted = []
        self.prompt = "總共運行了"
        for index in range(6):
            temp = self.end[index] - self.begin[index]

            # 低位不夠減,需向高位借位 
            if temp < 0:
                # 測試高位是否有得“借”,沒得借的話向再高位借......
                i = 1
                while self.lasted[index-i] < 1:
                    self.lasted[index-i] += self.borrow[index-i] - 1
                    self.lasted[index-i-1] -= 1
                    i += 1
                
                self.lasted.append(self.borrow[index] + temp)
                self.lasted[index-1] -= 1
            else:
                self.lasted.append(temp)

        # 由於高位隨時會被借位,所以列印要放在最後
        for index in range(6):
            if self.lasted[index]:
                self.prompt += str(self.lasted[index]) + self.unit[index]
        
        # 為下一輪計時初始化變數
        self.begin = 0
        self.end = 0

1. 相信大家已經意識到不對勁了:為毛一個月一定要31天?不知道有可能也是30天或者29天嗎?(上一題我們的答案是假設一個月31天)

沒錯,如果要正確得到月份的天數,我們還需要考慮是否閏年,還有每月的最大天數,所以太麻煩了……如果我們不及時糾正,我們會在錯誤的道路上越走越遠……

所以,這一次,小甲魚提出了更優秀的解決方案(Python官方推薦):用 time 模組的 perf_counter() 和 process_time() 來計算,其中 perf_counter() 返回計時器的精準時間(系統的執行時間); process_time() 返回當前程序執行 CPU 的時間總和。

題目:改進我們課堂中的例子,這次使用 perf_counter() 和 process_time() 作為計時器。另外增加一個 set_timer() 方法,用於設定預設計時器(預設是 perf_counter(),可以通過此方法修改為 process_time())。

import time as t

class MyTimer:
    def __init__(self):
        self.prompt = "未開始計時!"
        self.lasted = 0.0
        self.begin = 0
        self.end = 0
        self.default_timer = t.perf_counter
    
    def __str__(self):
        return self.prompt

    __repr__ = __str__

    def __add__(self, other):
        result = self.lasted + other.lasted
        prompt = "總共運行了 %0.2f 秒" % result
        return prompt
    
    # 開始計時
    def start(self):
        self.begin = self.default_timer()
        self.prompt = "提示:請先呼叫 stop() 停止計時!"
        print("計時開始...")

    # 停止計時
    def stop(self):
        if not self.begin:
            print("提示:請先呼叫 start() 進行計時!")
        else:
            self.end = self.default_timer()
            self._calc()
            print("計時結束!")

    # 內部方法,計算執行時間
    def _calc(self):
        self.lasted = self.end - self.begin
        self.prompt = "總共運行了 %0.2f 秒" % self.lasted
        
        # 為下一輪計時初始化變數
        self.begin = 0
        self.end = 0

    # 設定計時器(time.perf_counter() 或 time.process_time())
    def set_timer(self, timer):
        if timer == 'process_time':
            self.default_timer = t.process_time
        elif timer == 'perf_counter':
            self.default_timer = t.perf_counter
        else:
            print("輸入無效,請輸入 perf_counter 或 process_time")

2. 既然咱都做到了這一步,那不如再深入一下。再次改進我們的程式碼,讓它能夠統計一個函式執行若干次的時間。

要求一:函式呼叫的次數可以設定(預設是 1000000 次)
要求二:新增一個 timing() 方法,用於啟動計時器
 

函式演示:

>>> ================================ RESTART ================================
>>> 
>>> def test():
        text = "I love FishC.com!"
        char = 'o'
        if char in text:
                pass

        
>>> t1 = MyTimer(test)
>>> t1.timing()
>>> t1
總共運行了 0.27 秒
>>> t2 = MyTimer(test, 100000000)
>>> t2.timing()
>>> t2
總共運行了 25.92 秒
>>> t1 + t2
'總共運行了 26.19 秒'

程式碼清單:

import time as t

class MyTimer:
    def __init__(self, func, number=1000000):
        self.prompt = "未開始計時!"
        self.lasted = 0.0
        self.default_timer = t.perf_counter
        self.func = func
        self.number = number
    
    def __str__(self):
        return self.prompt

    __repr__ = __str__

    def __add__(self, other):
        result = self.lasted + other.lasted
        prompt = "總共運行了 %0.2f 秒" % result
        return prompt

    # 內部方法,計算執行時間
    def timing(self):
        self.begin = self.default_timer()
        for i in range(self.number):
            self.func()
        self.end = self.default_timer()
        self.lasted = self.end - self.begin
        self.prompt = "總共運行了 %0.2f 秒" % self.lasted
        
    # 設定計時器(time.perf_counter() 或 time.process_time())
    def set_timer(self, timer):
        if timer == 'process_time':
            self.default_timer = t.process_time
        elif timer == 'perf_counter':
            self.default_timer = t.perf_counter
        else:
            print("輸入無效,請輸入 perf_counter 或 process_time")

學習完這一節課後,我想告訴大家一件事,其實,關於 Python 程式碼優化你需要知道的最重要問題是,決不要自己編寫計時函式!!!!!

為一個很短的程式碼計時都很複雜,因為你不知道處理器有多少時間用於執行這個程式碼?有什麼在後臺執行?小小的疏忽可能破壞你的百年大計,後臺服務偶爾被 “喚醒” 在最後千分之一秒做一些像查收信件,連線計時通訊伺服器,檢查應用程式更新,掃描病毒,檢視是否有磁碟被插入光碟機之類很有意義的事。在開始計時測試之前,把一切都關掉,斷開網路的連線。再次確定一切都關上後關掉那些不斷檢視網路是否恢復的服務等等。

接下來是計時框架本身引入的變化因素。Python 直譯器是否快取了方法名的查詢?是否快取程式碼塊的編譯結果?正則表示式呢? 你的程式碼重複執行時有副作用嗎?不要忘記,你的工作結果將以比秒更小的單位呈現,你的計時框架中的小錯誤將會帶來不可挽回的結果扭曲。

Python 社群有句俗語:“Python 自己帶著電池。” 別自己寫計時框架。Python 具備一個叫做 timeit 的完美計時工具。

或許你現在怨恨我為什麼不早點說,但如果你在這節課的折騰中已經掌握了類的定製和使用,那我的目的就達到了。接下來,請認真閱讀更為專業的計時器用法及實現原始碼:Python擴充套件閱讀:timeit 模組詳解(準確測量小段程式碼的執行時間),