使用元类实现具有动态字段的描述符

Implementing descriptors with dynamic field using metaclasses

我有用户定义的 class,它实现了描述符协议。描述符被分配给客户端的属性 class。目前描述符使用通用变量名称,即描述符的 class 名称和一个键。

我希望根据客户端上的名称动态定义属性 class。我知道我需要为此使用 metaclasses,所以这是我尝试过的:

class MyDescriptor(object):
        __counter = 0

        def __init__(self, field=None):
                self.field = field or '{}_{}'.format(self.__class__.__name__, self.__counter)

        def __get__(self, obj, owner):
                print 'getter'
                return getattr(obj, self.field)

        def __set__(self, obj, val):
                print 'setter'
                setattr(obj, self.field, val)


class MyMeta(type):
        def __new__(mcl, name, bases, nspc):
                for k,v in nspc.items():
                        if isinstance(v, MyDescriptor):
                                nspc[k] = MyDescriptor(field=k)

                return super(MyMeta, mcl).__new__(mcl, name, bases, nspc)


class User(object):

        __metaclass__ = MyMeta

        desc = MyDescriptor()

        def __init__(self, desc):
                self.desc = desc

现在这似乎可以工作了,如果我检查我的用户的字典属性 class 我看到 MyDescriptor 对象的字段值具有值 'desc':

>>> User.__dict__['desc'].__dict__
{'field': 'desc'}

但是当我创建 User 的新对象时,我最终得到一个递归循环,它打印 'setter' 并以异常结束。

>>> User('x')
setter
setter
setter
setter
setter
...

为什么会这样?为什么当我尝试初始化对象时会递归调用 __set__ 方法?

如何根据客户端中的属性名称为 Descriptor 对象分配动态值 class? (在上面的示例中,我使用了 desc,但假设我还有其他内容,例如名称、位置等。)

我正在回答我的问题以将此问题标记为已回答。

感谢 @Aran-Fey 评论,我的问题是我使用了与属性相同的值字段,因此在查找时递归调用了 setattr(obj, self.field, val) self.field 值。将 self.field 值更改为 '_' + field 解决了我的问题。

关于超出递归深度的问题,另一种解决方法是直接操作对象字典,如 obj.__dict__[self.field] = val。我更喜欢这种方法,因为它不需要你在描述符 class 中有一个 __init__ 只是为了设置一个假的属性名称来解决使用 [=14= 时发生的递归问题] 和 setattr()

现在对于属性的动态命名,我就是这样解决这个问题的。我知道这与您的方法略有不同,但它对我有用。

class MyMeta(type):

    def __new__(mcs, cls, bases, dct):
        for k, v in dct.items():
            if if isinstance(v, Descriptor):
                dct[k].attr = k
        return super().__new__(mcs, cls, bases, dct)


class Descriptor:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.attr]

class Positive(Descriptor):

    def __set__(self, instance, value):
        if value < 1:
            raise ValueError("{}.{} can't be negative".format(
                instance.__class__.__name__, self.attr))
        else:
            instance.__dict__[self.attr] = value


class Rectangle(metaclass=MyMeta):
    length = Positive()
    breadth = Positive()

    def __init__(self, length, breadth):
        self.length = length
        self.breadth = breadth

    def __repr__(self):
        return "{}({!r}, {!r})".format(self.__class__.__name__, self.length,
                                       self.breadth)


r1 = Rectangle(4, 20)
r2 = Rectangle(100, 200)
print(r1, r2)
r1.breadth = 30
r2.length = 150
print(r1, r2)

输出

Rectangle(4, 20) Rectangle(100, 200)
Rectangle(4, 30) Rectangle(150, 200)

它的工作方式是从元 class 字典中拦截描述符实例 class 然后在描述符实例的字典中添加一个新属性并将其值实际设置为描述符实例名字.

所以在上面的例子中,MyMeta 将遍历 class 字典,当它找到一个属性时,它是 class Descriptor 的一个实例,lengthbreadth 是,它将分别为 lengthbreadth 描述符实例添加一个属性 attr=lenghtattr=breadth

请注意,这并不要求您在描述符 class 中包含 __init__。另外你可以看到 metaclass 不是特定于描述符的,你可以使用相同的 metaclass 它适用于任何描述符,你唯一需要做的就是创建一个新的描述符 class 继承自 Descriptor class

大家好让我向您展示如何使用 Python 元 class 编写最先进的描述符,以及如何对它们执行验证。以及如何使用 Metaclass

定义您自己的数据类型
__Author__ = "Soumil Nitin SHah "
__Version__ = "0.0.1"
__Email__ = ['soushah@my.bridgeport.edu',"shahsoumil519@gmail.com"]
__Github__ = "https://github.com/soumilshah1995"

Credits = """  Special Thanks to David Beazley for his Tutorials """

from inspect import  Parameter, Signature
import datetime


class Descriptor(object):

    def __init__(self, name):
        self.name = name

    def __get__(self, instance, owner):

        print(" __get__ ")
        return instance.__dict__[self.name]
        # This will return Value

        # if instance is not None:
        #     return self
        # else:
        #     return instance.__dict__[self.name]

    def __set__(self, instance, value):

        print(" __set__ ")
        instance.__dict__[self.name] = value

    def __delete__(self, instance):
        print(" __delete__ ")
        del instance.__dict__[self.name]

    def __repr__(self):
        return "Object Descriptor : {}".format(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 PositiveInterger(Descriptor):

    def __set__(self, instance, value):
        if value < 0:
            raise ValueError ("Value cannot be less than 0")


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('Too big')
        super().__set__(instance, value)


def make_signature(names):
    return Signature(
        Parameter(name, Parameter.POSITIONAL_OR_KEYWORD)
        for name in names)


class Integer(Typed):
    ty = int


class Float(Typed):
    ty = float


class String(Typed):
    ty = str


class StructMeta(type):

    def __new__(cls, clsname, base, clsdict):

        clsobj = super().__new__(cls, clsname, base, clsdict)
        sig = make_signature(clsobj._fields)
        # print("Sig", sig)
        setattr(clsobj, '__signature__', sig)
        return clsobj


class Structure(metaclass=StructMeta):

    _fields = []

    def __init__(self, *args, **kwargs):

        # print("**kwargs", kwargs)
        # print("*Args", args)

        bound = self.__signature__.bind(*args, **kwargs)
        # print("bound", bound)

        for name, val in bound.arguments.items():
            # print(name, val)
            setattr(self, name, val)


class Stock(Structure):

    _fields = ['name', 'shares', 'price', "number", "fullname"]

    name = String('name')
    shares = Integer('shares')
    price = Float('price')
    number = PositiveInterger('number')
    fullname = Sized('fullname', maxlen=8)

    def methodA(self):
        print("Total Shares {}".format(self.shares))


if __name__ == "__main__":
    obj = Stock(name="Soumil", shares=12, price=12.2, number =2, fullname='aaaaaa')
    print("="*55)
    print(obj.methodA())