Python type checking system

被刻印的时光 ゝ 提交于 2019-12-11 08:55:58

问题


I am trying to make custom type system in Python. Following is the code.

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)

Using the above type system, I got rid of all the boilerplate code and duplicate code that I would have to write in classes (mostly inside init) for checking types, validating values etc.

By using the code above, my classes would look as simple as this

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

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

Till here things work fine. Now I want to extend this type checks functionality and create classes holding objects of another classes. For example price is now no longer a Float but its of type Price (i.e. another 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. 

This won't work because line "price = Price()" will make call to constructor of Price and would expect currency and value to be passed to the constructor because Price is a Structure and not a Descriptor. It throws "TypeError: missing a required argument: 'currency'".

But I want it to work and make it look like above because at the end of the day Price is also a type just like PosInteger but at the same time it has to be Structure too. i.e. Price should be inheriting from Structure but at the same time it has to be a descriptor too.

I can make it work by defining another class say "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))

But this looks a bit weird - Price and PriceType as two difference classes. Can someone help me understand if I can avoid creating PriceType class?

I am also losing out on a functionality to provide default values to fields.

For example, how can I keep default value of share field in Stock to 0 or default value of currency field in Price to 'USD'? i.e. something like below.

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

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

回答1:


A quick thing to do there is to have a simple function that will build the "PriceType" (and equivalents) when you declare the fields.

Since uniqueness of the descriptor classes themselves is not needed, and the relatively long time a class takes to be created is not an issue, since fields in a body class are only created at program-load time, you should be fine with:

def typefield(cls, *args, extra_checkers = (), **kwargs):
    descriptor_class = type(
        cls.__name__,
        (Typed,) + extra_checkers,
        {'ty': cls}
    )
    return descriptor_class(*args, **kwargs)

And now, code like this should just work:

class Stock(Structure):
    name =  SizedString(maxlen=9)
    shares =  PosInteger()
    price = typefield(Price, "price")

(Also, note that Python 3.6+ have the __set_name__ method incorporated into the descriptor protocol - if you use this, you won't need to pass the field name as a parameter to the default descriptor __init__, and type field names twice)

update

In your comment, you seam to implicate want your Structure classes to work themselves as descriptors - that would not work well - the descriptors __get__ and __set__ methods are class methods - you want the fields to be populated with actual instances of your structures.

What can be done is to move the typefield method above to a class method in Structure, have it annotate the default parameters your want, and create a new intermediate descriptor class for these kind of fields that will automatically create an instance with the default values when it is read. Also, ty can simply be an instance attribute in the descriptor, so no need to create dynamic classes for the fields:

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)


来源:https://stackoverflow.com/questions/54539132/python-type-checking-system

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!