【问题标题】:dict attribute 'type' to select Subclass of dataclassdict 属性 'type' 选择数据类的子类
【发布时间】:2020-08-03 23:28:47
【问题描述】:

我有以下课程

@dataclass_json
@dataclass
class Source:
    type: str =None
    label: str =None
    path: str = None

和两个子类:

@dataclass_json
@dataclass
class Csv(Source):
    csv_path: str=None
    delimiter: str=';'

@dataclass_json
@dataclass
class Parquet(Source):
    parquet_path: str=None

现在给定字典:

parquet={type: 'Parquet', label: 'events', path: '/.../test.parquet', parquet_path: '../../result.parquet'}
csv={type: 'Csv', label: 'events', path: '/.../test.csv', csv_path: '../../result.csv', delimiter:','}

现在我想做类似的事情

Source().from_dict(csv) 

并且输出将是类 Csv 或 Parquet。我知道,如果您启动类源,您只需使用“from dict”方法“上传”参数,但是是否有可能通过某种类型的继承来执行此操作而不使用“构造函数”,这会产生 if-else if -else 所有可能的“类型”?

Pureconfig 是一个 Scala 库,当属性 'type' 具有所需子类的名称时,它会创建不同的案例类。在 Python 中这是可能的吗?

【问题讨论】:

  • 到目前为止你尝试过什么?为什么不想使用 if-else?是否需要支持其他类型? type 字段是否始终是目标类的名称,或者它们可以不同吗?
  • 不一定是重复的,而是同一问题的更通用版本:stackoverflow.com/questions/7273568/…
  • 你的类被标记了第三方装饰器dataclass_json,但是你的使用示例没有使用它的功能。您是否需要适用于任何 dataclass 的解决方案(从 dict 加载)还是您确实需要 JSON 功能(从 JSON 加载)?

标签: python json inheritance subclass python-dataclasses


【解决方案1】:

您可以构建一个帮助器来选择并实例化适当的子类。

def from_data(data: dict, tp: type):
    """Create the subtype of ``tp`` for the given ``data``"""
    subtype = [
        stp for stp in tp.__subclasses__()  # look through all subclasses...
        if stp.__name__ == data['type']     # ...and select by type name
    ][0]
    return subtype(**data)  # instantiate the subtype

这可以使用您的数据和从中选择的基类调用:

>>> from_data(
...     {'type': 'Csv', 'label': 'events', 'path': '/.../test.csv', 'csv_path': '../../result.csv', 'delimiter':','},
...     Source,
... )
Csv(type='Csv', label='events', path='/.../test.csv', csv_path='../../result.csv', delimiter=',')

如果您需要经常运行它,值得构建一个dict 来优化子类型查找。一种简单的方法是向您的基类添加一个方法,并将查找存储在那里:

@dataclass_json
@dataclass
class Source:
    type: str =None
    label: str =None
    path: str = None

    @classmethod
    def from_data(cls, data: dict):
        if not hasattr(cls, '_lookup'):
            cls._lookup = {stp.__name__: stp for stp in cls.__subclasses__()}
        return cls._lookup[data["type"]](**data)

这可以直接在基类上调用:

>>> Source.from_data({'type': 'Csv', 'label': 'events', 'path': '/.../test.csv', 'csv_path': '../../result.csv', 'delimiter':','})
Csv(type='Csv', label='events', path='/.../test.csv', csv_path='../../result.csv', delimiter=',')

【讨论】:

  • 非常感谢。唯一的问题是我看不出这与@dataclass_json 有什么关系。它不选择子类。
  • @Patricio 你能澄清一下“与@dataclass_json 兼容”是什么意思吗?您认为这不兼容怎么办?哪个子类没有被选中?
  • 我的意思是 - 老实说 - 非常特殊的情况,你有另一个数据类 X,其中一个字段,让我们说,称为“源”是 List[Sources] 类型。在这种情况下,当我将字典应用于 X 时,它不会获取源中的所有变量(只是类型、标签、路径,而不是其他类型的 parquet_path,当它是“Parquet”类型时)。
  • @Patricio 抱歉,我根本不遵循此描述。显示的代码确实将未修改的整个data 复制到子类型;除了type 之外的任何变量都被视为完全相同。您想recursivley 解析一些数据吗? X 也是 Source 吗?
  • 是的,我想要它递归。我的意思是 X 是一个数据类,它的一个字段是 List[Source] 类型。如果你仍然不明白我的意思,我会在编辑我的问题时举个例子。
【解决方案2】:

这是我对this question 的回答的变体。

@dataclass_json
@dataclass
class Source:
    type: str = None
    label: str = None
    path: str = None

    def __new__(cls, type=None, **kwargs):
        for subclass in cls.__subclasses__():
            if subclass.__name__ == type:
                break
        else:
            subclass = cls
        instance = super(Source, subclass).__new__(subclass)
        return instance

assert type(Source(**csv)) == Csv
assert type(Source(**parquet)) == Parquet
assert Csv(**csv) == Source(**csv)
assert Parquet(**parquet) == Source(**parquet)

您提出要求,我很乐意答应。但是,我质疑这是否真的是你需要的。我认为这对你的情况来说可能是矫枉过正。我最初想出了这个技巧,所以我可以直接从数据中实例化...

  • 我的数据是异构的,我事先不知道哪个子类适合每个数据,
  • 我无法控制数据,并且
  • 弄清楚要使用哪个子类需要对数据进行一些处理,我认为这些处理属于类内部(出于逻辑原因以及避免污染发生实例化的范围)。

如果这些条件适用于您的情况,那么我认为这是一种值得的方法。如果不是这样,使用__new__ 增加的复杂性——一个中等高级的操作——可能不会超过在用于实例化的代码中节省的复杂性。可能有更简单的替代方案。

例如,您似乎已经知道需要哪个子类;它是数据中的字段之一。如果你把它放在那里,大概你写的任何逻辑都可以用来在当时和那里实例化适当的子类,绕过对我的解决方案的需求。或者,不要将子类的名称存储为字符串,而是存储子类本身。然后你可以这样做:data['type'](**data)

我还想到,也许你根本不需要继承。 CsvParquet 是否存储相同类型的数据,只是它们读取的文件格式不同?那么也许你只需要一个带有from_csvfrom_parquet 方法的类。或者,如果其中一个参数是文件名,则很容易根据文件扩展名确定您需要哪种类型的文件解析。通常我会把它放在__init__,但由于你使用的是dataclass,我想这会发生在__post_init__

【讨论】:

  • 非常感谢您的回答。昨天我才发现我的问题实际上有点复杂。我为此提出了另一个问题:stackoverflow.com/questions/61541259/….
  • @Patricio 您在新问题中说,当您尝试查找 Source 的子类时会遇到困难。我的解决方案向您展示了如何做到这一点。宫城先生的回答也是如此。您是否真的尝试过我们的任何一种解决方案?你这样做的时候发生了什么?他们怎么没有解决你的问题?
  • 是的,你的两个答案都适用于这个问题。所以再次感谢你。但是我注意到,当 Source 是另一个类的子类时,库 dataclass_json 的继承行为在我的情况下无法正常工作。
【解决方案3】:

你需要这种行为吗?

from dataclasses import dataclass
from typing import Optional, Union, List

from validated_dc import ValidatedDC


@dataclass
class Source(ValidatedDC):
    label: Optional[str] = None
    path: Optional[str] = None


@dataclass
class Csv(Source):
    csv_path: Optional[str] = None
    delimiter: str = ';'


@dataclass
class Parquet(Source):
    parquet_path: Optional[str] = None


@dataclass
class InputData(ValidatedDC):
    data: List[Union[Parquet, Csv]]


# Let's say you got a json-string and loaded it:
data = [
    {
        'label': 'events', 'path': '/.../test.parquet',
        'parquet_path': '../../result.parquet'
    },
    {
        'label': 'events', 'path': '/.../test.csv',
        'csv_path': '../../result.csv', 'delimiter': ','
    }

]


input_data = InputData(data=data)

for item in input_data.data:
    print(item)

# Parquet(label='events', path='/.../test.parquet', parquet_path='../../result.parquet')
# Csv(label='events', path='/.../test.csv', csv_path='../../result.csv', delimiter=',')

validated_dc:https://github.com/EvgeniyBurdin/validated_dc

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-09-11
    • 1970-01-01
    • 2012-02-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-29
    • 1970-01-01
    相关资源
    最近更新 更多