将函数参数自动从字符串转换为日期时间

时间:2018-03-29 20:33:52

标签: python

我有很多通常接受日期/时间对象的函数和方法。我想调整它们以接受日期/时间对象的字符串表示。

考虑以下功能,这是一个简单的案例

def days_until(until_date, from_date=None):
    if from_date is None:
        from_date = datetime.datetime.now()
    delta = until_date - from_date
    return delta.days

Using dateutil,我会通过改变函数来解决这个问题。

def days_until(until_date, from_date=None):
    if isinstance(until_date, str): # preserve backwards compatibility with datetime objects
        until_date = dateutil.parser.parse(until_date)
    if isinstance(from_date, str):
        from_date = dateutil.parser.parse(from_date)
    # ... rest of function as it was before

虽然这样做有效,但代码非常重复,并且在大量函数集中进行操作非常繁琐,其中一些函数可以接受多达五个日期时间。

是否有自动/通用方法来完成此转换以实现DRY代码?

1 个答案:

答案 0 :(得分:3)

可以创建一个装饰器来执行此操作。在这里,我们只是盲目地尝试将每个参数转换为日期/时间。如果它有效,那么我们使用日期/时间,否则只需使用该对象。

import functools
import dateutil.parser

def dt_convert(obj):
    if not isinstance(obj, str):
        return obj
    try:
        return dateutil.parser.parse(obj)
    except TypeError:
        return obj

def parse_dates(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        new_args = map(dt_convert, args)
        new_kwargs = {kw: dt_convert(value) for kw, value in kwargs.items()}
        return func(*new_args, **new_kwargs)
    return wrapper

这使您可以简单地将装饰器添加到现有功能中,如此

@parse_dates
def days_until(until_date, from_date=None)
    # ... the original function logic

>>> christmas = dateutil.parser.parse('12/25')
>>> day_until(christmas, from_date='12/20')
5

这适用于这种特殊情况。但是,在某些情况下,如果字符串恰好是有效的日期时间,那么您可能会有一些实际上应该是字符串的参数,但会错误地转换为日期时间。

以下面的

为例
@parse_dates
def tricky_case(dt: datetime.datetime, tricky_string: str):
    print(dt, tricky_string)

结果可能是意外的

>>> tricky_case('12/25', '24')
2018-12-25 00:00:00 2018-03-24 00:00:00

作为解决方法,我们可以有一个装饰器,其参数是我们想要在装饰函数中转换的参数的名称。通过使用inspect来处理修饰函数的签名,此解决方案有点欺骗。但是,它允许我们绑定签名,适当地处理位置和关键字参数。

def parse_dates(*argnames):
    def decorator(func):
        sig = inspect.signature(func)

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            ba = sig.bind(*args, **kwargs)
            for argname in argnames:
                if argname in ba.arguments and isinstance(ba.arguments[argname], str):
                    ba.arguments[argname] = dateutil.parser.parse(ba.arguments[argname])
            return func(*ba.args, **ba.kwargs)

        return wrapper
    return decorator

然后,通过明确指定只应转换dt,可以避免在模糊情况下遇到的问题。

@parse_dates('dt')
def tricky_case(dt: datetime.datetime, tricky_string: str)
    print(dt, tricky_string)

然后结果不再是意料之外的

>>> tricky_case('12/25', '24')
2018-12-25 00:00:00 24

与天真方法相比,缺点是,在这种情况下,您仍需要访问每个函数并识别日期时间参数名称。此外,此hack使用Python {2}中不可用的inspect功能 - 在这种情况下,您需要使用inspect.get_arg_spec重新编写此功能,或使用提供反向端口的第三方模块对于遗留版本的Python。