数据转换/映射的最佳方法

时间:2020-02-20 15:57:00

标签: python oop properties data-conversion

任务是将字段从一个数据集映射到另一个,某些字段需要一些额外的解析/计算。

(在下面提供的示例中,我仅使用了几个字段,但原始数据集中还有很多字段。)

方法1:

起初,我虽然将dict用于字段映射,但只是将函数分配给需要其他数据操作的键:

import base64
import hashlib
import json

from datetime import datetime


def str2base64(event):
    md5 = hashlib.md5(event['id'].encode())
    return base64.b64encode(md5.digest())


def ts2iso(event):
    dt = datetime.fromtimestamp(event['timestamp'])
    return dt.isoformat()


MAPPINGS = {
    'id': id2hash,
    'region': 'site',
    'target': 'host',
    'since': ts2iso
}


def parser(event):
    new = dict()
    for k, v in MAPPINGS.items():
        if callable(v):
            value = v(event)
        else:
            value = event.get(v)
        new[k] = value
    return new


def main():
    for event in events:  # dicts
        event = parser(event)
        print(json.dumps(event, indent=2))


if __name__ == '__main__':
    main()

我不喜欢必须在顶部添加所有解析函数的事实,这样MAPPING字典才能看到它,而且我不确定这是否是最佳方法吗?另外,我看不到在dict.get函数中将默认值传递给parser的简便方法。

方法2(OOP):

import base64
import hashlib
import json

from datetime import datetime


class Event(object):
    def __init__(self, event):
        self.event = event

    @property
    def id(self):
        md5 = hashlib.md5(self.event['id'].encode())
        return base64.b64encode(md5.digest())

    @property
    def region(self):
        return self.event['site']

    @property
    def target(self):
        return self.event['host']

    @property
    def since(self):
        dt = datetime.fromtimestamp(self.event['timestamp'])
        return dt.isoformat()

    def data(self):
        return {
            attr: getattr(self, attr)
            for attr in dir(self)
            if not attr.startswith('__') and attr not in ['event', 'data']
        }


def main():
    for event in events:  # dicts
        event = Event(event).data()
        print(json.dumps(event, indent=2))


if __name__ == '__main__':
    main()

我确定有更好的方法来获取所有属性(仅适用于财产方法),以避免这种丑陋的data方法?我还想避免在相关方法中添加前缀,以便随后可以使用str.startswith或类似方法对其进行过滤。

此任务的最佳方法是什么?我还从functools看过@functools.singledispatch,但我认为在这种情况下它不会有帮助。

3 个答案:

答案 0 :(得分:3)

我认为您的第一种方法很有道理,并且,如果这对您很重要,则其性能将比OO方法好得多。万一您需要处理大量事件,将dict转换为object肯定会占用大量CPU。我发现它也非常明确。

在面向对象的方法中,您将dict转换为object 一无是处。拥有object没有任何好处,因为您稍后要做的就是将其转换为JSON(除非您编写JSON编码器,否则您无法使用自定义类进行此操作)。

这就是为什么我会选择第一个选项的原因,我会对其进行如下修改:

class SimpleConverter:

    def __init__(self, key, default=None):
        self.key = key
        self.default = default

    def __call__(self, event):
        return event.get(self.key, self.default)


class TimestampToISO:

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

    def __call__(self, event):
        dt = datetime.fromtimestamp(event[self.key])
        return dt.isoformat()


class StringToBase64:

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

    def __call__(self, event):
        md5 = hashlib.md5(event[self.key].encode())
        return base64.b64encode(md5.digest()).decode()  ## Without .decode() for Python2


def transform_event(event, mapping):
    return {key: convert(event) for key, convert in mapping.items()}


def main(events, mapping):
    for event in events:  # dicts
        event = transform_event(event, mapping)
        print(json.dumps(event, indent=2))


if __name__ == '__main__':
    mapping = {
        'id': StringToBase64("id"),
        'region': SimpleConverter("site"),
        'target': SimpleConverter("region"),
        'with_default': SimpleConverter("missing_key", "Not missing!"),
        'since': TimestampToISO("timestamp"),
    }
    events = [
        {
            'id': 'test',
            'site': 'X',
            'host': 'Y',
            'timestamp': 1582408754.5111449,
        }
    ]
    main(events, mapping)

哪个输出:

{
  "id": "CY9rzUYh03PK3k6DJie09g==",
  "region": "X",
  "target": null,
  "with_default": "Not missing!",
  "since": "2020-02-22T22:59:14.511145"
}

请注意,使用此解决方案如何将所有转换器类重用于不同的事件键,而这对于纯函数而言是不可能的。

答案 1 :(得分:2)

这是一个很酷的问题,但是我觉得解决方案的代码太重了:

MAPPINGS = {
    'id': id2hash,
    'region': ('site', 'default_region'),
    'target': ('host', 'default_target'),
    'since': ts2iso
}
# Unpack tuple if action is not callable. Equivalent to event.get(action[0], action[1])
mapped_event = [
    {key: action(event) if callable(action) else event.get(*action)
    for key, action in mapping} for event in events]

此解决方案可以完全实现您的第一种方法,但是所用的行数要少得多。我同意这是相当难以理解的,因此可以随意重用您想要的部分(也许在单独的函数中具有dict理解,并在list comp中调用它。)

如果要在映射中为'target': 'host'之类的键表示的内容是:event.get('target', 'host'),则理解将变为:

mapped_event = [
    {key: action(event) if callable(action) else event.get(key, action)
    for key, action in mapping} for event in events]

答案 2 :(得分:0)

我喜欢@matino的答案,但是我想提出一些相关的观点:

我认为您的第一种方法很有意义,如果可以的话 对您来说很重要,它的性能将比OO方法好得多。在 如果您需要处理大量事件,请将字典转换为 一个对象肯定会占用大量CPU。

您的解决方案还使用对象,而单个事件实际上使用了几个对象?!

在OO方法中,您将毫无疑问地从dict转换为object。 拥有对象没有任何好处,因为稍后您要做的所有事情 正在将其转换为JSON(您无法使用自定义类 除非您编写JSON编码器)。

同样,您的解决方案还创建了(许多)对象,并且由于某些字段默认为NoneTrue / False,我们实际上创建了一个对象,只是为了获得一个dict中的值:

mapping = {
    ...
    'example': SimpleConverter(None, True),
    ...
}

为了使布尔值能够正常工作,我不得不像这样修改transform_event函数:

def transform_event(event, mapping):
    new = dict()
    for key, value in mapping.items():
        if callable(value):
            value = value(event)
        new[key] = value
    return new

字段示例:

mapping = {
    ...
    'example': True,
    ...
}

因此,现在我们有了一个mapping字典,该函数可以转换字段和每次转换的一些类...

在研究您的解决方案时,我决定改进Event类。我进行了迭代,因此很容易将其转换为dict,json等。

class Event(object):
    __slots__ = ('event', 'fields')

    def __init__(self, event):
        self.event = event
        self.fields = [
            'id',
            'region',
            'target',
            'since',
        ]

    def __iter__(self):
        return (self._get(f) for f in self.fields)

    @property
    def id(self):
        md5 = hashlib.md5(self.event['id'].encode())
        return base64.b64encode(md5.digest())

    @property
    def region(self):
        return self.event['site']

    @property
    def target(self):
        return self.event['host']

    @property
    def since(self):
        dt = datetime.fromtimestamp(self.event['timestamp'])
        return dt.isoformat()

    def _get(self, field):
        try:
            return getattr(self, field)
        except AttributeError:
            return self.event.get(field)

    def asdict(self):
        return dict(zip(self.fields, self))

因此,我们将所有数据保存在一个易于扩展的对象中,我们具有不变的属性(属性)等。

相关问题