Python 类型检查系统
Python type checking system
我正在尝试在 Python 中创建自定义类型系统。以下是代码。
from inspect import Signature, Parameter
class Descriptor():
def __init__(self, name=None):
self.name = name
def __set__(self, instance, value):
instance.__dict__[self.name] = value
def __get__(self, instance, cls):
return instance.__dict__[self.name]
class Typed(Descriptor):
ty = object
def __set__(self, instance, value):
if not isinstance(value, self.ty):
raise TypeError('Expected %s' %self.ty)
super().__set__(instance, value)
class Integer(Typed):
ty = int
class Float(Typed):
ty = float
class String(Typed):
ty = str
class Positive(Descriptor):
def __set__(self, instance, value):
if value < 0:
raise ValueError('Expected >= 0')
super().__set__(instance, value)
class PosInteger(Integer, Positive):
pass
class Sized(Descriptor):
def __init__(self, *args, maxlen, **kwargs):
self.maxlen = maxlen
super().__init__(*args, **kwargs)
def __set__(self, instance, value):
if len(value) > self.maxlen:
raise ValueError('TooBig')
super().__set__(instance, value)
class SizedString(String, Sized):
pass
def make_signature(names):
return Signature([Parameter(name, Parameter.POSITIONAL_OR_KEYWORD) for name in names])
class StructMeta(type):
def __new__(cls, name, bases, clsdict):
fields = [key for key, value in clsdict.items() if isinstance(value, Descriptor)]
for name in fields:
#print(type(clsdict[name]))
clsdict[name].name = name
clsobj = super().__new__(cls, name, bases, clsdict)
sig = make_signature(fields)
setattr(clsobj, '__signature__', sig)
return clsobj
class Structure(metaclass = StructMeta):
def __init__(self, *args, **kwargs):
bound = self.__signature__.bind(*args, **kwargs)
for name, value in bound.arguments.items():
setattr(self, name, value)
使用上述类型系统,我摆脱了所有样板代码和重复代码,我必须在 classes 中(主要在 init 中)编写用于检查类型、验证值等的代码
通过使用上面的代码,我的 classes 看起来会像这样简单
class Stock(Structure):
name = SizedString(maxlen=9)
shares = PosInteger()
price = Float()
stock = Stock('AMZN', 100, 1600.0)
到这里一切正常。现在我想扩展此类型检查功能并创建 classes 持有另一个 classes 的对象。例如 price 现在不再是 Float 而是它的 Price 类型(即另一个 class Price)。
class Price(Structure):
currency = SizedString(maxlen=3)
value = Float()
class Stock(Structure):
name = SizedString(maxlen=9)
shares = PosInteger()
price = Price() # This won't work.
这行不通,因为第 "price = Price()" 行将调用 Price 的构造函数,并希望将货币和值传递给构造函数,因为 Price 是结构而不是描述符。它抛出 "TypeError: missing a required argument: 'currency'"。
但我希望它能正常工作并使其看起来像上面那样,因为归根结底,Price 也是一种类似于 PosInteger 的类型,但同时它也必须是 Structure。即 Price 应该从 Structure 继承,但同时它也必须是一个描述符。
我可以通过定义另一个 class 来使它工作 "PriceType"
class Price(Structure):
currency = SizedString(maxlen=3)
value = Float()
class PriceType(Typed):
ty = Price
class Stock(Structure):
name = SizedString(maxlen=9)
shares = PosInteger()
price = PriceType()
stock = Stock('AMZN', 100, Price('INR', 2400.0))
但这看起来有点奇怪 - Price 和 PriceType 是两个不同的 classes。有人可以帮助我了解是否可以避免创建 PriceType class 吗?
我也失去了为字段提供默认值的功能。
例如,如何将 Stock 中 share 字段的默认值保持为 0 或 Price 中 currency 字段的默认值 'USD'?即如下所示。
class Stock:
def __init__(name, price, shares=0)
class Price
def __init__(value, currency = 'USD')
一个快速的事情是有一个简单的函数,它将在您声明字段时构建 "PriceType"(和等价物)。
因为不需要描述符 classes 本身的唯一性,并且创建 class 相对较长的时间不是问题,因为正文中的字段 class 只在程序加载时创建,你应该没问题:
def typefield(cls, *args, extra_checkers = (), **kwargs):
descriptor_class = type(
cls.__name__,
(Typed,) + extra_checkers,
{'ty': cls}
)
return descriptor_class(*args, **kwargs)
现在,像这样的代码应该可以正常工作了:
class Stock(Structure):
name = SizedString(maxlen=9)
shares = PosInteger()
price = typefield(Price, "price")
(另请注意,Python 3.6+ 已将 __set_name__
方法合并到 descriptor protocol 中 - 如果您使用此方法,则无需将字段名称作为默认描述符的参数 __init__
,并键入字段名称两次)
更新
在您的评论中,您暗示希望您的 Structure
class 将它们自己用作描述符 - 这不会很好地工作 - 描述符 __get__
和 __set__
方法是 class 方法 - 您希望用结构的实际实例填充字段。
可以做的是将上面的typefield
方法移动到Structure中的class方法,让它注释你想要的默认参数,并创建一个新的中间描述符class 对于这些类型的字段,在读取时会自动创建一个具有默认值的实例。此外,ty
可以简单地作为描述符中的实例属性,因此无需为字段创建动态 classes:
class StructField(Typed):
def __init__(self, *args, ty=None, def_args=(), def_kw=None, **kw):
self.def_args = def_args
self.def_kw = def_kw or {}
self.ty = ty
super().__init__(*args, **kw)
def __get__(self, instance, owner):
if self.name not in instance.__dict__:
instance.__dict__[self.name] = self.ty(*self.def_args, **self.def_kw)
return super().__get__(instance, owner)
...
class Structure(metaclass=StructMeta):
...
@classmethod
def field(cls, *args, **kw):
# Change the signature if you want extra parameters
# for the field, like extra validators, and such
return StructField(ty=cls, def_args=args, def_kw=kw)
...
class Stock(Structure):
...
price = Price.field("USD", 20.00)
我正在尝试在 Python 中创建自定义类型系统。以下是代码。
from inspect import Signature, Parameter
class Descriptor():
def __init__(self, name=None):
self.name = name
def __set__(self, instance, value):
instance.__dict__[self.name] = value
def __get__(self, instance, cls):
return instance.__dict__[self.name]
class Typed(Descriptor):
ty = object
def __set__(self, instance, value):
if not isinstance(value, self.ty):
raise TypeError('Expected %s' %self.ty)
super().__set__(instance, value)
class Integer(Typed):
ty = int
class Float(Typed):
ty = float
class String(Typed):
ty = str
class Positive(Descriptor):
def __set__(self, instance, value):
if value < 0:
raise ValueError('Expected >= 0')
super().__set__(instance, value)
class PosInteger(Integer, Positive):
pass
class Sized(Descriptor):
def __init__(self, *args, maxlen, **kwargs):
self.maxlen = maxlen
super().__init__(*args, **kwargs)
def __set__(self, instance, value):
if len(value) > self.maxlen:
raise ValueError('TooBig')
super().__set__(instance, value)
class SizedString(String, Sized):
pass
def make_signature(names):
return Signature([Parameter(name, Parameter.POSITIONAL_OR_KEYWORD) for name in names])
class StructMeta(type):
def __new__(cls, name, bases, clsdict):
fields = [key for key, value in clsdict.items() if isinstance(value, Descriptor)]
for name in fields:
#print(type(clsdict[name]))
clsdict[name].name = name
clsobj = super().__new__(cls, name, bases, clsdict)
sig = make_signature(fields)
setattr(clsobj, '__signature__', sig)
return clsobj
class Structure(metaclass = StructMeta):
def __init__(self, *args, **kwargs):
bound = self.__signature__.bind(*args, **kwargs)
for name, value in bound.arguments.items():
setattr(self, name, value)
使用上述类型系统,我摆脱了所有样板代码和重复代码,我必须在 classes 中(主要在 init 中)编写用于检查类型、验证值等的代码
通过使用上面的代码,我的 classes 看起来会像这样简单
class Stock(Structure):
name = SizedString(maxlen=9)
shares = PosInteger()
price = Float()
stock = Stock('AMZN', 100, 1600.0)
到这里一切正常。现在我想扩展此类型检查功能并创建 classes 持有另一个 classes 的对象。例如 price 现在不再是 Float 而是它的 Price 类型(即另一个 class Price)。
class Price(Structure):
currency = SizedString(maxlen=3)
value = Float()
class Stock(Structure):
name = SizedString(maxlen=9)
shares = PosInteger()
price = Price() # This won't work.
这行不通,因为第 "price = Price()" 行将调用 Price 的构造函数,并希望将货币和值传递给构造函数,因为 Price 是结构而不是描述符。它抛出 "TypeError: missing a required argument: 'currency'"。
但我希望它能正常工作并使其看起来像上面那样,因为归根结底,Price 也是一种类似于 PosInteger 的类型,但同时它也必须是 Structure。即 Price 应该从 Structure 继承,但同时它也必须是一个描述符。
我可以通过定义另一个 class 来使它工作 "PriceType"
class Price(Structure):
currency = SizedString(maxlen=3)
value = Float()
class PriceType(Typed):
ty = Price
class Stock(Structure):
name = SizedString(maxlen=9)
shares = PosInteger()
price = PriceType()
stock = Stock('AMZN', 100, Price('INR', 2400.0))
但这看起来有点奇怪 - Price 和 PriceType 是两个不同的 classes。有人可以帮助我了解是否可以避免创建 PriceType class 吗?
我也失去了为字段提供默认值的功能。
例如,如何将 Stock 中 share 字段的默认值保持为 0 或 Price 中 currency 字段的默认值 'USD'?即如下所示。
class Stock:
def __init__(name, price, shares=0)
class Price
def __init__(value, currency = 'USD')
一个快速的事情是有一个简单的函数,它将在您声明字段时构建 "PriceType"(和等价物)。
因为不需要描述符 classes 本身的唯一性,并且创建 class 相对较长的时间不是问题,因为正文中的字段 class 只在程序加载时创建,你应该没问题:
def typefield(cls, *args, extra_checkers = (), **kwargs):
descriptor_class = type(
cls.__name__,
(Typed,) + extra_checkers,
{'ty': cls}
)
return descriptor_class(*args, **kwargs)
现在,像这样的代码应该可以正常工作了:
class Stock(Structure):
name = SizedString(maxlen=9)
shares = PosInteger()
price = typefield(Price, "price")
(另请注意,Python 3.6+ 已将 __set_name__
方法合并到 descriptor protocol 中 - 如果您使用此方法,则无需将字段名称作为默认描述符的参数 __init__
,并键入字段名称两次)
更新
在您的评论中,您暗示希望您的 Structure
class 将它们自己用作描述符 - 这不会很好地工作 - 描述符 __get__
和 __set__
方法是 class 方法 - 您希望用结构的实际实例填充字段。
可以做的是将上面的typefield
方法移动到Structure中的class方法,让它注释你想要的默认参数,并创建一个新的中间描述符class 对于这些类型的字段,在读取时会自动创建一个具有默认值的实例。此外,ty
可以简单地作为描述符中的实例属性,因此无需为字段创建动态 classes:
class StructField(Typed):
def __init__(self, *args, ty=None, def_args=(), def_kw=None, **kw):
self.def_args = def_args
self.def_kw = def_kw or {}
self.ty = ty
super().__init__(*args, **kw)
def __get__(self, instance, owner):
if self.name not in instance.__dict__:
instance.__dict__[self.name] = self.ty(*self.def_args, **self.def_kw)
return super().__get__(instance, owner)
...
class Structure(metaclass=StructMeta):
...
@classmethod
def field(cls, *args, **kw):
# Change the signature if you want extra parameters
# for the field, like extra validators, and such
return StructField(ty=cls, def_args=args, def_kw=kw)
...
class Stock(Structure):
...
price = Price.field("USD", 20.00)