Python: получить все месяцы в диапазоне?
Я хочу получить все месяцы с настоящего момента до августа 2010 года в виде списка, отформатированного так:
['2010-08-01', '2010-09-01', .... , '2016-02-01']
Прямо сейчас это то, что у меня есть:
months = []
for y in range(2010, 2016):
for m in range(1, 13):
if (y == 2010) and m < 8:
continue
if (y == 2016) and m > 2:
continue
month = '%s-%s-01' % (y, ('0%s' % (m)) if m < 10 else m)
months.append(month)
Что было бы лучшим способом сделать это?
11 ответов
dateutil.relativedelta
здесь удобно
Я оставил форматирование в качестве упражнения.
from dateutil.relativedelta import relativedelta
import datetime
result = []
today = datetime.date.today()
current = datetime.date(2010, 8, 1)
while current <= today:
result.append(current)
current += relativedelta(months=1)
Я посмотрел на dateutil
документация. Оказывается, это обеспечивает еще более удобный способ, чем использование dateutil.relativedelta
: правила повторения ( примеры)
Для поставленной задачи это так же просто, как
from dateutil.rrule import *
from datetime import date
months = map(
date.isoformat,
rrule(MONTHLY, dtstart=date(2010, 8, 1), until=date.today())
)
Мелкий шрифт
Обратите внимание, что мы немного обманываем, здесь. Элементы dateutil.rrule.rrule
производит типа datetime.datetime
, даже если мы проходим dtstart
а также until
типа datetime.date
, как мы делаем выше. Я позволяю map
накормить их date
"s isoformat
функция, которая просто превращает их в строки, как если бы это были просто даты без какой-либо информации о времени суток.
Поэтому, казалось бы, эквивалентное понимание списка
[day.isoformat()
for day in rrule(MONTHLY, dtstart=date(2010, 8, 1), until=date.today())]
вернул бы список как
['2010-08-01T00:00:00',
'2010-09-01T00:00:00',
'2010-10-01T00:00:00',
'2010-11-01T00:00:00',
⋮
'2015-12-01T00:00:00',
'2016-01-01T00:00:00',
'2016-02-01T00:00:00']
Таким образом, если мы хотим использовать понимание списка вместо map
мы должны сделать что-то вроде
[dt.date().isoformat()
for dt in rrule(MONTHLY, dtstart=date(2010, 8, 1), until=date.today())]
Использование datetime
а также timedelta
стандартные модули Python - без установки каких-либо новых библиотек
from datetime import datetime, timedelta
now = datetime(datetime.now().year, datetime.now().month, 1)
ctr = datetime(2010, 8, 1)
list = [ctr.strftime('%Y-%m-%d')]
while ctr <= now:
ctr += timedelta(days=32)
list.append( datetime(ctr.year, ctr.month, 1).strftime('%Y-%m-%d') )
Я добавляю 32
дни для ввода нового месяца каждый раз (самые длинные месяцы 31
дней)
Похоже, что есть очень простой и понятный способ сделать это, создав список дат и подмножеств, которые занимают только первый день каждого месяца, как показано в примере ниже.
import datetime
import pandas as pd
start_date = datetime.date(2010,8,1)
end_date = datetime.date(2016,2,1)
date_range = pd.date_range(start_date, end_date)
date_range = date_range[date_range.day==1]
print(date_range)
Подобно @Mattaf, но проще... pandas.date_range() имеет опцию Frequency freq='m'... Здесь я добавляю день (pd.Timedelta('1d')
), чтобы добраться до начала каждого нового месяца:
import pandas as pd
date_range = pd.date_range('2010-07-01','2016-02-01',freq='M')+pd.Timedelta('1d')
print(list(date_range))
Я получил другой способ, используя datetime, timedelta и calender:
from calendar import monthrange
from datetime import datetime, timedelta
def monthdelta(d1, d2):
delta = 0
while True:
mdays = monthrange(d1.year, d1.month)[1]
d1 += timedelta(days=mdays)
if d1 <= d2:
delta += 1
else:
break
return delta
start_date = datetime(2016, 1, 1)
end_date = datetime(2016, 12, 1)
num_months = [i-12 if i>12 else i for i in range(start_date.month, monthdelta(start_date, end_date)+start_date.month+1)]
monthly_daterange = [datetime(start_date.year,i, start_date.day, start_date.hour) for i in num_months]
Если у вас нет дубликатов месяцев и они расположены в правильном порядке, вы можете получить то, что хотите.
from datetime import date, timedelta
first = date.today()
last = first + timedelta(weeks=20)
date_format = "%Y-%m"
results = []
while last >= first:
results.append(last.strftime(date_format))
last -= timedelta(days=last.day)
Свежий питонический лайнер от меня
from dateutil.relativedelta import relativedelta
import datetime
[(start_date + relativedelta(months=+m)).isoformat() for m in range(0,relativedelta(start_date,end_date).months+1)]
Я не знаю, лучше ли это, но такой подход можно считать более "питонным":
months = [
'{}-{:0>2}-01'.format(year, month)
for year in xrange(2010, 2016 + 1)
for month in xrange(1, 12 + 1)
if not (year <= 2010 and month < 8 or year >= 2016 and month > 2)
]
Основными отличиями здесь являются:
- Поскольку мы хотим, чтобы итерация (и) создавала список, используйте понимание списка вместо агрегирования элементов списка в
for
петля. - Вместо того, чтобы явно проводить различие между числами ниже 10 и числами 10 и выше, используйте возможности мини-языка спецификации формата для
.format()
методstr
указать- ширина поля (
2
в{:0>2}
местодержатель) - выравнивание по правому краю поля (
>
в{:0>2}
местодержатель) - нулевое заполнение (
0
в{:0>2}
местодержатель)
- ширина поля (
xrange
вместоrange
возвращает генератор вместо списка, так что значения итерации могут быть получены по мере их использования и не должны храниться в памяти. (Не имеет значения для таких маленьких диапазонов, но это хорошая идея, чтобы привыкнуть к этому в Python 2.) Примечание: в Python 3 нетxrange
иrange
Функция уже возвращает генератор вместо списка.- Сделать
+ 1
для верхних оценок явно. Это облегчает читателям кода распознавать, что мы хотим указать инклюзивную привязку к методу (range
или жеxrange
) который рассматривает верхнюю границу как исключительную. В противном случае они могли бы задаться вопросом, в чем дело с номером 13.
Другой подход, который не требует никаких дополнительных библиотек, ни вложенных, ни циклов while. Просто конвертируйте ваши даты в абсолютное количество месяцев из некоторой контрольной точки (это может быть любая дата на самом деле, но для простоты мы можем использовать 1 января 0001). Например
a=datetime.date(2010,2,5)
abs_months = a.year * 12 + a.month
Если у вас есть число, представляющее месяц, в котором вы находитесь, вы можете просто использовать range
цикл по месяцам, а затем преобразовать обратно:
Решение обобщенной проблемы:
import datetime
def range_of_months(start_date, end_date):
months = []
for i in range(start_date.year * 12 + start_date.month, end_date.year*12+end_date.month + 1)
months.append(datetime.date((i-13) // 12 + 1, (i-1) % 12 + 1, 1))
return months
Дополнительные примечания / объяснения:
Вот //
делит округление до ближайшего целого числа и % 12
дает остаток при делении на 12, например 13 % 12
является 1
,
(Обратите внимание, что в приведенном выше date.year *12 + date.month
не дает количество месяцев с 1 января 0001 года. Например, если date = datetime.datetime(1,1,1)
, тогда date.year * 12 + date.month
дает 13
, Если бы я хотел указать фактическое количество месяцев, мне нужно было бы вычесть 1 из года и месяца, но это только усложнило бы вычисления. Все, что имеет значение, - это то, что у нас есть последовательный способ преобразовать в некоторое целое представление того месяца, в котором мы находимся.)
Вы могли бы уменьшить количество if
операторы в две строки вместо четырех строк, потому что имея вторую if
утверждение, которое делает то же самое с предыдущим if
Заявление немного избыточно.
if (y == 2010 and m < 8) or (y == 2016 and m > 2):
continue