Как группировать функции без побочных эффектов?
У меня есть функция с несколькими вспомогательными функциями. Это довольно распространенный случай. Я хочу сгруппировать их в общем контексте для удобочитаемости, и мне интересно, как это сделать правильно.
- они берут ~ 15 строк
- только основная функция вызывается из другого места
- нет планов по повторному использованию вспомогательных функций в ближайшем будущем
Упрощенный пример:
def create_filled_template_in_temp(path, values_mapping):
template_text = path.read_text()
filled_template = _fill_template(template_text, values_mapping)
result_path = _save_in_temp(filled_template)
return result_path
def _fill_template(template_text, values_mapping):
...
def _save_in_temp(filled_template):
_, pathname = tempfile.mkstemp(suffix='.ini', text=True)
path = pathlib.Path(pathname)
path.write_text(text)
return path
...
create_filled_template_in_temp(path, values_mapping)
Обратите внимание: я не хочу использовать вспомогательные методы на уровне модуля, потому что они принадлежат только одному методу. Представьте, что в одном модуле есть несколько таких примеров, как указано выше. Maany - непубличные функции на уровне модуля. Беспорядок (и это случается много раз). Также я хотел бы дать им контекст и использовать имя контекста, чтобы упростить именование внутри.
Решение № 0: модуль
Просто добавьте его в другой модуль:
template_fillers.create_in_temp(path, values_mapping)
Проблемы:
- что слишком мало кода для добавления файла, особенно когда есть много файлов (это создает беспорядок)
- это действие, и теперь я вынужден создать существительное имя для модуля (или нарушить правило именования модулей). Кроме того, упрощение делает его слишком широким (в данном случае создавая набор, который действительно является одиночным).
Наконец, это слишком маленький код для добавления модуля для него.
Решение №1: класс
Создайте класс без __init__
и только один общедоступный (по правилам именования) метод:
class TemplateFillerIntoTemp:
def run(self, path, values_mapping):
template_text = path.read_text()
filled_template = self._fill_template(template_text, values_mapping)
result_path = self._save_in_temp(filled_template)
return result_path
def _fill_template(self, template_text, values_mapping):
...
def _save_in_temp(self, filled_template):
_, pathname = tempfile.mkstemp(suffix='.ini', text=True)
path = pathlib.Path(pathname)
path.write_text(text)
return path
...
TemplateFillerIntoTemp().run(path, values_mapping)
Это то, что я делал много раз в прошлом. Проблемы:
- побочных эффектов нет, поэтому нет необходимости иметь экземпляр класса
- это действие, и теперь я вынужден создать имя-существительное для класса (или нарушить правило именования классов). Это приводит ко многим из "менеджеров" или "создателей".
- это неправильное использование концепции класса, это всего лишь небольшое дерево исполнения с одним функциональным интерфейсом, а не с классом вещей. Неправильные концепции замедляют понимание и могут привести к дальнейшему смешению между видами использования. Я знаю, что в ООП это распространено, потому что на некоторых языках вы не можете действительно выполнять функцию вне класса, но это слишком радикальный подход к порядку в коде. Объекты полезны, когда они являются ближайшим выражением вашей идеи. Это не так. Принуждение неулокального порядка парадоксально порождает беспорядок другого рода :)
Решение №2: Статический класс
Возьмите решение # 1, добавьте @staticmethod
всюду. Возможно также мета-класс ABC.
TemplateFillerIntoTemp.run(path, values_mapping)
Pro: есть четкое указание на то, что все это не зависит от экземпляра. Con: там больше кода.
Решение № 3: класс с __call__
Возьмите решение №1, создайте функцию __call__
с основным методом, а затем создайте на уровне модуля один экземпляр с именем create_filled_template_in_temp
.
create_filled_template_in_temp(path, values_mapping)
Pro: вызывает как одну функцию. Con: реализация раздута, не подходит для этой цели.
Решение №4: Вставьте вспомогательные функции в основную функцию
Добавьте их внутрь.
def create_filled_template_in_temp(path, values_mapping):
def _fill_template(template_text, values_mapping):
...
def _save_in_temp(filled_template):
_, pathname = tempfile.mkstemp(suffix='.ini', text=True)
path = pathlib.Path(pathname)
path.write_text(text)
return path
template_text = path.read_text()
filled_template = _fill_template(template_text, values_mapping)
result_path = _save_in_temp(filled_template)
return result_path
...
create_filled_template_in_temp(path, values_mapping)
Pro: это выглядит хорошо, если общее количество строк невелико и очень мало вспомогательных функций. Кон: это иначе.
Ответы
Ответ 1
Модификация № 4: Сделать внутренние функции, а также тело функции быть внутренней функцией. У этого есть прекрасное свойство по-прежнему читать сверху вниз, вместо того, чтобы иметь тело полностью на дне.
def create_filled_template_in_temp(path, values_mapping):
def body():
template_text = path.read_text()
filled_template = fill_template(template_text, values_mapping)
result_path = save_in_temp(filled_template)
return result_path
def fill_template(template_text, values_mapping):
...
def save_in_temp(filled_template):
_, pathname = tempfile.mkstemp(suffix='.ini', text=True)
path = pathlib.Path(pathname)
path.write_text(text)
return path
return body()
(Мне не нужны ведущие подчеркивания, поэтому они не выжили.)