关于我

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

Github:https://github.com/hylinux1024

微信公众号:终身开发者(angrycode)

上一篇《一个简单的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派生出来的,分别是ScheduleErrorScheduleValueErrorIntervalError

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

接着就是这个库的两个核心类SchedulerJob

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,并将参数intervalscheduler实例传入到构造方法中,最后返回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

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

最后就是执行了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库定义两个核心类SchedulerJob。在导入包时就默认创建一个Scheduler对象,并初始化任务列表。

schedule模块提供了链式调用的接口,在配置schedule参数时,就会创建任务对象job,并会将job添加到任务列表中,最后在执行run_pending方法时,就会调用我们自定义的方法。

这个库的核心思想是使用面向对象方法,对事物能够准确地抽象,它总体的逻辑并不复杂,是学习源码很不错的范例。

0x05 学习资料

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

  1. 【转】Python源码学习Schedule

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

  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. 【MySQL】(五)索引与算法

    本篇文章的主旨是对InnoDB存储引擎支持的索引做一个概述,并对索引内部的机制做一个深入的解析,通过了解索引内部构造来了解哪里可以使用索引. 1.InnoDB存储引擎支持以下几种常见的索引: B+树索 ...

  2. 如何处理MySQL经常出现CPU占用率达到99%

    如何处理MySQL经常出现CPU占用率达到99% 情况说明: 最近在自己购买的linux服务器上捣鼓了一个小项目,按理说不存在CPU占用率会达到100%的情况,但事实就是经常出现. 然后,我第一反应是 ...

  3. linux初学者-iscsi篇

     linux初学者-iscsi篇         之前介绍过网络文件共享系统NFS和CIFS.在系统中,设备也是可以共享的,这就是iSCSI,它可以用来建立和管理IP存储设备.主机和客户机等之间的相互 ...

  4. spark 源码分析之七--Spark RPC剖析之RpcEndPoint和RpcEndPointRef剖析

    RpcEndpoint 文档对RpcEndpoint的解释:An end point for the RPC that defines what functions to trigger given ...

  5. Spring WebClient vs. RestTemplate

    1. 简介 本教程中,我们将对比 Spring 的两种 Web 客户端实现 -- RestTemplate 和 Spring 5 中全新的 Reactive 替代方案 WebClient. 2. 阻塞 ...

  6. 阿里技术面全A,终面却被产品经理拉下马。。。

    大纲: 一.投递简历 二.准备面试 三.技术一面 四.健身房里的技术二面 五.产品经理的死亡三面 六.总结 一.投递简历 找内推.大公司投简历尽量找内推,无论是校招还是社招.校招可以去牛客网或知乎找, ...

  7. 曹工杂谈:Linux服务器上,Spring Boot 原地修改 jar 包配置文件/替换class文件,免去重复上传的麻烦

    一.前言 相信很多同学有这样的需求,现在很多公司都有多地的研发中心,经常需要跨地区部署,比如,博主人在成都,但是服务器是北京的.一般城市间网络都不怎么好,上传一个几十兆的jar包那是真的慢,别说现在微 ...

  8. 续集:白菜的内涵,更新nand分区为ubifs,替换overlay

    在上一篇真千兆路由的极限之OPENWRT MAKE, 某品牌白菜价QCA9558/QCA9880/QCA8337N纯种组合OS搭建时记中附带了128M nand的空间图示,在ar71xx profil ...

  9. MATLAB使用过程中遇到的问题(持续更新)

    note:如果对你有帮助,请点赞评论哟!!! 问题1:每次双击.m文件都会自动打开一个matlab程序 step1:下载这个文件 http://pan.baidu.com/s/1pL2ULOf ste ...

  10. A human being,who loves football and music

    ---title: aboutdate: 2019-08-09 20:52:27---[A human being,who loves football and music.](https://eel ...