I have a project that reads spreadsheets as a rectangular list of lists (matrix) transforms the matrix and then writes it to another spreadsheet. I want to be able to read from and write to multiple formats. Consequentially, the program should double as spreadsheet conversion tool.
Here is an example of just copying one sheet to another:
def ConvertSheet(in_sheet, out_sheet)
sheet_io = SheetIO()
data_matrix = sheet_io.read_sheet(in_sheet)
# different functions would operate on data_matrix before writing.
sheet_io.write_sheet(out_sheet, modified_data)
Depending on what type of object in_sheet
and out_sheet
are determine how SheetIO
writes it but as far as the client cares, that's SheetIO
's problem.
The design I have implemented is an odd factory pattern. It's primarily why I am asking for advice.
Abstract base class
This is pretty basic and would essentially be the interface in a proper factory pattern:
class ISheetIO(object):
""" Abstract base class...
"""
def read_sheet(self):
raise NotImplementedError("read_sheet not implemented")
def write_sheet(self):
raise NotImplementedError("write_sheet not implemented")
Concrete Class
The only concrete base class I have implemented so far. Note that it reads and writes sub-vectors as rows. I am specifying that all Concrete classes operate that way.
Also note that I am allowing optional arguments, specifying the range to write them to.
class ExcelSheet(ISheetIO):
def read_sheet(self, sheet, col_start=0, row_start=0, col_cnt=-1, row_cnt=-1):
# TODO: Raise an error instead. Research which error to raise
assert not sheet.ragged_rows
if row_cnt < 0:
row_cnt += sheet.nrows + 1
if col_cnt < 0:
col_cnt += sheet.ncols + 1
return [sheet.row_values(r, col_start, row_start)
for r in xrange(row_start, row_cnt + 1)]
def write_sheet(self, data, sheet, col_start=0, row_start=0):
for r, row, in enumerate(data):
for c, el in enumerate(row):
sheet.write(row_start + r, col_start + c, el)
# class CSVSheet(ISheetIO):
# class OpenXMLSheet(ISheetIO):
Wrapper Class
This is where things get hairy. My understanding of the factory pattern is that this should be the factory at it would return a concrete class. Instead the client instructs SheetIO
to read or write and SheetIO
gets and uses the concrete class itself through its own factory. The reason for composing the concrete class instead of returning it is, I am permitting the client to request the data with sub-lists as columns. SheetIO
encapsulates that state with read_by_row
and remembers to transpose the data again when writing.
class SheetIO(ISheetIO):
def __init__(self, read_by_row=True):
self.by_row = read_by_row
def _factory(self, sheet):
sheet_type = type(sheet)
if sheet_type is xlrd.sheet or sheet_type is xlwt.WorkSheet:
return ExcelSheet()
# elif CSV
# return CSVSheet()
# elif OpenXML
# return OpenXMLSheet()
else:
raise TypeError("Unknown Sheet type or not a sheet")
def read_sheet(self, sheet, **kwargs):
data = self._factory(sheet).read_sheet(sheet, **kwargs)
return self.orient_data(data)
def write_sheet(self, data, sheet, **kwargs):
oriented_data = self._orient_data(data)
self._factory(sheet).write_sheet(sheet, oriented_data, **kwargs)
def _orient_data(self, data):
if self.by_row:
return data
else:
return self._transpose(data)
def _transpose(self, matrix):
return zip(*matrix)
ISheetIO
andSheetIO
. I am not sureSheetIO
. I am pretty sure things should be more simple if theby_row
logic was just a parameter given to write_sheet. \$\endgroup\$ISheetIO
specifies the interface that readwrite classes must implement. If you don't think it's Pythonic or whatever please say so. I don't want to duplicate theby_row
in every concrete class so it is contained in the factory. Which isn't SOLID but where else? \$\endgroup\$