在Python中表示值的范围
我想在Python中表示一个范围,类似于Guava的Range
类型。具体来说,它应该有一个起始值和一个结束值,并表示这两个值之间的所有数(作为初步尝试,我只想表示标准的开闭区间,比如[5,10)
,但如果能正确表示任何开闭区间,那也是个不错的功能)。
我知道Python有内置的range()
函数,但我想支持任意类型(或者特别是日期,针对我的使用场景)。
查看Python的类型层次结构,似乎范围可以逻辑上归类为Sequence
(序列)或Set
(集合),但我不确定哪种更合适,或者是否应该放弃将我的类强行归入这个层次结构,而是简单地实现我想要的行为。
作为一个Sequence
:
- 符合规范,它是一个“有限有序集合”。
- 范围可以被计数、切片和迭代。
- 但是,我可能想支持无界范围,比如
[0,+∞)
,所以上面的说法可能不成立。
作为一个Set
:
- 稍微不符合规范,因为范围是明确有序的。
- 在概念上更像一个范围,因为集合运算(如交集和并集)更有意义。
- 能有效地表示包含检查。
作为一个独立的结构:
- 我们失去了遵循上述类型模式的好处(例如,我们需要定义一个单独的
range.slice()
方法)。 - 但我们更明确地表示这个结构不应该与这些类型混淆。Guava的
Range
没有继承自集合API,这似乎支持了这个观点。
我很好奇这里最符合Python风格的是什么,以及是否有人自己做过这样的数据结构。
1 个回答
1
这是我目前想到的实现方式。一个 Range
对象表示一个任意的开闭区间,它可以被哈希(也就是可以用来做字典的键),可以包含其他元素,也可以进行迭代,但它既不是序列也不是集合。DateRange
这个子类表示日期范围,主要是需要把增量参数定义为 timedelta(days=1)
,而不是简单的 1
。
class Range:
'''
Represents a range, in the spirit of Guava's Range class.
Endpoints can be absent, and (presently) all ranges are openClosed.
There's little reason to use this class directly, as the range()
builtin provides this behavior for integers.
'''
def __init__(self, start, end, increment=1):
if start and end and end < start:
raise ValueError("End date cannot be before start date, %s:%s" % (start,end))
self.start = start
self.end = end
self.increment = increment
def __repr__(self):
return '[%s\u2025%s)' % (
self.start or '-\u221E',
self.end or '+\u221E'
)
def __eq__(self, other):
return self.start == other.start and self.end == other.end
def __hash__(self):
return 31*hash(self.start) + hash(self.end)
def __iter__(self):
cur = self.start
while cur < self.end:
yield cur
cur = cur + self.increment
def __contains__(self, elem):
ret = True
if self.start:
ret = ret and self.start <= elem
if self.end:
ret = ret and elem < self.end
return ret
class DateRange(Range):
'''A range of dates'''
one_day = timedelta(days=1)
@staticmethod
def parse(daterange):
'''Parses a string into a DateRange, useful for
parsing command line arguments and similar user input.
*Not* the inverse of str(range).'''
start, colon, end = daterange.partition(':')
if colon:
start = strToDate(start) if start else None
end = strToDate(end) if end else None
else:
start = strToDate(start)
end = start + DateRange.one_day
return DateRange(start, end)
def __init__(self, start, end):
Range.__init__(self, start, end, DateRange.one_day)
def strToDate(date_str):
'''Parses an ISO date string, such as 2014-2-20'''
return datetime.datetime.strptime(date_str, '%Y-%m-%d').date()
这里有一些使用示例:
>>> DateRange(datetime.date(2014,2,20), None)
[2014-02-20‥+∞)
>>> DateRange(datetime.date(2014,1,1), datetime.date(2014,4,1))
[2014-01-01‥2014-04-01)
>>> DateRange.parse(':2014-2-20')
[-∞‥2014-02-20)
>>> DateRange.parse('2014-2-20:2014-3-22')
[2014-02-20‥2014-03-22)
>>> daterange = DateRange.parse('2014-2-20:2014-3-2')
>>> daterange
[2014-02-20‥2014-03-02)
>>> datetime.date(2014,1,25) in daterange
False
>>> datetime.date(2014,2,20) in daterange
True
>>> list(daterange)
[datetime.date(2014, 2, 20), datetime.date(2014, 2, 21), datetime.date(2014, 2, 22),
datetime.date(2014, 2, 23), datetime.date(2014, 2, 24), datetime.date(2014, 2, 25),
datetime.date(2014, 2, 26), datetime.date(2014, 2, 27), datetime.date(2014, 2, 28),
datetime.date(2014, 3, 1)]