Django RestFramework 请求流程、解析器、序列化器分析

本文重点在于讲解什么是REST规范及Django RestFramework中的APIview请求流程,这个流程包括源代码分析和后续的解析器组件及序列化器组件。

1.什么是REST

编程是数据结构和算法的结合,而在Web类型的App中,我们对于数据的操作请求是通过url来承载的,本文详细介绍了REST规范和CBV请求流程。

编程是数据结构和算法的结合,小程序如简单的计算器,我们输入初始数据,经过计算,得到最终的数据,这个过程中,初始数据和结果数据都是数据,而计算过程是我们所说的广义上的算法。

大程序,如一个智能扫地机器人,我们可以设置打扫的距离,左右摆动的幅度来打扫房间,这里面打扫的举例,摆动幅度,都是数据,而打扫的过程是较为复杂的算法过程,总之,也是算法,即程序的实现方式。

另外,我们还可以设置打扫时间等等初始数据。

总之一句话,编程即数据结构和算法的结合。简单的程序可能不需要跟用户交互数据,但是现代的应用程序几乎都需要跟用户进行交互,不分应用程序类型,不管是CS型还是BS型的程序都是如此,而Python最擅长的Web App即BS型的程序,就是通过url和http来跟用户进行数据交互的,通过url和http请求,用户可以操作服务器端的程序,主要操作分为:增、删、改、查几类

引入

在开始之前,我们回顾一下咱们之前写过的图书管理系统项目,请仔细回想一下,对于该项目的设计,我们大概是以下面这种方式来实现的

传统url设计风格
  • url各式各样,设计混乱

理论上来说,这种方式完全可以实现我们的需求,但是一旦项目丰富起来,随着数据量增加,随着各个业务系统之间的逻辑关系不断的复杂,url会越来越复杂,理论上来说,不管是什么类型、什么名称的url都能指向具体的业务逻辑(视图函数),从而实现业务需求,但是如果没有明确的规范,因每个人的思维方式不一样、命名方式不一样而导致的url非常的乱,不方便项目的后期维护和扩展。

  • 对于请求处理成功或者失败的返回信息没有明确的响应信息规范,返回给客户端的信息往往都是很随意的

以上这些情况的出现,导致了很多的问题,让互联网的世界变得杂乱不堪,日益复杂且臃肿。

因此http协议创始人警告我们这些凡人们正在错误的使用http协议,除了警告,他还发表了一篇博客,大概意思就是教大家如何正确使用http协议,如何正确定义url,这就是REST(Representational State Transfer),不需要管这几个英文单词代表什么意思,只需要记住下面一句话:

  • 用url唯一定位资源,用Http请求方式(GET, POST, DELETE, PUT)描述用户行为

根据这句话,我们重新定义图书管理系统中的url

RESTful Api设计风格

可以看到,url非常简洁优雅,不包含任何操作,不包含任何动词,简简单单,用来描述服务器中的资源而已,服务器根据用户的请求方式对资源进行各种操作。而对数据的操作,最常见的就是CRUD(创建,读取,更新,删除),通过不同的请求方式,就足够描述这些操作方式了。

如果不够用,Http还有其他的请求方式呢!比如:PATCH,OPTIONS,HEAD, TRACE, CONNECT。

REST定义返回结果

每一种请求方式的返回结果不同。

REST定义错误信息
{
    "error": "Invalid API key"
}

通过一个字典,返回错误信息。

这就是REST,上图中的url就是根据REST规范进行设计的RESTful api。

因此REST是一种软件架构设计风格,不是标准,也不是具体的技术实现,只是提供了一组设计原则和约束条件。

它是目前最流行的 API 设计规范,用于 Web 数据接口的设计。2000年,由Roy Fielding在他的博士论文中提出,Roy Fielding是HTTP规范的主要编写者之一。

那么,我们所要讲的Django RestFramework与rest有什么关系呢?

其实,DRF(Django RestFramework)是一套基于Django开发的、帮助我们更好的设计符合REST规范的Web应用的一个Django App,所以,本质上,它是一个Django App。

2.为什么使用DRF

从概念就可以看出,有了这样的一个App,能够帮助我们更好的设计符合RESTful规范的Web应用,实际上,没有它,我们也能自己设计符合规范的Web应用。下面的代码演示如何手动实现符合RESTful规范的Web应用。

class CoursesView(View):
    def get(self, request):
        courses = list()

        for item in Courses.objects.all():
            course = {
                "title": item.title,
                "price": item.price,
                "publish_date": item.publish_date,
                "publish_id": item.publish_id
            }

            courses.append(course)

        return HttpResponse(json.dumps(courses, ensure_ascii=False))

如上代码所示,我们获取所有的课程数据,并根据REST规范,将所有资源的通过对象列表返回给用户。

可见,就算没有DRF我们也能够设计出符合RESTful规范的接口甚至是整个Web App,但是,如果所有的接口都自定义,难免会出现重复代码,为了提高工作效率,我们建议使用优秀的工具。

DRF就是这样一个优秀的工具,另外,它不仅仅能够帮助我们快速的设计符合REST规范的接口,还提供诸如认证、权限等等其他的强大功能。

什么时候使用DRF?

前面提到,REST是目前最流行的 API 设计规范,如果使用Django开发你的Web应用,那么请尽量使用DRF,如果使用的是Flask,可以使用Flask-RESTful。

3.Django View请求流程

首先安装Django,然后安装DRF:

pip install django
pip install djangorestframework

安装完成之后,我们就可以开始使用DRF框架来实现咱们的Web应用了,本篇文章包括以下知识点:

  • APIView
  • 解析器组件
  • 序列化组件

介绍DRF,必须要介绍APIView,它是重中之重,是下面所有组件的基础,因为所有的请求都是通过它来分发的,至于它究竟是如何分发请求的呢?

想要弄明白这个问题,我们就必须剖析它的源码,而想要剖析DRF APIView的源码,我们需要首先剖析django中views.View类的源码,为什么使用视图类调用as_view()之后,我们的请求就能够被不同的函数处理呢?

源码中最后会通过getattr在self中查找request.method.lower(),也就是get、post或者delete这些方法中的一个,那么,self是谁,就是至关重要的一点,前面讲到过,谁调用类中的方法,self就指向谁,此时,一层层往回找,我们会发现,self = cls(**initkwargs),self就是我们视图类的实例化对象,所以,dispatch函数肯定会到该视图类中找对应的方法(get或者post)。

接下来是提问时间,请问如果有如下函数,不修改函数内容的情况下,如何给函数新增一个统计执行时间的功能:

def outer(func):
    def inner(*args, **kwargs):
        import time
        start_time = time.time()
        ret = func(*args, **kwargs)
        end_time = time.time()
        print("This function elapsed %s" % str(end_time - start_time))
        return ret
    return inner


@outer
def add(x, y):
    return x + y

这是函数,如果是类呢?面向对象编程,如何扩展你的程序,比如有如下代码:

class Person(object):
    def show(self):
        print("Person's show method executed!")
        

class MyPerson(Person):
    def show(self):
        print("MyPerson's show method executed")
        super().show()
        

mp = MyPerson()
mp.show()

这就是面向对象的程序扩展,现在大家是否对面向对象有了更加深刻的认识呢?接下来给大家十分钟时间,消化一下上面两个概念,然后请思考,那么假设你是Django RestFramework的开发者,你想自定制一些自己想法,如何实现。

好了,相信大家都已经有了自己的想法,接下来,我们一起来分析一下,Django RestFramework的APIView是如何对Django框架的View进行功能扩展的。

from django.shortcuts import HttpResponse

import json

from .models import Courses

# 引入APIView
from rest_framework.views import APIView
# Create your views here.


class CoursesView(APIView):  # 继承APIView而不是原来的View
    def get(self, request):
        courses = list()

        for item in Courses.objects.all():
            course = {
                "title": item.title,
                "description": item.description
            }

            courses.append(course)

        return HttpResponse(json.dumps(courses, ensure_ascii=False))

以上就是Django RestFramework APIView的请求处理流程,我们可以通过重写dispatch()方法或者重写as_view()方法来自定制自己的想法。

那么,Django RestFramework到底自定制了哪些内容呢?在本文的最开始,我们已经介绍过了,就是那些组件,比如解析器组件、序列化组件、权限、频率组件等。

Ajax发送Json数据给服务器

接下来,我们就开始介绍Django RestFramework中的这些组件,首先,最基本的,就是解析器组件,在介绍解析器组件之前,我提一个问题,请大家思考,如何发送Json格式的数据给后端服务器?

好了,时间到,请看下面的代码,通过ajax请求,我们可以发送json格式的数据到后端:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
  <script src="/static/jquery-1.10.2.min.js"></script>
</head>
<body>
  <form action="" method="post" enctype="application/x-www-form-urlencoded">
    {% csrf_token %}
    用户名: <input type="text" name="username"/>
    密码:  <input type="password" name="password"/>
    提交:  <input type="submit" value="提交"/>
  </form>

  <hr>
  <button class="btn">点击发送Ajax请求</button>

  <script>
    $(".btn").click(function () {
      $.ajax({
        url: '',
        type: 'post',
        contentType: 'application/json',
        data: JSON.stringify({
          username: "alex",
          password: 123
        }
        ),
        success: function (data) {
          console.log(data);
        }
      })
    })

  </script>

</body>
</html>

通过上文的知识点复习我们已经知道,Content-Type用来定义发送数据的编码协议,所以,在上面的代码中,我们指定Content-Type为application/json,即可将我们的Json数据发送到后端,那么后端如何获取呢?

服务器对Json数据的处理方式

按照之前的方式,我们使用request.POST, 如果打印该值,会发现是一个空对象:request post <QueryDict: {}>,该现象证明Django并不能处理请求协议为application/json编码协议的数据,我们可以去看看request源码,可以看到下面这一段:

if self.content_type == 'multipart/form-data':
    if hasattr(self, '_body'):
        # Use already read data
        data = BytesIO(self._body)
    else:
        data = self
    try:
        self._post, self._files = self.parse_file_upload(self.META, data)
    except MultiPartParserError:
        # An error occurred while parsing POST data. Since when
        # formatting the error the request handler might access
        # self.POST, set self._post and self._file to prevent
        # attempts to parse POST data again.
        # Mark that an error occurred. This allows self.__repr__ to
        # be explicit about it instead of simply representing an
        # empty POST
        self._mark_post_parse_error()
        raise
elif self.content_type == 'application/x-www-form-urlencoded':
    self._post, self._files = QueryDict(self.body, encoding=self._encoding), MultiValueDict()
else:
    self._post, self._files = QueryDict(encoding=self._encoding), MultiValueDict()

可见Django原生解析器并不处理application/json编码协议的数据请求,好了,有了这样的认识之后,咱们就可以开始正式介绍DRF的解析器了,解析器,顾名思义,就是用来解析数据的请求的。

虽然Django的原生解析器不支持application/json编码协议,但是我们可以通过拿到原始的请求数据(request.body)来手动处理application/json请求,虽然这种方式不方便,也并不推荐,请看如下代码:

class LoginView(View):
    def get(self, request):
        return render(request, 'classbasedview/login.html')

    def post(self, request):
        print(request.POST)  # <QueryDict: {}>
        print(request.body)  # b'{"username":"alex","password":123}'
        data = request.body.decode('utf-8')
        dict_data = json.loads(data)

        username = dict_data['username']
        password = dict_data['password']

        return HttpResponse(json.dumps(dict_data))

通过上面的代码,我们可以通过request.body手动处理application/json请求,不过,如上文所说,并不推荐。

4.DRF 解析器组件

首先,来看看解析器组件的使用,稍后我们一起剖析其源码:

from django.http import JsonResponse

from rest_framework.views import APIView
from rest_framework.parsers import JSONParser, FormParser
# Create your views here.


class LoginView(APIView):
    parser_classes = [FormParser]

    def get(self, request):
        return render(request, 'parserver/login.html')

    def post(self, request):
        # request是被drf封装的新对象,基于django的request
        # request.data是一个property,用于对数据进行校验
        # request.data最后会找到self.parser_classes中的解析器
        # 来实现对数据进行解析
        
        print(request.data)  # {'username': 'alex', 'password': 123}

        return JsonResponse({"status_code": 200, "code": "OK"})

使用方式非常简单,分为如下两步:

  • from rest_framework.views import APIView
  • 继承APIView
  • 直接使用request.data就可以获取Json数据

如果你只需要解析Json数据,不允许任何其他类型的数据请求,可以这样做:

  • from rest_framework.parsers import JsonParser
  • 给视图类定义一个parser_classes变量,值为列表类型[JsonParser]
  • 如果parser_classes = [], 那就不处理任何数据类型的请求了

问题来了,这么神奇的功能,DRF是如何做的?因为昨天讲到Django原生无法处理application/json协议的请求,所以拿json解析来举例,请同学们思考一个问题,如果是你,你会在什么地方加入新的Json解析功能?

首先,需要明确一点,我们肯定需要在request对象上做文章,为什么呢?

因为只有有了用户请求,我们的解析才有意义,没有请求,就没有解析,更没有处理请求的逻辑,所以,我们需要弄明白,在整个流程中,request对象是什么时候才出现的,是在绑定url和处理视图之间的映射关系的时候吗?我们来看看源码:

@classonlymethod
def as_view(cls, **initkwargs):
    """Main entry point for a request-response process."""
    for key in initkwargs:
        if key in cls.http_method_names:
            raise TypeError("You tried to pass in the %s method name as a "
                            "keyword argument to %s(). Don't do that."
                            % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

def view(request, *args, **kwargs):
    self = cls(**initkwargs)
    if hasattr(self, 'get') and not hasattr(self, 'head'):
        self.head = self.get
        self.request = request
        self.args = args
        self.kwargs = kwargs
        return self.dispatch(request, *args, **kwargs)
    view.view_class = cls
    view.view_initkwargs = initkwargs

    # take name and docstring from class
    update_wrapper(view, cls, updated=())

    # and possible attributes set by decorators
    # like csrf_exempt from dispatch
    update_wrapper(view, cls.dispatch, assigned=())
    return view

看到了吗?在执行view函数的时候,那么什么时候执行view函数呢?当然是请求到来,根据url查找映射表,找到视图函数,然后执行view函数并传入request对象,所以,如果是我,我可以在这个视图函数里面加入处理application/json的功能:

@classonlymethod
def as_view(cls, **initkwargs):
    """Main entry point for a request-response process."""
    for key in initkwargs:
        if key in cls.http_method_names:
            raise TypeError("You tried to pass in the %s method name as a "
                            "keyword argument to %s(). Don't do that."
                            % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

def view(request, *args, **kwargs):
    if request.content_type == "application/json":
        import json
        return HttpResponse(json.dumps({"error": "Unsupport content type!"}))

    self = cls(**initkwargs)
    if hasattr(self, 'get') and not hasattr(self, 'head'):
        self.head = self.get
        self.request = request
        self.args = args
        self.kwargs = kwargs
        return self.dispatch(request, *args, **kwargs)
    view.view_class = cls
    view.view_initkwargs = initkwargs

    # take name and docstring from class
    update_wrapper(view, cls, updated=())

    # and possible attributes set by decorators
    # like csrf_exempt from dispatch
    update_wrapper(view, cls.dispatch, assigned=())
    return view

看到了吧,然后我们试试发送json请求,看看返回结果如何?是不是非常神奇?事实上,你可以在这里,也可以在这之后的任何地方进行功能的添加。

那么,DRF是如何做的呢?我们在使用的时候只是继承了APIView,然后直接使用request.data,所以,我斗胆猜测,功能肯定是在APIView中定义的,废话,具体在哪个地方呢?

接下来,我们一起来分析一下DRF解析器源码,看看DRF在什么地方加入了这个功能。

上图详细描述了整个过程,最重要的就是重新定义的request对象,和parser_classes变量,也就是我们在上面使用的类变量。好了,通过分析源码,验证了我们的猜测。

5.序列化组件

首先我们要学会使用序列化组件。定义几个 model:

from django.db import models

# Create your models here.


class Publish(models.Model):
    nid = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    city = models.CharField(max_length=32)
    email = models.EmailField()

    def __str__(self):
        return self.name


class Author(models.Model):
    nid = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    age = models.IntegerField()

    def __str__(self):
        return self.name


class Book(models.Model):
    title = models.CharField(max_length=32)
    publishDate = models.DateField()
    price = models.DecimalField(max_digits=5, decimal_places=2)
    publish = models.ForeignKey(to="Publish", to_field="nid", on_delete=models.CASCADE)
    authors = models.ManyToManyField(to="Author")

    def __str__(self):
        return self.title

通过序列化组件进行GET接口设计

设计url,本次我们只设计GET和POST两种接口:

from django.urls import re_path

from serializers import views

urlpatterns = [
    re_path(r'books/$', views.BookView.as_view())
]

我们新建一个名为app_serializers.py的模块,将所有的序列化的使用集中在这个模块里面,对程序进行解耦:

# -*- coding: utf-8 -*-
from rest_framework import serializers

from .models import Book


class BookSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=128)
    publish_date = serializers.DateTimeField()
    price = serializers.DecimalField(max_digits=5, decimal_places=2)
    publish = serializers.CharField(max_length=32)
    authors = serializers.CharField(max_length=32)

接着,使用序列化组件,开始写视图类:

# -*- coding: utf-8 -*-
from rest_framework.views import APIView
from rest_framework.response import Response

# 当前app中的模块
from .models import Book
from .app_serializer import BookSerializer

# Create your views here.

class BookView(APIView):
    def get(self, request):
        origin_books = Book.objects.all()
        serialized_books = BookSerializer(origin_books, many=True)

        return Response(serialized_books.data)

如此简单,我们就已经,通过序列化组件定义了一个符合标准的接口,定义好model和url后,使用序列化组件的步骤如下:

  • 导入序列化组件:from rest_framework import serializers
  • 定义序列化类,继承serializers.Serializer(建议单独创建一个专用的模块用来存放所有的序列化类):class BookSerializer(serializers.Serializer):pass
  • 定义需要返回的字段(字段类型可以与model中的类型不一致,参数也可以调整),字段名称必须与model中的一致
  • 在GET接口逻辑中,获取QuerySet
  • 开始序列化:将QuerySet作业第一个参数传给序列化类,many默认为False,如果返回的数据是一个列表嵌套字典的多个对象集合,需要改为many=True
  • 返回:将序列化对象的data属性返回即可

上面的接口逻辑中,我们使用了Response对象,它是DRF重新封装的响应对象。该对象在返回响应数据时会判断客户端类型(浏览器或POSTMAN),如果是浏览器,它会以web页面的形式返回,如果是POSTMAN这类工具,就直接返回Json类型的数据。

此外,序列化类中的字段名也可以与model中的不一致,但是需要使用source参数来告诉组件原始的字段名,如下:

class BookSerializer(serializers.Serializer):
    BookTitle = serializers.CharField(max_length=128, source="title")
    publishDate = serializers.DateTimeField()
    price = serializers.DecimalField(max_digits=5, decimal_places=2)
    # source也可以用于ForeignKey字段
    publish = serializers.CharField(max_length=32, source="publish.name")
    authors = serializers.CharField(max_length=32)

下面是通过POSTMAN请求该接口后的返回数据,大家可以看到,除ManyToManyField字段不是我们想要的外,其他的都没有任何问题:

[
    {
        "title": "Python入门",
        "publishDate": null,
        "price": "119.00",
        "publish": "浙江大学出版社",
        "authors": "serializers.Author.None"
    },
    {
        "title": "Python进阶",
        "publishDate": null,
        "price": "128.00",
        "publish": "清华大学出版社",
        "authors": "serializers.Author.None"
    }
]

那么,多对多字段如何处理呢?如果将source参数定义为”authors.all”,那么取出来的结果将是一个QuerySet,对于前端来说,这样的数据并不是特别友好,我们可以使用如下方式:

class BookSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=32)
    price = serializers.DecimalField(max_digits=5, decimal_places=2)
    publishDate = serializers.DateField()
    publish = serializers.CharField()
    publish_name = serializers.CharField(max_length=32, read_only=True, source='publish.name')
    publish_email = serializers.CharField(max_length=32, read_only=True, source='publish.email')
    # authors = serializers.CharField(max_length=32, source='authors.all')
    authors_list = serializers.SerializerMethodField()

    def get_authors_list(self, authors_obj):
        authors = list()
        for author in authors_obj.authors.all():
            authors.append(author.name)

        return authors

请注意,get_必须与字段名称一致,否则会报错。

通过序列化组件进行POST接口设计

接下来,我们设计POST接口,根据接口规范,我们不需要新增url,只需要在视图类中定义一个POST方法即可,序列化类不需要修改,如下:

# -*- coding: utf-8 -*-
from rest_framework.views import APIView
from rest_framework.response import Response

# 当前app中的模块
from .models import Book
from .app_serializer import BookSerializer

# Create your views here.


class BookView(APIView):
    def get(self, request):
        origin_books = Book.objects.all()
        serialized_books = BookSerializer(origin_books, many=True)

        return Response(serialized_books.data)

    def post(self, request):
        verified_data = BookSerializer(data=request.data)

        if verified_data.is_valid():
            book = verified_data.save()
            # 可写字段通过序列化添加成功之后需要手动添加只读字段
            authors = Author.objects.filter(nid__in=request.data['authors'])
            book.authors.add(*authors)

            return Response(verified_data.data)
        else:
            return Response(verified_data.errors)

POST接口的实现方式,如下:

  • url定义:需要为post新增url,因为根据规范,url定位资源,http请求方式定义用户行为
  • 定义post方法:在视图类中定义post方法
  • 开始序列化:通过我们上面定义的序列化类,创建一个序列化对象,传入参数data=request.data(application/json)数据
  • 校验数据:通过实例对象的is_valid()方法,对请求数据的合法性进行校验
  • 保存数据:调用save()方法,将数据插入数据库
  • 插入数据到多对多关系表:如果有多对多字段,手动插入数据到多对多关系表
  • 返回:将插入的对象返回

请注意,因为多对多关系字段是我们自定义的,而且必须这样定义,返回的数据才有意义,而用户插入数据的时候,serializers.Serializer没有实现create,我们必须手动插入数据,就像这样:

# 第二步, 创建一个序列化类,字段类型不一定要跟models的字段一致
class BookSerializer(serializers.Serializer):
    # nid = serializers.CharField(max_length=32)
    title = serializers.CharField(max_length=128)
    price = serializers.DecimalField(max_digits=5, decimal_places=2)
    publish = serializers.CharField()
    # 外键字段, 显示__str__方法的返回值
    publish_name = serializers.CharField(max_length=32, read_only=True, source='publish.name')
    publish_city = serializers.CharField(max_length=32, read_only=True, source='publish.city')
    # authors = serializers.CharField(max_length=32) # book_obj.authors.all()

    # 多对多字段需要自己手动获取数据,SerializerMethodField()
    authors_list = serializers.SerializerMethodField()

    def get_authors_list(self, book_obj):
        author_list = list()

        for author in book_obj.authors.all():
            author_list.append(author.name)

        return author_list

    def create(self, validated_data):
        # {'title': 'Python666', 'price': Decimal('66.00'), 'publish': '2'}
        validated_data['publish_id'] = validated_data.pop('publish')
        book = Book.objects.create(**validated_data)

        return book

    def update(self, instance, validated_data):
        # 更新数据会调用该方法
        instance.title = validated_data.get('title', instance.title)
        instance.publishDate = validated_data.get('publishDate', instance.publishDate)
        instance.price = validated_data.get('price', instance.price)
        instance.publish_id = validated_data.get('publish', instance.publish.nid)

        instance.save()

        return instance

这样就会非常复杂化程序,如果我希望序列化类自动插入数据呢?

这是问题一:如何让序列化类自动插入数据?

另外问题二:如果字段很多,那么显然,写序列化类也会变成一种负担,有没有更加简单的方式呢?

答案是肯定的,我们可以这样做:

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book

        fields = ('title',
                  'price',
                  'publish',
                  'authors',
                  'author_list',
                  'publish_name',
                  'publish_city'
                  )
        extra_kwargs = {
            'publish': {'write_only': True},
            'authors': {'write_only': True}
        }

    publish_name = serializers.CharField(max_length=32, read_only=True, source='publish.name')
    publish_city = serializers.CharField(max_length=32, read_only=True, source='publish.city')

    author_list = serializers.SerializerMethodField()

    def get_author_list(self, book_obj):
        # 拿到queryset开始循环 [{}, {}, {}, {}]
        authors = list()

        for author in book_obj.authors.all():
            authors.append(author.name)

        return authors

步骤如下:

  • 继承ModelSerializer:不再继承Serializer
  • 添加extra_kwargs类变量:extra_kwargs = {‘publish’: {‘write_only’: True}}

使用ModelSerializer完美的解决了上面两个问题。好了,这就是今天的全部内容。

参考资料:

https://pizzali.github.io/2018/12/07/DRF%E4%B9%8BREST%E8%A7%84%E8%8C%83%E4%BB%8B%E7%BB%8D%E5%8F%8AView%E8%AF%B7%E6%B1%82%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90/

https://pizzali.github.io/2018/12/07/DRF%E4%B9%8B%E8%A7%A3%E6%9E%90%E5%99%A8%E7%BB%84%E4%BB%B6%E5%8F%8A%E5%BA%8F%E5%88%97%E5%8C%96%E7%BB%84%E4%BB%B6/

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!

给作者打赏,选择打赏金额
¥1¥5¥10¥20¥50¥100¥200 自定义

​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Django Celery 异步与定时任务实战教程

Django与Celery是基于Python进行Web后端开发的核心搭配,在运营开发(即面向企业内部)的场景中非常常见。

下面是基于Django的Celery异步任务和定时任务的实战教程,大家觉得有用的话点个赞/在看吧!

1.配置Django Celery

配置celery主要有几点:

1. 在settings.py的同级目录下,创建celery.py文件(名字自己随意取),这个文件主要是用来生成celery的实例app.

from __future__ import absolute_import, unicode_literals
import os
from celery import Celery
from django.conf import settings

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'NBAsite.settings')

app = Celery('NBAsite',broker='redis://localhost:6379/0',backend='redis://localhost')
app.config_from_object('django.conf:settings',namespace='CELERY')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

我们将 celery 实例的 broker 和 backend 都设为了redis.

其中 broker 的意思是“经纪人”,像股票经纪人一样,是用于促成“交易”的,Celery中它的职责就是给 worker 推送任务。

而backend的职责是存放执行信息和结果,这些数据需要被持久化存于数据库。但为了简化问题,我们将其与broker一样放置于redis当中。

2. 需要你在自己已经创建的app(不是celery的app,而是django项目的app)目录下面,创建task.py文件(这个文件名只能是这个)

因为Celery会统一从每个app下面的tasks里面监听任务。

3. 编写tasks.py的任务

看一下tasks内部的任务如何写:

from __future__ import absolute_import, unicode_literals
from NBAsite.celery import app
from celery import shared_task
import time

@shared_task
def waste_time():
    time.sleep(3)
    return "Run function 'waste_time' finished."

任务的目标是延迟3秒后,返回一个语句。

4. init.py中的设置

这个是非常关键的一点,如何让django在启动的时候,也把celery给启动了呢?
答案是在项目的init文件内,导入celery的app

from __future__ import absolute_import, unicode_literals
import pymysql

from .celery import app as celery_app

pymysql.install_as_MySQLdb()
__all__ = ('celery_app',)

2.Django 其他配置

为了能够触发该异步任务,我们接下来配置一些常规文件,views和url,首先是views函数:

from .tasks import waste_time

def test_c(request):
    result = waste_time.delay().get()
    return JsonResponse({'status':'successful'})

然后是url:

path('test_c', test_c, name='test_c'),

3.进行测试

首先,运行django项目

python manage.py runserver

这样,django项目和celery的app就被一起启动了,但是这个时候是无法执行这个task的,因为worker没有被启动,我们可以试一下:

访问http://127.0.0.1:8000/stats/test_c 会得到以下报错:

正确的姿势是怎么样的?需要先激活worker,然后再访问API:

celery -A NBAsite worker -l info

从上图下方的log信息里可以看到,在延迟了3秒后,任务启动并返回字符串,而在页面上,也可以看到成功返回。

需要注意的是,如果你修改了tasks的内容,是需要重启celery才能生效的,最简单的方法就是重启django项目。

这样,我们就完成了简单的异步任务的配置和使用。

4.定时任务配置

在异步任务中,我们只用到了worker,而在定时任务中,还要用到celery的beat调度器。

首先来看下如何配置定时任务,或者说如何配置这个调度器。

还是在celery.py里面进行配置:

from __future__ import absolute_import, unicode_literals
import os
from celery import Celery
from celery.schedules import crontab
from django.conf import settings

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'NBAsite.settings')

app = Celery('NBAsite',broker = 'redis://localhost:6379/0',backend='redis://localhost')

app.config_from_object('django.conf:settings',namespace='CELERY')

app.conf.beat_schedule ={
        'autosc':{                           
            'task':'stats.tasks.auto_sc',    
            'schedule':crontab(hour=20,minute=47),   
        },
}
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

重点是增加了app.conf.beat_schedule这个定时任务配置,指定了 stats 文件夹下 tasks.py 中的auto_sc函数,定时于20:47分执行。

5.具体任务页面tasks

增加一个对应要做定时任务的task

@shared_task
def auto_sc():
    print ('sc test?')
    return 'halo'

6.运行命令和结果

命令的话可以将激活worker和激活beat合并在一起,如下:

celery -A NBAsite worker -B -l info

不过,windows不被允许这么使用,因此在windows环境下,你需要同时打开worker和beater:

celery -A NBAsite worker -l info
celery -A NBAsite beat -l info

看上图下方的log可知定时任务被成功执行。

参考资料:
https://www.jianshu.com/p/173070bcdfaf
https://www.jianshu.com/p/ee32074a10de

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!

给作者打赏,选择打赏金额
¥1¥5¥10¥20¥50¥100¥200 自定义

​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Python 列表去重的4种方式及性能对比

​列表去重是Python中一种常见的处理方式,任何编程场景都可能会遇到需要列表去重的情况。

列表去重的方式有很多,本文将一一讲解他们,并进行性能的对比。

让我们先制造一些简单的数据,生成0到99的100万个随机数:

from random import randrange

DUPLICATES = [randrange(100) for _ in range(1000000)]

接下来尝试这4种去重方式中最简单最直观的一种方法:

1.新建一个数组,遍历原数组,如果值不在新数组里便加入到新数组中。

# 第一种方式
def easy_way():
    unique = []
    for element in DUPLICATES:
        if element not in unique:
            unique.append(element)
    return unique

进入ipython使用timeit计算其去重耗时:

%timeit easy_way()
# 1.16 s ± 137 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

平均耗时在1.16秒左右,但是在这个例子中我们使用了数组作为存储对象,实际上如果我们改成集合存储去重后的结果,性能会快不少:

def easy_way():
    unique = set()
    for element in DUPLICATES:
        if element not in unique:
            unique.add(element)
    return unique
%timeit easy_way()
# 48.4 ms ± 11.6 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

平均耗时在48毫秒左右,改善明显,这是因为集合和数组的内在数据结构完全不同,集合使用了哈希表,因此速度会比列表快许多,但缺点在于无序。

接下来看看第2种方式:

2.直接对数组进行集合转化,然后再转回数组:

# 第二种去重方式
def fast_way()
    return list(set(DUPLICATES))

耗时:

%timeit fast_way()
# 14.2 ms ± 1.73 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

平均耗时14毫秒,这种去重方式是最快的,但正如前面所说,集合是无序的,将数组转为集合后再转为列表,就失去了原有列表的顺序。

如果现在有保留原数组顺序的需要,那么这个方式是不可取的,怎么办呢?

3.保留原有数组顺序的去重

使用dict.fromkeys()函数,可以保留原有数组的顺序并去重:

def save_order():
    return list(dict.fromkeys(DUPLICATES))

当然,它会比单纯用集合进行去重的方式耗时稍微久一点:

%timeit save_order()
# 39.5 ms ± 8.66 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

平均耗时在39.5毫秒,我认为这是可以接受的耗时,毕竟保留了原数组的顺序。

但是,dict.fromkeys()仅在Python3.6及以上才支持。

如果你是Python3.6以下的版本,那么可能要考虑第四种方式了。

4. Python3.6以下的列表保留顺序去重

在Python3.6以下,其实也存在fromkeys函数,只不过它由collections提供:

from collections import OrderedDict
def save_order_below_py36():
    return list(OrderedDict.fromkeys(DUPLICATES))

耗时:

%timeit save_order_below_py36()
# 71.8 ms ± 16.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

平均耗时在72毫秒左右,比 Python3.6 的内置dict.fromkeys()慢一些,这是因为OrderedDict是用纯Python实现的。

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!

给作者打赏,选择打赏金额
¥1¥5¥10¥20¥50¥100¥200 自定义

​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Python 用5行代码学机器学习—线性回归

之前Python实用宝典讲过许多关于机器学习的文章,比如:

Python 短文本自动识别个体是否有自杀倾向

[准确率:98%] Python 改进朴素贝叶斯自动分类食品安全新闻 实战教程

准确率94%!Python 机器学习识别微博或推特机器人

Python 机器学习预测泰坦尼克号存活概率

等等…

但是这些文章所使用的模型,读者在第一次阅读的时候可能完全不了解或不会使用。

为了解决这样的问题,我准备使用scikit-learn,给大家介绍一些模型的基础知识,今天就来讲讲线性回归模型。

1.准备

开始之前,你要确保Python和pip已经成功安装在电脑上噢,如果没有,请访问这篇文章:超详细Python安装指南 进行安装。如果你用Python的目的是数据分析,可以直接安装Anaconda:Python数据分析与挖掘好帮手—Anaconda

Windows环境下打开Cmd(开始—运行—CMD),苹果系统环境下请打开Terminal(command+空格输入Terminal),准备开始输入命令安装依赖。

当然,我更推荐大家用VSCode编辑器,把本文代码Copy下来,在编辑器下方的终端运行命令安装依赖模块,多舒服的一件事啊:Python 编程的最好搭档—VSCode 详细指南。

在终端输入以下命令安装我们所需要的依赖模块:

pip install scikit-learn

看到 Successfully installed xxx 则说明安装成功。

2.简单的训练集

冬天快到了,深圳这几天已经准备开始入冬了。

从生活入手,外界温度对是否穿外套的影响是具有线性关系的:

外界温度是否穿外套
30度
25度
20度
15度
10度

现在,考虑这样的一个问题:如果深圳的温度是12度,我们应不应该穿外套?

这个问题很简单,上述简单的训练集中,我们甚至不需要机器学习就能轻易地得到答案:应该。但如果训练集变得稍显复杂一些呢:

你能看出其中x1, x2, x3和y之间的规律吗?

比较难,但是如果你有足够的数据(比如100个),机器学习能够迅速解决这个问题。

为了方便展示机器学习的威力,我们在这里生产100个这样的训练集(公式为: y=x1 + 2*x2 + 3*x3):

from random import randint
TRAIN_SET_LIMIT = 1000
TRAIN_SET_COUNT = 100

TRAIN_INPUT = list()
TRAIN_OUTPUT = list()
for i in range(TRAIN_SET_COUNT):
    a = randint(0, TRAIN_SET_LIMIT)
    b = randint(0, TRAIN_SET_LIMIT)
    c = randint(0, TRAIN_SET_LIMIT)
    op = a + (2*b) + (3*c)
    TRAIN_INPUT.append([a, b, c])
    TRAIN_OUTPUT.append(op)

然后让线性回归模型使用该训练集(Training Set)进行训练(fit),然后再给定三个参数(Test Data),进行预测(predict),让它得到y值(Prediction),如下图所示。

3.训练和测试

为什么我使用sklearn?因为它真的真的很方便。像这样的训练行为,你只需要3行代码就能搞定:

from sklearn.linear_model import LinearRegression

predictor = LinearRegression(n_jobs=-1)
predictor.fit(X=TRAIN_INPUT, y=TRAIN_OUTPUT)

需要注意线性回归模型(LinearRegression)的参数:

n_jobs:默认为1,表示使用CPU的个数。当-1时,代表使用全部CPU

predictor.fit 即训练模型,X是我们在生成训练集时的TRAIN_INPUT,Y即TRAIN_OUTPUT.

训练完就可以立即进行测试了,调用predict函数即可:

X_TEST = [[10, 20, 30]]
outcome = predictor.predict(X=X_TEST)
coefficients = predictor.coef_

print('Outcome : {}\nCoefficients : {}'.format(outcome, coefficients))

这里的 coefficients 是指系数,即x1, x2, x3.

得到的结果如下:

Outcome : [ 140.]
Coefficients : [ 1. 2. 3.]

验证一下:10 + 20*2 + 30*3 = 140 完全正确。

如何,机器学习模型,用起来其实真的没你想象中的那么难,大部分人很可能只是卡在了安装 scikit-learn 的路上…

顺便给大家留个小练习,将下列欧式距离,使用线性回归模型进行表示。

解决思路和本文的方案其实是类似的,只不过需要变通一下。

解决出来的同学可在后台回复:加群,将代码发给我验证,领取一份小红包并进入Python实用宝典的高质量学习交流群哦。

我们的文章到此就结束啦,如果你喜欢今天的Python 实战教程,请持续关注Python实用宝典。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

Python 凯利公式 — 最优投资本金计算

1955年,美国盛行答题积累奖金的电视节目,答题者通过连续答对题目来累计奖金池。而在电视外,庄家针对这个节目开设了答题者能否答对题目的赌盘,吸引了许多赌徒参与下注。

但是,节目在东海岸直播,西海岸则有直播延时,有赌徒便抓住了这个机会,利用延时提前通过电话得到了答题者答题情况,赶在西海岸直播前参与下注,从中套利。

受此启发,贝尔实验室的科学家约翰·拉里·凯利于1956年在《贝尔系统技术期刊》中提出了凯利公式:

“如果通信渠道的输入符号代表偶然事件的结果,在该偶然事件中,可以按照与其概率一致的赔率进行投注,那么一个赌徒就可以利用输入符号给他的信息,使他的钱以指数形式增长。”

论文中他以一个赛马模型提出了凯利公式的雏形:

其中:

f* = 应该放入投注的资本比值
p = 获胜的概率
q = 失败的概率
b = 赔率

举个例子,如果一个赌博你有60%的获胜率(p=0.6, q=0.4),并且赔率是1赔2(b=2),则赌客每次下注的资金是 40% (计算方式:(b*p-q)/b)

当然,当初提出这个公式的凯利其实是为了通信学研究的,并不是很贴近实际投资场景。不过,它有一个变形:

其中:

f* = 应该放入投注的资本比值
p = 获胜的概率
q = 失败的概率
rW = 净利润率
rL = 净损失率

这个公式很关键,因为它使得计算投资利润最大化的本金数额成为可能。比如说,使用我们之前的MACD量化投资策略:

Python 量化投资实战教程(2) —MACD策略

有1万元购买股票,10%的止盈点,10%的止损点,假设针对某只股票每次盈利的概率是7/8,此时rW=0.1, rL=0.1,那么每次交易我们应该投入 f=((7/8)*0.1 – (1/8)*0.1) / 0.1*0.1=7.5%。

也就是说,按照这个策略买股票,每次你只能投入本金的7.5%才能使利润获得最大化。

这个公式用Python来计算也非常简单:

def kelly(p, q, rW, rL):
    """
    计算凯利公式

    Args:
        p (float): 获胜概率
        q (float): 失败概率
        rW (float): 净利润率
        rL (float): 净亏损率

    Returns:
        float: 最大化利润的投资本金占比(%)
    """
    return (p*rW - q*rL)/(rW * rL)

基本就是把公式照搬下来计算。

凯利公式看起来真的很不错,不过,请大家注意了,量化投资中的获胜概率,比如我们上述计算中的盈利概率: 7/8,是基于某只股票的历史数据推测出来的,历史并不代表未来,因为未来是不可知的,我们只能说这只股票,在过去,表现的不错。

所以,模型永远只是一个近似的替代,并不能说明一切,凯利公式也是一样,如果凯利公式告诉你,要放大仓位,你可要三思,万一发生黑天鹅事件,分分钟教你做人,如果你还上了杠杆,那就要上人生最重要的一堂课了。

我们经常会认为凯利公式所针对投注比例是全资产,事实上,我更喜欢将凯利公式所针对的投注比例当做是你可承受损失的资产。

比如你有100万,你能承受10万的损失,那么这10万就能拿来根据凯利公式进行投资,因为这是最保险,能让你心情不那么痛苦的做法。

不要贪,很重要。

我们的文章到此就结束啦,如果你喜欢今天的Python 实战教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

Python MySQL与Influxdb对比及迁移方案

最近遇到一个新的应用场景:将MySQL存放的时序数据迁移到influxDB中。

这么做的好处在于:

1.Influxdb 读写速度更快。

写数据对比

读数据对比

2.在磁盘占用率上,Influxdb更低。

3.此外,Influxdb的数据可以使用Chronograf进行实时预览

如果以前是将时序数据存放在MySQL,现在为了获取更好的性能和使用更优的可视化工具,我们需要将数据从MySQL迁移到Influxdb中。

这看起来是一个常见场景,经过我一番查阅,发现了 GreatLakesEnergy/Mysql-to-influxdb 这个项目。

可惜的是,作者是基于Python2进行开发的,而且用了几个非常难搭建的模块。想在Python3中重新使用这个项目比较困难,因此我对它进行了改造,改造后的代码如下:

https://github.com/Ckend/Mysql-to-influxdb

如果你有这样的迁移需求,可以继续看下面的详细教程。

1.准备

开始之前,你要确保Python和pip已经成功安装在电脑上噢,如果没有,请访问这篇文章:超详细Python安装指南 进行安装。如果你用Python的目的是数据分析,可以直接安装Anaconda:Python数据分析与挖掘好帮手—Anaconda

Windows环境下打开Cmd(开始—运行—CMD),苹果系统环境下请打开Terminal(command+空格输入Terminal),准备开始输入命令安装依赖。

当然,我更推荐大家用VSCode编辑器,把本文代码Copy下来,在编辑器下方的终端运行命令安装依赖模块,多舒服的一件事啊:Python 编程的最好搭档—VSCode 详细指南。

下载或Git Clone我修改好的代码:
https://github.com/Ckend/Mysql-to-influxdb

进入该目录后,输入以下命令安装依赖:

pip install -r requirements.txt

看到 Successfully installed xxx 则说明安装成功。

2.迁移配置

在迁移开始前,请在你需要迁移的表里加一个字段 transfered,这个字段用于检测某条数据是否被迁移,默认设为0。一旦迁移完成,这个字段会被设为1.

此外,你需要找到你表里的时间序列字段(time)和分类字段(tag)。

分类字段可能比较难理解,比如说你有一张表记录了每支股票每天的开盘价,那么股票id字段便可理解为一个tag,即下面配置中的siteid_field.

在解压后的目录里新建一个settings.ini, 配置以下信息:

[mysql]
host : mysql host # (本地为127.0.0.1)
port : mysql 端口号 # Default is3306
username : 用户名
password : 密码
db : 数据库
table : 要迁移的表
check_field : 检测字段,默认为0,如果迁移完成,该字段会被设为1
time_field : 时间字段
siteid_field : 分类字段(tag)


[influx]
host : influxdb host # (本地为127.0.0.1)
port : 端口号 # Default:8086
username : 用户名
password : 密码
db : 要迁移进入的数据库

[server]
interval : 5 

配置完上述信息后,执行命令即可开始迁移:

python mysql2influx.py -d -c settings.ini -s

3.迁移是否完成

如何检测迁移任务是否完成,还记得我们刚新增了一个字段 transfered 用于检测某条数据是否被迁移吗?

你只需要在mysql中输入以下sql查询是否还有未被迁移的数据即可:

SELECT count(1) FROM your_table where transfered = 0;

若不为0则说明还有数据未被迁移成功。

不过值得注意的是,迁移脚本里是先进行数据迁移,再回来修改transfered的值。

如果你的数据量非常大,更新MySQL数据有可能会耗时极长,因此查询transfered数量的结果有可能不正确。这点需要特别关注。

我们的文章到此就结束啦,如果你喜欢今天的Python 实战教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

代号Mallow, 一款普通人用Python耗费两年制作的精美游戏!

blob:https://pythondict.com/f3977feb-9962-4cd2-8fb4-0e29c5dfdf92

代号Mallow,是国外一位叫u/Ancalabro的用户耗费了两年自学成才制成的一款多人本地/在线2D游戏。

这款游戏基于Pygame制作,针对绳索物理、粒子引擎等一系列难题做了针对性的解决方案。从画面效果来看,作者确实做的非常用心。

1.游戏介绍

该游戏拥有以下特点

1.最多同时可在线4名玩家一同游玩(需要转发UDP端口)

2.在线游戏和离线游戏都支持

3.支持玩家对战

4.十二个独特的场景

5.一击必杀的武器机制和“无臂”近战战斗

6.重力反转机制

7.支持三种系统(Mac\Linux\Windows)

不过,尽管这款游戏支持多人游戏,但是需要你对UDP端口进行转发,这会有一定的安全隐患,所以建议大家单人把玩就好了。

键盘控制方法如下:

Z – 菜单选择 / 跳
X – 菜单返回 / 滚动
C – 使用物品
Y – 菜单命令
ENTER – 暂停 / 取消暂停
ESC – 退出程序
箭头 – 移动
减号 – 切换全屏 / 窗口化

2.源代码

如果你很好奇作者是如何用pygame完成这么多功能的,你可以在
https://ancalabro.itch.io/codename-mallow

下载这款游戏的源代码和游戏程序体验一把,相信不会让你失望的。如下图所示,进入网站后拉到留言区上方便会看到下载地址。从上到下分别是windows、Linux、Mac对应的游戏程序,最下方是游戏源代码。

3.好评如潮

Python社区经常会出现许多精心制作的游戏分享,但是像作者这样花了两年时间沉浸在自己的开发世界里制作一款游戏的人并不多见。

社区里大家对这款游戏都赞赏有加,相信这对于作者而言是一个正反馈,让他对制作游戏更有激情。

也希望大家能跟这位作者一样,认真地坚持做一件事情,逐步地将成果分享于众,获得正反馈。

如果能获得超预期的正反馈,就能极大地唤起我们的热情,像催化剂一样,使得我们的“反应”加快。

我们的文章到此就结束啦,如果你喜欢今天的Python 实战教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

healthchecks, 基于Django监控Cron任务的神器

在运维服务器的时候经常会用到一些Crontab任务。

当你的Crontab中的任务数超过10个的时候,你会发现这些任务管理起来非常困难。

尤其是当这些Cron任务执行失败的时候,比如 Python 实用宝典网 每个月初都会执行一次https证书刷新,有一次协议更新之后,我的脚本失效了三个月,导致证书过期时网站宕机了一天,直到我发现并修复了这个问题。

这就是Crontab任务的一个劣势:没有方便的通知功能。

不过,现在有一个非常方便的开源Django项目能在这些Crontab失效的时候通知你,它就是healthchecks.

它通过一个回调接口判断你的Crontab任务有没有顺利执行。

比如说你有一个python脚本定时执行,healthchecks给定的回调URL是:

http://localhost:8000/ping/880cb4d2

在配置Crontab脚本的时候,就需要这么写:

8 6 * * * python /home/user/test.py && curl -fsS -m 10 --retry 5 -o /dev/null http://localhost:8000/ping/880cb4d2

如果未按时调用回调接口,healthchecks将会通过邮件等通知方式告警。

那么这个“未按时”能否设定宽限呢?比如我有个任务要跑1个小时左右,那么这个任务应该是预计在一个半小时内调用(Ping)回调接口,超过一个半小时如果没有调用回调接口则告警。答案是肯定的。

上图中Period指的是两次Ping之间的时间间隔。下方Grace表示“宽限期”,自从上次Ping以来的时间已超过Period+Grace则会发送告警通知。

如果你用不习惯这种可视化的选择器,它还提供了Crontab表达式给你定义Period和Grace:

真乃神器啊!支持的通知方式如下:

国内用户可能一般只会用到Email和Teams,高级点的用户可能会用到IFTTT的Webhooks和普罗米修斯。总之,按你的爱好来就行。

本地开发

下面教大家如何在本地搭建这个项目:

1. 下载项目

https://github.com/healthchecks/healthchecks

如果你访问不了github,可在【Python 实用宝典】公众号后台回复 healthchecks 下载完整源代码

2.创建虚拟环境

推荐使用Python 3.6+,如果你有conda,那就非常方便了,创建healthchecks虚拟环境:

conda create -n healthchecks python=3.6
activate healthchecks

如果你没有conda,你需要先安装Python3.6,然后使用pip安装virtualenv,在终端输入以下命令创建healthchecks虚拟环境

python3 -m venv healthchecks
source healthchecks/bin/activate

不同系统中命令可能不太一样,遇到问题多利用搜索引擎查询就好了。

3.安装依赖

进入到上述创建好的虚拟环境后,cd进入项目根目录,输入以下命令安装依赖:

pip install -r requirements.txt

4.数据库配置(可选)

该项目默认使用SQLite,这意味着你不需要特殊配置也可照常运转。

如果你需要配置MySQL或PostgreSQL,请阅读hc/local_settings.py.example文件进行配置即可。

5.数据表迁移

Django项目当然少不了这个环节,虚拟环境下,在根目录里运行以下命令进行数据表的迁移:

python manage.py migrate

当然,还要创建超管用户:

python manage.py createsuperuser

6.运行项目

大功告成,输入以下命令即可运行项目:

python manage.py runserver

点击右上角login in登录到超管用户就可以开始使用了。

如果你需要对这个项目进行大规模的改动,建议使用Pycharm作为编程工具,因为使用Pycharm来写Django实在是太爽了,详细可以参考这篇文章:《Pycharm+Django 安装及配置指南》

我们的文章到此就结束啦,如果你喜欢今天的Python 实战教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应红字验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

想成为时间管理大师?试试番茄工作法!

番茄工作法,是一种时间管理方法。

没错,掌握了它,或许你能成为时间管理大师。

当然,除非你天赋异禀,想成为罗志祥还是有一定难度的。

番茄工作法有五个基本步骤:

1.决定待完成的任务
2.设定番茄工作法定时器至 n 分钟(通常为25分钟)。
3.持续工作直至定时器提示,记下一个x。
4.短暂休息3-5分钟。
5.每四个x,休息15-30分钟。

四个x被称为一个完整的番茄周期。

番茄工作法的原理有专业的心理学解释(来自wiki):

番茄工作法的关键是规划,追踪,记录,处理,以及可视化。在规划阶段,任务被根据优先级排入”To Do Today” list。 这允许用户预计每个任务的工作量。当每个番茄时结束后,成果会被记录下来以提高参与者的成就感并为未来的自我观察和改进提供原始数据。

番茄时意指每个工作时段的时长。当任务完成后,所有番茄计时器剩下的时间会被用于过度学习。短休息时间可以辅助达到心理学上的同化作用,3-5分钟的短休息间隔开每个番茄工作时段。四个番茄工作时组成一组。一个15-50分钟的长休息间隔开每组作业。

这一时间管理技术的本质目的是减少内生和外在的干扰对意识流的影响。一个单位的番茄工作时不可再细分。当在番茄工作时中被打断的情况下,只可能有两种情况:干扰的活动被推迟(告知 – 协商 – 安排日程 – 回访),或者当前的番茄工作时废弃,必须重新开始。

其中,完成任务后所剩下的时间会被用于过度学习。这里的过度学习指的是达到一次完全正确再现后仍继续识记的记忆,也就是复习。

番茄工作法这么好用,不试试怎么行?实际生活中,我们可以通过电脑/手机/手表来通知自己每个番茄时的完成。下面给大家细数几个好用的APP:

1.Flat Tomato

Flat Tomato应该是我最强烈推荐的,APP做的很用心,操作简洁、人性化,音效、动画看起来非常舒服。支持苹果全家桶的所有设备(iPhone、Mac、iPad、iWatch)。

免费版的Flat Tomato没有任何广告,支持基础的定时器、提醒、打断记录功能,如果你只是为了使用番茄工作法来让自己保持自律,免费版的功能完全够你使用,而且非常简单,没有花里胡哨的功能。

Pro版支持时间线呈现、统计图表以及时间支出类别,这些功能有助于自己回顾时间的利用率,并加以改进时间管理能力。

美中不足的是,它不支持Windows和安卓机器。

更加详细的介绍可以看少数派的推荐:
https://sspai.com/post/34014

2.小番茄

小番茄的强大之处在于,它支持所有设备:

  • Android
  • Android Tablet
  • iPhone
  • iPad
  • Apple Watch
  • Mac
  • Windows
  • Chrome Extension

没错,甚至是Chrome的扩展程序它都支持。

它还能定期发送日报、周报、月报总结自己的工作或学习情况,查看定制目标的完成度。

美中不足的是它缺少打断记录功能,这同时使得它的报表功能相对鸡肋。在功能的使用上也相对而言比较复杂,没那么容易上手。

免费版的功能限制较多,相比于Flat Tomato还是逊色一些。

3.Python写的?

关于番茄工作法相关的软件实在太多了,我也没办法给大家一个个都去试,大家如果觉得以上两个APP还是满足不了自己,可以继续在APP Store或少数派上寻宝。

作为一个技术类公众号,当然要教大家用上Python写的计时器啦!

不过我也不想做重复性的工作,用“Python Pomodoro”关键词一搜,你会发现网上一大堆开源代码。

如果你想在PC机或笔记本上实现我们文首提到的最最最基本的番茄工作法,你只需要安装tomato-clock:

pip install tomato-clock

然后在终端或命令行中输入以下任意一个语句即可开始计时:

$ tomato         # 开启一个25分钟的番茄计时器 + 5分钟休息时间
$ tomato -t      # 开启一个25分钟的番茄计时器
$ tomato -t <n>  # 开启一个<n>分钟的番茄计时器
$ tomato -b      # 休息5分钟
$ tomato -b <n>  # 休息<n>分钟
$ tomato -h      # 帮助

美中不足的是,tomato不支持图表统计。

没关系,我们还有大杀器 pomodoro-cli,它不仅支持计时,还支持图表统计,安装方式:

pip install pomodoro-cli

使用方式,终端输入命令:

pomodoro 60 5 --notif=True --alarm=False

这个语句的意思是,工作时长60分钟,每5分钟休息一次,消息框启用、警告不启用。

每次执行完番茄周期,它都会将数据记录在Home/.pomodoro中,要可视化统计这些信息,可以使用pomostat:

pomostat overall
pomostat week
pomostat thisweek
pomostat lastweek
pomostat week --weekof='2018-01-01'
pomostat stats
pomostat weeks
pomostat today
pomostat yesterday

给大家展示一个pomostat lastweek的效果:

效果还是很不错的。当然,离专业的APP如Flat Tomato还有比较大的差距,但是,这样的一个最简单的功能,或许就够我们用了。

总之,番茄工作法真的是一个让自己保持自律的好工具,强烈推荐大家试一试,严格按照步骤走,很快你就会成为一个高效的人。

我们的文章到此就结束啦,如果你喜欢今天的Python 实战教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

关于Python3.9,你不可不知的4个新特性

1.词典联合运算符

这是我最喜欢的功能之一,语法非常优美。

在Python3.9,如果你有两个词典,现在可以用这些运算符进行合并和更新。

合并运算符 “|”:

还有update运算符|=,它会更新原始字典:

a = {1: 'a', 2: 'b', 3: 'c'}
b = {4: 'd', 5: 'e'}
a |= b
print(a)
{1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e'}

如果我们的词典共享一个key,那么将使用第二个词典中的value:

a = {1: 'a', 2: 'b', 3: 'c', 6: 'in both'}
b = {4: 'd', 5: 'e', 6: 'but different'}
print(a | b)
{1: 'a', 2: 'b', 3: 'c', 6: 'but different', 4: 'd', 5: 'e'}

使用可迭代对象进行字典更新

|=操作符的另一个很酷的特性是能够使用可迭代对象(例如列表或生成器)使用新的键值对更新字典:

a = {'a': 'one', 'b': 'two'}
b = ((i, i**2) for i in range(3))
a |= b
print(a)
{'a': 'one', 'b': 'two', 0: 0, 1: 1, 2: 4}

当然,如果你用|这样做,则会得到TypeError,因为它只能用于dict类型之间的联合。

2.字符串方法

removeprefix()和removesuffix()

str.removeprefix(substring: string) 是一个方法,接收一个substring参数,顾名思义,它将删除字符串对应的substring后缀,如果没有对应的后缀,返回原字符串。

str.removesuffix(substring: string) 是一个方法,接收一个substring参数,它将删除字符串的对应substring前缀,如果没有对应的前缀,返回原字符串。

当然,两个函数执行你可以通过使用string[len(prefix):]前缀和string[:-len(suffix)]后缀来实现。

这些是非常简单的操作,因此也是非常简单的功能,考虑到你可能经常执行这些操作,Python3.9 提供的这两个内置函数应该能让你非常爽。

3.新的数学函数

Python 3.9 的数学模块进行了不少的优化并添加了许多新功能。

比如以前gcd计算最大公因数的函数只能应用于2个数字,这就很蛋疼,我们必须使用 math.gcd(80, math.gcd(64, 152))来处理大于2个数字的情况。

现在 gcd 允许计算任意数量的数字。

import math

# Greatest common divisor
math.gcd(80, 64, 152)
# 8

Math模块中,第一个新增的功能是:

# 最小公倍数
math.lcm(4, 8, 5)
# 40

用于计算最小公倍数:math.lcm,与gcd一样,它允许可变数量的参数。

4.新解析器

这一个更改你可能看不见、摸不着,但它可能改变Python的未来。

以前Python使用 LL(1) 解析器,现在Python开始使用 PEG 解析器,官方认为,这个更改会使得他们更加方便地构建新功能。

因此,请期待Python 3.10,Python团队或许能给我们带来更多的惊喜!

我们的文章到此就结束啦,如果你喜欢今天的Python 实战教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

有趣好用的Python教程

退出移动版
微信支付
请使用 微信 扫码支付