如何创建一个月份迭代器

39
我想创建一个Python函数,允许我从起始点迭代到终止点的月份。例如,它看起来会像这样:
def months(start_month, start_year, end_month, end_year):

调用months(8, 2010, 3, 2011)将返回:

((8, 2010), (9, 2010), (10, 2010), (11, 2010), (12, 2010), (1, 2011), (2, 2011), (3, 2011))

该函数可以返回一个元组嵌套的元组,但我希望它作为生成器 (即使用yield)。

我已经检查了Python模块calendar,但它似乎没有提供此功能。我可以轻松地编写一个丑陋的for循环来完成它,但我对看到一个优美的解决方案感兴趣。

谢谢。


2
为什么专业人士不应该使用两个嵌套循环编写代码?更好的可读性略显冗长的代码比使用难以理解的花哨语言功能编写的紧凑且棘手的代码更好。 - user2665694
1
解决方案肯定至少涉及一个for循环,但我能想到的唯一方法还涉及各种if语句,例如if start_year == end_yearif end_year - start_year > 1等等... 这似乎不是最优美的解决方案。 - dgel
dateutil模块在这里可能很有用。 - k107
9个回答

61

日历的工作原理如下。

def month_year_iter( start_month, start_year, end_month, end_year ):
    ym_start= 12*start_year + start_month - 1
    ym_end= 12*end_year + end_month - 1
    for ym in range( ym_start, ym_end ):
        y, m = divmod( ym, 12 )
        yield y, m+1

所有的复合单位都是这样工作的。英尺和英寸、小时、分钟和秒等等。唯一不简单的是月-天或月-周,因为月份是不规则的。其他所有的都是规则的,需要在最细粒度的单位上进行运算。


1
这就是我一直在寻找的美妙之处。我绝对没有像这样想过。非常感谢。 - dgel
非常好的答案,但是所有常规事物并不完全像这样工作,因为月份从1..12编号,而不是0..11(就像英尺和英寸一样),所以必须加上或减去1。 - martineau
@martineau:没错。但所有事情都在它们最细粒度的单位中得到很好的解决。(除了月份,它们是不规则的)。±1是一种编码细微差别,通常很明显。从你的评论中,我得出结论,这并不总是显而易见的。 - S.Lott
@martineau:这并不是什么“微妙之处”。但还是谢谢你。我相信有人会对序数和基数产生困惑。 - S.Lott
要获取一个月的天数(包括闰年特殊情况),Python有一个方便的calendar.monthrange函数。 - Pēteris Caune
显示剩余2条评论

19

使用dateutil模块实现months函数

from dateutil.rrule import rrule, MONTHLY
from datetime import datetime

def months(start_month, start_year, end_month, end_year):
    start = datetime(start_year, start_month, 1)
    end = datetime(end_year, end_month, 1)
    return [(d.month, d.year) for d in rrule(MONTHLY, dtstart=start, until=end)]

使用示例

print months(11, 2010, 2, 2011)
#[(11, 2010), (12, 2010), (1, 2011), (2, 2011)]

或者以迭代器的形式

def month_iter(start_month, start_year, end_month, end_year):
    start = datetime(start_year, start_month, 1)
    end = datetime(end_year, end_month, 1)

    return ((d.month, d.year) for d in rrule(MONTHLY, dtstart=start, until=end))

迭代器用法

for m in month_iter(11, 2010, 2, 2011):
    print m
    #(11, 2010)
    #(12, 2010)
    #(1, 2011)
    #(2, 2011)

3
请注意,dateutil 是一个第三方模块。 - Peter Wood

19

由于其他人已经提供了生成器的代码,我想补充一下Pandas有一个叫做'period_range' 的方法,在这种情况下,可以输入开始和结束年份和月份,并返回一个适合迭代的周期索引。

import pandas as pd

pr = pd.period_range(start='2010-08',end='2011-03', freq='M')

prTupes=tuple([(period.month,period.year) for period in pr])

#This returns: ((8, 2010), (9, 2010), (10, 2010), (11, 2010), (12, 2010), (1, 2011), (2, 2011), (3, 2011))

6
也许这个方案的优雅度或速度可以改进,但它是一个简单易懂的解决方案。
def months(start_month, start_year, end_month, end_year):
    month, year = start_month, start_year
    while True:
        yield month, year
        if (month, year) == (end_month, end_year):
            return
        month += 1
        if (month > 12):
            month = 1
            year += 1

编辑:这里有一个更不直接的方法,甚至无需使用 yield,而是使用生成器推导式:

def months2(start_month, start_year, end_month, end_year):
    return (((m_y % 12) + 1, m_y / 12) for m_y in
            range(12 * start_year + start_month - 1, 12 * end_year + end_month))

最好将结束月份设为排除在外的,就像范围一样。参见:http://www.cs.utexas.edu/users/EWD/transcriptions/EWD08xx/EWD831.html - Devin Jeanpierre
1
同意,我只是在匹配问题陈述。 - dfan

2
    for year in range(2017, 2021):
        for month in range(1, 13):
            this_month = datetime.date.today().replace(year=year, month=month, day=1)

这样做会不会导致每年重复从“start_month_number”开始的月份,而不是在外部循环中每次更改年份时将其重置为月份=1? - Abhinav

2

dfan的方法简化版,也比S.Lott的解决方案更简单(无需除法,无需模运算):

def months(start_month, start_year, end_month, end_year):

    month, year = start_month, start_year

    while (year, month) <= (end_year, end_month):

        yield month, year

        month += 1
        if month > 12:
            month = 1
            year += 1

这种方法接近于手动处理的方法。它运行的时间与S.Lott的时间相同(上面代码中的测试需要的时间大约与除法和模数运算一样长)。


1

这个解决方案不像其他的那么简短,但很容易理解。基本上,它有两个分支。

  • 开始年份与结束年份相同
  • 开始年份与结束年份不同

后一种情况有三个阶段:

  • 从开始月份到开始年份的12月
  • 从开始年份到结束年份之间每年的每个月
  • 从结束年份的1月到结束月份

如果结束年份是开始年份的下一年,则跳过上述第二阶段(无需显式测试,范围为空)。

def months(start_month, start_year, end_month, end_year):
    if start_year == end_year:
        for month in xrange(start_month, end_month+1):
           yield month, start_year
    else:
        for month in xrange(start_month, 13):
            yield month, start_year
        for year in xrange(start_year+1, end_year):
            for month in xrange(1, 13):
               yield month, year
        for month in xrange(1, end_month+1):
           yield end_month, end_year

对于 Python 3.x,将 xrange 改为 range


0

你的问题有点模糊,因为你要求一个迭代器,但是又展示了一个返回元组的函数。所以这里提供两者:

import calendar
import datetime

def months_iter(start_month, start_year, end_month, end_year):
    start_date = datetime.date(start_year, start_month, 1)
    end_date = datetime.date(end_year, end_month, 1)
    date = start_date
    while date <= end_date:
        yield (date.month, date.year)
        days_in_month = calendar.monthrange(date.year, date.month)[1]
        date += datetime.timedelta(days_in_month)

def months(start_month, start_year, end_month, end_year):
    return tuple(d for d in months_iter(start_month, start_year, end_month, end_year))

print months(8, 2010, 3, 2011)

# ((8, 2010), (9, 2010), (10, 2010), (11, 2010), (12, 2010), (1, 2011), (2, 2011), (3, 2011))

0

在使用Python内置迭代器时有点儿好玩,但绝对不是优雅的方式 ;)

from datetime import timedelta, date

class MonthRange:
    def __init__ (self, date1, date2):
        self.start_date = date1 - timedelta(days=1)
        self.end_date = date2
        self.data = self.start_date
    def __iter__(self):
        return self
    def next(self):
        if self.data >= self.end_date.replace(day=1) + timedelta(days=32):
            raise StopIteration
        ret = self.data
        self.data = self.data + timedelta(days=32)
        return ret.replace(day=1)

for x in MonthRange(date.today(), date(2012, 11, 01)):
    print (x.year, x.month)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接