在Python中表示值的范围

0 投票
1 回答
1000 浏览
提问于 2025-04-18 06:21

我想在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)]

撰写回答