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)
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.