Pythonic的“安全大小”切片方法
以下是来自 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 个回答
这里有一个更符合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
,并给我们一个结果列表,如果抛出了错误,我们会在处理循环中的第一个项目之前就知道。
这里有一种可以说更符合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,不知道那是什么:原来的错误信息能告诉你字符串哪里出问题,而后者只告诉你有问题。
使用像 safe_slice 这样的函数会比单纯为了切片而创建一个对象要快,但如果速度不是问题,你想要一个更好用的界面,可以定义一个类,并在里面使用 __getitem__
方法,这样在返回切片之前就可以进行一些检查。
这样你就可以使用更简单的切片语法,而不需要同时传入 start
和 stop
这两个参数给 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]