哪个更符合 Pythonic?Pythonic 与速度的比较

6 投票
3 回答
569 浏览
提问于 2025-04-16 10:07

我刚开始学习Python,写了一个模块级别的函数:

def _interval(patt):
    """ Converts a string pattern of the form '1y 42d 14h56m'
    to a timedelta object.
    y - years (365 days), M - months (30 days), w - weeks, d - days,
    h - hours, m - minutes, s - seconds"""

    m = _re.findall(r'([+-]?\d*(?:\.\d+)?)([yMwdhms])', patt)

    args = {'weeks': 0.0,
            'days': 0.0,
            'hours': 0.0,
            'minutes': 0.0,
            'seconds': 0.0}

    for (n,q) in m:
        if q=='y':
            args['days'] += float(n)*365
        elif q=='M':
            args['days'] += float(n)*30
        elif q=='w':
            args['weeks'] += float(n)
        elif q=='d':
            args['days'] += float(n)
        elif q=='h':
            args['hours'] += float(n)
        elif q=='m':
            args['minutes'] += float(n)
        elif q=='s':
            args['seconds'] += float(n)

    return _dt.timedelta(**args)

我遇到的问题是这里的for循环,也就是那个很长的ifelif块。我在想有没有更符合Python风格的方法来做这件事。
于是我把这个函数重新写了一遍:

def _interval2(patt):

    m = _re.findall(r'([+-]?\d*(?:\.\d+)?)([yMwdhms])', patt)

    args = {'weeks': 0.0,
            'days': 0.0,
            'hours': 0.0,
            'minutes': 0.0,
            'seconds': 0.0}

    argsmap = {'y': ('days', lambda x: float(x)*365),
               'M': ('days', lambda x: float(x)*30),
               'w': ('weeks', lambda x: float(x)),
               'd': ('days', lambda x: float(x)),
               'h': ('hours', lambda x: float(x)),
               'm': ('minutes', lambda x: float(x)),
               's': ('seconds', lambda x: float(x))}

    for (n,q) in m:
        args[argsmap[q][0]] += argsmap[q][1](n)

    return _dt.timedelta(**args)

我用timeit模块测试了这两段代码的执行时间,发现第二段代码大约多花了5到6秒(在默认的重复次数下)。

所以我想问:
1. 哪段代码更符合Python的风格?
2. 还有没有更符合Python风格的写法?
3. 在编程中,Python风格和其他方面(比如速度)之间有什么权衡?

附注:我对优雅的代码有点强迫症。

编辑:在看到这个回答后,我修改了_interval2

argsmap = {'y': ('days', 365),
           'M': ('days', 30),
           'w': ('weeks', 1),
           'd': ('days', 1),
           'h': ('hours', 1),
           'm': ('minutes', 1),
           's': ('seconds', 1)}

for (n,q) in m:
    args[argsmap[q][0]] += float(n)*argsmap[q][1]

3 个回答

-1

是的,有这个方法。你可以使用 time.strptime 来实现:

这个方法可以把一个表示时间的字符串,按照你指定的格式进行解析。返回的结果是一个 struct_time 对象,就像 gmtime()localtime() 返回的那样。

3

(我没有具体计时,但)如果你打算经常使用这个函数,提前编译正则表达式可能会更划算。

这是我对你函数的看法:

re_timestr = re.compile("""
    ((?P<years>\d+)y)?\s*
    ((?P<months>\d+)M)?\s*
    ((?P<weeks>\d+)w)?\s*
    ((?P<days>\d+)d)?\s*
    ((?P<hours>\d+)h)?\s*
    ((?P<minutes>\d+)m)?\s*
    ((?P<seconds>\d+)s)?
""", re.VERBOSE)

def interval3(patt):
    p = {}
    match = re_timestr.match(patt)
    if not match: 
        raise ValueError("invalid pattern : %s" % (patt))

    for k,v in match.groupdict("0").iteritems():
        p[k] = int(v) # cast string to int

    p["days"] += p.pop("years")  * 365 # convert years to days
    p["days"] += p.pop("months") * 30  # convert months to days
    return datetime.timedelta(**p)

更新

根据这个问题,看起来提前编译正则表达式并不会带来明显的性能提升,因为Python会自动缓存和重用它们。你只是节省了检查缓存所需的时间,而这个时间如果不是重复很多次的话,几乎可以忽略不计。

更新2

正如你所指出的,这个解决方案不支持 interval3("1h 30s" + "2h 10m")。不过,timedelta支持算术运算,这意味着你仍然可以把它写成 interval3("1h 30s") + interval3("2h 10m")

另外,正如一些评论提到的,你可能想避免在输入中支持“年”和“月”。timedelta不支持这些参数是有原因的;因为它们无法正确处理(而且错误的代码几乎从来都不优雅)。

这是另一个版本,这次支持浮点数、负值和一些错误检查。

re_timestr = re.compile("""
    ^\s*
    ((?P<weeks>[+-]?\d+(\.\d*)?)w)?\s*
    ((?P<days>[+-]?\d+(\.\d*)?)d)?\s*
    ((?P<hours>[+-]?\d+(\.\d*)?)h)?\s*
    ((?P<minutes>[+-]?\d+(\.\d*)?)m)?\s*
    ((?P<seconds>[+-]?\d+(\.\d*)?)s)?\s*
    $
""", re.VERBOSE)

def interval4(patt):
    p = {}
    match = re_timestr.match(patt)
    if not match: 
        raise ValueError("invalid pattern : %s" % (patt))

    for k,v in match.groupdict("0").iteritems():
        p[k] = float(v) # cast string to int

    return datetime.timedelta(**p)

示例用法:

>>> print interval4("1w 2d 3h4m")  # basic use
9 days, 3:04:00

>>> print interval4("1w") - interval4("2d 3h 4m") # timedelta arithmetic 
4 days, 20:56:00

>>> print interval4("0.3w -2.d +1.01h") # +ve and -ve floats 
3:24:36

>>> print interval4("0.3x") # reject invalid input 
Traceback (most recent call last): 
  File "date.py", line 19, in interval4
    raise ValueError("invalid pattern : %s" % (patt)) 
ValueError: invalid pattern : 0.3x

>>> print interval4("1h 2w") # order matters 
Traceback (most recent call last):   
  File "date.py", line 19, in interval4
    raise ValueError("invalid pattern : %s" % (patt)) 
ValueError: invalid pattern : 1h 2w
4

你在解析的时候似乎每次都创建很多个lambda(匿名函数)。其实你并不需要用lambda,只需要一个乘数就可以了。试试这个:

def _factor_for(what):
    if what == 'y': return 365
    elif what == 'M': return 30
    elif what in ('w', 'd', 'h', 's', 'm'): return 1
    else raise ValueError("Invalid specifier %r" % what)

for (n,q) in m:
    args[argsmap[q][0]] += _factor_for([q][1]) * n

不过不要把 _factor_for 变成方法的局部函数或者方法,这样可以提高速度。

撰写回答