Inheritance and Pandas

Viewed 392

I am trying to create a file writer based on Pandas' ExcelWriter. I proceeded as I usually do with classes in Python (3) with inheritance:

import pandas as pd

class Writer(pd.ExcelWriter):
    def __init__(self, fname, engine='openpyxl'):
        pd.ExcelWriter.__init__(self, fname, engine=engine)
        self.newvar = 0

However, when I try to use it, I cannot access newvar:

test = Writer('test.xlsx')
test.newvar

returns:

AttributeError: '_XlsxWriter' object has no attribute 'nmax'

And when I check the type of test, it returns:

pandas.io.excel._XlsxWriter  

I don't understand what I am missing since I used this kind of inheritance in many other cases. Any idea would be appreciated!

1 Answers

This is because pandas.ExcelWriter.__new__ returns a different class than itself (actually it is an abc.ABCMeta). The class is chosen based on the extension of the file path and the engine which is used - you could observe that when you checked the type of the newly created instance. That means the __init__ method of whatever class is returned gets called. You can think of ExcelWriter as some kind of proxy for the specific writers for each format and engine (though it also defines the API which such a writer must provide).

In order to make your writer available (for the given engine), you need to register it.

But before you can do that you need to make your class compatible by following the instructions which you'll find via help(pandas.ExcelWriter). For the sake of completeness I cite them here:

# Defining an ExcelWriter implementation (see abstract methods for more...)

# - Mandatory
#   - ``write_cells(self, cells, sheet_name=None, startrow=0, startcol=0)``
#     --> called to write additional DataFrames to disk
#   - ``supported_extensions`` (tuple of supported extensions), used to
#      check that engine supports the given extension.
#   - ``engine`` - string that gives the engine name. Necessary to
#     instantiate class directly and bypass ``ExcelWriterMeta`` engine
#     lookup.
#   - ``save(self)`` --> called to save file to disk
# - Mostly mandatory (i.e. should at least exist)
#   - book, cur_sheet, path

# - Optional:
#   - ``__init__(self, path, engine=None, **kwargs)`` --> always called
#     with path as first argument.

So with that in mind we can extend your class:

class Writer(pd.ExcelWriter):
    engine = 'openpyxl'
    supported_extensions = ('xlsx',)

    def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):
        # Implement something useful here.
        pass

    def save(self):
        # Implement something useful here.
        pass

    def __init__(self, fname, engine='openpyxl', **kwargs):
        super().__init__(self, fname, engine=engine, **kwargs)

Now you can use pd.io.excel.register_writer(Writer) to register the writer. But you need to make sure the engine which you've specified matches your version of openpyxl. You can check the process of how a specific writer is chosen here; the writers which are currently registered for each version can be checked via print(pd.io.excel._writers).

As a side note: You can also subclass one of the already available specific writers and reuse their write_cells and save methods for example (however you'll need to register your writer also in that case):

Related