如何导出来字典对象的一些属性

问题描述 投票:0回答:5

我有一个 python 类,它有几个属性。我想实现一个方法,它将返回一些属性作为字典。我想用装饰器标记属性。这是一个例子:

class Foo:
    @export_to_dict  # I want to add this property to dict
    @property
    def bar1(self):
        return 1

    @property # I don't want to add this propetry to dict
    def bar2(self):
        return {"smth": 2}

    @export_to_dict # I want to add this property to dict
    @property
    def bar3(self):
        return "a"

    @property
    def bar4(self):
        return [2, 3, 4]

    def to_dict(self):
        return ... # expected result: {"bar1": 1, "bar3": "a"}

实现它的一种方法是使用export_to_dict装饰器为属性设置一个附加属性,如下所示:

def export_to_dict(func):
    setattr(func, '_export_to_dict', True)
    return func

并在调用

_export_to_dict
时搜索具有
to_dict
属性的属性。

还有其他方法可以完成任务吗?

python properties decorator
5个回答
2
投票

标记每个属性会强制

to_dict
在每次调用时扫描所有方法/属性,这是缓慢且不优雅的。这是一个独立的替代方案,可以使您的示例用法保持相同。

在类属性中保留导出属性的列表

通过将

export_to_dict
创建为一个类,我们可以使用
__set_name__
(Python 3.6+) 来获取 Foo 类的引用并向其添加新属性。现在
to_dict
确切地知道要提取哪些属性,并且由于该列表是针对每个类的,因此您可以注释不同的类而不会发生冲突。

当我们这样做时,我们可以让

export_to_dict
自动为任何具有导出属性的类生成
to_dict
函数。

装饰器还会在类创建后恢复原始属性,因此属性可以正常工作,不会影响性能。

class export_to_dict:
    def __init__(self, property):
        self.property = property

    def __set_name__(self, owner, name):
        if not hasattr(owner, '_exported_properties'):
            owner._exported_properties = []
            assert not hasattr(owner, 'to_dict'), 'Class already has a to_dict method'
            owner.to_dict = lambda self: {prop.__name__: prop(self) for prop in owner._exported_properties}
        owner._exported_properties.append(self.property.fget)

        # We don't need the decorator object anymore, restore the property.
        setattr(owner, name, self.property)

class Foo:
    @export_to_dict  # I want to add this property to dict
    @property
    def bar1(self):
        return 1

    @property # I don't want to add this propetry to dict
    def bar2(self):
        return {"smth": 2}

    @export_to_dict # I want to add this property to dict
    @property
    def bar3(self):
        return "a"

    @property
    def bar4(self):
        return [2, 3, 4]

    # to_dict is not needed anymore here!

print(Foo().to_dict())
{'bar1': 1, 'bar3': 'a'}

如果您不希望 Foo 类具有额外的属性,您可以将映射存储在静态字典中

export_to_dict.properties_by_class = {class: [properties]}


带有 setter 的属性

如果您需要支持属性设置者,情况会稍微复杂一些,但仍然可行。传递

property.setter
是不够的,因为 setter 替换了 getter 并且
__set_name__
没有被调用(毕竟它们具有相同的名称)。

这可以通过拆分注释过程并为

property.setter
创建包装类来解决。

class export_to_dict:
    # Used to create setter for properties.
    class setter_helper:
        def __init__(self, setter, export):
            self.setter = setter
            self.export = export

        def __set_name__(self, owner, name):
            self.export.annotate_class(owner)
            setattr(owner, name, self.setter)

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

    @property
    def setter(self):
        return lambda fn: export_to_dict.setter_helper(self.property.setter(fn), self)

    def annotate_class(self, owner):
        if not hasattr(owner, '_exported_properties'):
            owner._exported_properties = []
            assert not hasattr(owner, 'to_dict'), 'Class already has a to_dict method'
            owner.to_dict = lambda self: {prop.__name__: prop(self) for prop in owner._exported_properties}
        owner._exported_properties.append(self.property.fget)
        
    def __set_name__(self, owner, name):
        self.annotate_class(owner)
        # We don't need the decorator object anymore, restore the property.
        setattr(owner, name, self.property)

class Foo:
    @export_to_dict  # I want to add this property to dict
    @property
    def writeable_property(self):
        return self._writeable_property

    @writeable_property.setter
    def writeable_property(self, value):
        self._writeable_property = value

foo = Foo()
foo.writeable_property = 5
print(foo.to_dict())
{'writeable_property': 5}

1
投票

我个人会采用你原来的方法。我认为这是最干净的,并且未来维护的量最少:

import inspect

EXPORTABLE = "_exportable"

def export_to_dict(prop):
    if prop.fget:
        setattr(prop.fget, EXPORTABLE, True)
    return prop

class Foo:
    @export_to_dict
    @property
    def bar1(self):
        return 1

    @property
    def bar2(self):
        return {"smth": 2}

    @export_to_dict
    @property
    def bar3(self):
        return "a"

    @property
    def bar4(self):
        return [2, 3, 4]

    def to_dict(self):
        statics = {inspect.getattr_static(self, prop) for prop in dir(self)}
        props = {prop for prop in statics if isinstance(prop, property) and prop.fget is not None}
        return {prop.fget.__name__: prop.fget(self) for prop in props if hasattr(prop.fget, EXPORTABLE)}

1
投票

免责声明:仅在EDIT 3EDIT 2中实现了完整的读/写协议。特别是,EDIT 2没有“副作用”[请参阅我对BoppreH的答案的评论]。


[编辑 3] 自定义属性

如果需要完整的读/写/...协议,则通过对内置类型property进行子类化来创建您的

属性
。请参阅 doc 了解标准实现的完整描述。

然后使用 custom 属性

PropertyTracker
来跟踪您需要的方法,并为您不介意的方法使用默认的
property

这里只是一个示例,请注意

to_dict
是一个属性而不是方法,并且只有在显式触发 custom 属性后才可以访问它。

class PropertyTracker(property):

    PROPERTY_TRACKER_ID = 'to_dict' # it will be an attribute (not a method!) of the target class

    def __get__(self, obj, objtype=None):       
        value = super().__get__(obj, objtype)
        
        if not hasattr(obj, self.PROPERTY_TRACKER_ID):
            setattr(obj, self.PROPERTY_TRACKER_ID, {})

        getattr(obj, self.PROPERTY_TRACKER_ID).update({self.fget.__name__: value})
        return value

    def __set__(self, obj, value):
        super().__set__(obj, value)

        p_name =  self.fget.__name__
        getattr(obj, self.PROPERTY_TRACKER_ID).update({p_name: value})


class Foo:

    @PropertyTracker #    I want to add this property to dict
    def bar1(self):
        return 1

    @property #         I don't want to add this propetry to dict
    def bar2(self):
        return {"smth": 2}

    @PropertyTracker #    I want to add this property to dict
    def bar3(self):
        return self._bar3

    @bar3.setter
    def bar3(self, v):
        self._bar3 = v

    @property
    def bar4(self): #   I don't want to add this propetry to dict
        return [2, 3, 4]


f = Foo()
# print(f.to_dict) # <- will raise an error, "to_dict" doesn't exist yet!
f.bar1 #             <- after calling a method decorated with the custom property the "to_dict" will become available 
f.bar2
print(f.to_dict)
#{'bar1': 1}
f.bar3 = 3
print(f.to_dict)
#{'bar1': 1, 'bar3': 3}
f.bar3 = 333
print(f.to_dict)
#{'bar1': 1, 'bar3': 333}

[编辑2]类装饰器+组合方法装饰器装饰类属性,Python >= 3.8。

支持读/写协议任何副作用。

初始化时间线

  1. 装饰者的
    __init__
  2. 装饰器的方法
    record
    s
  3. 装饰者的
    __call__
  4. 更新
    dict
  5. 实例的 setter 和 getter
class PropertyReader:
    # add "to_dict" method and "_recorded_props" to the target class

    def __init__(self):
        self._recorded_props = {}

    def record(self, prop) -> property:
        self._recorded_props[prop.fget.__name__] = None # default value
        return prop

    def __call__(self, target_cls):
        # dynamically add "to_dict" method and "recorded_props" attr
        setattr(target_cls, self.to_dict.__name__, self.to_dict)        
        setattr(target_cls, '_recorded_props', self._recorded_props)

        # apply getter/setter decorations to the target class
        for prop_name in self._recorded_props:
            self.decorated_descriptors(target_cls, prop_name)

        return target_cls

    @staticmethod
    def to_dict(target_cls) -> dict:
        # don't show the attributes set to None
        return {k: v for k, v in target_cls._recorded_props.items() if v is not None}
        return target_cls._recorded_props # all key-value pairs, also those with None

    @staticmethod
    def setter_wrapper(prop):
        # setter wrapper to be applied to
        def __setter_wrapper(target_cls, v):
            prop.fset(target_cls, v)
            target_cls._recorded_props.update({prop.fget.__name__: v})
        return __setter_wrapper

    @classmethod
    def decorated_descriptors(cls, target_cls, prop_name) -> None:
        # add to the decorated class the new getter/setter
        prop = getattr(target_cls, prop_name)
        if prop.fset:
            setattr(target_cls, prop_name, prop.setter(cls.setter_wrapper(prop)))
        else:
            target_cls._recorded_props.update({prop_name: prop.fget(target_cls)})


@(prop:=PropertyReader()) # walrus decoration
class Foo:

    @prop.record
    @property
    def bar1(self):
        return 1

    @property
    def bar2(self):
        return {"smth": 2}

    @prop.record
    @property
    def bar3(self):
        return self._bar3

    @bar3.setter
    def bar3(self, v):
        self._bar3 = v


f = Foo()
print(f.to_dict())
#{'bar1': 1}
f.bar1
f.bar3 = 333
print(f.to_dict())
#{'bar1': 1, 'bar3': 333}
f.bar3 = 300
print(f.to_dict())
#{'bar1': 1, 'bar3': 300}
f.bar3 = 333
print(f.to_dict())
#{'bar1': 1, 'bar3': 333}

[编辑1]类装饰器,带有装饰类属性的参数。

装饰器动态地将实例方法

to_dict
添加到类中。

class PropertyReader:
    # add "to_dict" method to a class

    @staticmethod
    def meta_to_dict(target_cls, prop_dict):
        # contains a function that will be the method of the class 
        def to_dict(self):
            # method of the instance
            return {k: prop(self) for k, prop in prop_dict.items()}
        setattr(target_cls, to_dict.__name__, to_dict)
    
    def __init__(self, *method_names):
        self.method_names = method_names

    def __call__(self, cls):
        # filter attributes by property and identifier
        props_dict = {} # dictionary of callable getters
        for attr_name in dir(cls):
            attr = getattr(cls, attr_name)
            if isinstance(attr, property):
                if attr_name in self.method_names:
                    props_dict[attr_name] = attr.fget # callable!

        # bind method to the class
        self.meta_to_dict(cls, props_dict)
        return cls

@PropertyReader('bar1', 'bar3')
class Foo:
   ...

print(Foo().to_dict())

[原创]使用函数来装饰类。

def export_to_dict(*method_names):

    def __wrapper(cls):

        def meta_to_dict(prop_dict):
            # contains a function that will be the method of the class 
            def to_dict(self):
                # method of the instance
                return {k: prop(self) for k, prop in prop_dict.items()}
            setattr(cls, to_dict.__name__, to_dict)

        # filter attributes by property and identifier
        props_dict = {}
        for attr_name in dir(cls):
            attr = getattr(cls, attr_name)
            if isinstance(attr, property):
                if attr_name in method_names:
                    props_dict[attr_name] = attr.fget # callable!
        
        # bind method to the class
        meta_to_dict(props_dict)

        return cls
    return __wrapper


@export_to_dict('bar1', 'bar3')
class Foo:
    @property
    def bar1(self):
        return 1

    @property # I don't want to add this propetry to dict
    def bar2(self):
        return {"smth": 2}

    @property
    def bar3(self):
        return "a"

    def bar4(self):
        return [2, 3, 4]


f = Foo()
print(f.to_dict())
# {'bar1': 1, 'bar3': 'a'}

可以通过使用类来使装饰器变得“更漂亮”,以避免难以阅读嵌套函数的声明。


0
投票

如果使用 attrs 是您的一个选择,它可以提供一个非常简洁的解决方案:

import attrs

EXPORT_METADATA = '__export_metadata'


def my_field(default=attrs.NOTHING, validator=None, repr=True, eq=True, order=None, hash=None, init=True,
             metadata=None, converter=None, export=False):
    metadata = metadata or {}
    metadata[EXPORT_METADATA] = export
    return attrs.field(default=default, validator=validator, repr=repr, eq=eq, order=order, hash=hash, init=init,
                       metadata=metadata, converter=converter)


def export_filter(attribute, value):
    return attribute.metadata.get(EXPORT_METADATA, False)


@attrs.frozen
class Foo:
    bar1 = my_field(default=1, init=False, export=True)
    bar2 = my_field(default={"smth": 2}, init=False)
    bar3 = my_field(default="a", init=False, export=True)
    bar4 = my_field(default=[2, 3, 4], init=False)

    def to_dict(self):
        return attrs.asdict(self, filter=export_filter)


print(Foo().to_dict())

改编自 attrs 的 元数据示例


0
投票

没有装饰器

没有代码生成(循环每个调用)

class Exportable:
    export_to_dict = ()

    def to_dict(self):
        return {prop: getattr(self, prop) for prop in self.export_to_dict}


class Foo(Exportable):
    export_to_dict = ["bar1", "bar2"]

    @property
    def bar1(self):
        return 1

    @property  # I don't want to add this propetry to dict
    def bar2(self):
        return {"smth": 2}

    @property
    def bar3(self):
        return "a"

    @property
    def bar4(self):
        return [2, 3, 4]


assert Foo().to_dict() == {"bar1": 1, "bar2": {"smth": 2}}

使用代码生成(无额外循环)

Exportable
替换为以下内容:

from convtools import conversion as c

class Exportable:
    export_to_dict = ()

    def to_dict(self):
        raise NotImplementedError

    def __init_subclass__(cls, **kwargs):
        cls.to_dict = c(
            {prop: c.attr(prop) for prop in cls.export_to_dict}
        ).gen_converter()

在底层,

gen_converter
生成以下代码,其中不包含循环:

def converter(data_):
    try:
        return {"bar1": data_.bar1, "bar2": data_.bar2}
    except __exceptions_to_dump_sources:
        __convtools__code_storage.dump_sources()
        raise
© www.soinside.com 2019 - 2024. All rights reserved.