【问题标题】:Python: Dynamically generating attributes from a listPython:从列表中动态生成属性
【发布时间】:2020-10-29 18:52:09
【问题描述】:

我希望能够从列表或字典中动态生成类的属性。这个想法是我可以定义一个属性列表,然后能够使用my_class.my_attribute访问这些属性

例如:

class Campaign(metaclass=MetaCampaign):
    _LABELS = ['campaign_type', 'match_type', 'audience_type'] # <-- my list of attributes
    
    for label in _LABELS:
        setattr(cls, label, LabelDescriptor(label))
    
    def __init__(self, campaign_protobuf, labels)
        self._proto = campaign_protobuf
        self._init_labels(labels_dict)
        
    def _init_labels(self, labels_dict):
        # magic...

这显然行不通,因为cls 不存在,但我想:

my_campaign = Campaign(campaign, label_dict)
print(my_campaign.campaign_type)

campaign 返回值campaign_type。这显然有点复杂,因为campaign_type 实际上是一个Descriptor,并做了一些工作来从Label 基对象中检索一个值。


描述符:

class DescriptorProperty(object):
    def __init__(self):
        self.data = WeakKeyDictionary()

    def __set__(self, instance, value):
        self.data[instance] = value


class LabelTypeDescriptor(DescriptorProperty):
    """A descriptor that returns the relevant metadata from the label"""
    def __init__(self, pattern):
        super(MetaTypeLabel, self).__init__()
        self.cached_data = WeakKeyDictionary()
        # Regex pattern to look in the label:
        #       r'label_type:ThingToReturn'
        self.pattern = f"{pattern}:(.*)"

    def __get__(self, instance, owner, refresh=False):
        # In order to balance computational speed with memory usage, we cache label values
        # when they are first accessed.        
        if self.cached_data.get(instance, None) is None or refresh:
            ctype = re.search(self.pattern, self.data[instance].name) # <-- does a regex search on the label name (e.g. campaign_type:Primary)
            if ctype is None:
                ctype = False
            else:
                ctype = ctype.group(1)
            self.cached_data[instance] = ctype
        return self.cached_data[instance]

这使我可以轻松访问标签的值,如果标签是我关心的类型,它将返回相关值,否则将返回False


标签对象:

class Label(Proto):
    _FIELDS = ['id', 'name']
    _PROTO_NAME = 'label'
    #  We define what labels can pull metadata directly through a property
    campaign_type = LabelTypeDescriptor('campaign_type')
    match_type = LabelTypeDescriptor('match_type')
    audience_type = LabelTypeDescriptor('audience_type')

    def __init__(self, proto, **kwargs):
        self._proto = proto
        self._set_default_property_values(self)  # <-- the 'self' is intentional here, in the campaign object a label would be passed instead.

    def _set_default_property_values(self, proto_wrapper):
        props = [key for (key, obj) in self.__class__.__dict__.items() if isinstance(obj, DescriptorProperty)]
        for prop in props:
            setattr(self, prop, proto_wrapper)

所以如果我有一个 protobuf 标签对象存储在我的 Label(基本上只是一个包装器)中,看起来像这样:

resource_name: "customers/12345/labels/67890"
id {
  value: 67890
}
name {
  value: "campaign_type:Primary"
}

然后my_label.campaign_type 将返回Primary,同样my_label.match_type 将返回False


原因是我正在创建许多以相同方式标记的类,并且可能有很多标签。目前这一切都按描述工作,但我希望能够更动态地定义属性,因为它们基本上都遵循相同类型的模式。所以而不是:

    campaign_type = LabelTypeDescriptor('campaign_type')
    match_type = LabelTypeDescriptor('match_type')
    audience_type = LabelTypeDescriptor('audience_type')
    ... # (many more labels)

我只是有: _LABELS = ['campaign_type', 'match_type', 'audience_type', ... many more labels],然后有一些循环来创建属性。

反过来,我可以将类似的方法级联到我的其他类,这样虽然Campaign 对象可能包含Label 对象,但我可以简单地通过my_campaign.campaign_type 访问标签的值。如果活动没有适当类型的标签,它将简单地返回False

【问题讨论】:

    标签: python metaclass python-descriptors


    【解决方案1】:

    虽然类体运行时cls不存在,但您可以通过在类体内部locals()返回的字典中简单地设置then来设置属性:

    class Campaign(metaclass=MetaCampaign):
        _LABELS = ['campaign_type', 'match_type', 'audience_type'] # <-- my list of attributes
        
        for label in _LABELS:
            locals()[label] = label, LabelDescriptor(label)
        del label  # so you don't get a spurious "label" attribute in your class 
    
    

    除此之外,您可以使用元类,是的,但也可以在基类上使用__init_suclass__。更少的元类意味着更少的“移动部件”会在你的系统中以奇怪的方式运行。

    因此,假设您的 Proto 类是所有其他需要此功能的人的基础:

    class Proto:
        def __init_subclass__(cls, **kwd):
            super().__init_subclass__(**kwd)
            for label in cls._LABELS:
                setattr(cls, label, LabelDescriptor(label))
        ...
    

    我在那里查看了您的描述符和代码 - 如果它们已经在工作,我会说它们没问题。

    我可以评论说,将描述符相关数据存储在实例的 __dict__ 本身中更为常见,而不是在描述符本身中创建 datacached_data - 所以不需要关心weakrefs - 但两种方法都有效(就在本周,我已经以这种方式实现了一个描述符,尽管我通常会选择实例的 __dict__

    【讨论】:

    • 如何将描述符相关数据存储在实例的 dict 中?
    • @QuinRiva :就像人们对属性所做的那样,可能只是在描述符名称前加上 _ 并正常使用 setattr/getattr:getattr(instance, "_" + self.name) on getter 和 setter 上的一样。不过,较长的前缀可以保持实例的字典更清晰 - 而不是 _,也许使用 _labeltype_ 作为前缀。而且,可以直接使用instance.__dict__ 而不是 get/setattr - 在这种情况下,存储的属性可以与描述符本身具有相同的名称。 instance.__dict__[self.name] = value.
    • 我现在基本上一切正常除了在我的一个描述符中进行一次检查:isinstance(self.data[instance], Proto)。由于某种原因,这总是返回 false。如果我尝试检查这个:print(isinstance(campaigns['customers/12345/campaigns/67890'].campaign_type, Label)) 返回Falseprint(type(campaigns['customers/12345/campaigns/67890'].campaign_type)) 返回&lt;class '__main__.Label'&gt;。不知道这里发生了什么。
    • 没关系,我现在可以工作了 - 我怀疑这只是我的测试环境的命名空间问题。我重置了 Jupyter 内核,它现在工作正常。
    【解决方案2】:

    你可以定义一个classmethod来初始化这些属性,并在类声明之后调用这个方法:

    class Campaign(metaclass=MetaCampaign):
        _LABELS = ['campaign_type', 'match_type', 'audience_type'] # <-- my list of attributes
        
        @classmethod
        def _init_class(cls):
           for label in cls._LABELS:
            setattr(cls, label, LabelDescriptor(label))
    # After the class has been declared, initialize the attributes
    Campaign._init_class()
    

    【讨论】:

    • 其实从 Python 3.6 开始,语言规范本身就定义了一个特殊的方法,它会在创建类时自动运行。这与您编写的内容相同,但该方法应在基类上命名为 __init_subclass__ - 然后 Python 在创建子类时调用它。
    猜你喜欢
    • 2014-03-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-30
    • 2014-10-18
    • 1970-01-01
    • 2021-12-13
    相关资源
    最近更新 更多