Linux 进程管理

最近开始对 Linux 进行一次比较深入的学习,对于一个操作系统来说,提供运行程序的能力是其本质,而在 Linux 中,轻量、相应快速的进程管理也是其优良特性之一。我会分两篇文章介绍 Linux 进程。这是第一篇,重点在于 Linux 进程的描述、和生命周期,下一篇将介绍 Linux 下的进程调度。

描述符与任务结构

进程就是处于执行期的程序,而通常还包括部分资源,比如:文件、挂起的信号、内核内部数据、处理器状态、内存空间以及一个或多个执行线程(thread of execution)。因此,可以认为一个进程就是一段可执行程序和计算机资源的集合。

进程的描述

在 Linux 内核中,会有一个被称作任务队列(Task list)的双向循环链表,被用来保存进程的描述(后面会讲到 Linux 线程和进程不做区别,任务队列也会用来保存线程描述)。链表中的每一个元素为 task_struct 类型的结构体(定义在 linux/sched.h 中),task_struct 相对较大,在 32 位系统的机器上,大约有 1.7KB 的大小,该结构体中包含的数据能完整的描述一个正在执行的程序:打开的文件、地址空间、信号、状态等。

Linux 通过 slab 分配器 分配 task_struct 结构。在 2.6 之前,各个进程的 task_struct 存放在他们内核栈的尾端,这样可以使得 x86 等寄存器比较少的硬件体系结构只要通过栈指针就能计算出它的位置,而对于现在比较新的硬件体系上,会专门拿出一个寄存器指向 task_struct,而只在进程内核栈的尾端保留一个 thread_info(定义在 asm/thread_info.h),其记录了执行线程的基础信息,并在 task 域中保留一个指向 task_struct 的指针。

PID

内核通过一个唯一的进程标识值(process identification value,PID)来表示每个进程,PID 是一个宏定义类型 pid_t,实际上就是一个 short int(为了保证兼容,采用 short 类型,最大值仅为 32768),对于 PID 最大有多大其实是受 linux/threads.h 中定义的限制,可以通过修改 /proc/sys/kernel/pid_max 来把 PID 增加至高达 400 万。但考虑到进程描述是存储在一个双向循环链表中的,进程越少意味着转一圈越快,进程数量巨大并不意味是一件好事。

进程的状态

task_struct 中,state 专门用来描述进程的当前状态,系统中的每个进程必然处于下面五种状态之一:

  • TASK_RUNNING(运行)
  • TASK_INTERRUPTIBLE(可中断)
  • TASK_UNINTERRUPTIBLE(不可中断)
  • __TASK_TRACED(被其他进程跟踪)
  • __TASK_STOPPED(停止)

TASK_RUNNING 状态下意味着进程是可以被执行的,或许进程正在执行,也可能在运行队列中等待被执行(后面的进程调度会讲到),只有该状态下,进程才有被执行的可能。

TASK_INTERRUPTIBLE 状态意味着进程正在休眠,也许因为被阻塞,或者等待某些条件的达成,一旦这些条件达成,内核就会把进程状态设置为运行,处于此状态的进程也会因为接收到信号被唤醒并随时准备投入运行。

TASK_UNINTERRUPTIBLE 和 TASK_INTERRUPTIBLE 类似,但是不同点在于即使进程收到信号也不会被唤醒或者准备投入运行。该状态下的进程可以处于一个不受打扰的状态,以等待完成某些条件,比如进程创建初期,在没有初始化完成时便是处于该状态。

__TASK_TRACED 意味着被其他进程跟踪,比如通过调试程序进程跟踪等。

__TASK_STOPPED 意味着进程已经停止了执行,比如在收到 SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU 等信号,此外,在调试期收到任何信号都会进入该状态。更多进程的停止时发生的事情后面会讲到。

上下文与家族树

一般程序在用户空间执行,但当一个程序执行了系统调用或者触发了某个异常,它就会陷入内核空间,此时被称作内核 “代表进程执行” 并处于进程上下文中,一般情况下,在内核退出时,程序恢复到用户空间继续执行,系统调用和异常处理程序是对内核明确定义的接口,进程只有通过这些接口才能陷入内核执行,即对内核的所有访问都必须通过这些接口。在后面会单独拿一篇文章介绍系统调用。

Unix 系统的进程之间存在一个明显的继承关系,在 Linux 系统中也是如此,所有的进程都是 PID 为 1 的 init 进程的后代。而 init 进程是在系统启动的最后阶段启动的第一个进程。该进程负责读取系统的初始化脚本并执行其他的相关程序,以完成系统启动的最后过程。

除了 init 进程,系统的每个进程必有一个父进程,而每个进程也可以拥有零个到多个子进程,进程间呈现的树状关系,使得拥有相同父进程的两个子进程被称为 “兄弟”。进程的关系存放在进程描述符中,每个 task_struct 都包含一个指向其父进程 task_struct 的指针(被称为 parent 指针)。还包含一个称为 children 的子进程链表。也正是拥有这样的数据结构,可以从任意进程的 task_struct 遍历系统中存在的整个家族树。

进程的创建

Unix 系统的进程创建很特别,其他系统都提供了 Spawn 进程机制,首先在新的地址空间里创建进程,读入可执行文件,最好开始执行。这是一种符合直觉的进程创建方式,但是 Unix 采用了与众不同的方式,它把上述过程分成了两步(分成两个独立的函数),fork 和 exec。

首先 fork() 通过拷贝当前进程创建一个子进程,而子进程与父进程的区别仅在于 PID(进程 ID)、PPID(父进程的 PID)、部分资源和统计量(被挂起的信号等没必要继承的资源)。除此之外,新创建的子进程将直接使用父进程的资源(直接将相关指针指向父进程的资源)。而 exec() 函数负责读取可执行文件并将其载入地址空间开始运行。将这两个函数连起来使用可以达到其他系统单独使用 spawn 函数的作用。

Linux 的 fork() 使用写时拷贝(copy-on-wirte)页实现,这就意味着在创建子进程时可以推迟甚至免除拷贝父进程的数据。在 fork 时内核并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝,只有到了需要写入的时候,数据才会被复制,从而使得各个进程拥有各自的拷贝。换句话说,资源的复制只会在需要写入的时候进行,在此之前,都是以只读的方式共享。因此 fork 的实际开销只有复制父进程的页表以及给子进程创建唯一的进程描述符。而且 Linux 做了一个巧妙的优化,在进程创建后会马上运行一个可执行文件,即先执行子进程,这样就避免了父进程对内存空间的修改导致触发写时拷贝。

Linux 通过 clone() 系统调用来实现 fork,而 fork() vfork() __clone() 函数都通过不同的参数来调用 clone() 系统调用,而 clone() 通过 do_frok()(定义在 kernel/fork.c)来完成 fork 绝大部分工作。

do_frok() 首选调用 copy_process 函数来拷贝进程,然后让进程开始运行,而 copy_process 函数即是 Linux 复制进程的具体实现:

  1. copy_process 首先调用 dup_task_struck() 为子进程创建内核栈、thread_info 结构和 task_struct,目前这些结构和内容与父进程完全一致。
  2. 检查并确保创建子进程后,用户所拥有的进程数目没有超过分配的资源限额。
  3. 着手使子进程与父进程区别开来,进程描述符内的不能继承的成员被清零或者初始化。
  4. 将进程状态设置为 TASK_UNINTERRUPTIBLE 避免被投入运行。
  5. 调用 copy_flags() 更新 task_struct 的标记变量,比如标明进程是否拥有超级用户权限的 PF_SUPERPRIV 被清零,标明进程没有调用 exec() 函数的 PF_FORKNOEXEC 被设置。
  6. 调用 alloc_pid() 获得一个有效的 PID
  7. 根据 clone() 系统调用获得的参数来复制或者共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。
  8. 最后 copy_process 做些收尾工作并返回一个指向子进程的指针。

这时运行权又回到了 do_frok() 上,do_frok() 会检查 copy_process 返回的指针,并让子进程投入运行,正如上面所说,会优先让子进程投入运行,然后是父进程。至此,一个子进程的创建便完成了。

线程

线程机制是现代编程技术中常用的一种抽象概念,该机制提供了在同一程序内共享内存地址空间运行的同一组线程。这些线程还可以共享如打开的文件等其他资源,但是如上文提到的,从 Linux 内核的角度来看,并没有线程这个概念,这和 Linux 线程的实现有关。

Linux 没为线程准备特别的调度算法或定义特别的数据结构,而是被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的 task_struct,从内核的角度看,它就是一个普通的进程(只是线程和其他的一些进程共享某些资源而已)。

因此,Linux 的线程实现和 Windows 等其他操作系统的实现差距非常大,后者都在内核提供了专门的支持线程的机制(这些系统常把线程称为轻量级进程 lightweight processes)。”轻量级进程“ 这个说法本身就表现了 Linux 与其他系统的差异。其他系统中,相较于重量级进程,线程被抽象成一种耗费较少资源,运行迅速的执行单元,而在 Linux 中,进程本身就足够轻量,而线程只是进程间共享资源的手段而已。

因此,线程的创建也是通过 clone() 系统调用来实现,只不过在调用 clone() 时传递一些参数来标记需要共享的资源。

除了用户空间的线程外,内核经常需要在后台执行一些操作,这些任务一般是通过内核线程(kernel thread)完成。内核线程是独立运行在内核空间的标准进程,内核线程与普通进程的区别在于内核线程没有独立的地址空间(指向地址空间的 mm 指针为 NULL),它们只在内核空间运行,从不切换到用户空间去,而且内核进程和普通进程一样,可以被调度,可以被抢占。

在 Linux 可以通过 ps -ef 查看内核线程,内核线程只能通过内核线程创建,内核是通过从 kthreadd 内核进程中衍生出所有新的内核线程,其接口声明在 linux/kthread.h 中。内核线程启动后就一直运行直到调用 do_exit() 退出,或者由内核其他部分调用 kthread_stop() 退出,线程或者进程的终结,将在下面介绍。

进程的终结

当一个进程(线程)终结时,内核必须释放它所占有的资源并告知其父进程,而这两步便完成了资源释放和删除进程描述符。

释放资源

一般来说,进程的结束是由自身引起的,比如执行 exit()。但无论是主动或者被动,进程结束时都要靠 do_exit() 函数(定义在 kernel/exit.c)来 “处理身后事”:

  1. task_struct 中的标记成员设置为 PF_EXITING
  2. 调用 del_timer_sync() 删除内核定时器,以确保没有定时器在排队,也没有定时器处理程序在运行。
  3. 如果 BSD 的进程记账程序功能是开启的,则输出记账信息。
  4. 调用 exit_mm() 来释放进程占用的 mm_struct,如果没有别的进程共享它,便彻底释放。
  5. 调用 sem_exit(),如果进程排队等候 IPC 信号,则使它离开队列。
  6. 调用 exit_file()exit_fs(),以分别递减文件描述符、文件系统数据的引用计数,如果引用计数减低到零,则彻底释放。
  7. 把存放在 task_struct 的 exit_code 成员设置为 exit() 函数提供的退出码。
  8. 调用 exit_notify() 向父进程发送信号,并给退出进程的子进程寻找养父(养父为线程组中的其他任意进程或者 init 进程),并把退出进程的进程状态设置为 EXIT_ZOMBIE,即僵死状态。
  9. 调用 schedule() 释放执行权,因为此进程已经设置为僵死状态,所以该进程再也不会被执行, do_exit() 永不会返回。

至此,进程的所有资源就已经全部被释放了,但是为了方便父进程查询退出进程的状态(退出码等),进程的 task_struct 会被保留,直到父进程执行 wait() 函数。

删除进程描述符

wait() 这一族函数都是通过唯一的系统调用 wait4() 实现的,它实现了挂起自己的进程,直到其中一个子进程退出,此时函数会返回该子进程的 PID。此外,调用该函数时提供的指针会包含子函数退出时的退出码。

当最终需要释放进程描述符时,release_task() 会被调用并执行下面的任务:

  1. 调用 __exit_signal(),该函数调用 _unhash_process(),而后者又调用 detach_pid(),从 pidhash 上删除该进程,同时也会从任务列表中删除该进程。
  2. _exit_signal() 释放目前僵死进程所使用的所有剩余资源,并进行最终统计和记录。
  3. 如果这个进程是线程组的最后一个进程,并且领头进程已经死掉,那么继续通知僵死的领头进程的父进程。
  4. 调用 put_task_struct() 释放进程内核栈和 thread_info 结构所占的页,并释放 task_struct 所占的 slab 高速缓存。

至此,进程描述符和进程独享资源就全部释放掉了。

孤儿进程

如果父进程在子进程退出前退出,必须有机制保证子进程能找到一个新的父亲,否则这些成为孤儿的进程会在退出时永远处于僵死状态。系统会遍历进程所在线程组的其他进程寻找养父,如果线程组没有其他进程,他就会被挂在 init 进程上。

一旦系统为进程成功找到并设置了新的父进程,就不会再有出现驻留僵死进程的风险了,而 init 进程也会例行调用 wait() 来检查其子进程,清除所有与其相关的僵死进程。

总结

进程是一个非常基础和关键的抽象概念,位于每种现代操作系统的核心位置,本文通过介绍进程及其生命周期来描述其基本概念,下一篇会通过介绍进程的调度来描述进程运行时的样子。

2018/5/19 posted in  Kernel

Python 魔术方法

魔术方法是 Python 类中那些定义名称类似 __XXX__ 的函数,使用 Python 的魔术方法的优势在于可以使用 Python 提供的内置方法,比如 len()str(),或者重载运算符,让自定义对象类型可以表现得像内置类型一样。

Python 语言参考手册中一共列出了 83 个特殊方法的名字,其中 47 个用于实现算术运算、位运算和比较操作,概览如下。

跟运算符无关的特殊方法:

跟运算符相关的特殊方法:

有一份文档详细的介绍了所有的魔术方法的使用方式,本文只整理了常用魔术方法以及常见的坑。 文档见: http://pycoders-weekly-chinese.readthedocs.io/en/latest/issue6/a-guide-to-pythons-magic-methods.html

构造与析构

Python 对象通过 __init__ 进行初始化,但是在执行 __init__ 之前,首先被执行的是 __new__ 函数。在初始化一个对象时,会先调用 __new__ 方法,该方法是一个类方法,执行完相关逻辑之后调用 __init__ 并将初始化参数传递给后者。

__del__ 函数是 Python 对象的析构函数,无论是垃圾回收还是手动删除,都是执行这个方法:

class Model(object):
    def __init__(self):
        print("Run: __init__")

    def __del__(self):
        print("Run: __del__")


if __name__ == "__main__":
    m = Model()
    del m

结果:

Run: __init__
Run: __del__

打印

在 Python 魔术方法中,有两个和打印有关,__repr____str__

如果在 Python 直接打印一个类,将会获得一个内存地址:

<__main__.Model object at 0x10408fda0>

如果在解释器中想获得一个更友好的显示,__repr__ 是一个非常好的选择。另外,在不把 object 当做字符串处理的场合,也是会使用该方法。

而在把 object 当做字符串处理或者转换为字符串的场合,比如 print(object)"{}".format(object)str(object) 等,__str__ 将会被使用。

class Model(object):
    def __init__(self):
        self.id = uuid.uuid4()
        self.content = "fluent python demo"

    def __str__(self):
        return self.content

    def __repr__(self):
        return "<Model: {}>".format(self.id)

一般在 __str__ 中展现更人性化的信息,可能直接返回给用户;而 __repr__ 更多为了在调试中使用方便(解释器中等)。

另外,如果 __str__ 没有定义,那么将会直接使用 __repr__ 方法,因此如果不需要太多差别,只定义后者即可。

数组与字典

如果需要定义一个类似列表的自定义类,为了支持通过下标访问,可以覆写 __getitem__

class Model(object):
    def __init__(self):
        self.content = [r for r in range(0, 3)]

    def __getitem__(self, item):
        return self.content[item]

该类从此支持了下标访问,并且还支持切片,排序等 Python 内置方法:

if __name__ == "__main__":
    m = Model()
    print(m[1:], sorted(m, reverse=True))

结果:

[1, 2] [2, 1, 0]

对于赋值操作,只需要覆写 __setitem__ 方法:

def __setitem__(self, key, value):
    self.content[key] = value

同理,对于将自定义对象进行类似字典的操作,可以这样实现:

class Model(object):
    def __init__(self):
        self.content = {
            "key": "value"
        }

    def __getitem__(self, item):
        return self.content[item]

    def __setitem__(self, key, value):
        self.content[key] = value

类成员变量

类成员方法 __getattr____getattribute____setattr__ 是三个方便且危险的方法,可以用来定义类成员属性,前两个是用来获取,最后一个是用来赋值。

前两个的区别是 __getattribute__ 会被首先调用,只有 __getattribute__ 找不到的时候,才会调用 __getattr__

因为本身的类属性操作也是基于这几个方法,因此在覆写时应该避免循环调用最后栈溢出。

总结

简单讲了几种常见的魔术方法,魔术方法可以使得自定义类型支持 Python 原生、内置方法,因而使得代码风格更加简洁和一致。但是,也容易写出难以调试的魔法代码,简洁(或者说 geek)和可维护性可能本身就是两个极端,在使用魔术方法时还需要考虑后继维护者是否能理解自己的行为。

2018/5/16 posted in  流畅的 Python

Flask Api 文档管理与 Swagger 上手

Flask 是一个以自由度高、灵活性强著称的 Python Web 框架。但高灵活性也意味着无尽的代码维护成本、高自由度意味着代码质量更依赖程序员自身而没有一致的标准和规范。因此团队内开发时 Flask 项目更需要建立代码和文档规范以保证不会出现太大的偏差。

本文从 Api 的角度探究 Flask 项目的 Api 规范以及获得 Api 文档的最佳姿势。众数周知,文档的编写和整理工作将花费巨大精力甚至不亚于代码的编写,因此在时间紧任务重的情况下,文档是首先被忽略的工作。不过,就算项目在初期存在文档,但在后面的迭代中,文档落后严重,其产生的误导比没有文档更加可怕。

因此,个人认为 文档随代码走,代码改动时文档也应该跟进变动,但本着 人是不可靠的 原则,文档理想上是应该由代码生成,而不是靠人工维护。如果代码有任何改动,文档也能自动更新,这将是一件非常优雅的事情。虽然对很多文档来说这并不现实,但对于 Api 文档来说,实现成本并不高。

Flask-RESTPlus

对于 REST Api 来说,Flask-RESTPlus 是一个优秀的 Api 文档生成工具,这个包将会替换 Flask 路由层的编写方式,通过自己的语法来规定 Api 细节,并生成 Api 文档。

安装

安装 Flask-RESTPlus

pip install flask-restplus

或者:

easy_install flask-restplus

最小 Demo

使用 Flask-RESTPlus 时需要按照这个库规定的方式编写 Api 层,包括 request 的参数解析,以及 response 的返回格式。一个 hello world 级的示范:

from flask import Flask
from flask_restplus import Resource, Api

app = Flask(__name__)
api = Api(app, prefix="/v1", title="Users", description="Users CURD api.")

@api.route('/users')
class UserApi(Resource):
    def get(self):
        return {'user': '1'}

if __name__ == '__main__':
    app.run()

运行之后效果如下:

实践

这里我会实现一个完整的小项目来实践和介绍 Flask-RESTPlus 这个库。我们实现一个简单的 图书订单系统 ,实现用户、图书和订单的 CURD。

Model

用户 model,包含 id 和 username:

class User(object):
    user_id = None
    username = None

    def __init__(self, username: str):
        self.user_id = str(uuid.uuid4())
        self.username = username

图书 model,包含 id,名称和价格:

class Book(object):
    book_id = None
    book_name = None
    price = None

    def __init__(self, book_name: str, book_price: float):
        self.book_id = str(uuid.uuid4())
        self.book_name = book_name
        self.price = book_price

订单 model,包含 id,购买者 id,图书 id 和创建时间:

class Order(object):
    order_id = None
    user_id = None
    book_id = None
    created_at = None

    def __init__(self, user_id, book_id):
        self.order_id = str(uuid.uuid4())
        self.user_id = user_id
        self.book_id = book_id
        self.created_at = int(time.time())

蓝图

在 Flask 中构建大型 Web 项目,可以通过蓝图为路由分组,并在蓝图中添加通用的规则(url 前缀、静态文件路径、模板路径等)。这个项目我们只用一个 api 蓝图,在实际中可能会使用 openapi 蓝图,internal api 蓝图来区分大的分类。

Flask-RESTPlusclass::Api 将直接挂在在蓝图下面,这么我们即利用了 Flask 的蓝图进行对功能模块分类,也可以利用 Api 的版本对 Api 版本进行管理,对于小的模块分类,我们可以利用 Api 的 namespace,着这里我们可以分为 user namespacebook namespaceorder namespace:

Api 蓝图:

from flask import Blueprint
from flask_restplus import Api

api_blueprint = Blueprint("open_api", __name__, url_prefix="/api")
api = Api(api_blueprint, version="1.0",
          prefix="/v1", title="OpenApi", description="The Open Api Service")

然后,就可以创建出不同的 namespace,来编写自己的 api 代码了。而只需要在 app 工厂中注册该 blueprint,便可将自己的编写的 api 挂载到 flask app 中。

def create_app():
    app = Flask("Flask-Web-Demo")

    # register api namespace
    register_api()

    # register blueprint
    from apis import api_blueprint
    app.register_blueprint(api_blueprint)

    return app

要注意的是,因为 Api 中很多工具方法依赖 api 对象,因此在注册 namespace 的时候要避免循环引用,而且,这注册蓝图的时候,需要先将 namespace 注册,否则会 404。这个库的很多方法太依赖 api 对象,感觉设计并不合理,很容易就循环引用,并不是非常优雅。

注册 namespace:

def register_api():
    from apis.user_api import ns as user_api
    from apis.book_api import ns as book_api
    from apis.order_api import ns as order_api
    from apis import api
    api.add_namespace(user_api)
    api.add_namespace(book_api)
    api.add_namespace(order_api)

下面就是 Api 的编写了。

编写 Api

列表和创建

我们先完成用户的列表和创建 Api,代码如下:

from flask_restplus import Resource, fields, Namespace

from model import User
from apis import api

ns = Namespace("users", description="Users CURD api.")

user_model = ns.model('UserModel', {
    'user_id': fields.String(readOnly=True, description='The user unique identifier'),
    'username': fields.String(required=True, description='The user nickname'),
})
user_list_model = ns.model('UserListModel', {
    'users': fields.List(fields.Nested(user_model)),
    'total': fields.Integer,
})


@ns.route("")
class UserListApi(Resource):
    # 初始化数据
    users = [User("HanMeiMei"), User("LiLei")]

    @ns.doc('get_user_list')
    @ns.marshal_with(user_list_model)
    def get(self):
        return {
            "users": self.users,
            "total": len(self.users),
        }

    @ns.doc('create_user')
    @ns.expect(user_model)
    @ns.marshal_with(user_model, code=201)
    def post(self):
        user = User(api.payload['username'])
        return user

解释下上面的代码,首先需要创建一个 user model 来让 Flask-RESTPlus 知道我们如何渲染和解析 json:

user_model = ns.model('UserModel', {
    'user_id': fields.String(readOnly=True, description='The user unique identifier'),
    'username': fields.String(required=True, description='The user nickname'),
})

这里面定义了字段以及字段的描述,这些字段并不参与参数检查,而只是渲染到 api 文档上,来标记 api 将返回什么结果,以及应该怎么调用 api。

然后介绍下目前用到的装饰器:

  1. @ns.doc 来标记这个 api 的作用
  2. @ns.marshal_with 来标记如何渲染返回的 json
  3. @ns.expect 来标记我们预期什么样子的 request

运行程序我们可以看到以下结果:

我们也可以通过 try it 来调用 api:

查询和更新

因为路由是绑定到一个类上的,因此限定了这个类能处理的 url,对于 '/users/user_id' 类似的路径,需要单独的类来处理:

@ns.route("/<string:user_id>")
@ns.response(404, 'User not found')
@ns.param('user_id', 'The user identifier')
class UserInfoApi(Resource):
    users = [User("HanMeiMei"), User("LiLei")]
    print([u.user_id for u in users])

    @ns.doc("get_user_by_id")
    @ns.marshal_with(user_model)
    def get(self, user_id):
        for u in self.users:
            if u.user_id == user_id:
                return u
        ns.abort(404, "User {} doesn't exist".format(user_id))

    @ns.doc("update_user_info")
    @ns.expect(user_model)
    @ns.marshal_with(user_model)
    def put(self, user_id):
        user = None
        for u in self.users:
            if u.user_id == user_id:
                user = u
        if not user:
            ns.abort(404, "User {} doesn't exist".format(user_id))
        user.username = api.payload['username']
        return user

在这里面可以看到更改了 url 和新引入了两个装饰器:

  1. @ns.response 用来标记可能出现的 Response Status Code 并渲染在文档中
  2. @ns.param 用来标记 URL 参数

运行程序之后我们可以尝试根据 id 获得一个用户:

注意namespace 的 name 会被拼接到 url 中,比如上面 url 中的 “users” 即是 namespace name。

带嵌套的 Api

用户 Api 和图书 Api 基本一样而且简单,但是对于订单 Api 中,需要包含用户信息和图书信息,在实现上略微不同。

from flask_restplus import Resource, fields, Namespace

from model import Order, Book, User
from apis.user_api import user_model
from apis.book_api import book_model

ns = Namespace("order", description="Order CURD api.")

order_model = ns.model('OrderModel', {
    "order_id": fields.String(readOnly=True, description='The order unique identifier'),
    "user": fields.Nested(user_model, description='The order creator info'),
    "book": fields.Nested(book_model, description='The book info.'),
    "created_at": fields.Integer(readOnly=True, description='create time: unix timestamp.'),
})
order_list = ns.model('OrderListModel', {
    "orders": fields.List(fields.Nested(order_model)),
    "total": fields.Integer(description='len of orders')
})

book = Book("Book1", 10.5)
user = User("LiLei")
order = Order(user.user_id, book.book_id)


@ns.route("")
class UserListApi(Resource):

    @ns.doc('get_order_list')
    @ns.marshal_with(order_list)
    def get(self):
        return {
            "orders": [{
                "order_id": order.order_id,
                "created_at": order.created_at,
                "user": {
                    "user_id": user.user_id,
                    "username": user.username,
                },
                "book": {
                    "book_id": book.book_id,
                    "book_name": book.book_name,
                    "price": book.price,
                }
            }],
            "total": 1}

    @ns.doc('create_order')
    @ns.expect(order_model)
    @ns.marshal_with(order_model, code=201)
    def post(self):
        return {
            "order_id": order.order_id,
            "created_at": order.created_at,
            "user": {
                "user_id": user.user_id,
                "username": user.username,
            },
            "book": {
                "book_id": book.book_id,
                "book_name": book.book_name,
                "price": book.price,
            }
        }

这里使用了更灵活的格式组合,包括 fields.Nested 可以引入其他 model,因为 model 可以相互引用,因此还是有必要把这些 model 放在一起,来避免循环引用。不过由此也可以看出,Response 解析还是比较自由的。

备注:这里 return 的是一个字典,但是理想状态下应该是一个类(user 字段和 book 字段),只是因为没有数据库操作,简化处理。

到这里,这个小项目就是写完了,最后运行效果图如下:

改造

可以通过这个简单的 Demo 了解 Flask-RESTPlus 的使用,但是目前只是从零到一的写一个完成的项目,因此看起来非常容易上手,但是如果是旧项目改造,我们需要做什么?

通过上述代码,我们可以看到要做的主要是两件事:

  1. Api 层的改造
  2. 设计 Api Model

Api 层改造涉及到两点,因为 url 是由 blueprint、api obj、namespace 三个东西共同组成的,因此需要设计怎么分配,可能还有重写部分 api 的实现。但是理想的 api-service-model 架构的程序, api 应该是比较薄的一层,要接入并不困难,只是琐碎。

Api Model 一般是原有项目没有的,需要引入,其中包括的参数检查的 model(Flask-RESTPlus 提供了 Request Parsing,本文并没讨论,可以参考文档: Request Parsing )和解析 Response 的 model,这些需要梳理所有 api 和字段,工作量不小,如果数据库模型设计合理的话也许能减轻部分工作量。

Swagger

Swagger 是一款非常流行的 Api 文档管理、交互工具,适用于在团队中的 Api 管理,以及服务组件对接。其好用与重要程度不必赘言,下面基于上文的 demo,完成一个 Swagger 文档以及基于文档生成用于对接的 client。

获得 Swagger 文档

Flask-RESTPlus 是已经集成了 Swagger UI 的,在运行时所获得界面即是通过 Swagger UI 渲染的。而我们目前需要的是获取 Swagger 文档 json 或 yaml 文件。

在控制台可以看到,在访问程序时:

是的,这就是 Swagger 文档:

代码生成

使用 Swagger 生成文档需要

在 macOS 下载:

brew install swagger-codegen

然后可以通过 help 名称查看帮助:

Hypo-MBP:~ hypo$ swagger-codegen help
usage: swagger-codegen-cli <command> [<args>]

The most commonly used swagger-codegen-cli commands are:
    config-help   Config help for chosen lang
    generate      Generate code with chosen lang
    help          Display help information
    langs         Shows available langs
    meta          MetaGenerator. Generator for creating a new template set and configuration for Codegen.  The output will be based on the language you specify, and includes default templates to include.
    validate      Validate specification
    version       Show version information

See 'swagger-codegen-cli help <command>' for more information on a specific
command.

生成 Python client:

swagger-codegen generate -i http://127.0.0.1:5000/api/swagger.json -l python

执行完成后,便可以在当前路径的 swagger_client 下找到 api client 了。

总结

本文介绍了 Flask-RESTPlus 的使用,因为其本身就支持 Swagger 语法并内置了 Swagger UI,所以 Swagger 对接简单异常。因此,主要工作量放在了编写 api 层上,包括 model,以及 api 中起到解释说明作用的装饰器。虽然在代码上需要编写不少不必要的代码(介绍说明用的描述等),但是这些额外代码辅助生成了与代码一致的文档,在组件对接和维护上,实则降低了成本。

2018/5/5 posted in  Python 黑魔法

GTD 入门笔记

虽然时间线拉的比较长,《Getting things done》 总算是看完了。

看完 GTD 之后对 GTD 理论有了更进一步的理解,通过通读,更加清晰了 GTD 四步法:收集清理组织回顾,也明确的四步法与执行的关系,并顺便根据自己的理解将 GTD 这一套理论简化为 4 个原则,以方便执行和安利。

四步法与执行

曾经在知乎看到有人反对在不了解时间管理的情况下直接实践 GTD,而更是推荐番茄工作法之类简单的理论。在看完 GTD 这本书之后对这个观点有些认可,GTD 的执行需要形成一大堆习惯,而作者更是在书中坦言,将 GTD 完全接受并融入自己生活中,可能需要大约两年的摸索。 因此,在正式实践 GTD 之前请确定自己有改变生活的决心与坚持的勇气。

收集

GTD 的启动是从收集开始的,一切事情皆在被收集的清单内(没看错,是一切事物)。书中认为第一次收集一般花费 3-6 个小时,需要将一切事物,桌子上的、抽屉里的、角落里的,所有的东西都放在收集篮,而是只是单纯的放进收集篮子,并不做任何处理,处理这个工作放在后面。

而收集这个动作自 GTD 实践开始,就不会停止,而且实时进行,任何的灵光一闪,任何的想到的事情,都需要收集起来。

我恰好是在工作最混乱的时候启用 GTD 的(17 年 9 月左右),当时并没有完全理解 GTD,而是比着葫芦画瓢,将所有想做的事情扔到收件箱里,并每次做事情的时候从收件箱找找能做的事情。

这是一种非常糟糕的实践,但是却有用,因为我知道我所有要做的事情,开会确定的效果,吃饭时想到的新实现,洗澡时想到的潜在 bug,全在收件箱里,我不怕遗漏,我信任这个系统,因此不会像无头苍蝇不知出路与莫名烦躁。

收集的意义就在于把所有突然想到的事情记录下来,让这件事情在变成事故前完成,这样就可以大量减少救火的情况,而且一个良好的收集系统会让人产生安全感,我可以依赖这个系统,不怕自己遗忘,就像一个外脑。

清理

在收集的时候避免任何进一步思考和执行,而是把思考的阶段放在清理阶段。

清理阶段非常简单,就是把收集阶段的东西进行清理与梳理,当处理一件事情时,可以采取以下的选择:

  1. 该事情没啥意义:扔到垃圾箱
  2. 该事情两分钟内就能做完:立马去做
  3. 该事情两分钟内做不完:推迟、委托他人

要明白为什么这样选择首先要明白两点:

  1. 清理是什么
  2. 两分钟原则

清理是什么?收件箱其实就是一个未处理清单,一堆从没有思考过是什么,怎么做,目标是什么的三无任务清单,清理就是将这堆任务想清楚,要不要做,要做成什么样子,要怎么做,并把收件箱清空。

因此,对于没有意义的事情,应该立即丢弃,而把有用的保留下来。不能让 GTD 系统中存在任何垃圾,毕竟垃圾越多意味着越低效。

两分钟原则是清理阶段的执行方式,如果一件事两分钟就可以解决,那就不要让这件事流入后面的处理阶段,因为一件事在 GTD 中经过的阶段越长,这件事的执行效率越低,一般一件事从收集后,经过清理,组织,回顾需要 3 分钟,而这件事本身不过两分钟就可以完成,那么把这件事在清理阶段就完成的,不要再拖延了。

我曾经疑惑为什么超过两分钟的事情就要 delay 呢?因为 清理 本身就是一件事情,如果在清理的时候就执行非常耗时的任务,就会中断清理,从而进入了不同从收件箱拿任务执行的恶性循环,从而让收件箱里的垃圾越来越多,最后放弃 GTD。因此我们要确保清理这件事情本身能成功完成,而耗时任务以后再说,哪怕是等清理完之后再说。

清理是对事情初步思考的环节,将收件箱里的垃圾丢掉,简单的事情完成,复杂的事情简单思考,那么剩下的事情,就是那些需要好好思考的任务了,而这些任务可以流入下一个环节。

组织

无论是小到写篇关于 GTD 的博客,还是大到计划买一套房,无论是今天下午就要着手做的事情,还是未来两年内完成的事情,只要这件事又完成的意义,就会流入组织环节。

虽然书中使用了 “组织” 一词,不过我认为 “梳理” 更适合。因为组织是在做两件事情:

  1. 梳理清单,分门别类
  2. 梳理任务,想清楚怎么执行

如果这件事近期不需要执行,而且你也不知道什么时候会执行,比如要看到书单,要学一门外语,那么可以把这件事放到 “也许、将来” 清单。如果这些事情需要在家完成,那么可以将这件事归到 “在家” 清单。将事情分门别类,可以方便下一步的执行,比如在家的时候,可以看一下 “在家” 这个清单,而书荒的时候,可以去翻翻看 “也许、将来” 里的书单。

清单的分类一般按照地点或客观条件,比如 “公司”、“在家”、“杂货店”、“需要电脑”,也可能会依赖某个人:“老板”,“女朋友”。这些都可以成为清单的梳理规则,考虑到清单是执行和回顾的依据,可以怎么有利于执行和回顾怎么分类。

除了梳理清单,还有把 “任务” 变为 “项目”,任务指的是我们在收件箱收集的事项,比如:“在卧室中安装空气净化器”。而这种任务完全没有可执行力,因此我们需要 “梳理任务”,想清楚下一步怎么做,比如:“调研有哪些空气净化器性价比高”。

回顾

如果一件事被收集到了 GTD 系统,而这个体系并没有经常更新,从而出现了遗忘的事项,那么你就不会再信任这个系统,从而又一次放弃 GTD。

因此,当一件件事被收集清理组织之后,会产生一个又一个的清单,如果清单没有被更新,这就和我们把要做的随手一记一样,并没有实质改观。因此我们需要定期回顾清单,比如曾经记下:“学习日语”,那么在回顾的时候你会又想起这件事情,说不定这次你就会考虑下怎么去做。

而有的项目因为没有更新,可能已经没有了执行的必要,或者有新的步骤需要添加,这时候在回顾环节可以更新这些项目。

回顾是更新整个系统的重要一环,也是防止整个系统不会落后于现实的保险。而作者建议在最后一个工作日拿出下班前的一到两个小时来回顾,以防止遗忘当周没有完成的事项。

执行

这里的执行分两种,一个是 GTD 整个流程的执行和 GTD 中托管的任务的执行。

GTD 四步走包括:收集,清理,组织,回顾。其中收集是无时无刻执行的,只要想起来就可以向收件箱中添加一项,因为有手机等移动设备,其实只要养成习惯,基本就没有耗时的感觉。而回顾是定期执行的,比如周五下班前,那么我们只需要设定一个闹钟或者提醒事项,提前预留出时间,或者稍微加一下班,也能让这个环节执行完。而 GTD 比较麻烦就是清理和组织。

清理和组织环节是比较消耗时间和脑力的,我们要把事情想清楚并归类,甚至还要想清楚下一步怎么执行,作者说他一般是在出差路上完成的,对于长期出差的商务人士,当然是个不错的选择。而对于打工仔,我一般是在饭间完成的,我更建议能经常拿出时间来清理和组织,尤其是组织,因为组织环节会梳理好下一步清单,而再次审视要执行的下一步清单,往往会产生改进,并获得更优的下一步行动列表。不过要注意的是,这两个环节需要脑力,在不清醒的时候并不适合这样的行动。

在执行任务上,人和计算机最大的不同点是人非常不擅长进程(正在执行的事情)切换,如果有新的事情插进来,往往会花很多事情来回忆(已经完成了啥)和思考(现在要做啥),就和每个进程都有一个上下文一样,任何任务也同样需要整个上下文,而 GTD 的下一步列表其实就是充当上下文的记录的作用。

因此,对于事情的事情就是在下一步清单中找到可以做的事情,并完成它,在执行的时候不应该产生 “我该怎么做”,“我已经做了啥” 这样的疑问,如果有的话可能还需要反省 GTD 的实践。

任务的时间点

在 GTD 中好像没有提到 “每日任务” 这样带有时间点的任务,因为在 GTD 中不建议,或者说反对设定每日任务,因为生活充满变数,人们经常完不成每日任务,而在多次把 “今日完成” 移动到 “明日完成” 之后,人们就会对这个日常管理系统失望并不再信任。

GTD 中认为一件事情分为两种:

  1. ASAP(As Soon As Possible,越快越好)的任务
  2. 有时间点的任务

第一类任务比如:“写一篇博客”,第二类比如:“明天 9 点预约了牙医”。

而 GTD 的下一步清单维护的其实只是第一种,而第二种才应该放在日程表中,这样就可以在指定日期提醒自己这件事在今天必须要做。而且日程表中的任务是比下一步清单中的任务要高的。

GTD 原则

GTD 是一套复杂的实践,我在尝试把它简化(方便执行和安利给别人),总结为:收集习惯下一步清单关注结果定期回顾四个原则。

收集习惯

收集事无巨细,应该养成一种习惯,任何灵光一闪,或者将要提醒自己的事情,都要被收集。

下一步清单

收集的事情要被清理和整理,我们要明确每件事的下一步是什么,并总结出下一步清单,而剩下的就是无脑执行下一步清单里的内容。

关注结果

所有的事情都是可量化的,我们要想清楚这件事我们的目标是什么,想要达成的结果是什么,只有下一步可量化,我们才有执行和推进事情的依据。

定期回顾

定期回顾是保证 GTD 系统是最新有效的保险,定期的回顾非常重要,更新过去的想法,添加新的想法。

总结

作者在最后提到,很多人认为如此繁琐的步骤是浪费时间,只是让行动的效率变得更低下。那是因为还没完全依赖这个系统,毕竟如果考虑到全盘依赖大脑时,每次切换事情都要思考:“我之前做了啥”,“我现在应该做啥”,“我要做到什么程度” 所消耗的时间其实是更多的。

GTD 的一些思想在软件行业好像已经有了广泛应用,这一套收集、清理、组织、回顾,如果我们把 “task” 这个词换为 “user story”,把清单换成泳道是不是仿佛很熟悉。

无论是个人,还是团队,时间管理其实并不能获得更多时间,相反会消耗时间在管理本身上,但这道流程的存在,能防止事情失控,一切尽在掌控之中。


附图一张,在星巴克写了一下午,写完才发现被冷风吹的手脚冰凉。

2018/4/30 posted in  坏笔记不如好记性

非 flask 程序实现 Flask Shell

2018/4/29 posted in  Python 黑魔法