Python类型检查系统

时间:2019-02-05 16:41:18

标签: python metaprogramming descriptor

我正在尝试在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)

使用上述类型系统,我摆脱了所有必须在类(主要在init内)中编写的用于检查类型,验证值等的样板代码和重复代码。

通过使用上面的代码,我的类看起来像这样简单

class Stock(Structure):
        name =  SizedString(maxlen=9)
        shares =  PosInteger()
        price = Float()

 stock = Stock('AMZN', 100, 1600.0)

直到这里一切正常。现在,我想扩展此类型检查功能,并创建包含其他类的对象的类。例如,现在价格不再是浮点数,而是价格类型(即另一类价格)的价格。

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:缺少必需的参数:'currency'”。

但是我希望它能像上面那样工作,因为在一天结束时,Price就像PosInteger一样,但是同时也必须是Structure。即价格应该从结构继承,但同时也必须是描述符。

我可以通过定义另一个类“ 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是两个不同的类。有人可以帮助我了解是否可以避免创建PriceType类吗?

我也迷失了为字段提供默认值的功能。

例如,如何将“股票”中“份额”字段的默认值保持为0或将“价格”中的货币字段的默认值保持为“ USD”?即如下所示。

class Stock:
    def __init__(name, price, shares=0)

class Price
    def __init__(value, currency = 'USD')

1 个答案:

答案 0 :(得分:2)

要做的一件事是拥有一个简单的函数,当您声明字段时,该函数将构建“ PriceType”(和等效项)。

由于不需要描述符类本身的唯一性,并且创建类花费的时间相对较长也不是问题,因为主体类中的字段仅在程序加载时创建,因此:

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+将descriptor protocol中集成了__set_name__方法-如果使用此方法,则无需将字段名称作为参数传递给默认描述符__init__,然后输入两次字段名称)

更新

在您的评论中,您可能想暗示要让您的Structure类作为描述符自己工作-效果不好-描述符__get____set__方法是类方法-您希望用您的结构的实际实例填充字段。

可以做的是将上面的typefield方法移动到Structure中的类方法,让它注释您想要的默认参数,并为这些类型的字段创建一个新的中间描述符类,该类将自动创建读取时具有默认值的实例。另外,ty可以只是描述符中的实例属性,因此无需为字段创建动态类:

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)