为什么我的简单 Python 秒表脚本占用了如此多的计算机资源?

Min*_*ran 2 python command-line scripts tkinter

我今天开始为秒表使用 Python 脚本,并注意到我打开的所有其他东西(Firefox、Sublime Text、终端)都显着变慢。系统监视器告诉我我的秒表脚本使用了大约 24% 的 CPU。如此琐碎的事情使用这么多资源似乎很奇怪。

我能得到一些关于如何改进这一点的建议吗?我真的很想在后台运行它并跟踪我花在各种事情上的时间。

这是脚本:

#! /usr/bin/env python3
import tkinter
import time
import datetime
import numpy as np 
import subprocess

class StopWatch(tkinter.Frame):

    @classmethod
    def main(cls):
        tkinter.NoDefaultRoot()
        root = tkinter.Tk()
        root.title('Stop Watch')
        root.resizable(False, False)
        root.grid_columnconfigure(0, weight=1)
        root.geometry("200x235")
        padding = dict(padx=5, pady=5)
        widget = StopWatch(root, **padding)
        widget.grid(sticky=tkinter.NSEW, **padding)
        icon = tkinter.PhotoImage(file='stopwatch.ico')
        root.tk.call('wm', 'iconphoto', root._w, icon)
        root.mainloop()

    def __init__(self, master=None, cnf={}, **kw):
        padding = dict(padx=kw.pop('padx', 5), pady=kw.pop('pady', 5))
        super().__init__(master, cnf, **kw)

        self.grid_columnconfigure(0,weight=1)

        self.__total = 0
        self.start_time=datetime.datetime.now().strftime("%H:%M")
        self.start_date=datetime.datetime.now().strftime("%m/%d/%Y")
        self.start_dt=tkinter.StringVar(self, self.start_time+" "+self.start_date)

        self.__label = tkinter.Label(self, text='Session Time:')
        self.__time = tkinter.StringVar(self, '00:00')
        self.__display = tkinter.Label(self, textvariable=self.__time,font=(None, 26),height=2)
        self.__button = tkinter.Button(self, text='Start', relief=tkinter.RAISED, bg='#008000', activebackground="#329932", command=self.__click)
        self.__record = tkinter.Button(self, text='Record', relief=tkinter.RAISED, command=self.__save)
        self.__startdt = tkinter.Label(self, textvariable=self.start_dt)

        self.__label.grid   (row=0, column=0, sticky=tkinter.NSEW, **padding)
        self.__display.grid (row=1, column=0, sticky=tkinter.NSEW, **padding)
        self.__button.grid  (row=2, column=0, sticky=tkinter.NSEW, **padding)
        self.__record.grid  (row=3, column=0, sticky=tkinter.NSEW, **padding)
        self.__startdt.grid (row=4, column=0, sticky=tkinter.N, **padding)

    def __click(self):
        if self.__total==0:
            self.start_time=datetime.datetime.now().strftime("%H:%M")
            self.start_date=datetime.datetime.now().strftime("%m/%d/%Y")
            self.__time.set(self.start_time+" "+self.start_date)
        if self.__button['text'] == 'Start':
            self.__button['text'] = 'Stop'
            self.__button['bg']='#ff0000'
            self.__button['activebackground']='#ff3232'
            self.__record['text']='Record'
            self.__record['state']='disabled'
            self.__record['relief']=tkinter.SUNKEN
            self.__start = time.clock()
            self.__counter = self.after_idle(self.__update)
        else:
            self.__button['text'] = 'Start'
            self.__button['bg']='#008000'
            self.__button['activebackground']='#329932'
            self.__record['state']='normal'
            self.__record['relief']=tkinter.RAISED
            self.after_cancel(self.__counter)

    def __save(self):
        duration = int(self.__total//60)
        if duration > 0:
            subprocess.call("cp test_data.dat ./backup", shell=True)
            data = np.loadtxt('test_data.dat', dtype="str")

            time_data = data[:, 0]
            date_data = data[:, 1]
            duration_data = data[:, 2]

            time_data=np.append(time_data,self.start_time)
            date_data=np.append(date_data,self.start_date)
            duration_data=np.append(duration_data,str(duration))

            new_data=np.column_stack((time_data,date_data,duration_data))
            np.savetxt('test_data.dat', new_data, header="*Time* | *Date* | *Duration*", fmt="%s")

            self.__record['text']='Saved'
        else:
            self.__record['text']='Not Saved'

        self.start_time=datetime.datetime.now().strftime("%H:%M")
        self.start_date=datetime.datetime.now().strftime("%m/%d/%Y")
        self.__time.set(self.start_time+" "+self.start_date)
        self.__total=0
        self.__time.set('00:00')

        self.__record['state']='disabled'
        self.__record['relief']=tkinter.SUNKEN


    def __update(self):
        now = time.clock()
        diff = now - self.__start
        self.__start = now
        self.__total += diff
        mins,secs=divmod(self.__total,60)
        self.__time.set('{:02.0f}:{:02.0f}'.format(mins,secs))
        self.start_dt.set(datetime.datetime.now().strftime("%H:%M %m/%d/%Y"))
        self.__counter = self.after_idle(self.__update)

if __name__ == '__main__':
    StopWatch.main()

Run Code Online (Sandbox Code Playgroud)

Jac*_*ijm 6

如何防止处理器在轮询时间发疯

在您的代码段中:

def __update(self):
    now = time.clock()
    diff = now - self.__start
    self.__start = now
    self.__total += diff
    mins,secs=divmod(self.__total,60)
    self.__time.set('{:02.0f}:{:02.0f}'.format(mins,secs))
    self.start_dt.set(datetime.datetime.now().strftime("%H:%M %m/%d/%Y"))
    self.__counter = self.after_idle(self.__update)
Run Code Online (Sandbox Code Playgroud)

您可以在空闲时重新运行该功能而没有任何限制。这意味着您的处理器将每时每刻都处于空闲状态以更新时间。这将导致处理器负载接近 100%。由于它仅使用四分之一的内核,因此您将看到(接近)25%。

只需使用“智能”变量 while 循环;原则

如果我们使用time.sleep(),因为我们没有使用真正的处理器时钟时间,我们会有轻微的偏差。处理器总是需要一点时间来处理命令,所以

time.sleep(1)
Run Code Online (Sandbox Code Playgroud)

实际上会像

time.sleep(1.003)
Run Code Online (Sandbox Code Playgroud)

如果不采取进一步行动,这将导致累积偏差,但是

我们可以使过程变得智能。我在桌面应用程序中总是做的是sleep()在每秒或每分钟之后校准,具体取决于所需的精度。一个循环用作处理时间的内容从下一个循环中收回,因此永远不会累积偏差。

原则上:

time.sleep(1)
Run Code Online (Sandbox Code Playgroud)

由于您使用秒为单位,这似乎就足够了。额外负担:无法衡量。

在终端中运行此代码段,您将看到显示的时间和固定偏差:

time.sleep(1.003)
Run Code Online (Sandbox Code Playgroud)

使用 after() 而不是 while?

同样,你也可以在()方法使用Tkinters,如所描述这里,使用具有可变时间校准同样的伎俩。


编辑

根据要求:使用 Tkinter 的 after() 方法的示例

如果你使用固定的循环时间,你是:

  1. 不可避免地浪费资源,因为您的循环时间(时间分辨率)需要是显示时间单位的一小部分。
  2. 即使你这样做了,比如你的 200 毫秒,显示的时间有时会显示与实际时间(接近)200 毫秒的差异,随后跳到下一个显示的秒太短。

如果您使用after(), 并且想要使用可变时间周期,就像上面的非 gui 示例一样,下面是一个示例,提供与您的答案中的代码段完全相同的选项:

在此处输入图片说明

import time

seconds = 0 # starttime (displayed)
startt = time.time() # real starttime
print("seconds:", seconds)

wait = 1

while True:
    time.sleep(wait)
    seconds = seconds + 1 # displayed time (seconds)
    target = startt + seconds # the targeted time
    real = time.time() # the "real" time
    calibration = real - target # now fix the difference between real and targeted
    nextwait = 1 - calibration # ...and retract that from the sleep of 1 sec
    wait = nextwait if nextwait >= 0 else 1  # prevent errors in extreme situation
    print("correction:", calibration)
    print("seconds:", seconds)
Run Code Online (Sandbox Code Playgroud)

笔记

...如果您从 Gtk 应用程序中的第二个线程更新 GUI,您总是需要从空闲状态更新。