So I have a bunch of processing functions and all of them use a (for lack of a better word) 'master' function. This master function basically is a big AND operation that returns the relevant lines from a pandas data frame according to the value of a bunch of boolean or string columns (btw, the data are about rodent behavior).
def trial_selector(session,
init='NSWE', odor='ABCD', action='LRFB',
action_choice='LRFB', goal='NSWE', qNa='both',
cl=False, invalid=False):
trials = load_trials(session) # Wrapper load func that is somwhere else
# input checks transform str in lists... ugly but needed for now
if type(init) == str:
init = [init] if len(init) == 1 else [x for x in init]
if type(odor) == str:
odor = [odor] if len(odor) == 1 else [x for x in odor]
mapping = {'A': 1, 'B': 2, 'C': 3, 'D': 4}
odor = [mapping[x] for x in odor]
if type(action) == str:
action = [action] if len(action) == 1 else [x for x in action]
if type(action_choice) == str:
action_choice = [action_choice] if len(action_choice) == 1 else [x for x in action_choice]
if type(goal) == str:
goal = [goal] if len(goal) == 1 else [x for x in goal]
# init odor action action_choice goal selection
tr = trials[trials.init.isin(init) & trials.valve_number.isin(odor)
& trials.action.isin(action)
& trials.action_choice.isin(action_choice)
& trials.goal_choice.isin(goal)]
# TODO: Invalid, correction loop and trial type (not complete)
if not invalid:
tr = tr[tr.valid]
if not cl:
tr = tr[~tr.correction_loop]
if qNa == 'both':
tr = tr
elif qNa == 'q':
tr = tr[~tr.solution]
elif qNa == 'a':
tr = tr[tr.solution]
return tr
The processor functions prepare the data to be plotted by a corresponding plotting function, i.e., tperformance returns (x, y, yerr) and its used by tplot_performance.
# tsargs are the arguments of the trial_selector function
def tperformance_uni(sessionName, **tsargs):
trials = trial_selector(sessionName, **tsargs)
x = trials.correct.dropna().index
y = trials.correct.dropna().values
return (x, y)
@check_session
def tperformance(sessionList, smooth=False, **tsargs):
temp = []
out = pd.DataFrame()
for session in sessionList:
x, y = tperformance_uni(session, **tsargs)
temp.append(pd.Series(y, index=x, name=session))
out = pd.concat(temp, axis=1, )
x = out.mean(axis=1).index
y = out.mean(axis=1).values
yerr = out.std(axis=1)/np.sqrt(len(out.columns))
yerr[yerr.isnull()] = 0
if not smooth or type(smooth) is bool:
win = win_size(x)
else:
win = win_size(x, default=smooth)
ysmooth = sm(y, win)
yerrsmooth = sm(yerr, win)
if len(x) != len(ysmooth):
ysmooth = ysmooth[1:]
if len(x) != len(yerrsmooth):
yerrsmooth = yerrsmooth[1:]
return (x, y, yerr) if not smooth else (x, ysmooth, yerrsmooth)
And the plotting function eg is:
def tplot_performance(sessionName, ax=False, decor=False, err=False,
c='b', ls='-', m='',
smooth=False,
**tsargs):
"""
Plots correct across trials
"""
if not ax:
ax = plt.subplot2grid((1, 1), (0, 0), rowspan=1, colspan=1)
# ---
x, y, yerr = tperformance(sessionName, smooth=smooth, **tsargs)
# ---
ax.plot(x, y, ls=ls, marker=m, color=c, linewidth=2)
if err:
ax.fill_between(x, y-yerr, y+yerr, color='gray', alpha=0.25)
if decor:
tplot_performance_template(sessionName, ax=ax)
return (x, y, yerr)
I managed to successfully implement an argument check @check_session using decorators that basically ensures the session is a list of strings.
def check_session(func):
"""Ensures session is of type list, if string will make list of one value
"""
def wrapper(session, **kwargs):
session = [session] if type(session) is str else session
return func(session, **kwargs)
return wrapper
So far so good. Now I wanted to add the default values for the trial_selector function without being completely explicit, i.e., exposing init, odor, action,... in all functions nor completely generic, i.e. the way its implemented now that uses **tsargs.
Basically I would like to use a decorator like @tsargs_defaults so that I can use the default values in the processing function to do stuff. I could have input argument modules that would allow me to declare something like this:
@defalut_bla
@tsargs_defaults
def example_func(*args, **kwargs):
if init == 'N':
do something
if var_in_defalut_bla == someVal:
do something else
The decorators should add groups of variables that are declared in the inner scope locals() of the func.
What I tried so far:
def tsargs_defaults(func):
"""Adds trial_selector arguments and their defaults to function
tsargs = init='NSWE', odor='ABCD', action='LRFB', action_choice='LRFB',
goal='NSWE', qNa='both', cl=False, invalid=False,
"""
def wrapper(*args, **kwargs):
defaults = {'init': 'NSWE',
'odor': 'ABCD',
'action': 'LRFB',
'action_choice': 'LRFB',
'goal': 'NSWE',
'qNa': 'both',
'cl': False,
'invalid': False}
for k in kwargs:
if k in defaults:
defaults[k] = kwargs[k]
elif k not in defaults:
defaults.update({k: kwargs[k]})
return func(*args, **defaults)
return wrapper
However this will add what I want not to the local scope but to a kwargs dict (**defaults in the example). This means that I have to use kwargs['init'] == 'N'
in stead of init == 'N'
in the inner scope of the function.
I understand that this is a huge explanation for a non problem as the code kind of works like this, however I have a bunch of processing and plotting functions that use exposed default arguments to do different things and would like to avoid refactoring all of it. Maybe there is no way or my question is ill posed keep in mind its my first attempt of using python decorators. In any case I would like to understand a bit more. Any help is appreciated! Thanks
btw: I'm using python 3.4
TL;DR
# some_kwargs {'one': 1, 'two': 2}
# some_other_kwargs {'three': 3, 'four': 4}
@some_other_kwargs
@some_kwargs
def example_func(*args, **kwargs):
print(one, two, three, four) # does not work
print(kwargs['one'], kwargs['two'], kwargs['three'], kwargs['four']) # works