【转】Python源码学习Schedule
原文: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的更多相关文章
- Python源码学习Schedule
		
关于我 一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android.Python.Java和Go,这个也是我们团队的主要技术栈. Github:https:/ ...
 - python源码学习(一)——python的总体架构
		
python源码学习(一)——python的总体架构 学习环境: 系统:ubuntu 12.04 STLpython版本:2.7既然要学习python的源码,首先我们要在电脑上安装python并且下载 ...
 - Python源码学习(一)
		
考虑到性能的要求,我在工作中用的最多的是c/c++,然而,工作中又经常会有一些验证性的工作,这些工作对性能的要求并不高,反而对完成的效率要求更高,对于这样的工作,用一种开发效率高的语言是合理的想法,鉴 ...
 - Python源码学习七 .py文件的解释
		
Python源码太复杂了... 今天看了下对.py文件的parse, 云里雾里的 py文件是最简单的, 在python的交互式窗口 import这个模块 a = 10 print(a) 开始分析,堆栈 ...
 - Python 源码学习之内存管理 -- (转)
		
Python 的内存管理架构(Objects/obmalloc.c): _____ ______ ______ ________ [ int ] [ dict ] [ list ] ... [ str ...
 - Python源码学习之初始化(三)-PyDictObject的初始化
		
先来看它的定义 typedef struct _dictobject PyDictObject; struct _dictobject { PyObject_HEAD Py_ssize_t ma_fi ...
 - Python源码学习十一 一个常用的内存分配函数
		
void * _PyObject_DebugMallocApi(char id, size_t nbytes) { uchar *p; /* base address of malloc'ed blo ...
 - python源码剖析学习记录-01
		
学习<Python源码剖析-深度探索动态语言核心技术>教程 Python总体架构,运行流程 File Group: 1.Core Modules 内部模块,例如:imp ...
 - python 协程库gevent学习--gevent源码学习(二)
		
在进行gevent源码学习一分析之后,我还对两个比较核心的问题抱有疑问: 1. gevent.Greenlet.join()以及他的list版本joinall()的原理和使用. 2. 关于在使用mon ...
 
随机推荐
- 常见问题:Web/Servlet生命周期与Spring Bean生命周期
			
Servlet生命周期 init()初始化阶段 Servlet容器加载Servlet(web.xml中有load-on-startup=1;Servlet容器启动后用户首次向Servlet发请求;Se ...
 - centos7 双网口绑定
			
1.关闭和停止NetworkManager服务 systemctl stop NetworkManager.service # 停止NetworkManager服务 systemctl disable ...
 - LeetCode 144. 二叉树的前序遍历(Binary Tree Preorder Traversal)
			
144. 二叉树的前序遍历 144. Binary Tree Preorder Traversal 题目描述 给定一个二叉树,返回它的 前序 遍历. LeetCode144. Binary Tree ...
 - java后台面试之计算机网络问题集锦
			
1.http和https的区别 2.对称加密和非对称加密 3.三次握手与四次挥手的流程 4.为什么TCP需要三次握手?两次不可以吗?为什么 5.为什么TCP挥手需要四次?三次不行吗? 6.TCP协议如 ...
 - SSRS Reporting Service安装与部署
			
安装与部署SSRS步骤 什么是SSRS SQL Server Reporting Serivces(SSRS) 是一种强大的报表设计开发工具或者说是服务,它提供了一系列本地工具和服务,用于创建.部署和 ...
 - 通过tushare获取股票价格
			
# Author llll # coding=utf-8 # ---描述# 完成股票 价格查询和展示# 不直接根据网页进行爬虫获取股票价格,而是通过已有组件查询股票价格,并保存到csv文件或者exce ...
 - Golang常用快捷键以及常见快捷键冲突
			
配置快捷键: 跳转到函数定义 回退 查找函数使用 File/Settings/Keymap 工具: gofmt/golint File/Settings/Tools/File Watchers gol ...
 - PHP 中 include 和 require 的区别详解
			
require() 语句的性能与 include() 相类似,都是包括并运行指定文件.除了处理失败的方式不同之外.require 在出错时产生 E_COMPILE_ERROR 级别的错误,终止脚本运行 ...
 - JAVA堆,栈的区别,用AarrayList、LinkedList自定义栈
			
大家都知道java模拟机在运行时要开辟空间所以它有特定的五个内存划分: 1.寄存器: 2.本地方法区: 3.方法区: 4.栈内存: 5.堆内存: 但是我们今天来注重讲一下栈和堆 ...
 - centos7安装oracle11g(根据oracle官方文档安装,解决图形界面安装问题)
			
一.系统及安装包 操作系统:centos 7.4 oracle版本:oracle 11g r2 二.centos环境配置 安装数据库所需要的软件包 [root@localhost data]# yum ...