Pythonic的“安全大小”切片方法

8 投票
4 回答
2055 浏览
提问于 2025-04-17 06:02

以下是来自 https://stackoverflow.com/users/893/greg-hewgill 对于 解释Python的切片表示法 的回答中的一段话。

在Python中,如果你请求的项目数量比实际数量少,Python会对程序员很友好。举个例子,如果你请求 a[:-2],而 a 里只有一个元素,Python会返回一个空列表,而不是报错。有时候你可能更希望看到错误信息,所以你需要注意这种情况可能会发生。

那么,当你希望看到错误时,Python中有什么更好的处理方式呢?有没有更符合Python风格的方法来重写这个例子呢?

class ParseError(Exception):
    pass

def safe_slice(data, start, end):
    """0 <= start <= end is assumed"""
    r = data[start:end]
    if len(r) != end - start:
        raise IndexError
    return r

def lazy_parse(data):
    """extract (name, phone) from a data buffer.
    If the buffer could not be parsed, a ParseError is raised.

    """

    try:
        name_length = ord(data[0])
        extracted_name = safe_slice(data, 1, 1 + name_length)
        phone_length = ord(data[1 + name_length])
        extracted_phone = safe_slice(data, 2 + name_length, 2 + name_length + phone_length)
    except IndexError:
        raise ParseError()
    return extracted_name, extracted_phone

if __name__ == '__main__':
    print lazy_parse("\x04Jack\x0A0123456789") # OK
    print lazy_parse("\x04Jack\x0A012345678") # should raise ParseError

补充:这个例子用字节字符串写起来更简单,但我实际的代码是用列表的。

4 个回答

2

这里有一个更符合Python风格的、更加通用的代码重写:

class ParseError(Exception):
    pass

def safe_slice(data, start, end, exc=IndexError):
    """0 <= start <= end is assumed"""
    r = data[start:end]
    if len(r) != end - start:
        raise exc()
    return r

def lazy_parse(data):
    """extract (name, phone) from a data buffer.
    If the buffer could not be parsed, a ParseError is raised."""
    results = []
    ptr = 0
    while ptr < len(data):
        length = ord(data[ptr])
        ptr += 1
        results.append(safe_slice(data, ptr, ptr + length, exc=ParseError))
        ptr += length
    return tuple(results)

if __name__ == '__main__':
    print lazy_parse("\x04Jack\x0A0123456789") # OK
    print lazy_parse("\x04Jack\x0A012345678") # should raise ParseError

大部分的改动发生在lazy_parse的主体部分——现在它可以处理多个值,而不仅仅是两个。整个过程的正确性仍然依赖于最后一个元素能够被准确解析出来。

另外,不再让safe_slice抛出IndexError,然后再由lazy_parse把它转变为ParseError。我让lazy_parse直接给safe_slice想要的异常,这样在出错时就会抛出这个异常(如果没有传递任何东西给lazy_parse,它默认会使用IndexError)。

最后,lazy_parse并不是“懒惰”的——它一次性处理整个字符串并返回所有结果。在Python中,“懒惰”意味着只做必要的事情来返回下一个部分。在lazy_parse的情况下,这意味着先返回名字,然后在后续调用中返回电话。只需稍微修改一下,我们就可以让lazy_parse变得懒惰:

def lazy_parse(data):
    """extract (name, phone) from a data buffer.
    If the buffer could not be parsed, a ParseError is raised."""
    ptr = 0
    while ptr < len(data):
        length = ord(data[ptr])
        ptr += 1
        result = (safe_slice(data, ptr, ptr + length, ParseError))
        ptr += length
        yield result

if __name__ == '__main__':
    print list(lazy_parse("\x04Jack\x0A0123456789")) # OK
    print list(lazy_parse("\x04Jack\x0A012345678")) # should raise IndexError

现在lazy_parse是一个生成器,每次返回一部分。注意,在主程序部分调用lazy_parse时,我们必须把它放在list()里,这样才能按顺序获取所有结果并打印出来。

不过,根据你的需求,这种方式可能并不是最理想的,因为在出错时可能更难恢复:

for item in lazy_parse(some_data):
    result = do_stuff_with(item)
    make_changes_with(result)
    ...

ParseError被抛出时,你可能已经做了一些更改,这些更改可能很难或不可能撤回。在这种情况下,解决方案和我们在主程序的print部分所做的一样:

for item in list(lazy_parse(some_data)):
    ...

list调用会完全消耗lazy_parse,并给我们一个结果列表,如果抛出了错误,我们会在处理循环中的第一个项目之前就知道。

5

这里有一种可以说更符合Python风格的方法。如果你想解析一个字节字符串,可以使用专门为此提供的struct模块:

import struct
from collections import namedtuple
Details = namedtuple('Details', 'name phone')

def lazy_parse(data):
    """extract (name, phone) from a data buffer.
    If the buffer could not be parsed, a ParseError is raised.

    """
    try:
        name = struct.unpack_from("%dp" % len(data), data)[0]
        phone = struct.unpack_from("%dp" % (len(data)-len(name)-1), data, len(name)+1)[0]
    except struct.error:
        raise ParseError()
    return Details(name, phone)

我觉得不太符合Python风格的是,抛弃了有用的struct.error错误追踪信息,换成了一个ParseError,不知道那是什么:原来的错误信息能告诉你字符串哪里出问题,而后者只告诉你有问题。

3

使用像 safe_slice 这样的函数会比单纯为了切片而创建一个对象要快,但如果速度不是问题,你想要一个更好用的界面,可以定义一个类,并在里面使用 __getitem__ 方法,这样在返回切片之前就可以进行一些检查。

这样你就可以使用更简单的切片语法,而不需要同时传入 startstop 这两个参数给 safe_slice

class SafeSlice(object):
    # slice rules: http://docs.python.org/library/stdtypes.html#sequence-types-str-unicode-list-tuple-bytearray-buffer-xrange
    def __init__(self,seq):
        self.seq=seq
    def __getitem__(self,key):
        seq=self.seq
        if isinstance(key,slice):
            start,stop,step=key.start,key.stop,key.step
            if start:
                seq[start]
            if stop:
                if stop<0: stop=len(seq)+stop
                seq[stop-1]
        return seq[key]

seq=[1]
print(seq[:-2])
# []
print(SafeSlice(seq)[:-1])
# []
print(SafeSlice(seq)[:-2])
# IndexError: list index out of range

如果速度是个问题,我建议你直接测试切片的起始和结束点,而不是进行复杂的计算。对于 Python 列表,访问每个元素的时间是 O(1)。下面的 safe_slice 版本还允许你传入 2、3 或 4 个参数。如果只传入 2 个参数,第二个参数会被当作结束值(这和 range 的用法类似)。

def safe_slice(seq, start, stop=None, step=1):
    if stop is None:
        stop=start
        start=0
    else:
        seq[start]
    if stop<0: stop=len(seq)+stop
    seq[stop-1]        
    return seq[start:stop:step]

撰写回答