【问题标题】:Combining a descriptor class with dataclass and field将描述符类与数据类和字段相结合
【发布时间】:2021-08-09 05:45:08
【问题描述】:

我正在使用数据类和字段来传递默认值。 提供参数时,我想使用描述符类对其进行验证。

有什么方法可以利用字段(repr、default、init 等)的好处,同时获得描述符类的验证器好处?

from dataclasses import dataclass, field


class Descriptor:
    def __init__(self, default):
        self.default = default

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, objtype=None):
        if obj:
            return vars(obj).get(self.name)
        else:
            return None

    def __set__(self, obj, value):
        if not value:
            value = self.default
        else:
            value = field(default=int(value), repr=False)
        vars(obj)[self.name] = value


@dataclass
class Person:
    age: str = Descriptor(field(default=3, repr=False))

    # Many additional attributes 
    # using same descriptor class


p = Person()
r = Person(2.37)

【问题讨论】:

  • 为什么要将field 对象传递给描述符对象?老实说,我认为您正在尝试做的事情与dataclass 不兼容。
  • 描述符有效地取代了字段;我认为您想要age = ClassVar(Descriptor(3)) 之类的东西,而Descriptor 本身将处理将实际值存储在附加到其obj 参数的属性中。
  • 您可以考虑只使用pydantic 库,它有一个dataclass 实现,具有验证功能。否则,标准库dataclass 不会允许您尝试执行此操作。
  • 顺便说一句,vars(obj)[self.name] = value 没有任何意义 value = field(default=int(value), repr=False)... 将字段分配给实例属性对您没有任何作用。 dataclass 的全部意义在于,它自省类主体中的注释以生成各种样板方法当类本身被实例化时....当您创建实例以对该 field 对象执行任何操作时
  • 基本上,人们需要了解数据类是针对特定用例的,即数据类,基本上充当记录类型的类。它们提供了一种避免样板文件的方法,但您必须坚持创建类定义的文档化方法。如果您想做其他更花哨的事情,那么最简单的解决方案就是不要使用dataclasses.dataclass

标签: python python-3.x python-dataclasses


【解决方案1】:

你的想法没问题,但不漂亮而且行不通
我的第一个想法是让Descriptor 继承dataclasses.Field
但这也不起作用

你应该做的是有点迂回
但是,即使没有数据类,也应该这样做
就是做2个属性age_age

import dataclasses as dc

class GetSet:
    def __init__(self, predicate = lambda value: True):
        self.predicate = predicate

    def __set__(self, obj, value):
        if isinstance(value, GetSet):
            return # init will try to assign `obj.age = GetSet(...)`, but we will ignore that
            # this actually happens because it thinks `GetSet` should be default argument
        if not self.predicate(value):
            raise ValueError
        obj._age = value

    def __get__(self, obj, cls):
        if obj is not None: # called obj.age
            return obj._age
        return self # called Person.age

@dc.dataclass
class Person:
    # this one doesn't go to init, hence init=False, rest is as you wish
    _age: int = dc.field(default = 20, repr=False, init=False)
    # this one goes to init, if that is not desired, just remove annotation `: int`
    age: int = GetSet(lambda age: age >= 18) # this attribute will not be allowed to be under 18

另一种方法是使用内置的property

@dc.dataclass
class Person:
    _age: int = dc.field(default = 20, repr=False, init=False)
    age: int
    @property
    def age(self):
        return self._age
    @age.setter
    def age(self, value):
        if isinstance(value, property):
            return # we still need to ignore this
        if value < 18:
            raise ValueError('no underaged')
        self._age = value

【讨论】:

    【解决方案2】:

    有一种方法可以获得所有好处。请注意,dataclass 正在为您生成代码,因此您可以通过继承进行修改。

    from dataclasses import dataclass
    from dataclasses import field
    from typing import Union
    
    
    class Descriptor:
    
        def __set_name__(self, owner, name):
            self.name = name
    
        def __get__(self, obj, obj_type=None):
            if obj:
                return vars(obj)[self.name]
            else:
                return None
    
        def __set__(self, obj, value):
            if isinstance(value, str):
                value = int(value)
            vars(obj)[self.name] = value
    
    
    @dataclass
    class PersonData:
        name: str
        age: Union[int, str] = field(default=3, repr=False)
    
        # Many additional attributes you want to get the benefit of field for
    
    
    class Person(PersonData):
        age = Descriptor()
    
        # Many additional attributes you want to use descriptors for
    
    
    if __name__ == '__main__':
        p = Person('Mike')
        r = Person('Mary', 2)
    
        print(p, p.age)
        print(r, r.age)
    
        r.age = 5
        print(p, p.age)
        print(r, r.age)
    

    您将得到以下打印:

    Person(name='Mike') 3
    Person(name='Mary') 2
    Person(name='Mike') 3
    Person(name='Mary') 5
    

    您可以看到PersonData 中定义的默认值自动发布到Person。不再需要将default 存储在Descriptor 中。

    【讨论】:

      【解决方案3】:

      可以使用带有dataclasses.field 的描述符(至少在修复this bug 之后)。

      您的代码中只有几处需要更改:

      dataclass 开始,关于哪个对象调用哪个对象的顺序错误。字段对象应附加到dataclass,默认为Descriptor

      @dataclass
      class Person:
          age: str = field(default=Descriptor(default=3), repr=False)
      

      接下来在Descriptor.__set__ 中,当age 参数没有提供给构造函数时,value 参数实际上将是Descriptor 类的实例。所以我们需要换个守卫看看value是不是self

      class Descriptor:
          ...
          def __set__(self, obj, value):
              if value is self:
                  value = self.default
              ....
      

      最后,我又做了一项更改,以呼应我在 python 生态系统中看到的模式:使用 getattrsetattr 函数获取和设置类的属性。

      不幸的是,这引入了一个无限递归错误,所以我将值存储在 Person 对象上的位置更改为 _age

      话虽如此,这正如你所愿:

      from dataclasses import dataclass, field
      
      
      class Descriptor:
          def __init__(self, default):
              self.default = default
      
          def __set_name__(self, owner, name):
              self.private_name = '_' + name
      
          def __get__(self, obj, objtype=None):
              return getattr(obj, self.private_name)
      
          def __set__(self, obj, value):
              if value is self:
                  value = self.default
              else:
                  value = int(value)
              setattr(obj, self.private_name, value)
      
      
      @dataclass
      class Person:
          age: str = field(default=Descriptor(default=3), repr=False)
      
          # Many additional attributes
          # using same descriptor class
      
      
      r = Person(2.37)
      assert r.age == 2
      p = Person()
      assert p.age == 3
      print(r)
      print(p)
      print(vars(p))
      
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-10-16
        • 1970-01-01
        • 2016-01-05
        • 2022-11-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多