В последнее время я работал над переносом своих сценариев PowerShell 7 на Python 3.9.5, повторно реализую ту же логику на другом языке программирования, сохраняя то, что заставляет его работать, и удаляя ненужные части, улучшая код, и это один из сценариев. это было повторно реализовано.
Код прост, и я знаю, что это реализовывалось бесчисленное количество раз, но он выполняет свою работу без использования встроенной datetime
модуль, и он принимает две строки, представляющие даты в девяти форматах дат, и возвращает разницу между двумя датами:
'yyyy-MM-dd'
'yyyy/MM/dd'
'MM/dd/yyyy'
'MMM dd, yyyy'
'dd MMM, yyyy'
'MMMM dd, yyyy'
'dd MMMM, yyyy'
'yyyy, MMM dd'
'yyyy, MMMM dd'
И он полностью рабочий.
Итак, вот код:
import re
import sys
Months = (
{'Month':'January', 'Days':31},
{'Month':'February', 'Days':28},
{'Month':'March', 'Days':31},
{'Month':'April', 'Days':30},
{'Month':'May', 'Days':31},
{'Month':'June', 'Days':30},
{'Month':'July', 'Days':31},
{'Month':'August', 'Days':31},
{'Month':'September', 'Days':30},
{'Month':'October', 'Days':31},
{'Month':'November', 'Days':30},
{'Month':'December', 'Days':31}
)
Culture = (
{'Format':'yyyy-MM-dd', 'Regex':'^d{4}-0?([2-9]|1[0-2]?)-(0?(3[01]|[12][0-9]|[1-9]))$'},
{'Format':'yyyy/MM/dd', 'Regex':'^d{4}/0?([2-9]|1[0-2]?)/(0?(3[01]|[12][0-9]|[1-9]))$'},
{'Format':'MM/dd/yyyy', 'Regex':'^0?([2-9]|1[0-2]?)/(0?(3[01]|[12][0-9]|[1-9]))/d{4}$'},
{'Format':'MMM dd, yyyy', 'Regex':'^[A-Za-z]{3} (0?(3[01]|[12][0-9]|[1-9])), d{4}$'},
{'Format':'dd MMM, yyyy', 'Regex':'^(0?(3[01]|[12][0-9]|[1-9])) [A-Za-z]{3}, d{4}$'},
{'Format':'MMMM dd, yyyy', 'Regex':'^[A-Za-z]{3,9} (0?(3[01]|[12][0-9]|[1-9])), d{4}$'},
{'Format':'dd MMMM, yyyy', 'Regex':'^(0?(3[01]|[12][0-9]|[1-9])) [A-Za-z]{3,9}, d{4}$'},
{'Format':'yyyy, MMM dd', 'Regex':'^d{4}, [A-Za-z]{3} (0?(3[01]|[12][0-9]|[1-9]))$'},
{'Format':'yyyy, MMMM dd', 'Regex':'^d{4}, [A-Za-z]{3,9} (0?(3[01]|[12][0-9]|[1-9]))$'}
)
def splitter(date, x, y):
date = date.replace(',','').split(x)
y = list(map(int, y.split(',')))
year = int(date[y[0]])
month = date[y[1]]
if not re.match('^d+$', month):
for i in Months:
if re.match(f'{month}',i['Month']): month = Months.index(i) + 1
month = int(month)
day = int(date[y[2]])
return {'Year': year, 'Month': month, 'Day': day}
def parsedate(date):
for a in range(len(Culture)):
if re.match('%s' % Culture[a]['Regex'], date): i = a; break
if i == 0: return splitter(date, '-', '0,1,2')
elif i == 1: return splitter(date, "https://codereview.stackexchange.com/", '0,1,2')
elif i == 2: return splitter(date, "https://codereview.stackexchange.com/", '2,0,1')
elif i == 3: return splitter(date, ' ', '2,0,1')
elif i == 4: return splitter(date, ' ', '2,1,0')
elif i == 5: return splitter(date, ' ', '2,0,1')
elif i == 6: return splitter(date, ' ', '2,1,0')
elif i == 7: return splitter(date, ' ', '0,1,2')
elif i == 8: return splitter(date, ' ', '0,1,2')
def totaldays(date):
ye = date['Year']
y = ye
mon = date['Month']
d = date['Day']
if mon <= 2: y -= 1
leaps = y // 4 - y // 100 + y // 400
m = 0
for b in range(mon - 1): m += int(Months[b].get('Days'))
days = (ye - 1) * 365 + m + d + leaps
return days
def diffdate(start, end):
date1 = totaldays(parsedate(start))
date2 = totaldays(parsedate(end))
ddate = date2 - date1
print(ddate)
start = sys.argv[1]
end = sys.argv[2]
diffdate(start, end)
Как обычно, мне интересно, как это можно улучшить, может быть несколько областей, которые можно улучшить, но теперь я в основном забочусь о замене переключателя и о том, как получить параметры из командной строки, 8 операторов elif выполняют свою работу, но заставляют код выглядит ужасно, я попытался использовать словарь, но он выдает ошибку date is undefined, и я не хочу импортировать sys только для того, чтобы получить аргументы из командной строки, я не знаю, есть ли автоматическая переменная, такая как PowerShell $Args
.
Пожалуйста, помогите мне улучшить мой код, вы можете поделиться всем, что вы думаете о моем коде.
2 ответа
Современный Python имеет так много возможностей для создания простых объектов данных (namedtuple, dataclass и превосходный attrs library), что обычно неправильно структурировать ваши внутренние данные с помощью обычных dicts, списков или кортежей. Эти простые объекты данных не только очищают код с точки зрения удобочитаемости (как показано в нескольких местах ниже), но также побуждают вас сосредоточить свое внимание там, где оно должно быть: определение значимых объектов данных для облегчения и поддержки алгоритмических потребностей программы . Организуйте данные правильно, и алгоритмы обычно будут работать естественным образом.
Показательный пример: вам не нужен 8-позиционный переключатель; вам нужны более богатые данные. В
splitter()
аргументы принадлежат области данных (Culture
экземпляры в коде ниже), а не алгоритм.
Другой пример: не используйте в своем алгоритме данные, требующие синтаксического анализа. Например, если вы считаете, что вам нужен кортеж индексов, таких как (2, 0, 1)
, не храните его в строке, разделенной запятыми, и не усложняйте свой алгоритмический код логикой распаковки. Просто сохраните индексы напрямую как кортеж целых чисел. Основная проблема при написании программного обеспечения — это уменьшение сложности и управление ею, а самая большая сложность всегда связана с алгоритмами. Если вы инвестируете больше средств в создание «более интеллектуальных» или «более богатых» данных, это поможет вам упростить понимание и поддержку алгоритмических частей кода.
Python имеет встроенный sum()
функция.
Когда возможно (что означает почти всегда), выполнять итерацию непосредственно по коллекции (т. Е. Списку, кортежу), а не по индексам. Если вам тоже нужны индексы, используйте enumerate()
.
Откажитесь от своего стиля попытки втиснуть лишнюю логику в каждую строчку:
if foo > 1233: x = 12; y = 34
Я прошел через эту фазу на короткое время в начале своего пути к Python. Он плохо масштабируется, и его сложнее поддерживать в течение длительного периода в проекте. Опять же, сложность — самый большой враг в разработке программного обеспечения: один из способов немного снизить сложность (и повысить удобочитаемость) — отдать предпочтение более коротким строкам кода над более длинными. В пределах разумного, более высокие / тонкие тексты легче для читателей, чем более короткие / широкие тексты (издательская индустрия понимает это на протяжении десятилетий), поэтому, если вы втисываете больше кода в отдельные строки, вы экономите не на том (т. Е. Экономите строки пока трачу читаемость).
Точно так же оставьте свое отвращение к импорту частей стандартной библиотеки Python. Это нормально — избегать этих вещей во имя обучения. Но это единственная веская причина, о которой я знаю. Так что получите аргументы командной строки из sys.argv
без вины. Это путь!
После внесения упрощений из этих изменений splitter()
функция кажется ненужной. Вместо этого я бы предложил другую вспомогательную функцию просто для анализа месяца. И эта логика не требует регулярного выражения: вы можете использовать более простые вещи, например isdigit()
а также startswith()
. Также Month
должен знать его номер. Опять же, используйте более обширные данные, чтобы упростить алгоритм.
Другие улучшения, которые вам стоит рассмотреть:
Если вы вложите больше средств в свои регулярные выражения, чтобы они использовали именованные захваты, вы можете отказаться от утомительных деталей в
Culture.separator
а также
Culture.indexes
. Вместо этого вы можете получить год, месяц и день прямо изre.Match
объект, по имени. Еще раз, умные данные, простой код.При неверном вводе ваш код просто взрывается. Подумайте, хотите ли вы инвестировать в лучшую обработку ошибок / отчетность.
Даты, как известно, сложны. В вашем коде, вероятно, есть странные крайние ошибки. Вы уверены, что не хотите использовать
datetime
?
import re
import sys
from collections import namedtuple
# Simple data objects are handy as hell. Use more of them.
Month = namedtuple('Month', 'name number days')
Culture = namedtuple('Culture', 'format separator indexes regex')
Date = namedtuple('Date', 'year month day')
Months = (
Month('January', 1, 31),
Month('February', 2, 28),
Month('March', 3, 31),
Month('April', 4, 30),
Month('May', 5, 31),
Month('June', 6, 30),
Month('July', 7, 31),
Month('August', 8, 31),
Month('September', 9, 30),
Month('October', 10, 31),
Month('November', 11, 30),
Month('December', 12, 31),
)
Cultures = (
Culture('yyyy-MM-dd', '-', (0,1,2), '^d{4}-0?([2-9]|1[0-2]?)-(0?(3[01]|[12][0-9]|[1-9]))$'),
Culture('yyyy/MM/dd', "https://codereview.stackexchange.com/", (0,1,2), '^d{4}/0?([2-9]|1[0-2]?)/(0?(3[01]|[12][0-9]|[1-9]))$'),
Culture('MM/dd/yyyy', "https://codereview.stackexchange.com/", (2,0,1), '^0?([2-9]|1[0-2]?)/(0?(3[01]|[12][0-9]|[1-9]))/d{4}$'),
Culture('MMM dd, yyyy', ' ', (2,0,1), '^[A-Za-z]{3} (0?(3[01]|[12][0-9]|[1-9])), d{4}$'),
Culture('dd MMM, yyyy', ' ', (2,1,0), '^(0?(3[01]|[12][0-9]|[1-9])) [A-Za-z]{3}, d{4}$'),
Culture('MMMM dd, yyyy', ' ', (2,0,1), '^[A-Za-z]{3,9} (0?(3[01]|[12][0-9]|[1-9])), d{4}$'),
Culture('dd MMMM, yyyy', ' ', (2,1,0), '^(0?(3[01]|[12][0-9]|[1-9])) [A-Za-z]{3,9}, d{4}$'),
Culture('yyyy, MMM dd', ' ', (0,1,2), '^d{4}, [A-Za-z]{3} (0?(3[01]|[12][0-9]|[1-9]))$'),
Culture('yyyy, MMMM dd', ' ', (0,1,2), '^d{4}, [A-Za-z]{3,9} (0?(3[01]|[12][0-9]|[1-9]))$'),
)
# A purely stylistic point: organize modules the way you want to read them.
# Most humans like to read things top to bottom.
def main(args):
start, end = args
ddate = diffdate(start, end)
print(ddate)
def diffdate(start, end):
d1 = totaldays(parsedate(start))
d2 = totaldays(parsedate(end))
return d2 - d1
def totaldays(date):
y = date.year
if date.month <= 2:
y -= 1
leaps = y // 4 - y // 100 + y // 400
m = sum(Months[b].days for b in range(date.month - 1))
days = (date.year - 1) * 365 + m + date.day + leaps
return days
def parsedate(date):
for c in Cultures:
if re.match(c.regex, date):
parts = date.replace(',', '').split(c.separator)
return Date(
int(parts[c.indexes[0]]),
parsemonth(parts[c.indexes[1]]),
int(parts[c.indexes[2]]),
)
# Raise an error if we get here.
def parsemonth(month):
if month.isdigit():
return int(month)
else:
for m in Months:
if m.name.lower().startswith(month.lower()):
return m.number
# Raise an error if we get here.
if __name__ == '__main__':
main(sys.argv[1:])
Это хороший подход, который вы сделали, если вам нужно пропустить datetime
модуль.
Вы не добавили еще много культур, но это уже другая тема.
Сначала я отвечу на ваш вопрос, связанный с аргументом командной строки. НЕТ есть нет чистого / хорошего решения для этого.
Во-вторых, я рассмотрю только основной вопрос, заменив if-else
лестница с switch
.
Вы уже проделали за меня основную работу. 🙂
Decider = [
('-', '0,1,2'), ("https://codereview.stackexchange.com/", '0,1,2'), ("https://codereview.stackexchange.com/", '2,0,1'),
(' ', '2,0,1'), (' ', '2,1,0'), (' ', '2,0,1'),
(' ', '2,1,0'), (' ', '0,1,2'), (' ', '0,1,2')
]
# use like
splitter(date, *Decider[i])
Надеюсь это поможет.