Python: динамическое создание атрибутов из списка - PullRequest
1 голос
/ 09 июля 2020

Я хочу иметь возможность динамически генерировать атрибуты класса из списка или словаря. Идея состоит в том, что я могу определить список атрибутов, а затем иметь доступ к этим атрибутам, используя 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_type для campaign. Это, очевидно, немного сложно, поскольку 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, хранящийся в моей метке (который, по сути, просто оболочка), он выглядит следующим образом:

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], а затем есть некоторый l oop, который создает атрибуты.

В свою очередь, я могу применить аналогичный подход к моему другие классы, так что, хотя объект Campaign может содержать объект Label, я могу получить доступ к значению метки просто по my_campaign.campaign_type. Если в кампании нет метки соответствующего типа, она просто вернет False.

Ответы [ 2 ]

1 голос
/ 09 июля 2020

Хотя cls не существует при запуске тела класса, вы можете установить атрибуты, просто установив их в словаре, возвращаемом locals() внутри тела класса:

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__ в базовом классе. Меньшее количество метаклассов означает меньше «движущихся частей», которые могут вести себя странным образом в вашей системе. 1012 * Я взглянул на ваши дескрипторы и код там - если они уже работают, я бы сказал, что с ними все в порядке.

Я могу прокомментировать, что обычно данные, относящиеся к дескрипторам, хранятся в самого экземпляра __dict__, вместо того, чтобы создавать data и cached_data в самом дескрипторе - поэтому не нужно заботиться о weakrefs - но оба подхода работают (только на этой неделе я реализовал дескриптор в этом Кстати, хотя я обычно go для экземпляра __dict__)

1 голос
/ 09 июля 2020

Вы можете определить 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()
...