第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 模块详解(准确测量小段代码的执行时间)

 

 

posted @ 2020-07-27 11:10  廖海清  阅读(254)  评论(0编辑  收藏  举报