* Updating Parameters class: now based on dict.

* Can add two Parameters together, which means joining the two or more dicts,
  much like what function dict_join does.
master
wirawan 14 years ago
parent 379e83fdec
commit f1b8746147
  1. 215
      sugar.py

@ -1,6 +1,6 @@
#!/usr/bin/ipython -pylab #!/usr/bin/ipython -pylab
# #
# $Id: sugar.py,v 1.5 2010-08-12 19:35:55 wirawan Exp $ # $Id: sugar.py,v 1.6 2010-09-10 21:24:56 wirawan Exp $
# #
# Created: 20100121 # Created: 20100121
# Wirawan Purwanto # Wirawan Purwanto
@ -11,6 +11,7 @@
# #
# #
import sys import sys
import weakref
def ifelse(cond, trueval, *args): def ifelse(cond, trueval, *args):
"""An alternative to python's own ternary operator, but with multiple """An alternative to python's own ternary operator, but with multiple
@ -109,20 +110,24 @@ class ranges_type:
ranges = ranges_type() ranges = ranges_type()
class Parameters(object): class Parameters(dict):
"""A standardized way to define and/or pass parameters (with possible """A standardized way to define and/or pass parameters (with possible
default values) among routines. default values) among routines.
This provides a very flexible lookup scheme for a parameter with a given name. This provides a very flexible lookup scheme for a parameter with a given name.
It scans through the namescopes (dicts) in a deterministic order, returning It scans through the namescopes (dicts) in a deterministic and sequential
the first one found. order, returning the first one found.
This, hopefully, gets rid of kitchen-sink parameter passing, at least from This, hopefully, gets rid of kitchen-sink parameter passing, at least from
programmer's point of view. programmer's point of view.
WARNING: This object is derived object instead of python dict, so as to avoid WARNING: This object is derived from python dict with ALL method names removed,
messing with standard dict names. so as to avoid collosion of these names with user-defined parameters with the
same name.
Names reserved by this class begin and end with an underscore. Names reserved by this class begin and end with an underscore.
Names reserved by python begin and end with two underscores. Names reserved by python begin and end with two underscores.
So, avoid specifying parameters with both leading and trailing underscores. So, avoid specifying parameters with both leading and trailing underscores.
WARNING: Be careful modifying this class; endless recursive calls are
possible.
Some uses: Some uses:
@ -135,21 +140,118 @@ class Parameters(object):
for step in prm.steps: for step in prm.steps:
... ...
Reserved members: Parameters can also be updated in this way:
* _no_null_ = (True/False, default False) look for non-null values in all
the parameter lists until one is found. a = Parameters(...)
updates = {'nblk': 7, 'nbasis': 32}
a += updates
or, to call a function with a combination of parameters:
Reserved private members of the Parameters object:
* _no_null_ = (True/False, default False) look for non-null (non-"None") values
in all the parameter lists until one is found.
* _list_ = (list) the list of parameter dicts to search from. * _list_ = (list) the list of parameter dicts to search from.
* _prm_ = (dict) the most overriding list of parameters. * _kwparam_ = (string, default "_opts_") the default name of the function argument
that will hold excess named arguments.
Used in _create_() function below.
If this is set to None, we will not use this feature.
* _userparam_ = (string, default "_p") the default name of the function argument
that will contain Parameters-like object given by the user.
Used in _create_() function below.
If this is set to None, we will not use this feature.
The most overriding list of parameters, as provided via excess key=value
arguments in creating this Parameters object, are stored in "self".
""" """
class _self_weakref_:
"""A minimal proxy object, just enough to get a weakref to the 'self' object
below to be accesible via a few dict-like lookup mechanisms.
Also needed to avoid recursive `in' and [] get operators below."""
def __init__(self, obj):
self.ref = weakref.ref(obj)
def __contains__(self, key):
return dict.__contains__(self.ref(), key)
def __getitem__(self, key):
return dict.__getitem__(self.ref(), key)
def __init__(self, *_override_dicts_, **_opts_): def __init__(self, *_override_dicts_, **_opts_):
""" """
Again, keyword arguments passed here will become the most overriding options. Creates a new Parameters() object.
The unnamed arguments are taken to be dict-like objects from which we will
search for parameters.
We silently ignore `None' values which are passed in this way.
Parameters will be searched in left-to-right order of these dict-like
objects.
Then the keyword-style arguments passed on this constructor will become
the most overriding options.
The dict-like objects must contain the following functionalities:
* for key in obj:
...
(in other words, the __iter__() method).
* key in obj
* obj[key]
That's it!
Example:
defaults = { 'walltime': '6:00:00', 'nwlk': 100 }
# ...
p = Parameters(defaults, walltime='7:00:00', nblk=300)
Then when we want to use it:
>> p.nwlk
100
>> p.walltime
'7:00:00'
>> p.nblk
300
Options:
* _no_null_ = if True, look for the first non-None value.
* _flatten_ = will flatten the key-value pairs.
Note that this may make the Parameters object unnecessarily large in memory.
Additionally, this means that the updates in the contents of the dicts
passed as the _override_dicts_ can no longer be reflected in this object
because of the shallow copying involved here.
* _kwparam_
* _userparam_
At present, the `flatten' attribute will not be propagated to the child
Parameters objects created by this parent object.
""" """
prm = _opts_
# Remove standard dict procedure names not beginning with "_":
for badkw in self.__dict__:
if not badkw.startswith("_"):
del self.__dict__[badkw]
# Store the user-defined overrides in its own container:
dict.clear(self)
dict.update(self, _opts_)
if _opts_.get('_flatten_', False):
for p in paramlist:
dict.update(self, p)
else:
# WARNING: Using weakref proxy is important:
# - to allow clean deletion of Parameters() objects when not needed
# - to avoid recursive 'in' and 'get[]' operators.
paramlist = (Parameters._self_weakref_(self),) + _override_dicts_ #+ tuple(deflist))
#paramlist = (self,) + _override_dicts_ #+ tuple(deflist))
self.__dict__["_list_"] = [ p for p in paramlist if p != None ]
self.__dict__["_kwparam_"] = _opts_.get("_kwparam_", "_opts_")
self.__dict__["_userparam_"] = _opts_.get("_userparam_", "_p")
self.__dict__["_no_null_"] = ifelse(_opts_.get("_no_null_"), True, False) self.__dict__["_no_null_"] = ifelse(_opts_.get("_no_null_"), True, False)
self.__dict__["_prm_"] = prm # Finally, filter out reserved keywords from the dict:
paramlist = (prm,) + _override_dicts_ #+ tuple(deflist)) for badkw in ("_kwparam_", "_userparam_", "_no_null_", "_flatten_"):
self.__dict__["_list_"] = [ p for p in paramlist if p != None ] #if badkw in self: del self[badkw] -- recursive!!!
if dict.__contains__(self,badkw): del self[badkw]
def _copy_(self):
"""Returns a copy of the Parameters() object."""
return Parameters(_no_null_=self._no_null_,
_kwparam_=self._kwparam_,
_userparam_=self._userparam_,
*self._list_[1:],
**self)
def __getattr__(self, key): def __getattr__(self, key):
"""Allows options to be accessed in attribute-like manner, like: """Allows options to be accessed in attribute-like manner, like:
opt.niter = 3 opt.niter = 3
@ -162,12 +264,12 @@ class Parameters(object):
else: else:
for ov in self._list_: for ov in self._list_:
if key in ov: return ov[key] if key in ov: return ov[key]
# Otherwise: # Otherwise: -- but most likely this will return attribute error:
return object.__getattribute__(self, key) return dict.__getattribute__(self, key)
def __setattr__(self, key, value): def __setattr__(self, key, value):
"""This method always sets the value on the object's dictionary. """This method always sets the value on the object's dictionary.
Values set will override any values set in the input parameter lists.""" Values set will override any values set in the input parameter lists."""
self._prm_[key] = value self[key] = value
def __contains__(self, key): def __contains__(self, key):
if self._no_null_: if self._no_null_:
for ov in self._list_: for ov in self._list_:
@ -184,70 +286,99 @@ class Parameters(object):
for ov in self._list_: for ov in self._list_:
if key in ov: return ov[key] if key in ov: return ov[key]
raise KeyError, "Cannot find parameter `%s'" % key raise KeyError, "Cannot find parameter `%s'" % key
def __setitem__(self, key, value): #def __setitem__(self, key, value): # -- inherited from dict
self._prm_[key] = value # self._prm_[key] = value
# TODO in the future for iterative accesses: # TODO in the future for iterative accesses:
# -- not that essential because we know the name of # -- not that essential because we know the name of
# the parameters we want to get: # the parameters we want to get:
#def __iter__(self): #def __iter__(self): # -- inherited from dict
# return self._prm_.__iter__ # """Returns the iterator over key-value pairs owned by this object.
# This does NOT return key-value pairs owned by the _override_dicts_.
# """
# return self._prm_.__iter__()
#def _iteritems_(self): #def _iteritems_(self):
# return self._prm_.iteritems() # return self._prm_.iteritems()
def _update_(self, srcdict): def _update_(self, srcdict):
self._prm_.update(srcdict) """Updates the most overriding parameters with key-value pairs from
def _create_(self, kwparams="_opts_", userparams="opts", *defaults): srcdict.
Srcdict can be either a dict-derived object or a Parameters-derived
object."""
dict.update(self, srcdict)
def __add__(self, srcdict):
"""Returns a copy of the Parameters() object, with the most-overriding
parameters updated from the contents of srcdict."""
rslt = self._copy_()
rslt._update_(srcdict)
return rslt
def _create_(self, kwparam=None, userparam=None, *defaults):
"""Creates a new Parameters() object for standardized function-level """Creates a new Parameters() object for standardized function-level
parameter lookup. parameter lookup.
This routine *must* be called by the function where we want to access these This routine *must* be called by the function where we want to access these
parameters, and where some parameters are to be overriden via function arguments, parameters, and where some parameters are to be overriden via function
etc. arguments, etc.
The order of lookup is definite: The order of lookup is definite:
* * local variables of the calling subroutine will take precedence
* the excess keyword-based parameters,
* user-supplied Parameters-like object, which is
* the dicts (passed in the `defaults' unnamed parameter list) is searched
*last*.
I suggest that this is used only as a last-effort safety net.
Ideally, the creating Parameters object itself should contain the
'factory defaults', as shown in the example below.
class Something(object): class Something(object):
def __init__(self, ...): def __init__(self, ...):
# self.opts holds the factory default
self.opts = Parameters() self.opts = Parameters()
self.opts.cleanup = True # example self.opts.cleanup = True # example
def doit(self, src=None, info=None, def doit(self, src=None, info=None,
_defaults_=dict(src="source.txt", info="INFO.txt", debug=1), _defaults_=dict(src="source.txt", info="INFO.txt", debug=1),
**_opts_): **_opts_):
# FIXME: use self-introspection to reduce kitchen-sink params here: # FIXME: use self-introspection to reduce kitchen-sink params here:
p = self.opts._create_() p = self.opts._create_(_defaults_)
# ^ This will create an equivalent of: # ^ This will create an equivalent of:
# Parameters(locals(), _opts_, _opts_.get('opts'), self.opts, _defaults) # Parameters(locals(), _opts_, _opts_.get('opts'), self.opts, _defaults)
# Now use it:
if p.cleanup:
... do something
""" """
# Look up the stack of the calling function in order to retrieve its # Look up the stack of the calling function in order to retrieve its
# local variables # local variables
from inspect import stack from inspect import stack
caller = stack()[1][0] # one frame up; element-0 is the stack frame caller = stack()[1][0] # one frame up; element-0 is the stack frame
# local variables will be the first to look for if kwparam == None: kwparam = self._kwparam_
if userparam == None: userparam = self._userparam_
# local variables will be the first scope to look for
localvars = caller.f_locals localvars = caller.f_locals
contexts = [ localvars ] contexts = [ localvars ]
# then _opts_ excess-keyword parameters (see example of doit() above) # then _opts_ excess-keyword parameters (see example of doit() above)
if kwparams in localvars: if kwparam in localvars:
_opts_ = localvars[kwparams] _opts_ = localvars[kwparam]
contexts.append(_opts_) contexts.append(_opts_)
else: else:
_opts_ = {} _opts_ = {}
# then opts, an explicitly-defined argument carrying set of parameters # then opts, an explicitly-defined argument which contain a set of parameters
if userparams in localvars: if userparam in localvars:
opts = localvars[userparams] opts = localvars[userparam]
contexts.append(opts) contexts.append(opts)
else: else:
opts = {} opts = {}
if userparams in _opts_: if userparam in _opts_:
contexts.append(_opts_[userparams]) contexts.append(_opts_[userparam])
# then this own Parameters data will come here: # then this own Parameters data will come here:
contexts.append(self) contexts.append(self)
# then any defaults # then any last-minute defaults
contexts += [ d for d in defaults ] contexts += [ d for d in defaults ]
# Now construct the Parameters() class for this calling function: # Now construct the Parameters() class for this calling function:
return Parameters(*contexts) return Parameters(_kwparam_=kwparam, _userparam_=userparam, *contexts)
#def __dict__(self):
# return self._prm_

Loading…
Cancel
Save