公司有个项目使用 Django 开发的,前段时间我们把 Django 的版本升级到最新版本了,然后就出现问题了,我们发现生产环境出现了一个慢 SQL:
1 | select * from user where not is_delete |
似乎看着很正常的一个 SQL,唯一需要注意的是这个 is_delete 字段,在 model.py
中是使用 BooleanField
上面已经说到了我们使用的字段类似是 BooleanField
,而我们使用的数据库是 MySQL 数据库,MySQL 数据库原生字段类型没有 bool 类型,所以 Django 是以 tinyint
去实现的,所以问题就来了,当你把一个 tinyint
字段当成 bool
字段去使用,就会出现 类型转换 的问题,这就是索引之所以失效的原因。
那么为什么在之前的版本没有出现呢?
因为在之前的版本,上面的 SQL 在 Django 会是这样:
1 | select * from user where is_delete = 0 |
在 Django 3.2 版本之后就变成了前言中的那个 SQL 了,也有人对此提出了疑问,但是很遗憾,官方最后并没有修复这个问题。所以后续在其他数据库中使用 BooleanField
需要尤其注意⚠️。
官方没有修复这个问题,那么就需要用户就解决了,那么有没有解决办法呢?答案是有的,我们需要将涉及 BooleanField
的 ORM 改成如下:
1 | .filter(bool_field=models.Value(0)) # models.Value(1) |
Performance regression in Exact lookup on BooleanField on MySQL.
]]>Python 可以通过 ctypes 模块调用 C 语言实现的函数,调用的方式很简单:
CDLL
的实例{CDLL_instance}.{function_name}({function_parameters})
格式调用 c 语言对应的函数1 | // my_functions.c. |
使用下述命令,创建 so 文件
1 | cc -fPIC -shared -o my_functions.so my_functions.c |
调用 C 的函数
1 | >> from ctypes import * |
从上面的 C 语言的调用过程,不难发现,最重要的是生成的 .so
文件,如果我们可以将 go 编辑成 .so
文件就可以得到 Python 调用 Go 的目的了。
Go 有个标准库 CGO 可以用来创建调用 C 代码的 Go 包,当然反过来也可以。
1 | package main |
需要注意的是:
使用下面的命令进行编译
1 | go build -buildmode=c-shared -o s1.so s1.go |
注意📢:
go build -ldflags "-s -w" -o main.dll -buildmode=c-shared s1.go
1 | from ctypes import cdll |
Python 和 GO 之间的参数传递需要经过 C 的疏忽类型转换,所以还需要了解 python 中 ctypes 数据类型和 python 数据类型以及 C 的数据类型对应关系
三种数据类型使用场景:
对应关系如下:
也就是说,如果写如下的 go 代码:
1 | //export hello |
也就意味着,我们的 Python 调用代码中参数类型也需要对应起来:
1 | hello = library.hello |
在过去一年,我一直使用 github action + hexo 的方式去自动化部署我的博客到 github pages,这种方式的好处就是,你只需要配置好一次将代码仓库,然后你就只需要一个浏览器然后写 hexo 格式的 markdown 文件就够了。
下面是我的构建历史记录
到目前位置累计构建了 93 次,当然并不是意味着我写了 93 篇,更新也会导致文章重新构建。这一切似乎看着还不错,但是有几点一直让我觉得很痛苦:
hexo 有很多固定的格式,有时候每次写文章,我都不得不去找之前的文章把格式复制过来,然后在重新修改一下。除此之外,就是上传图片这件事情了,尝试过各种图床的方案,有的图床也很好用,但是基本上都需要打开他们的网站,或者借助三方的软件然后将图片上传,这种中断,让我觉得很难受,所以我想有没有更好的方式去写博客。
作为一个程序员,github 是我最喜欢逛的网站了,所以我在想有没有可以基于 github 的仓库构建的网站,然后 github issue 和 github discussion 就成了我的备选项了,所以我检索了这两个关键词
最后让我发现了 浮之静
网站挺好看的,但是但是没有部署文档,遂放弃了。
没办法了,没有枪没有炮,敌人给我们造,没有合适的项目,只好自己动手撸了。牺牲了我两个工作日晚上的休息时间,总算把 Pure 初版构建出来了。
部署也很简单,golang 项目通用部署步骤,只需要部署前配置一下四个环境变量(一个非必须的),然后启动项目,配置一下 nginx 就大功告成了。
]]>1 | # example.py |
上面的表达式其实等价于
1 | def is_admin(user_roles): |
其中 __debug__
是内置变量,默认情况下为 True
,这个时候我们传入一个 user_roles = ['admin','guest']
:
1 | 'admin','guest']) is_admin([ |
如果传入 ['guest']
:
1 | line 4, in is_admin |
如果我们有办法改变 __debug__
的值为 False
,那么上面的函数将永远不会出现 AssertionError
,尝试直接去修改这个参数
1 | __debug__ = False |
显然直接修改是行不通的,但是可以在命令行下运行的时候设置 PYTHONOPTIMIZE 达到修改 __debug__
的目的
1 | ❯ python -O example.py |
所在在使用 assert 时候需要注意启动程序的命令,避免 assert 失效导致程序出现问题。
]]>mybatis 支持一对多的关系映射,如果需要在这种关系下,使用 pagehelper 的话,需要注意原本的 sql 以及 resultmap 的用法, 否则可能会导致 pagehelper 返回的 total 比预期的数量大。
1 | <resultMap id="authorWithBlogsResult" type="AuthorWithBlogs"> |
1 | public class AuthorWithBlogs { |
假设现在有一个作者写了两篇文章, 调用 selectAuthorWithBlog
这个方法,会发现返回的实际数量是 1 条,但是如果使用 pagehelper
去获取 total
,返回的 total
是 2 条,这个不难理解,上面的 sql 实际执行结果也是 2 条,pagehelper
的获取 total
也是根据对原 sql 的结果进行 count
得出的。
嵌套 select 查询(Nested Select for Collection)
将原本的关联查询,从 mapper 中抽离,使用 嵌套 select 查询
1 | <resultMap id="authorWithBlogsResult" type="AuthorWithBlogs"> |
主要修改是在 select=”selectBlogByAuthor” column=”id”
这个时候 pagehelper 的查询结果就返回正常了。
Python 的枚举也是一个在时常被被经常使用的一个特性,在 Python3.11 之前,Python 标准库支持枚举类型除了枚举基类 Enum,就只有 IntEnum 了(创建 int 子枚举类型常量的基类),Python 3.11 开始支持了 StrEnum,如果你的当前使用版本是 Python3.11 则可以略过一下内容。
接下来要介绍的内容,则是在 Python3.11 之前的版本,如何实现 StrEnum 以及一些注意事项。
StrEnum 的实现很简单
1 | class StrEnum(str, Enum): |
当子类化 Enum 时,在基类序列中的混合类型必须出现于 Enum 本身之前,否则会报错
1 | class StrEnum(Enum,str): |
使用同样很简单
1 | class WorkDay(str, Enum): |
使用过 Enum 的肯定对 auto 这个不陌生,它会以合适的值代替 Enum 的实例,默认从 1 开始,在 StrEnum 则是以 “1” 的字符串形式开始
1 | class AutoStr(str, Enum): |
如果希望可以自定义 auto 的起始值的话,你可以这样
1 | class CustomAuto(Enum): |
也可以使用 _generate_next_value_
方法去实现
1 | class A(Enum): |
Field
是 MySQL 中的一个函数,其基本使用方法如下:
作用:
用于返回指定值在给定值列表中的索引位置
语法:
1 | FIELD(value, val1, val2, val3, ...) |
例子:
1 | SELECT FIELD("q", "s", "q", "l"); |
上面的 sql 就是返回字母 q
在 s,q,l
中的位置,这里和编程语言中不一样,返回的下标是从 1 开始的,所以上述的返回结果是 2
如果查找的字符不在列表里呢?
1 | SELECT field("a","s", "q", "l"); |
返回为 0 ,结果如下:
说完了,基本用法,说一下 Field 的应用场景,单纯从函数推测出它的应用场景,但是它的实际应用场景,在某种程度上还是很实用的,它可以自定义排序。
所谓的自定义排序就是按照我们给定的顺序对数据结果进行排序,通常情况下,我们通过 order by field
无外乎就是降序或者升序排列,但是在某些特殊的业务场景下,你可能预期按照自己的规则去定义排序。
如下表所示
默认的情况下,显然是按照 id 进行升序排列的,但是如果需要按照人名 a,c,b,d
来排序。也就是 name=a,c,b,d
来排序。
这个时候就可以借由 field
函数去实现了:
1 | SELECT * FROM user ORDER BY FIELD(name, 'a','c','b','d') |
执行结果如下:
现在 user 表里新增了三条记录:
现在有这样子的需求,需要将记录先按照 x,y,z
排序,后面按照正常排序即结果如下:
如何可以做到?
原理其实很简单,归根结底就是 order by
和 field
的联合应用。首先需要明白,order by
的排序原理,order by
原理其实很简单,就是按照我们给定的排序字段(升序或者降序排列)
这个时候,再回到最初 user 表只有四条记录时,按照 a,c,b,d 排序
1 | SELECT * FROM user ORDER BY FIELD(name, 'a','c','b','d') |
这个 sql 发生了什么?
我们用 Field
函数构建了一个临时的排序字段,不同的 name 返回在 field 下返回不同的值,相当于构成了一个新的 sort_field
字段
所以这个时候结果就会按照 a,c,b,d
的顺序排列了。
首先如果如果按照 x,y,z
在最前面的要求,sql 应当是:
1 | SELECT * FROM user ORDER BY FIELD(name, 'x','y','z') |
结果如下:
这个时候的 sort_field
表现是这样子的:
我们预期的应当是
如果只是将排序由升序改为降序
1 | SELECT * FROM user ORDER BY FIELD(name, 'x','y','z') DESC |
这个时候结果如下:
这个结果已经接近 预期了,我们只需要将 field
后面的 x,y,z
变成 z,y,x
就可以了
1 | SELECT * FROM user ORDER BY FIELD(name, 'z','y','x') DESC |
结果如下:
]]>以下例子均的运行环境为 Python 3.9.5,不同版本的实际运行结果可能有所不同
前端时间看了 Golang 的 学习了一下 数组 和 Slice,其中 Slice 相当于动态数组,其中数组的长度是固定的,而 Slice 则是不定长的。在 Python 中是没有数组和 Slice 的概念,它们可以通通归类为 List(列表),那么问题来了,定长的 List 和 不定长的 List 在表现上会有区别吗?(这里的定长的 List 是指对 List 进行初始化,也就是所谓的预分配)
接下来通过几个例子去验证这个问题。
首先验证一下 空列表 以及在不断新增元素的情况下 List 的 size 的变化
1 | from sys import getsizeof |
看一下输出结果
1 | 空列表:56 |
从上面的运行结果可以发现,其实 List 也存在一定的扩容机制的,列表初始化过程中将会申请一个存储四个元素的存储区,当存储区填满时,列表会再次申请四个存储空间存储元素。当元素量达到原存储空间的两倍时,列表会再次申请原来旧的存储空间的两倍的容量存储元素。
不管任何语言在一旦进行扩容操作必然会存在:
1. 重新申请内存
2. 复制老数组到新的内存
3. 回收老数组的内存
上面的过程如果出现的次数比较频繁必然会导致运行速度的降低,那么究竟会降低多少呢?
以下例子运行环境为 ipython
1 | %%timeit |
运行结果如下:
1 | %%timeit |
和上面唯一不同的是,我们在遍历之前,给列表 l 预先分配了一个长度为 1000 的 None 的空数组,运行结果如下:
从这两个例子不难看出,如果预先知道要使用数组的大小,并且对列表进行初始化的话,是可以在一定程度上提高运行效率的。
上面的例子,如果使用列表推导式速度会更快。
列表预先配可能并不适合复杂场景,但是一旦有符合这个特殊场景的情况下效果应该是蛮不错的。
]]>以下例子以 mysql 为准
django orm 中关于更新,有两种操作分别是:
对于 create
和 bulk_create
这俩类,毫无疑问存在大量数据插入的时候,后者效率更高一些。但是对于更新则需要根据实际情况进行分析了,了解 django 是如何实现 bulk_update
,对于我们使用会有一定的好处。
bulk_update
的应用场景是,每个需要更新的数据的 value 值各不相同,如果是相同,则没有必要去使用 bulk_update
对于 update
,没什么好说的,就是底层 sql 就是一个常见的 update 语句,那么 bulk_update
呢?首先要明确知道,mysql 的 update 并不存在类似 insert 这种 insert t values (x),(y),...(z)
语法的,那么 Django 中是如何实现 bulk_update
呢?
源码地址:bulk_update
1 | def bulk_update(self, objs, fields, batch_size=None): |
从源码不难看得出来,django 的批量更新是通过 case when
去实现的。
网上也有对于 case when 和常规 update 的性能对比,这里也不再通过实例对比,直接贴出结论:
更新条数小(一般小于500条),CASE WHEN
优于 UDPATE
更新条数较大(千级别),CASE WHEN
效率迅速下降
CASE WHEN
优于 UPDATE
更新条数小,CASE WHEN
优于 UDPATE
更新条数较大(千级别),CASE WHEN
效率迅速下
所以说,数据过多的情况下,优先使用 update
? 理论上是这样子的,但是 django 的 bulk_update
中还有一个额外的参数 batch_size
,通过 batch_size
可以限制一次批量更新的数据长度,也就避免了大量数据更新造成的不良影响。
最后需要 bulk_update 的时候,记得使用事务
在实际开发中,有时候需要用 Python 去处理一些 JSON 文件,一旦 JSON 文件过大,就有可能出现加载时间过长,内存消耗过大的问题,甚至会导致内存耗尽。所以如果正确地处理大的 JSON 文件呢?
首先构造一个大的 JSON 文件,这里我们使用一个在 github上开源的一个 json 文件 large-file.json,文件大小为 24.9 M
文件内容大致如下:
1 | [ |
接下来我们将会通过不同的方案去解析这个json文件,然后提取对应的 actor.id 和 repo.id 的内容,借助 fileprofiler 去验证不同方案中内存的消耗情况。
1 | import json |
通过 fileprofiler 看一下运行结果:
从上面的图,不难看出,内存重灾区为两处:
1. 读取文件
2. 结果进行 decode
看一下 json
模块的实现源码,不难发现,在解析之前,json.load()
将整个文件都加载到内存当中去了
1 | def load(fp, *, cls=None, object_hook=None, parse_float=None, |
所以显然易见,如果直接加载文件将会占用大量内存。这里还有一个值得思考的问题,为什么文件大小只有 24.9 M,而使用的内存竟然高达 124 M 呢?这个主要是和 Python 字符串的内存表示 有关,简单看一下例子:
1 | import sys |
三个字符串的长度都是 1000,但是它们占用的内存却是各不相同,所以 JSON 文件占用内存的大小并不是取决于它们包含的字符串的长度,而是字符串里包含什么类似的字符。
回归正题,显然使用内置的标准库 json
的效果并不是很理想,所以有没有更好的方案呢?对于大的 JSON 文件并不是一次性加载到内存当中去,而是按需加载,这种方式也有一个名称叫做 - 流(stream)
有很多三方库支持这个特性,接下来我们尝试使用其中一个库 ijson
1 | import ijson |
同样使用 fileprofiler 看一下运行结果
从图片不难看出来,内存消耗一下子就降低了下来。所以下次有大的 JSON 文件,可以尝试用 ijson
库
在 Java 中有 [[重载]] 的概念:
重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
Python 本身不支持 重载
这个特性,但是通过 functools.singledispatch
可以实现函数的重载。接下来通过一个例子,简单地演示一下 Python 的函数重载。
1 | from functools import singledispatch |
运行程序查看一下结果
1 | arg is list: [1, 2, 3] |
根据参数的不同的类型,成功了执行了对应的函数,到这里我们完成了 Python 中对函数的重载
。Java 中 重载
不仅仅针对普通函数,其类方法也支持这个特性,那么 Python 如何实现类方法的重载
呢?
当 Python 版本不低于 3.8 的时候,functools
新增了一个 singledispatchmethod
方法,这个方法可以让 Python 的类方法支持 重载
,使用方法和 singledispatch
类似,唯一需要注意的是,重载的类型是由类函数中第一个非 self
和 cls
的参数类型决定的。
看一下官方实例:
1 | class Negator: |
除此之外,singledispatchmethod
还支持嵌套的装饰器,不过需要注意的是,dispatcher.register
和 singledispatchmethod
需要在绝大多数装饰器的最上层,基于上面的例子修改一下
1 | class Negator: |
当 Python 版本小于 3.8 的时候,由于 Python 本身不支持类方法的重载
,则需要我们自行实现。在实现之前,可以先借鉴一下 [singledispatch
的源码](cpython: f6f691ff27b9 Lib/functools.py)
1 | def wrapper(*args, **kw): |
从源码中不难看得出 singledispatch
最终返回的就是上述的 wrapper
函数,并且函数是根据函数的第一个参数的类型(args[0].__class__
)最终来实现函数的重载
的,对于函数这种实现方式没有任何问题,但是如果是类方法的话,第一个参数始终是 self
,则无法进行区分了,重载
就更无从谈起了。
所以实现类方法的重载难点在于重写 wrapper
函数,所以在 singledispatch
基础上我们定义一个函数对wrapper
进行重写,把 wrapper 中的参数 args[0]
调整为 args[1]
1 | from functools import singledispatch, update_wrapper |
接下来测试一下这个方法:
1 | class Negator: |
运行程序:
1 | -1 |
到此手动实现类方法的重载成功。
在 pypi 上也有对应的 singledispatchmethod · PyPI 库,可以通过 pip 安装后直接使用,用法和 3.8 之后版本用法一致。
bulk_create
不支持 signal
,可以通过自定义 models.Manager
支持这个特性1 | class CustomManager(models.Manager): |
使用实例:
1 | class Test(db.models): |
Django.db.models
自带的 Round
的函数没有不支持小数点位数的保留,默认保留2位,通过自定义的 Func
可以实现调用 MySQL
的 ROUND
函数1 | class RoundPlus(Func): |
每年都要折腾一下自己的博客,今年准备把所有的博客再次迁移到静态博客 Hexo 中,配合 Github Action 实现,只需要一个浏览器就可以自动部署博客。
整体思路很简单,在你的 github-pages 的项目建立两个 分支 master
和 gh-pages
,在 gh-pages
分支上完成 hexo
的构建,然后将构建后的静态网页推送到 master
分支上
其中在 github actions 涉及的主要 action 为 github-pages-deploy-action,参考说明文档,需要使用到 token
或者 ssh-key
,这里我使用到是前者,获取 token 的方法可以参考 encrypted-secrets,然后在你的 github-pages 仓库 secrets
设置一下 token
,命名为 ACCESS_TOKEN
1 | name: Blog CI/CD |
这里略过了关于 hexo 的配置,如果对 hexo 的配置不太了解可以参考 hexo官网,到此,只要每次直接更新 gh-pages 就可以触发自动构建流程了。
]]>lru_cache 是 functools 库中的一个函数,它为函数提供缓存功能的装饰器,缓存 maxsize 组传入参数,在下次以相同参数调用时直接返回上一次的结果。
从它的功能来说是一个不错的方法,可以在一定程度上提高函数的运行速度,但是它存在一个问题,当你用functools.lru_cache装饰器来装饰一个实例方法时,封装该方法的类的实例在持有它们的进程中永远不会被垃圾回收。
接下来让我们通过一个简单的例子去论证上述的观点。(以下代码运行于 python3.6.8)
1 | # test.py |
这里我们创建一个类,这个类有个 calculate
方法,这个方法使用 lru_cache
装饰,为了更好的体现 lru_cache
的作用,加上了一个 delay
的参数,其中 __del__
的魔术方法作用就是当开始垃圾回收的时候,__del__
方法就会被执行。
接下来我们尝试运行该程序,看一下输出结果:
1 | 计算耗时 2.0021407306194305 s, result: 3. |
从输出结果不难看的出,使用 lru_cache
后,第二次调用 calculate
方法,运行的时间降低了很多,不难理解,第二次我们请求的参数完全一样,所以结果直接从字典中就获取到了。但是需要注意到一点,在程序的生命周期中,TestLruCache
的实例被没有被垃圾回收。
上面的例子可能说服力还不够,没关系,接下来我们再看几个例子。
如果我们通过 python -i
的方式执行上述的脚本,你就清晰地发现垃圾回收没有生效。
1 |
|
在交互模式下,我将 test
赋值为 None
,并且主动调用垃圾回收器,能看到 gc.collect()
的输出结果为 22
,但是并没有看到 __del__
方法的输出,这说明程序里依然存在 TestLruCache
的实例引用,垃圾回收器并不能将对象成功的回收。
让我们排查一下究竟是谁还在引用实例
1 | $ python3 -i test.py |
上面的 cache_info
输出结果表明,这个缓存保存着实例的引用,除非手动清除。当我们手动清除缓存并将变量 test
重新分配为 None
时,垃圾收集器才会删除该实例。
除此之外,在这个例子里我们设置的 maxsize
是 128
,表明最大缓存的结果个数是 128
个,一旦我们将 maxsize
置为 None
的话,LRU 特性将被禁用且缓存可无限增长。在这情况下将会很危险,一旦我们在一次请求中创建很多实例,这些实例不能为垃圾回收,一旦达到一定程序,必然会导致内存溢出,从而造成程序崩溃。
那么能不能解决这个问题呢?别着急,往下看
解决这个问题很简答,我们只需要让缓存成为实例的本地变量就行了,这样一来,从缓存到实例的引用就会随着实例一起被删除。看一下改造后的例子:
1 | import time |
好了让我们验证一下改造后的程序是否可以:
1 | $ python3 -i test.py |
注意到这次,我们不需要主动去清除缓存,但是需要显示的调用 gc.collect()
去执行垃圾回收。这是因为这种诡计创造了循环引用,垃圾回收需要做一些特殊的魔术来清除内存。在真正的代码中,Python 解释器会在后台为我们清理这些,而不需要我们调用垃圾回收。
上面提到的问题,会对类方法和静态方法产生影响吗?答案是否定的,让我们验证一下:
1 |
|
同样的,我们在交互模式下验证结果
1 | $ python -i test1.py |
同理我们也可以验证一下静态方法,基本上一样,这里就提供一下代码
1 |
|
functools.lru_cache
Python LRU cache in a class disregards maxsize limit when decorated with a staticmethod or classmethod decorator
为了保证 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-commit
是 git-hooks
中的一个重要的钩子,它在键入提交信息前运行。可以用于检查即将提交的快照,例如,检查是否有所遗漏,确保测试运行,以及核查代码。 如果该钩子以非零值退出,Git 将放弃此次提交。 上面提到的机制是整个工作流可以进行的关键。
完全可以自定义 pre-commit
钩子的,但是如果只是为了检验的话,可以使用现成的方案 pre-commit/pre-commit,用 Python 构建,支持多语言的管理器。通过 pre-commit
这个库,简单地几步就可以实现自动化工作流。
1 | pip install pre-commit |
然后通过 pre-commit --version
确定是否安装成功
1 | $ pre-commit --version |
.pre-commit-config.yaml
的配置文件可以通过 pre-commit sample-config
生成一个默认的配置文件,这里贴一下关于 black
和 isort
的配置文件
1 | repos: |
支持的配置项很多,具体参考 plugins
1 | $ pre-commit install |
然后就就大功告成了
]]>在 3.6 之前可以通过 meta class
去实现,3.6 之后可以通过 __init_subclass__
实现
meta class
通过 meta class
的 __new__
方法可以实现自动注册 class
的功能,原理很简单,就是通过元类去控制类的创建,在调用 __new__
方法的时候自动将 class
注册
1 | class MetaClass(type): |
__init_subclass__
__init_subclass__
是 3.6 后引入的一个新的特性,一个 hook,可以让所有的子类在创建之后执行一些初始化的操作,通过这个特性我们就可以更简单地实现上述通过元类实现的功能。
1 | class ParentClass: |
需要注意的是 __init_subclass__
没有非关键字参数。
代码存在如下的逻辑:
1 | if category == 'a': |
存在很多都处理逻辑,每个逻辑可能由不同的开发人员编写,全部放到一个代码块中,可以预见随着分支的增多,这部分代码会变得越来越庞大,不利于后期维护,所以最简单的方案,存在一个 dict 保存 category
和 处理方法的映射关系:
1 | category_to_func = { |
然后就可以将最初的代码简化为:
1 | return category_to_func.get(category)() |
基于上面的思路,可以写出最简单的方法,让大家自行编写自己的处理方法,然后在 category_to_func
中统一注册自己的方法。
1 | category_to_func = { |
这样子没有任何问题,但是不够优雅,开发人员撰写了自己的处理方法之后,还要去指定的地方注册自己的方法,开发体验不太好。
如果存在一种方式,开发人员只需要撰写处理方法,代码可以自定注册就更好了。
通过上面的自注册的方法,让子类继承父类,然后将其自动注册到全局当中,这样子开发人员只需要关注自己的业务实现就可以了。
PEP 487 – Simpler customisation of class creation
python - How to auto register a class when it’s defined - Stack Overflow
Django 自带的 SearchFilter 是不支持空格搜索的,如果需要支持空格搜索并且保留之前的搜索功能则需要自定义 SearchFilter。
继承 SeachFilter 然后实现 get_search_terms
方法,如果同时支持 space 然后又可以保持之前的搜索特性呢?
简单的做法就是,对输入的参数进行校验,如果只有含有空格就返回空格,否则就执行之前的搜索逻辑
1 | class CanSerachBothCharFilter(SearchFilter): |
在 PEP 484 中引入的 Typing hints (类型提示) 可以用来为静态类型检查器和其他第三方工具指定类型元数据。但是在,PEP 484 只指定了名义子类型的语义。在这个 PEP 544 中,指定了协议类的静态和运行时语义,这将为结构性子类型(静态鸭子类型)提供一个支持。
If it walks like a duck and it quacks like a duck, then it must be a duck。
简单地说,”如果它走路像鸭子,叫起来像鸭子,那它一定是鸭子“。
在编程中这就意味着当我们编写接收特定输入的函数时,我们只需要关心该函数输入的行为、属性,而不是该函数输入的显式类型。
例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为”鸭子”的对象,并调用它的”走”和”叫”方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的”走”和”叫”方法。— 百度百科
那么如何在 Python 中实现“鸭子类型”呢?
现在有这样子场景,我们需要编写一个函数去计算一个拥有边长(len_side)的物体的周长,伪代码如下:
1 | function calcute_circumference(shape): |
从伪代码不难看出,我们不关心是什么的物体,我们只需要保证这个物体可以有一个 len_side(列表) 字段就可以了。
现在我们可以借由 Protocol
去实现它。
1 | from typing import Protocol |
从上面的例子不难看出 Protocol
有点类似 Java 中的接口,只需要在函数上使用它,并不需要去关注输入的参数的具体类型。
我们还可以配合使用 typing
中的 TypeVar
实现泛型参数化,让我们的函数更加抽象化,只要符合协议,无论什么类型的输入都可以。
1 | from typing import Protocol |
上述的例子都可以借由 Python 中的 ABCs 去实现的,但是两者的侧重点有所不同,由于篇幅有限,在下一篇文章,让我们仔细对比一下两者的的区别。
]]>在使用 Git 作为版本控制工具时,每次文件发生修改的时候提交都需要 git commit
命令去记录本次的修改,否则就不允许提交,显然 git commit
是一个重要的环节,因此制定一个 Git Commit 规范是有必要的,否则就会出现混乱的提交信息,这里腾讯某团队的规范作为例子,然后通过一定的手段去帮助我们把这个规范落到实处。
目的:
Git commit
日志标准,便于后续代码 review,版本发布以及日志自动化生成等等。1 | <type>(<scope>): <subject> |
type代表某次提交的类型,比如是修复一个bug还是增加一个新的feature
1 | # 标题行:50个字符以内,描述主要变更内容 |
node 项目已经有比较成熟的方案,这里以 Python 项目为例,考虑实际使用的便利性,希望可以可以实现以下的目的:
git push
否则则需要重新提交 commit 的信息。Git 在执行 git init
进行初始化的时候,会在 .git/hooks
目录下生成一系列 hooks 脚本:
从上图可以看到每个脚本的后缀都是以 .sample
结尾的,在这个时候,脚本是不会自动执行的。我们需要把后缀去掉之后才会生效,即将 pre-commit.sample
变成 pre-commit
才会起作用。由于只是对 commit msg 做校验,所以只需要使用 commit-msg
脚本即可。
在 commit-msg
hooks 中完成对 commit 消息校验
1 | #!/bin/sh |
将上述脚本保持在项目所在对 .git/hooks
目录下命名为 commit-msg
,然后执行 chmod
命令:
1 | chmod +x .git/hooks/commit-msg |
Git 在服务端也同样有一些 hooks:
pre-receive
update
post-receive
每个 hooks 的具体功能可以参考 Server-Side Hooks ,其中 pre-receive
和 update
均符合使用场景,唯一区别是用户同时推送到多个分支时, update 针对每个分支都会被触发执行,而 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 | package main |
同时支持添加配置文件
1 | { |
参考 Server Hooks 需要将编译后 pre-receive
放到制定的 repository 的钩子目录即可,具体步骤如下:
.git
目录custom_hooks
的目录pre-receive
放到该目录下(如果有配置文件也上传到该目录下)chmod +x pre-receive
让该文件可执行,同时将该文件的用户组切换为 git:git