原文:https://www.cnblogs.com/angrycode/p/11433283.html

-----------------------------------------------------------------------

上一篇《一个简单的Python调度器》介绍了一个简单的Python调度器的使用,后来我翻阅了一下它的源码,惊奇的发现核心库才一个文件,代码量短短700行不到。这是绝佳的学习材料。
让我喜出望外的是这个库的作者竟然就是我最近阅读的一本书《Python Tricks》的作者!现在就让我们看看大神的实现思路。

0x00 准备

项目地址

https://github.com/dbader/schedule

将代码checkout到本地

环境

PyCharm+venv+Python3

0x01 用法

这个在上一篇也介绍过了,非常简单

import schedule

# 定义需要执行的方法
def job():
print("a simple scheduler in python.") # 设置调度的参数,这里是每2秒执行一次
schedule.every(2).seconds.do(job) if __name__ == '__main__':
while True:
schedule.run_pending() # 执行结果
a simple scheduler in python.
a simple scheduler in python.
a simple scheduler in python.
...

这个库的文档也很详细,可以浏览 https://schedule.readthedocs.io/ 了解库的大概用法

0x02 项目结构

(venv) ➜  schedule git:(master) tree -L 2
.
...
├── requirements-dev.txt
├── schedule
│ └── __init__.py
├── setup.py
├── test_schedule.py
├── tox.ini
└── venv
├── bin
├── include
├── lib
├── pip-selfcheck.json
└── pyvenv.cfg 8 directories, 18 files
  • schedule目录下就一个__init__.py文件,这是我们需要重点学习的地方。
  • setup.py文件是发布项目的配置文件
  • test_schedule.py是单元测试文件,一开始除了看文档外,也可以从单元测试中入手,了解这个库的使用
  • requirements-dev.txt 开发环境的依赖库文件,如果核心的库是不需要第三方的依赖的,但是单元测试需要
  • venv是我checkout后创建的,原本的项目是没有的

0x03 schedule

我们知道__init__.py是定义Python包必需的文件。在这个文件中定义方法、类都可以在使用import命令时导入到工程项目中,然后使用。

schedule 源码

以下是schedule会用到的模块,都是Python内部的模块。

import collections
import datetime
import functools
import logging
import random
import re
import time logger = logging.getLogger('schedule')

然后定义了一个日志打印工具实例

接着是定义了该模块的3个异常类的结构体系,是由Exception派生出来的,分别是ScheduleError、ScheduleValueError和IntervalError

class ScheduleError(Exception):
"""Base schedule exception"""
pass class ScheduleValueError(ScheduleError):
"""Base schedule value error"""
pass class IntervalError(ScheduleValueError):
"""An improper interval was used"""
pass

还定义了一个CancelJob的类,用于取消调度器的继续执行

class CancelJob(object):
"""
Can be returned from a job to unschedule itself.
"""
pass

例如在自定义的需要被调度方法中返回这个CancelJob类就可以实现一次性的任务

# 定义需要执行的方法
def job():
print("a simple scheduler in python.")
# 返回CancelJob可以停止调度器的后续执行
return schedule.CancelJob

接着就是这个库的两个核心类Scheduler和Job。

class Scheduler(object):
"""
Objects instantiated by the :class:`Scheduler <Scheduler>` are
factories to create jobs, keep record of scheduled jobs and
handle their execution.
""" class Job(object):
"""
A periodic job as used by :class:`Scheduler`. :param interval: A quantity of a certain time unit
:param scheduler: The :class:`Scheduler <Scheduler>` instance that
this job will register itself with once it has
been fully configured in :meth:`Job.do()`. Every job runs at a given fixed time interval that is defined by: * a :meth:`time unit <Job.second>`
* a quantity of `time units` defined by `interval` A job is usually created and returned by :meth:`Scheduler.every`
method, which also defines its `interval`.
"""

Scheduler是调度器的实现类,它负责调度任务(job)的创建和执行。

Job则是对需要执行任务的抽象。

这两个类是这个库的核心,后面我们还会看到详细的分析。
接下来就是默认调度器default_scheduler和任务列表jobs的创建。

# The following methods are shortcuts for not having to
# create a Scheduler instance: #: Default :class:`Scheduler <Scheduler>` object
default_scheduler = Scheduler() #: Default :class:`Jobs <Job>` list
jobs = default_scheduler.jobs # todo: should this be a copy, e.g. jobs()?

在执行import schedule后,就默认创建了default_scheduler。而Scheduler的构造方法为

def __init__(self):
self.jobs = []

在执行初始化时,调度器就创建了一个空的任务列表。

在文件的最后定义了一些链式调用的方法,使用起来也是非常人性化的,值得学习。
这里的方法都定义在模块下,而且都是封装了default_scheduler实例的调用。

def every(interval=1):
"""Calls :meth:`every <Scheduler.every>` on the
:data:`default scheduler instance <default_scheduler>`.
"""
return default_scheduler.every(interval) def run_pending():
"""Calls :meth:`run_pending <Scheduler.run_pending>` on the
:data:`default scheduler instance <default_scheduler>`.
"""
default_scheduler.run_pending() def run_all(delay_seconds=0):
"""Calls :meth:`run_all <Scheduler.run_all>` on the
:data:`default scheduler instance <default_scheduler>`.
"""
default_scheduler.run_all(delay_seconds=delay_seconds) def clear(tag=None):
"""Calls :meth:`clear <Scheduler.clear>` on the
:data:`default scheduler instance <default_scheduler>`.
"""
default_scheduler.clear(tag) def cancel_job(job):
"""Calls :meth:`cancel_job <Scheduler.cancel_job>` on the
:data:`default scheduler instance <default_scheduler>`.
"""
default_scheduler.cancel_job(job) def next_run():
"""Calls :meth:`next_run <Scheduler.next_run>` on the
:data:`default scheduler instance <default_scheduler>`.
"""
return default_scheduler.next_run def idle_seconds():
"""Calls :meth:`idle_seconds <Scheduler.idle_seconds>` on the
:data:`default scheduler instance <default_scheduler>`.
"""
return default_scheduler.idle_seconds

我们看下入口方法run_pending(),从本文一开头的Demo可以知道这个是启动调度器的方法。这里它执行了default_scheduler中的方法。

default_scheduler.run_pending()

所以我们就把目光定位到Scheduler类的相应方法

def run_pending(self):
"""
Run all jobs that are scheduled to run. Please note that it is *intended behavior that run_pending()
does not run missed jobs*. For example, if you've registered a job
that should run every minute and you only call run_pending()
in one hour increments then your job won't be run 60 times in
between but only once.
"""
runnable_jobs = (job for job in self.jobs if job.should_run)
for job in sorted(runnable_jobs):
self._run_job(job)

这个方法中首先从jobs列表将需要执行的任务过滤后放在runnable_jobs列表,然后将其排序后顺序执行内部的_run_job(job)方法

def _run_job(self, job):
ret = job.run()
if isinstance(ret, CancelJob) or ret is CancelJob:
self.cancel_job(job)

在_run_job方法中就调用了job类中的run方法,并根据返回值判断是否需要取消任务。

这时候我们要看下Job类的实现逻辑。

首先我们要看下Job是什么时候创建的。还是从Demo中的代码入手

schedule.every(2).seconds.do(job)

这里先执行了schedule.every()方法

def every(interval=1):
"""Calls :meth:`every <Scheduler.every>` on the
:data:`default scheduler instance <default_scheduler>`.
"""
return default_scheduler.every(interval)

这个方法就是scheduler类中的every方法

def every(self, interval=1):
"""
Schedule a new periodic job. :param interval: A quantity of a certain time unit
:return: An unconfigured :class:`Job <Job>`
"""
job = Job(interval, self)
return job

在这里创建了一个任务job,并将参数interval和scheduler实例传入到构造方法中,最后返回job实例用于实现链式调用。

跳转到Job的构造方法

def __init__(self, interval, scheduler=None):
self.interval = interval # pause interval * unit between runs
self.latest = None # upper limit to the interval
self.job_func = None # the job job_func to run
self.unit = None # time units, e.g. 'minutes', 'hours', ...
self.at_time = None # optional time at which this job runs
self.last_run = None # datetime of the last run
self.next_run = None # datetime of the next run
self.period = None # timedelta between runs, only valid for
self.start_day = None # Specific day of the week to start on
self.tags = set() # unique set of tags for the job
self.scheduler = scheduler # scheduler to register with

主要初始化了间隔时间配置、需要执行的方法、调度器各种时间单位等。

执行every方法之后又调用了seconds这个属性方法

@property
def seconds(self):
self.unit = 'seconds'
return self

设置了时间单位,这个设置秒,当然还有其它类似的属性方法minutes、hours、days等等。

最后就是执行了do方法

def do(self, job_func, *args, **kwargs):
"""
Specifies the job_func that should be called every time the
job runs. Any additional arguments are passed on to job_func when
the job runs. :param job_func: The function to be scheduled
:return: The invoked job instance
"""
self.job_func = functools.partial(job_func, *args, **kwargs)
try:
functools.update_wrapper(self.job_func, job_func)
except AttributeError:
# job_funcs already wrapped by functools.partial won't have
# __name__, __module__ or __doc__ and the update_wrapper()
# call will fail.
pass
self._schedule_next_run()
self.scheduler.jobs.append(self)
return self

在这里使用functools工具的中的偏函数partial将我们自定义的方法封装成可调用的对象

然后就调用_schedule_next_run方法,它主要是对时间的解析,按照时间对job排序,我觉得这个方法是本项目中的技术点,逻辑也是稍微复杂一丢丢,仔细阅读就可以看懂,主要是对时间datetime的使用。由于篇幅,这里就不再贴出代码。

这里就完成了任务job的添加。然后在调用run_pending方法中就可以让任务执行。

0x04 总结一下

schedule库定义两个核心类Scheduler和Job。在导入包时就默认创建一个Scheduler对象,并初始化任务列表。
schedule模块提供了链式调用的接口,在配置schedule参数时,就会创建任务对象job,并会将job添加到任务列表中,最后在执行run_pending方法时,就会调用我们自定义的方法。
这个库的核心思想是使用面向对象方法,对事物能够准确地抽象,它总体的逻辑并不复杂,是学习源码很不错的范例。

0x05 学习资料

    • https://github.com/dbader/schedule
    • https://schedule.readthedocs.io

【转】Python源码学习Schedule的更多相关文章

  1. Python源码学习Schedule

    关于我 一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android.Python.Java和Go,这个也是我们团队的主要技术栈. Github:https:/ ...

  2. python源码学习(一)——python的总体架构

    python源码学习(一)——python的总体架构 学习环境: 系统:ubuntu 12.04 STLpython版本:2.7既然要学习python的源码,首先我们要在电脑上安装python并且下载 ...

  3. Python源码学习(一)

    考虑到性能的要求,我在工作中用的最多的是c/c++,然而,工作中又经常会有一些验证性的工作,这些工作对性能的要求并不高,反而对完成的效率要求更高,对于这样的工作,用一种开发效率高的语言是合理的想法,鉴 ...

  4. Python源码学习七 .py文件的解释

    Python源码太复杂了... 今天看了下对.py文件的parse, 云里雾里的 py文件是最简单的, 在python的交互式窗口 import这个模块 a = 10 print(a) 开始分析,堆栈 ...

  5. Python 源码学习之内存管理 -- (转)

    Python 的内存管理架构(Objects/obmalloc.c): _____ ______ ______ ________ [ int ] [ dict ] [ list ] ... [ str ...

  6. Python源码学习之初始化(三)-PyDictObject的初始化

    先来看它的定义 typedef struct _dictobject PyDictObject; struct _dictobject { PyObject_HEAD Py_ssize_t ma_fi ...

  7. Python源码学习十一 一个常用的内存分配函数

    void * _PyObject_DebugMallocApi(char id, size_t nbytes) { uchar *p; /* base address of malloc'ed blo ...

  8. python源码剖析学习记录-01

    学习<Python源码剖析-深度探索动态语言核心技术>教程         Python总体架构,运行流程   File Group: 1.Core Modules 内部模块,例如:imp ...

  9. python 协程库gevent学习--gevent源码学习(二)

    在进行gevent源码学习一分析之后,我还对两个比较核心的问题抱有疑问: 1. gevent.Greenlet.join()以及他的list版本joinall()的原理和使用. 2. 关于在使用mon ...

随机推荐

  1. 安装Windows和Ubuntu双系统

    发现关注消息 安装Windows和Ubuntu双系统     安装Windows和Ubuntu双系统 0.552016.12.10 15:54:41字数 2101阅读 6644 这几天开始动手做毕设啦 ...

  2. PHP中奖概率写法

    PHP中奖概率写法 <pre><?phpheader("Content-type: text/html; charset=utf-8");/* * 经典的概率算法 ...

  3. 【Webservice】2 counts of IllegalAnnotationExceptions Two classes have the same XML type name

    在使用客户端调用服务端的时候发生了2 counts of IllegalAnnotationExceptions Two classes have the same XML type name的错误, ...

  4. 01 IO流(一)—— 流的概念、File类

    1 流的概念理解(重要) 理解流的概念非常重要. 流,就是程序到数据源或目的地的一个通道. 我们把这个通道实例化得到一个具体的流,相当于一个数据传输工具,它可以在程序与资源之间进行数据交换. 换言之, ...

  5. WUSTOJ 1324: Base64 Coding(Java)未解决,求题解

    题目链接:1324: Base64 Coding 资料:ASCII码表 原文是英文,而且篇幅较长.因此下面不粘贴原文,只写中文大意. Description Base64是一种编码算法.它的工作原理是 ...

  6. iis 经典模式和集成模式

    IIS7.0中的Web应用程序有两种配置模式:经典模式和集成模式.两者区别大家可以参考下,根据实际情况选用.  经典模式是为了与之前的版本兼容,使用ISAPI扩展来调用ASP.NET运行库,原先运行于 ...

  7. Python之数据处理-2

    一.数据处理其实是一个很麻烦的事情. 在一个样本中存在特征数据(比如:人(身高.体重.出生年月.年龄.职业.收入...))当数据的特征太多或者特征权重小或者特征部分满足的时候. 这个时候就要进行数据的 ...

  8. Gitlab Runner实现CI/CD自动化部署asp.net core应用

    环境说明 一台git服务器(192.168.169.7),安装gitlab,docker. 一台web服务器(192.168.169.6),安装git,gitlab runner,docker,dot ...

  9. Node.js 实战(一)之—优化汇总

    Express 页面缓存 app.set("cache view",true); --设置页面缓存 开发模式下博主建议不要这么做,因为开发中我们会频繁的对页面的样式.js等进行修改 ...

  10. 发现一个对列排版挺好用的命令:column

    help [root@hdpool1 tmp]# column -h Usage: column [options] [file ...] Options: -c, --columns <wid ...