Фабрика фабрик - как правильно
В задачах обработки данных все рутинно. Получили пакет (или письмо), прочитали файл(ы), обработали файлы, создали отчет нужного типав зависимости от пришедшего набора файлов (например xlsx-файл), отформатировали отчет, отправили результирующий файл назад. Вот для такой задачи соорудил я абстрактный класс:
from pathlib import Path
from abc import ABC, abstractmethod # , abstractproperty
class IZI_report(ABC):
"""Class for reports."""
def __init__(self, path_to_files_folder: Path):
self.path_to_files_folder = path_to_files_folder
self.response = { # dict_to_return
"exit_is_ok": False,
"exit_message": "",
# ...
}
@abstractmethod
def files_reading(self, path_to_files_folder):
...
@abstractmethod
def data_processing(self, **pd_files):
...
@abstractmethod
def excel_writer(self, path_to_files_folder, client_name, files):
...
@abstractmethod
def excel_file_formatting(self, file_to_attach):
...
Здесь предполагается, что предварительно файл(ы) уже лежат в папке file_to_attach. Но это не так важно.
И все бы ничего, только вот форматирование эксель файлов в среде виндовс родной библиотекой бывает до 10 раз быстрее, чем "универсальной", не зависящей от операционной системы. Поэтому каждая конкретная реализация метода excel_file_formatting(self, file_to_attach) выглядит у меня примерно так:
def excel_file_formatting(self, file_to_attach):
def o_excel_file_formatting(file_to_attach):
# используем медленный универсальный модуль
...
def w_excel_file_formatting(file_to_attach):
# используем быстрый windows-модуль
...
return (w_excel_file_formatting if current_os_is_win else o_excel_file_formatting)(file_to_attach)
Но мне это не казалось красивым. Поскольку в каждом конкретном методе повторялись написанные вспомогательные функции, обеспечивающие форматирование. Например o_header_wrap_and_size или o_search_and_colorise. Думаю по названию понятно чем они занимаются. Если их везде дублировать, то нарушается принцип DRY.
Можно было бы включить эти вспомогательные мелкие функции прямо в абстрактный класс, но это как то нарушает "соразмерность" методов в абстрактном классе.
А делать вместо метода абстрактного класса целую фабрику классов как-то боязно и громоздко.
Вопрос - какие стандартные правила создания методов в таких случаях считаются правильными, "питонячими"?
Похоже здесь подойдет вложенный класс, нет?
Однако мнения о приемлемости вложенных (inner, nested) классов разделились. Есть против, есть и за.
Есть ли какие то рекомендации на этот счет?
Ответы (2 шт):
Я пошел по пути создания вложенных классов и создал явную "фабрику" прямо внутри __init__.
Надеюсь такая реализация не слишком вычурная. Меня в конце концов, такое решение устроило: повторяющиеся функции описаны внутри класса, так что их переписывать в каждой реализации не нужно. А создание класса зависящего от типа ОС выписано явно. Так что "скрытых" последствий нет.
from pathlib import Path
import os
import sys
from copy import copy # , deepcopy
from openpyxl.styles import Color, PatternFill, Font, Border
import openpyxl as opx
from abc import ABC, abstractmethod # , abstractproperty
import win32com.client
class IZI_report(ABC):
"""Class for reports."""
def __init__(self, path_to_files_folder: Path):
print('IZI_report(ABC) init')
self.current_os_is_win = sys.platform in ['win32', 'cygwin'] and os.name == "nt"
if self.current_os_is_win:
print('win32com.client')
import win32com.client
if self.current_os_is_win:
self.xlsx_formater = self.Windows_excel_file_formatting()
else:
self.xlsx_formater = self.Common_excel_file_formatting()
self.path_to_files_folder = path_to_files_folder
self.response = { # dict_to_return
"exit_is_ok": False,
"exit_message": "",
"files": dict(),
'zip_path': None,
}
@ abstractmethod
def files_reading(self, path_to_files_folder):
...
@ abstractmethod
def data_processing(self, **pd_files):
...
@ abstractmethod
def excel_writer(self, path_to_files_folder, client_name, files):
...
class Windows_excel_file_formatting(ABC):
def search_and_colorise(self, work_sheet, searched_texts_list, color=4):
""""""
if type(searched_texts_list) is str:
raise Exception('list of str expected!')
for seached in searched_texts_list:
work_sheet.Cells.Find(seached).Interior.ColorIndex = color
def header_wrap_and_size(self, ws, size):
""""""
ws.Rows('1:1').WrapText = True
ws.Columns(ws.Range("A1").CurrentRegion.Columns).ColumnWidth = size
ws.Rows("1:1").EntireRow.AutoFit()
@ abstractmethod
def excel_file_formatting(self, file_to_format):
...
class Common_excel_file_formatting(ABC):
"""Format xlsx file."""
def search_and_colorise(self, work_sheet, searched_texts_list, color='EE1111'):
"""Renewed."""
if type(searched_texts_list) is str:
raise Exception('list of str expected!')
# openpx style
fill_color = PatternFill(start_color=color,
end_color=color,
fill_type='solid')
for searched in searched_texts_list:
for j in range(1, work_sheet.max_column + 1):
print(work_sheet.cell(row=1, column=j).value)
if work_sheet.cell(row=1, column=j).value:
if work_sheet.cell(row=1, column=j).value.find(searched) >= 0:
work_sheet.cell(row=1, column=j).fill = fill_color
def header_wrap_and_size(self, ws, size):
"""Renewed."""
for j in range(1, ws.max_column + 1):
ws.cell(row=1, column=j).alignment = \
opx.styles.Alignment(
horizontal='center', # 'general',
vertical='bottom',
text_rotation=0,
wrap_text=True,
shrink_to_fit=False,
indent=0)
ws.column_dimensions[opx.utils.cell.get_column_letter(j)].width = size
@ abstractmethod
def excel_file_formatting(self, file_to_format):
...
В тоже время такая реализация несколько, я бы сказал, "некрасивая" )) Ведь для того, чтобы создать класс надо внутри класса наследоваться от родителя с явным указанием имени абстрактного класса. Вот так:
class Windows_excel_file_formatting(IZI_report.Windows_excel_file_formatting)
Итого, работает, но хотелось бы чего то более изящного.
Пример создания тестового класса, прямо читающего файл с диска с последующей его разметкой описаной в абстрактном классе функцией search_and_colorise ...
class Test(IZI_report):
def __init__(self, path: Path):
super().__init__(path)
self.SNAPSHOTS_PATTERNS = ["rec", "adj", "rei"]
self.separators = " _-"
def files_reading(self, path_to_files_folder):
...
def data_processing(self, **pd_files):
...
def excel_writer(self, path_to_files_folder, client_name, files):
...
class Windows_excel_file_formatting(IZI_report.Windows_excel_file_formatting):
def excel_file_formatting(self, file_to_format):
print('excel_file_formatting')
Excel = win32com.client.DispatchEx("Excel.Application")
print(1)
wb = Excel.Workbooks.Open(os.path.join(os.getcwd(), "sh1.xlsx"))
print(2)
ws_rec = wb.Worksheets("Аркуш1")
print("---- before =====")
self.search_and_colorise(ws_rec, ("01_", '10_', '20_'))
wb.Save()
print(3)
wb.Close()
Excel.Application.Quit()
Excel.Quit()
class Common_excel_file_formatting():
"""Format xlsx file."""
def excel_file_formatting(self, file_to_format):
wb = opx.load_workbook("sh1.xlsx")
ws_rec = wb["Sheet01"]
path_to = Path("nothng.txt")
zz = Test(path_to)
zz.xlsx_formater.excel_file_formatting('ff')
Все работает, но как то не изящно что ли, с излишним расходованием ресурсов во время создания класса.
Буду признателен за подсказки!
Есть ощущение, что тут может помочь композиция.
Ты делаешь базовый класс ExcelFormatter, реализуешь на его основе WindowsExcelFormatter и GenericExcelFormatter. Они должны предоставлять одинаковый API, конечно же.
В таком случае ты выделяешь низкоуровневую логику форматирования из класса репортов, вызывая методы базового ExcelFormatter, а инстанс и настройки ExcelFormatter задаёшь при инициализации репортера, определив систему.