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

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

課題筆記:

這節課完成一個類的定製:

效果:

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

>>> class A():
    def __str__(self):
        return "hale"

    
>>> a = A()
>>> print(a)
hale
>>> a
<__main__.A object at 0x00000281B66C33A0>
>>> class B():
    def __repr__
(self): return "Hale" >>> b = B() >>> b Hale >>>

所以重寫這2個魔法方法就可以做到這些要的功能。

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

先試寫了,使用pycharm需要設定斷點? 但是不知道在哪裡設定,如何設定。

import time as t #首先我們需要time的模組,就先匯入

class MyTimer():
    #開始計時
    def start(self):
        self.star = t.localtime()
        
print('計時開始...') #停止計時 def stop(self): self.stop = t.localtime() self._calc() print('計時結束!') #到此地基寫好了,考慮下怎麼計算,直接相減? localtime返回的是一個元祖的結構 #內部方法,計算執行時間。 def _calc(self): self.lasted = [] self.prompt = '總共運行了' #這是提示 for index in range(6):#
要用前面的6個元素,就是localtime返回的元組結構 self.lasted.append(self.stop[index] - self.start[index]) self.prompt += str(self.lasted[index]) print(self.prompt)

找到錯誤了,是def start的裡面少了個t

改正:

 1 import time as t #首先我們需要time的模組,就先匯入
 2 
 3 class MyTimer():
 4     #開始計時
 5     def start(self):
 6         self.start = t.localtime()
 7         print('計時開始...')
 8 
 9     #停止計時
10     def stop(self):
11         self.stop = t.localtime()
12         self._calc()
13         print('計時結束!')
14 
15     #到此地基寫好了,考慮下怎麼計算,直接相減? localtime返回的是一個元祖的結構
16 
17     #內部方法,計算執行時間。
18     def _calc(self):
19         self.lasted = []
20         self.prompt = '總共運行了' #這是提示
21         for index in range(6):#要用前面的6個元素,就是localtime返回的元組結構
22             self.lasted.append(self.stop[index] - self.start[index])
23             self.prompt += str(self.lasted[index])
24 
25         print(self.prompt)
實驗版本

接下來就是加功能,print(t1) 還有直接呼叫t1它會顯示結果,需要重寫str和repr這2個魔法方法來實現:

如果這裡不按常理出牌,:

>>> t1 = MyTimer()
>>> t1
Traceback (most recent call last):
  File "<pyshell#45>", line 1, in <module>
    t1
  File "C:\Users\1\AppData\Local\Programs\Python\Python38\lib\idlelib\rpc.py", line 620, in displayhook
    text = repr(value)
  File "D:/python練習/MyTimer.py", line 5, in __str__
    return self.prompt
AttributeError: 'MyTimer' object has no attribute 'prompt'

當我們直接呼叫t1的時候,Python會直接找到這個str的魔法方法,找到repr這個魔法方法,而repr又等於str,所以會找到str方法。str魔法方法裡面呼叫了self.prompt, 那它沒定義,它在哪裡定義呢?

在calc裡面的賦值那,賦值就是定義。那一來就讓他顯示出來肯定不行,要先通過start 在stop,然後才被賦值,才被定義,才能夠使用它。所以所有屬於示例物件的變數在init裡面先定義就不會出現問題了、。

測試:

t1 = MyTimer()
t1.start()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: 'int' object is not callable

又出錯了汗。。。。屬性名和方法名重複了!!!!

初始化時候,屬性覆蓋了方法!錯誤型別就是整形不能被呼叫。

改下:

import time as t #首先我們需要time的模組,就先匯入

class MyTimer():
    def __init__(self):
        self.prompt = '未開始計時'
        self.lasted = []
        self.begin = 0
        self.end = 0
    def __str__(self):
        return self.prompt
    __repr__ = __str__
    #開始計時
    def start(self):
        self.begin = t.localtime()
        print('計時開始...')

    #停止計時
    def stop(self):
        self.end = t.localtime()
        self._calc()
        print('計時結束!')

    #到此地基寫好了,考慮下怎麼計算,直接相減? localtime返回的是一個元祖的結構

    #內部方法,計算執行時間。
    def _calc(self):
        self.lasted = []
        self.prompt = '總共運行了' #這是提示
        for index in range(6):#要用前面的6個元素,就是localtime返回的元組結構
            self.lasted.append(self.end[index] - self.begin[index])
            self.prompt += str(self.lasted[index])
屬性和方法名不能一樣

接下來就是顯示的結果問題,顯示000002不合適,我們希望顯示年月日時分秒,值為0就不顯示。

我們新增一個列表用來存放對應的單位,只顯示有資料的單位

 1 import time as t #首先我們需要time的模組,就先匯入
 2 
 3 class MyTimer():
 4     def __init__(self):
 5         self.unit = ['', '', '', '小時', '分鐘', '']
 6         self.prompt = '未開始計時'
 7         self.lasted = []
 8         self.begin = 0
 9         self.end = 0
10     def __str__(self):
11         return self.prompt
12     
13     __repr__ = __str__
14     
15     #開始計時
16     def start(self):
17         self.begin = t.localtime()
18         print('計時開始...')
19 
20     #停止計時
21     def stop(self):
22         self.end = t.localtime()
23         self._calc()
24         print('計時結束!')
25 
26     #到此地基寫好了,考慮下怎麼計算,直接相減? localtime返回的是一個元祖的結構
27 
28     #內部方法,計算執行時間。
29     def _calc(self):
30         self.lasted = []
31         self.prompt = '總共運行了' #這是提示
32         for index in range(6):#要用前面的6個元素,就是localtime返回的元組結構
33             self.lasted.append(self.end[index] - self.begin[index])
34             if self.lasted[index] : #這邊只顯示索引不為0的,也就是顯示有數值的單位
35                 self.prompt += (str(self.lasted[index]) + self.unit[index])
顯示結果有單位,只顯示有資料的單位

需要在適當的地方新增一些溫馨提示,例如:還沒有開始你就點選了stop,提示先執行start開始計時。

import time as t #首先我們需要time的模組,就先匯入

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 start(self):
        self.begin = t.localtime()
        print('計時開始...')
        self.prompt = '提示:請先呼叫stop()停止計時!'
    #停止計時
    def stop(self):
        if not self.begin:
            print('請先呼叫start()進行計時!')
        else:
            self.end = t.localtime()
            self._calc()
            print('計時結束!')

    #到此地基寫好了,考慮下怎麼計算,直接相減? localtime返回的是一個元祖的結構

    #內部方法,計算執行時間。
    def _calc(self):
        self.lasted = []
        self.prompt = '總共運行了' #這是提示
        for index in range(6):#要用前面的6個元素,就是localtime返回的元組結構
            self.lasted.append(self.end[index] - self.begin[index])
            if self.lasted[index] : #這邊只顯示索引不為0的,也就是顯示有數值的單位
                self.prompt += (str(self.lasted[index]) + self.unit[index])
        #為下一輪計時初始化變數
        self.begin = 0
        self.end = 0
增加溫馨提示,最後重新初始化變數為下一輪計時

結果:

>>> runfile('D:/python練習/MyTimer.py', wdir='D:/python練習')
t1 = MyTimer()
t1
未開始計時
t1.stop()
請先呼叫start()進行計時!
t1.start()
計時開始...
t1
提示:請先呼叫stop()停止計時!
t1.stop()
計時結束!
t1
總共運行了8秒

最後重寫一下add魔法方法,讓2個計時器的物件相加會自動返回他們的時間和。

import time as t #首先我們需要time的模組,就先匯入

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 = "總共運行了"  #這裡沒有self!
        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()
        print('計時開始...')
        self.prompt = '提示:請先呼叫stop()停止計時!'
    #停止計時
    def stop(self):
        if not self.begin:
            print('請先呼叫start()進行計時!')
        else:
            self.end = t.localtime()
            self._calc()
            print('計時結束!')

    #到此地基寫好了,考慮下怎麼計算,直接相減? localtime返回的是一個元祖的結構

    #內部方法,計算執行時間。
    def _calc(self):
        self.lasted = []
        self.prompt = '總共運行了' #這是提示
        for index in range(6):#要用前面的6個元素,就是localtime返回的元組結構
            self.lasted.append(self.end[index] - self.begin[index])
            if self.lasted[index] : #這邊只顯示索引不為0的,也就是顯示有數值的單位
                self.prompt += (str(self.lasted[index]) + self.unit[index])
        #為下一輪計時初始化變數
        self.begin = 0
        self.end = 0
修改了add魔法方法

結果:

t1 = MyTimer()
t1.start()
計時開始...
t1.stop()
計時結束!
t1
總共運行了4秒
t2 = MyTimer()
t2.start()
計時開始...
t2.stop()
計時結束!
t2
總共運行了4秒
t1 + t2
'總共運行了8秒'

有1分鐘-30秒是什麼意思,

動動手:

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

思路:全部轉化為秒嗎? 我的做法是加一個數組,

有負數出現就補充相應的一整個單位,且前一個時間單位減1.

答:參考程式碼寫的比較“糾結”,期待魚油們寫出更漂亮的實現。

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  #預設初始化的計時器為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):
        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 的完美計時工具。

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