[Python] 也可以有静态语言的特性吗?

观前提示:本文从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_classdef 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"))
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值