Search code examples
pythoninheritancesuperfacademethod-resolution-order

Constructing Proper Python Facade Class with Super?


I thought I had a handle on Multiple Inheritance with Super() and was trying to use it inside a Facade class, but I am running into a weird bug. I'm using a well made Python Workflow software called Fireworks, but the structures of the classes and Workflow Tasks are pretty rigid so I was creating a Facade class for ease of use.

Below is the Basic Structure of the Workflow Task:

class BgwInputTask(FireTaskBase):

    required_params = ['structure', 'pseudo_dir', 'input_set_params', 'out_file']
    optional_params = ['kpoints', 'qshift', 'mat_type']

    def __init__(self, params): 
        self.structure = Structure.from_dict(params.get('structure').as_dict())
        self.pseudo_dir = params.get('pseudo_dir')
        self.kpoints = params.get('kpoints', None)
        self.qshift = params.get('qshift', None)
        self.isp = params.get('input_set_params')
        self.run_type = params.get('run_type', None)
        self.mat_type = params.get('mat_type', 'metal')
        self.filename = params.get('out_file')

        '''
        misc code for storing pseudo_potentials in:
        self.pseudo_files
        self.occupied_bands
        etc...
        '''

        params = {'structure': self.structure, 'pseudo_dir': self.pseudo_dir,
            'kpoints': self.kpoints, 'qshift': self.qshift,
            'input_set_params': self.isp, 'run_type': self.run_type,
            'mat_type':self.mat_type, 'out_file': self.filename}
        self.update(params)

    def write_input_file(self, filename):
        <code for setting up input file format and writing to filename>

I structured my Facade class with super below:

class BgwInput(BgwInputTask):

    def __init__(self, structure, pseudo_dir, isp={},
                kpoints=None, qshift=None, mat_type='semiconductor',
                out_file=None):

        self.__dict__['isp'] = isp
        self.__dict__['run_type'] = out_file.split('.')[0]
        self.__dict__['params'] = {'structure': structure, 'pseudo_dir': pseudo_dir,
            'kpoints': kpoints, 'qshift': qshift,
            'input_set_params': self.isp, 'mat_type': mat_type,
            'out_file': out_file, 'run_type': self.run_type}
        print("__init__: isp: {}".format(self.isp))
        print("__init__: runtype: {}".format(self.run_type))

        super(BgwInput, self).__init__(self.params)

    def __setattr__(self, key, val):
        self.proc_key_val(key.strip(), val.strip()) if isinstance(
            val, six.string_types) else self.proc_key_val(key.strip(), val)

    def proc_key_val(self, key, val):
        <misc code for error checking of parameters being set>
        super(BgwInput, self).__dict__['params']['input_set_params'].update({key:val})

This works well except for one small caveat that is confusing me. When creating a new instance of BgwInput, it is not creating an empty instance. Input Set Parameters that were set in previous instances somehow are carried over to the new instance, but not kpoints or qshift. For example:

>>> epsilon_task = BgwInput(structure, pseudo_dir='/path/to/pseudos', kpoints=[5,5,5], qshift=[0, 0, 0.001], out_file='epsilon.inp')
__init__: isp: {}
__init__: runtype: epsilon

>>> epsilon_task.epsilon_cutoff = 11.0
>>> epsilon_task.number_bands = 29


>>> sigma_task = BgwInput(structure, pseudo_dir='/path/to/pseudos', kpoints=[5,5,5], out_file='sigma.inp')
__init__: isp: {'epsilon_cutoff': 11.0, 'number_bands': 29}
__init__: runtype: sigma

However, if I change self.__dict__['isp'] = isp in my Facade class to self.__dict__['isp'] = isp if isp else {} everything seems to work as expected. Parameters that were set in previous instances aren't carried over to the new instance. So, why isn't the Facade class defaulting to isp={} (given this is the default in the __ init __) as it should if not given input set parameters upon creation? Where is it pulling the previous parameters from since the default SHOULD be a blank dictionary?

Just to be clear, I've got a solution to make the Facade Class function as I expected it should (by changing isp in the Facade to self.__dict__['isp'] = isp if isp else {}), but I'm trying to figure out why this is needed. I believe I'm missing something fundamental with super or the method resolution order in Python. I'm curious as to why this is occurring and trying to expand my knowledge base.

Below is the Method Resolution Order for the Facade Class.

>>> BgwInput.__mro__

(pymatgen.io.bgw.inputs.BgwInput,
pymatgen.io.bgw.inputs.BgwInputTask,
fireworks.core.firework.FireTaskBase,
collections.defaultdict,
dict,
fireworks.utilities.fw_serializers.FWSerializable, 
object)

Solution

  • Mutable default arguments in python don't work like you expect them to; Your function definition(irrelevant arguments are omitted):

    def __init__(self, ..., isp={}, ...):
       <CODE>
    

    Is equivalent to this:

    DEFAULT_ISP = {}
    def __init__(self, ..., isp=DEFAULT_ISP, ...):
       <CODE>
    

    (except that the DEFAULT_ISP variable is not available.)

    The above code clearly shows that your two tasks are using the same isp dictoinary, which is apparently being modified by the attribute setters.