问题
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