观前提示:本文从Python抽象类和类型检查入手,探讨了Python引入静态语言特性的可能
引子
Why:我猜点进来的同学多半是有点好奇,但不屑一顾,Python本身作为动态编译的语言,最大的优势就是灵活,现在非要加上静态语言的特性,这不是自断双臂吗。我之前也是这样的想法,最开始接触的是R语言后来转Python,对于C、Java那一套先编译后运行的静态代码嗤之以鼻(主要是当时能力有限,知道静态语言的运行速度快,但对其繁琐的语法不屑一顾),但后来写了很多Go语言的代码后,发现“真香”了。一个很大的好处就是,Go这种静态语言的代码从业务角度来说,只要写完了,代码不会差很多,运行时的bug也相对来说少很多。而另一个好处,也是我最近在写一个开源的python库时遇到的,我希望定义一个抽象类,让使用者继承并重写其中重要的几个方法,没有Java那种继承并可以借助IDE直接一键生成override方法的优势,python的抽象类方法无法做到友好的提示 相信很多同学也有同样的困惑,想到之前学习python时整理的python静态语言特性的几个case,分享出来。
静态特性
抽象类继承
场景:第一个就是静态类的继承,场景正如前文所描述的那样,我想在自己的开源库中加入几个模板类方法,让使用者继承并重写里面的关键方法,如果没有重写,能有个相对友好的提示。
方法1:raise error
第一个比较容易想到的方法是在模板类的方法中,直接return error,如果使用者继承了这个类,并没有重写其中的方法,自然会在调用时,找到父类的方法并报错。
class Abstract_class:
def must_override():
raise What_ever_you_like_error # 这里可以自定义一种类型的报错
局限性讨论: 方法一存在比较大的局限性,如果使用者继承了这个类,但是没有实现特定的方法,需要到调用的时候才会报错,如果不调用就不会报错(隐蔽性太强),但是在我们这个场景中因为是模板方法,一定会被调用,所以读者同学也可以自己取舍。
方法2:abc
这里的 abc 并不是字母表,而是python中的abstract base class的缩写,抽象基类,可以利用python的abc包中的方法让某些方法必须被继承。
import abc
from abc import abstractmethod
class Abstract_class(abc.ABC):
@abc.abstractmethod
def must_override():
pass
此时的Abstract_class就真的变成了抽象基类,相比于直接在方法中抛异常,这种方法,可以让使用者在实例化的时候就报错(如果没有正确的重写指定的方法的话)
类型检查
语法
类型检查是一个有意思的话题,有时类型检查被看作是静态语言的标志。相信和大家一样,我最早接触python的类型检查,是在LeetCode刷题的时候。关于简单的类型声明部分比较简单,直接将语法列在下方。
age: int = 3
name: str = "a"
x: bytes = b"a"
from typing import List, Dict, Tuple
x: List[str] = ["a"] # 复杂类型在 from typing import List 中
x: Dict[str, str] = {"a": "b"} # 复杂类型在 from typing import Dict 中
x: Tuple[int, ...] = (1,2,3) # 复杂类型在 from typing import Tuple 中
因为复杂类型在typing包中,这也就是为什么LeetCode最上面都会有 from typing import *
这句话
注:这里typing包中的类型,并非是python中复杂类型的真正实现,python中关于list和dict的实现基于C,无法在python中导入,这里只是一个“替身”
到这里,我们的类型检查似乎完成一小半了,剩下还有两个任务,一个是把类型作用在函数中,一个是函数实参的类型检查。
函数中的类型检查
1. 将类型放到函数中:注意返回值的写法
def test(a: int, b: int=3.5) -> float:
print(a+b)
2. 函数实参的类型检查
def is_validate(func, **kwargs):
for param_name, param_type in func.__annotations__.items():
if not isinstance(kwargs[param_name], param_type):
raise TypeError(f"func: [{func.__name__}] param '{param_name}' value {param_value} type error{type(param_value)} -> {param_type}")
def add(a:int, b:int=2):
return a+b
if __name == "__main__":
is_validate(add, a="1", b="1")
输出:TypeError: func: [add] param ‘a’ value 1 type error<class ‘str’> -> <class ‘int’>
装饰器版本的自动类型检查
实现到这里基本就OK了,但是似乎还是差点意思,如果能在智能一点,不用手动调用就好了,聪明的同学一定想到了”装饰器“,难度不大,建议同学自己完成,再看我实现的这一版
提示,func.__annotations__中返回值的变量名为"return",自己完成后再看哦
提示,func.__annotations__中返回值的变量名为"return",自己完成后再看哦
提示,func.__annotations__中返回值的变量名为"return",自己完成后再看哦
提示,func.__annotations__中返回值的变量名为"return",自己完成后再看哦
提示,func.__annotations__中返回值的变量名为"return",自己完成后再看哦
提示,func.__annotations__中返回值的变量名为"return",自己完成后再看哦
提示,func.__annotations__中返回值的变量名为"return",自己完成后再看哦
提示,func.__annotations__中返回值的变量名为"return",自己完成后再看哦
提示,func.__annotations__中返回值的变量名为"return",自己完成后再看哦
提示,func.__annotations__中返回值的变量名为"return",自己完成后再看哦
提示,func.__annotations__中返回值的变量名为"return",自己完成后再看哦
import functools
def is_validate(func, **kwargs):
for param_name, param_type in func.__annotations__.items():
if param_name == "return":
continue
param_value = kwargs[param_name]
if not isinstance(param_value, param_type):
raise TypeError(f"func: [{func.__name__}] param '{param_name}' value {param_value} type error{type(param_value)} -> {param_type}")
def param_check(func):
@functools.wraps(func)
def decorator(*args, **kwargs):
is_validate(func, **kwargs)
return func(*args, **kwargs)
return decorator
@param_check
def add(a:int, b:int=2) -> int:
return a+b
if __name__ == "__main__":
print(add(a="1", b="1"))
总结
在python中引入静态类型算是很有趣的一种尝试,但是这种检查只是为了方便使用者,或者说调用方,给编译器强加的任务,和静态语言的那种在编译中就可以告诉你答案不一样,python还是在运行中完成这一切,这就给原本就不快的python又加了一些任务。另一方面,这种写法损失了python一部分的灵活性,像是失去了动态语言的灵魂。
以上只是讨论,读者同学们根据情况自己取舍吧。
补充
以上内容还是有些问题:函数中的类型检查无法约束位置参数,只能校验关键字参数。多方查找资料,补充如下:
import functools
from inspect import getfullargspec # 获取位置参数对应的变量名
def is_validate(func, **kwargs):
for param_name, param_type in func.__annotations__.items():
if param_name == "return":
continue
param_value = kwargs[param_name]
if not isinstance(param_value, param_type):
raise TypeError(f"func: [{func.__name__}] param '{param_name}' value {param_value} type error{type(param_value)} -> {param_type}")
def param_check(func):
@functools.wraps(func)
def decorator(*args, **kwargs):
args_name = getfullargspec(func)[0] # 获取位置参数对应的变量名
args_param = dict(zip(args_name, args)) # 拼接位置参数对应的变量名和变量值
is_validate(func, **{**args_param, **kwargs}) # 传递得到所有参数
return func(*args, **kwargs)
return decorator
@param_check
def add(a:int, b:int=2) -> int:
return a+b
if __name__ == "__main__":
print(add("1", b="1"))