Leetao's Blog

Talk is cheap, show me the code

0%

前言

在 Java 中有 [[重载]] 的概念:

重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。

Python 本身不支持 重载 这个特性,但是通过 functools.singledispatch 可以实现函数的重载。接下来通过一个例子,简单地演示一下 Python 的函数重载。

阅读全文 »

django 默认 bulk_create 不支持 signal,可以通过自定义 models.Manager 支持这个特性

1
2
3
4
5
6
7
8
9
10
class CustomManager(models.Manager):  
def bulk_create(self, items, **kwargs):
for i in items:
post_save.send(i.__class__, instance=i, created=True)
return super().bulk_create(items, **kwargs)

def bulk_update(self, items, fields, **kwargs):
super().bulk_update(items, fields, **kwargs)
for i in items:
post_save.send(i.__class__, instance=i, created=False)

使用实例:

1
2
3
class Test(db.models):
objects = CustomManager()
...

前言

lru_cache 是 functools 库中的一个函数,它为函数提供缓存功能的装饰器,缓存 maxsize 组传入参数,在下次以相同参数调用时直接返回上一次的结果。

从它的功能来说是一个不错的方法,可以在一定程度上提高函数的运行速度,但是它存在一个问题,当你用functools.lru_cache装饰器来装饰一个实例方法时,封装该方法的类的实例在持有它们的进程中永远不会被垃圾回收。

阅读全文 »

为了保证 Python 的代码规范,在使用 git commit 提交代码之前,需要使用 blake、isort 工具对提交的文件进行格式化,如果提交的代码符合规法则 commit 成功,否则自动格式化文件,然后重新 commit

整个工作流大概是这样子:

其中 black 是代码格式化工具,可以通过 pip install black 后直接使用,使用方法如下:

1
black {source_file_or_directory}...

同时也支持配置文件自定义规则,详细内容可以参考官方文档 The uncompromising code formatter — Black

isort 则是用来规范 python 库的引入的,按字母顺序对 packages 进行排序,并自动分为不同的部分和类型,同样可以通过 pip install isort 后直接使用,使用方法如下:

1
isort mypythonfile.py mypython file2.py

black 一样也支持配置文件自定义规则,具体内容参考官网 isort (pycqa.github.io)

pre-commit 是整个工作流最重要的一环,pre-commitgit-hooks 中的一个重要的钩子,它在键入提交信息前运行。可以用于检查即将提交的快照,例如,检查是否有所遗漏,确保测试运行,以及核查代码。 如果该钩子以非零值退出,Git 将放弃此次提交。 上面提到的机制是整个工作流可以进行的关键。

完全可以自定义 pre-commit 钩子的,但是如果只是为了检验的话,可以使用现成的方案 pre-commit/pre-commit,用 Python 构建,支持多语言的管理器。通过 pre-commit 这个库,简单地几步就可以实现自动化工作流。

  1. 安装 pre-commit
1
pip install pre-commit

然后通过 pre-commit --version 确定是否安装成功

1
2
$ pre-commit --version
pre-commit 2.16.0
  1. 添加 .pre-commit-config.yaml 的配置文件

可以通过 pre-commit sample-config 生成一个默认的配置文件,这里贴一下关于 blackisort 的配置文件

1
2
3
4
5
6
7
8
9
10
repos:  
- repo: https://github.com/psf/black
rev: 21.12b0
hooks:
- id: black

- repo: https://github.com/PyCQA/isort
rev: 5.10.1
hooks:
- id: isort

支持的配置项很多,具体参考 plugins

  1. 安装 git hooks 脚本
1
2
$ pre-commit install
pre-commit installed at .git/hooks/pre-commit

然后就就大功告成了

在 3.6 之前可以通过 meta class 去实现,3.6 之后可以通过 __init_subclass__ 实现

meta class

通过 meta class__new__ 方法可以实现自动注册 class 的功能,原理很简单,就是通过元类去控制类的创建,在调用 __new__ 方法的时候自动将 class 注册

1
2
3
4
5
6
7
8
class MetaClass(type):
def __new__(cls, clsname, bases, attrs):
newclass = super(MetaClass, cls).__new__(cls, clsname, bases, attrs)
register(newclass) # 注册函数
return newclass

class MyClass(metaclass=MetaClass):
pass

__init_subclass__

__init_subclass__ 是 3.6 后引入的一个新的特性,一个 hook,可以让所有的子类在创建之后执行一些初始化的操作,通过这个特性我们就可以更简单地实现上述通过元类实现的功能。

1
2
3
4
5
6
7
8
9
class ParentClass:
def __init_subclass_(cls, **kwargs):
super().__init_subclass(**kwargs)
register(cls)


class ChildClass(ParentClass):
pass

需要注意的是 __init_subclass__ 没有非关键字参数

应用

代码存在如下的逻辑:

1
2
3
4
5
6
7
8
if category == 'a':
handle_a()
elif cateogory == 'b':
handle_b()
elif category == 'c':
handle_c()
else:
handle_else()

存在很多都处理逻辑,每个逻辑可能由不同的开发人员编写,全部放到一个代码块中,可以预见随着分支的增多,这部分代码会变得越来越庞大,不利于后期维护,所以最简单的方案,存在一个 dict 保存 category 和 处理方法的映射关系:

1
2
3
4
category_to_func = {
'a': handle_a
...
}

然后就可以将最初的代码简化为:

1
return category_to_func.get(category)()

实现

方法一

基于上面的思路,可以写出最简单的方法,让大家自行编写自己的处理方法,然后在 category_to_func 中统一注册自己的方法。

1
2
3
4
5
category_to_func = {
'a': handle_a # A 撰写注册
'b': handle_b # B 撰写注册
...
}

这样子没有任何问题,但是不够优雅,开发人员撰写了自己的处理方法之后,还要去指定的地方注册自己的方法,开发体验不太好。
如果存在一种方式,开发人员只需要撰写处理方法,代码可以自定注册就更好了。

方法二

通过上面的自注册的方法,让子类继承父类,然后将其自动注册到全局当中,这样子开发人员只需要关注自己的业务实现就可以了。

参考链接

PEP 487 – Simpler customisation of class creation
python - How to auto register a class when it’s defined - Stack Overflow

前言

Django 自带的 SearchFilter 是不支持空格搜索的,如果需要支持空格搜索并且保留之前的搜索功能则需要自定义 SearchFilter。

自定义 SearchFilter

继承 SeachFilter 然后实现 get_search_terms 方法,如果同时支持 space 然后又可以保持之前的搜索特性呢?

简单的做法就是,对输入的参数进行校验,如果只有含有空格就返回空格,否则就执行之前的搜索逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CanSerachBothCharFilter(SearchFilter):

def get_search_terms(self, request):
params = request.query_params.get(self.search_param, '')
# 判断原来的参数是否有只有空格
only_space = False
if len(params) != 0 and len(params.strip()) == 0:
only_space = True

params = params.replace('\x00', '') # strip null characters
params = params.replace(',', ' ')
if only_space:
return [params]
return params.split()

前言

PEP 484 中引入的 Typing hints (类型提示) 可以用来为静态类型检查器和其他第三方工具指定类型元数据。但是在,PEP 484 只指定了名义子类型的语义。在这个 PEP 544 中,指定了协议类的静态和运行时语义,这将为结构性子类型(静态鸭子类型)提供一个支持。

什么是鸭子类型(Duck Typing)?

If it walks like a duck and it quacks like a duck, then it must be a duck。

简单地说,”如果它走路像鸭子,叫起来像鸭子,那它一定是鸭子“。

在编程中这就意味着当我们编写接收特定输入的函数时,我们只需要关心该函数输入的行为、属性,而不是该函数输入的显式类型。

例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为”鸭子”的对象,并调用它的”走”和”叫”方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的”走”和”叫”方法。— 百度百科

那么如何在 Python 中实现“鸭子类型”呢?

鸭子类型

现在有这样子场景,我们需要编写一个函数去计算一个拥有边长(len_side)的物体的周长,伪代码如下:

1
2
3
4
5
function calcute_circumference(shape):
circumference = 0
for side in shape.len_side: /// 遍历边长
circumference += side
return circumference /// 周长

从伪代码不难看出,我们不关心是什么的物体,我们只需要保证这个物体可以有一个 len_side(列表) 字段就可以了。

现在我们可以借由 Protocol 去实现它。

1
2
3
4
5
6
7
8
9
10
from typing import Protocol

class ObjWithSideLen(Protocol):
side_len: list

def calcute_circumference(shape: ObjWithSideLen):
circumference = 0
for side in shape.side_len:
circumference += side
return circumference

从上面的例子不难看出 Protocol 有点类似 Java 中的接口,只需要在函数上使用它,并不需要去关注输入的参数的具体类型。

泛型

我们还可以配合使用 typing 中的 TypeVar 实现泛型参数化,让我们的函数更加抽象化,只要符合协议,无论什么类型的输入都可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
from typing import Protocol

class ObjWithSideLen(Protocol):
side_len: list

T = TypeVar('T', bound=ObjWithSideLen)

def calcute_circumference(shape: T) -> T:
circumference = 0
for side in shape.side_len:
circumference += side
shape.circumference = circumference
return shape

延伸

上述的例子都可以借由 Python 中的 ABCs 去实现的,但是两者的侧重点有所不同,由于篇幅有限,在下一篇文章,让我们仔细对比一下两者的的区别。

前言

在使用 Git 作为版本控制工具时,每次文件发生修改的时候提交都需要 git commit 命令去记录本次的修改,否则就不允许提交,显然 git commit 是一个重要的环节,因此制定一个 Git Commit 规范是有必要的,否则就会出现混乱的提交信息,这里腾讯某团队的规范作为例子,然后通过一定的手段去帮助我们把这个规范落到实处。

目的:

  • 统一团队 Git commit 日志标准,便于后续代码 review,版本发布以及日志自动化生成等等。
  • 统一团队的 Git 工作流,包括分支使用、tag 规范、issue 等

Git commit 日志参考案例

总体方案

来自 feflow 的 git commit 规范

Git commit日志基本规范

1
2
3
4
5
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

type 类型

type代表某次提交的类型,比如是修复一个bug还是增加一个新的feature

  • feat: 新增 feature
  • fix: 修复 bug
  • docs: 仅仅修改了文档,比如 README, CHANGELOG, CONTRIBUTE等等
  • style: 仅仅修改了空格、格式缩进、逗号等等,不改变代码逻辑
  • refactor: 代码重构,没有加新功能或者修复 bug
  • perf: 优化相关,比如提升性能、体验
  • test: 测试用例,包括单元测试、集成测试等
  • chore: 改变构建流程、或者增加依赖库、工具等
  • revert: 回滚到上一个版本

格式要求

1
2
3
4
5
6
7
8
9
# 标题行:50个字符以内,描述主要变更内容
#
# 主体内容:更详细的说明文本,建议72个字符以内。 需要描述的信息包括:
#
# * 为什么这个变更是必须的? 它可能是用来修复一个bug,增加一个feature,提升性能、可靠性、稳定性等等
# * 他如何解决这个问题? 具体描述解决问题的步骤
# * 是否存在副作用、风险?
#
# 尾部:如果需要的化可以添加一个链接到issue地址或者其它文档,或者关闭某个issue。

校验方案

node 项目已经有比较成熟的方案,这里以 Python 项目为例,考虑实际使用的便利性,希望可以可以实现以下的目的:

  1. 成员在本地执行 git commit 的命令时就完成校验,通过则允许执行 git push 否则则需要重新提交 commit 的信息。
  2. 提交到 gitlab,由服务端完成再次校验

本地校验

git hooks

Git 在执行 git init 进行初始化的时候,会在 .git/hooks 目录下生成一系列 hooks 脚本:

git-hooks

从上图可以看到每个脚本的后缀都是以 .sample 结尾的,在这个时候,脚本是不会自动执行的。我们需要把后缀去掉之后才会生效,即将 pre-commit.sample 变成 pre-commit 才会起作用。由于只是对 commit msg 做校验,所以只需要使用 commit-msg 脚本即可。

commit-msg 脚本

commit-msg hooks 中完成对 commit 消息校验

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/sh

# 获取当前提交的 commit msg
commit_msg=`cat $1`

msg_re="^(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|release|workflow)(\(.+\))?: .{1,100}"

if [[ ! $commit_msg =~ $msg_re ]]
then
echo "\n不合法的 commit 消息提交格式,请使用正确的格式\n <type>(<scope>): <subject>"
# 异常退出
exit 1
fi

将上述脚本保持在项目所在对 .git/hooks 目录下命名为 commit-msg,然后执行 chmod 命令:

1
chmod +x .git/hooks/commit-msg

验证结果

  1. 不符合规法的 commit msg

不合规的git-commit-msg

  1. 符合规范的 commit-msg

合规的git-commit-msg

服务端校验

Git 在服务端也同样有一些 hooks:

  • pre-receive
  • update
  • post-receive

每个 hooks 的具体功能可以参考 Server-Side Hooks ,其中 pre-receiveupdate 均符合使用场景,唯一区别是用户同时推送到多个分支时, update 针对每个分支都会被触发执行,而 pre-receive 只执行一次。

pre-receive

说明

在任何文件被更新时,如果$GIT_DIR/hooks/pre-receive 存在并且是可执行的文件,则 pre-receive 会被无参数触发执行一次,正常 pre-receive 触发执行的时候会接收如下的参数:

1
sha1-old SP sha1-new SP refname LF

其中 sha1-old 为多次 commit 的最早一次的 commit 的 id,而 sha1-new 则是最新一次的 id。除此之外,git push 的时候还会传递其他的信息,可以参考 pre-receive-hooks

校验方法

通过 git log old-commit-ID new-commit-ID -pretty=format:%s 提取出俩个 commit 之间的所有 commit-msg 然后逐一校验。

考虑易用性,用 golang 构建校验脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package main

import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/exec"
"regexp"
"strings"
)

type CommitTypeConfig struct {
commitTypeList []string
strictMode bool // 严格模式下将校验所有的提交信息格式(多 commit 下)
}

const checkFailedMeassge = `
Commit message 格式校验失败❌
Commit message 格式必须符合下述规则:
^(\w+)(\(\w+\))*: (.+)|^Merge\ branch(.*)|^Merge\ remote-tracking branch(.*)
Example:
feat(test): test commit style check.`

const ZERO_COMMIT = "0000000000000000000000000000000000000000"

func main() {
config := CommitTypeConfig{
commitTypeList: []string{"feat", "fix", "docs", "style", "refactor", "test", "chore", "perf", "hotfix"},
strictMode: true}
input, _ := ioutil.ReadAll(os.Stdin)
param := strings.Fields(string(input))
// allow branch/tag delete
if param[1] == ZERO_COMMIT {
os.Exit(0)
}
if isExists("pre-receive.json") {
fmt.Println("检测到存在配置文件,加载配置文件规则💪")
file, _ := ioutil.ReadFile("pre-receive.json")
tmpConfig := CommitTypeConfig{}
err := json.Unmarshal([]byte(file), &tmpConfig)
if err == nil {
fmt.Println("成功加载配置文件规则😊")
config = tmpConfig
} else {
fmt.Println("加载配置文件规则失败,使用默认规则文件😭")
}
} else {
fmt.Println("开始加载默认配置文件规则💪")
}
commitMsg := getCommitMsg(param[0], param[1])
checkCommitMsg(commitMsg, config)
}

func getCommitMsg(oldCommitID string, commitID string) []string {
s := fmt.Sprintf("git log %s..%s --pretty=format:%%s", oldCommitID, commitID)
if oldCommitID == ZERO_COMMIT {
s = fmt.Sprintf("git rev-list --pretty=format:%%s $(git for-each-ref --format='%%(refname)' refs/heads/* | sed 's/^/\\^/g') %s | grep -v ^commit", commitID)
}
commitMsg := strings.Split(runCmd(s), "\n")
return commitMsg
}

func checkFailed(tmpStr string) {
fmt.Fprintln(os.Stderr, "===================================")
fmt.Fprintln(os.Stderr, tmpStr)
fmt.Fprintln(os.Stderr, "===================================")
fmt.Fprintln(os.Stderr, checkFailedMeassge)
os.Exit(1)
}

func isExists(path string) bool {
_, err := os.Stat(path)
if err == nil {
return false
}
if os.IsNotExist(err) {
return false
}
return true
}

/// 规则校验
/// @commitMsg []string 提交的信息数组
/// @config 规则配置
func checkCommitMsg(commitMsg []string, config CommitTypeConfig) {
var CommitMessagePattern = `^(\w+)(\(\w+\))*: (.+)|^Merge\ branch(.*)|^Merge\ remote-tracking branch(.*)`
var commitMsgReg = regexp.MustCompile(CommitMessagePattern)
for _, tmpStr := range commitMsg {
if strings.Trim(tmpStr, " ") == "" {
continue
}
commitTypes := commitMsgReg.FindAllStringSubmatch(tmpStr, -1)
if len(commitTypes) != 1 {
checkFailed(tmpStr)
} else {
if !strings.HasPrefix(tmpStr, "Merge") && !stringInSlice(commitTypes[0][1], config.commitTypeList) {
checkFailed(tmpStr)
}
}
if !config.strictMode {
fmt.Println("恭喜🎉,commit message 校验通过✅")
os.Exit(0)
}
}
fmt.Println("恭喜🎉,commit message 校验通过✅")
}
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}

func runCmd(s string) string {
cmd := exec.Command("/bin/bash", "-c", s)
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
r, err := cmd.Output()
if err != nil {
fmt.Printf("Run command failed, [%s]\nerr: %s", s, err)
}
return string(r)
}

同时支持添加配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"commitTypeList":[ // 支持的 commitType
"feat",
"fix",
"docs",
"style",
"refactor",
"test",
"chore",
"perf",
"hotfix"
],
"strictMode": true // 严格模式会校验多次 commit 的所有信息
}

服务器配置 pre-receive

参考 Server Hooks 需要将编译后 pre-receive 放到制定的 repository 的钩子目录即可,具体步骤如下:

  1. 找到对应 repository 的 .git 目录
  2. 在该目录下创建 custom_hooks 的目录
  3. 将编译后的 pre-receive 放到该目录下(如果有配置文件也上传到该目录下)
  4. 通过 chmod +x pre-receive 让该文件可执行,同时将该文件的用户组切换为 git:git
  5. 推送代码验证结果

pre-receive-校验通过结果