uType——一个宝藏Python类型注解库

一、引言

众所周知——

Python的类型检查发生在运行阶段,因此Python是一门动态类型语言(Dynamically Typed Language)

Python的变量在某一时刻只能有唯一的类型,因此Python是一门强类型语言(Strongly Typed Language)

我们在享受Python高灵活性的同时,类型约束也给我们带来了一定的困扰。uType为Python开发者提供了一套类型校验解决方案,通过使用uType,开发者可以使用声明式的约束定义代替自己编写复杂的校验逻辑。

在uType中, 约束类型 = 源类型(int,str等)+ 约束属性(最小值,长度等),调用约束类型得到的结果是 符合约束 的源类型实例(不符合约束或无法完成转化则抛出错误)

为什么使用使用uType?

  • 约束条件声明式定义、提升代码的简洁性
  • 无需声明 __init__ 便能够接收对应的参数,并且完成类型转化和约束校验
  • 提供清晰可读的 __repr____str__ 函数使得在输出和调试时方便直接获得内部的数据值
  • 在属性赋值或删除时根据字段的类型与配置进行解析与保护,避免出现脏数据

也许你会觉得比较抽象,无需担心,我们会通过举一些简明的例子来说明uType的用法,对比用与不用uType代码的区别。

二、类型约束

先来看一段程序:

class PositiveInt:
    def __init__(self, value):
        if value <= 0:
            raise ValueError("Value must be greater than 0")
        self.value = value


class MyStr:
    def __init__(self, value):
        if len(value) < 10:
            raise ValueError("String length must be at least 10")
        if len(value) > 20:
            raise ValueError("String length must be at most 20")
        self.value = value


try:
    number1 = PositiveInt(7)        # Legal input
    print(number1.value)            # 7
    number2 = PositiveInt(-10)      # Error: String length must be at least 10
    print(number2.value)            # skipped
except ValueError as e:
    print(f"Error: {e}")

try:
    str1 = MyStr("ValidString")     # Legal input
    print(str1.value)               # ValidString
    str2 = MyStr("Invalid")         # Error: String length must be at least 10
    print(str2.value)               # skipped
except ValueError as e:
    print(f"Error: {e}")

如上,我们定义了两个类:一个正整数类(约束条件:大于零),一个字符串类(约束条件:长度在10-20之间)。为了保持程序的健壮性,我们通常需要自己编写异常处理。

可不可以不对自己的变量进行约束、不写异常处理呢?当然是可以的!只不过…

先生/夫人,你也不想自己的代码变成屎山罢

但是如果你用uType,代码量可以大大化简。

对于上面那两个类,我们从utype库中继承<Rule类>作为校验规则,实现相同的功能,它是这样的:

from utype import Rule


class PositiveInt(int, Rule):
    gt = 0


class MyStr(str, Rule):
    min_length = 10
    max_length = 20


try:
    number1 = PositiveInt(7)        # Legal input
    print(number1)                  # 7
    number2 = PositiveInt(-10)      # Error: Constraint: <gt>: 0 violated
    print(number2)                  # skipped
except ValueError as e:
    print(f"Error: {e}")

try:
    str1 = MyStr("ValidString")     # Legal input
    print(str1)                     # ValidString
    str2 = MyStr("Invalid")         # Error: Constraint: <min_length>: 10 violated
    print(str2)                     # skipped
except ValueError as e:
    print(f"Error: {e}")

uType 支持方便地为类型施加约束,你可以使用常用的约束条件(比如大小,长度,正则等)构造约束类型,目前内置支持的约束包括:

  • 范围约束:约束值的最大值,最小值(gt, ge, lt, le
  • 长度约束:约束值的长度或者长度范围(lengthmax_lengthmin_length
  • 常量与枚举约束:约束值必须为某个常量或者在固定的取值范围中(constenum
  • 正则约束:约束值必须满足一个正则表达式(regex
  • 数字约束:约束数字值的最大数字长度,保留位数等(max_digitsround
  • 列表约束:约束列表值的元素唯一性,包含的类型等(unique_itemscontains
import utype
class User:
    class Username(utype.Rule, str):
        regex = r'^[a-zA-Z0-9_]{3,10}$'
    class EmailAddress(utype.Rule, str):
        regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    class Age(utype.Rule, int):
        gt = 0
    def __init__(self, name: Username, email: EmailAddress, age: Age):
        self.name = name
        self.email = email
        self.age = age
try:
    user1 = User(User.Username("Annie_123"), User.EmailAddress("annie@example.com"), User.Age(30))
    print(user1.name, user1.email, user1.age)
    user2 = User(User.Username("Bob"), User.EmailAddress("invalid-email"), User.Age(-5))
except utype.exc.ParseError as e:
    print(f"Error: {e}")

三、自定义类型

并且,源类型也可以是一个自定义的类型,不一定是基本类型:

from utype import Rule


class Age(int, Rule):
    ge = 0
    le = 114514
    pass


class Adult(Age, Rule):
    ge = 18


try:
    bob = Adult(25)  # Legal input
    vinnie = Adult(15)  # Error: Constraint: <ge>: 18 violated
except utype.exc.ParseError as e: # Exception handling from uType
    print(f"Error: {e}")

在 Python 中,一般使用 isinstance(obj, t) 来检测对象 obj 是否是类型 t 的实例(包括 t 的子类的实例),而对于uType的约束类型,这个行为实际上检测的是对象是否是约束类型的源类型的实例,并且满足约束条件:

from utype import Rule


class PositiveInt(int, Rule):
    gt = 0

    
num1 = PositiveInt(1)           # Legal input
num2 = PositiveInt(1.2)         # Legal input, will be automatically converted to 1

print(isinstance(num1, PositiveInt))    # True
print(isinstance(num2, PositiveInt))    # True
print(isinstance(1.2, PositiveInt))     # False

如果用户想要在num2 = PositiveInt(1.2)在创建对象时弹出类型错误(因为1.2不属于int),对于数据类,uType定义了专用的Schema,这里暂时不做介绍

四、内置的约束类型

uType 已经声明好了一些常用的约束类型,你可以直接从 utype.types 中进行导入,如

  • PositiveInt:正数,不包括 0
  • NaturalInt:自然数,包括 0
  • Month:月数,1 到 12
  • Day:一个月中的天数,1 到 31
  • Week:一年中的周数,1 到 53
  • WeekDay:周中的天数,1 到 7
  • Quater:季度,1 到 4
  • Hour:小时,0 到 23
  • Minute:分钟,0 到 59
  • Second:秒,0 到 59
  • SlugStr:常用于文章 URL 的字符串格式,由小写字母,数字和连字符 - 组成
  • EmailStr:满足邮箱地址格式要求的字符串
from utype import types, exc # Import exception handling from uType

try:
    positive_int = types.PositiveInt(1)         
    natural_int = types.NaturalInt(1)          
    month = types.Month(12)                    
    day = types.Day(31)                        
    week = types.Week(7)                       
    weekDay = types.WeekDay(3)                 
    quarter = types.Quarter(1)                 
    hour = types.Hour(23)                      
    minute = types.Minute(0)                   
    second = types.Second(59)                  
    slug = types.SlugStr('article-01')         
    email = types.EmailStr('Vitalik@eth.com')  
    
except exc.ParseError as e:	# Use exc directly 
    print(e)

五、嵌套类型

在原生Python语法中,我们可以使用如 List[int] 的语法声明嵌套类型,例如:

Python2:

values: list[float] = ['invalid', 0.2]

Python3:

from typing import List

values: List[float] = ['invalid', 0.2]

print(values)
# >['invalid', 0.2]

如上方代码所示,即使你往List[float]里捅一个str类型,Python也不会报错,因此这种声明更多是起到提升可读性的作用,Python并没有对数据类型进行校验。

uType 提供了一些可嵌套的类型,提供与类型注解语法一致的声明方式,但是声明出的类型可以直接用于校验与转化,如

from utype import types, exc

array = types.Array[float]

values = [1, 2.5, 'invalid']

try:
    result = array(values)
except exc.ParseError as e:
    print(e)
    
"""
      # parse item: [2] failed: could not convert string to float: 'invalid'
"""

utype 目前支持的嵌套类型有 :

  • types.Array:支持为列表,元组,集合等序列结构声明元素类型和约束,默认源类型是 list

  • types.Object:支持为字典,映射声明元素类型和约束,默认源类型是 dict

你可以继承这些嵌套类型,赋予约束并指定其他的源类型等,用法和 Rule 相似(因为这些嵌套类型也是继承自 Rule)

from utype import types, exc


class UniqueTuple(types.Array):
    __origin__ = tuple
    unique_items = True     # types.Array is an extended class of Rule class

    
unique_tuple = UniqueTuple[int, int, str]
print(unique_tuple(['1', '2', 't']))

try:
    unique_tuple(['1', '1', '3'])
except exc.ParseError as e:
    print(e)
    """
    ConstraintError: Constraint: <unique_items>: True violated: value is not unique
    """
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值