一、引言
众所周知——
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
) - 长度约束:约束值的长度或者长度范围(
length
,max_length
,min_length
) - 常量与枚举约束:约束值必须为某个常量或者在固定的取值范围中(
const
,enum
) - 正则约束:约束值必须满足一个正则表达式(
regex
) - 数字约束:约束数字值的最大数字长度,保留位数等(
max_digits
,round
) - 列表约束:约束列表值的元素唯一性,包含的类型等(
unique_items
,contains
)
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
:正数,不包括 0NaturalInt
:自然数,包括 0Month
:月数,1 到 12Day
:一个月中的天数,1 到 31Week
:一年中的周数,1 到 53WeekDay
:周中的天数,1 到 7Quater
:季度,1 到 4Hour
:小时,0 到 23Minute
:分钟,0 到 59Second
:秒,0 到 59SlugStr
:常用于文章 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
"""