Общий принцип факторинга заключается в том, чтобы общий код оставался неизменным и превращал различия в параметры функции.
Разница между двумя версиями функции заключается в том, какой подэлемент извлекается при понимании списка. Мы можем абстрагировать разницу в функцию:
def get_next_train(params, select_element):
res = requests.get(base, params=params)
parsed_json = res.json()
# extract times from parsed_json
time_strings = [select_element(d)
for d in parsed_json["connections"]]
...
Теперь вы можете позвонить
get_next_train(params, lambda d: d["from"]["prognosis"]["departure"])
get_next_train(params, lambda d: d["from"]["departure"])
В качестве альтернативы мы могли бы использовать
time_strings = [select_element(d["from"])["departure"]
for d in parsed_json["connections"]]
в функции и
get_next_train(params, lambda x: x["prognosis"])
get_next_train(params, lambda x: x)
в вызове, который устраняет всю избыточность, но намного сложнее понять и обобщить ИМХО.