1

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
nico
  • 385
  • 4
  • 19
  • 2
    It's not *totally* clear, but are you looking for e.g. `kwargs.get(key, defaults[key])`? Could you cut this down to a simpler example? – jonrsharpe Jun 30 '15 at 15:05
  • The second to last code block is the simple example where the kwarg _init_ is added to the inner scope from the *@tsargs_defaults* decorator and the _var_in_default_bla_ is added by the *@default_bla* decorator. right now the only way I can check if init == 'N' is by using `if kwargs['init'] == 'N'` – nico Jun 30 '15 at 15:14
  • 1
    No; if you're using arbitrary `kwargs` you have to get them from the dictionary (unless you do something hacky like update `locals`). Also note that you should be using `isinstance` not `type`. – jonrsharpe Jun 30 '15 at 15:18
  • I would like to inject kwargs when I call the function, like a macro that adds the kwargs with the defaults whenever I call the func. Can't I do this using decorators? or any other way? – nico Jun 30 '15 at 15:39
  • Not that I can think of. And if you *could* do it, you would end up with less readable code, as it would be unclear within each function whether the names you were referring to were *"injected"* `kwargs` or from some outer (nonlocal/closure/global) scope. In short: even if possible, this is a bad idea. – jonrsharpe Jun 30 '15 at 15:44
  • Hummm.... ok then, how would you go about organizing different groups of kwargs so that they remain tractable, explicit and functional (the python way)? On a different note: Yes, isinstance inheritances et al. not defining any classes of my own yet, although I get the impression that a plotter class that implements the ax check and the error plot etc that can easily be overridden if needed might be the way to go :) but that's the next problem – nico Jun 30 '15 at 15:47
  • 1
    The typical way to refactor over-long parameter lists is to extract groups of related parameters into objects of their own - see e.g. http://stackoverflow.com/q/439574/3001761 – jonrsharpe Jun 30 '15 at 15:52
  • Thanks! it is useful. – nico Jun 30 '15 at 16:00

1 Answers1

2

update

The original answer, suggesting the use of functools.partial is bellow. In th meantime, years after answering this question, I needed the functionality being described: A decorator to add new specific keyword-args to the decorated function, and have these keywords show up in the function signature itself, without the wrapped function ever need to know about them.

I created a "metadecorator" called conbine_sginatures which can imbue any ordinary decorator with this "power" - I had not, yet, refactored it to a more specific Python package from whre it can more easily be reused - it remains in the context where it was created, in the "utils" package of my unicode-art Terminedia project:

Example usage:

In [13]: from terminedia.utils import combine_signatures                                                                      

In [14]: def add_parameter_b(func): 
    ...:     @combine_signatures(func) 
    ...:     def wrapper(*args, b, **kwargs): 
    ...:         print("parameter b", b) 
    ...:         return func(*args, **kwargs) 
    ...:     return wrapper 
    ...:                                                                                                                      

In [15]: @add_parameter_b 
    ...: def a(a): 
    ...:     print("parameter a", a) 
    ...:                                                                                                                      

In [16]: a(42, b=1138)                                                                                                        
parameter b 1138
parameter a 42

The resulting code was subject of a talk on PyCon Sweden in 2020: https://www.youtube.com/watch?v=eva0s7up5Oc&t=1262s&ab_channel=PyConSweden

(This answer got an upvote today (2021-04-25), that is why I revisited here. By coincidence, I had just today added an extra feature to the combine_signatures decorator: it now works with co-routine functions as well)

original answer

If all you want are Python functions that are different versions of the same function with different default parameters, you can use functools.partial to create them easily, with no need for decorators.

so, if you have def trial_selector(par1=..., par2=..., ...): and want a callables with different default sets for the various parameters, you can declare them like this:

from functools import partial

search1 = partial(trial_decorator, par1="ABCD", par2="EFGH") search2 = partial(trial_decorator, par1=None, par3="XZY", ...0

And just call the searchN functions only having to care about new parameters, or parameters you want to override again.


Now if you need decorators for other functionalities than that - there are some extra remarks about your code:

WHat you are likely not aware of is that if you make use of **kwargs to call a function, there is no need for the function signature itself to use kwargs.

So, for your inner function, instead of havign just def example_func(*args, **kwargs): as a signature, you can have a full list of explict parameters (like on your first listng -

def trial_selector(session,
                   init='NSWE', odor='ABCD', action='LRFB',
                   action_choice='LRFB', goal='NSWE', qNa='both',
                   cl=False, invalid=False):

and still wrap it with a decorator that pass kwargs to it, just as you made in your "what I have tried" code. Moreover, on your example decorator, you have recreated made a dictionary "update" method in a rather complicated way - it can be written just like:

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',
                    ...
                    'invalid': False}
        defaults.update(kwargs)
        return func(*args, **defaults)
    return wrapper

If that is all you want, that is all you will need. Moreover, the decorator syntax is meant to help - in this case, it looks like you can make use of several of these "default args" decorators without using the decorator syntax - you can write it just like:

def full_search_function(all, default, parameters, ...):
    ...

def decorator_for_type1_search(...):
    ...
type1_search = decorator_for_type1_search(full_Search_function)

And at this point you have type1_search as a function with the parameters added in decorator_for_type1_search - and you can just create as many of those as you want.

jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • Thanks I'll definitely take a look at partial, didn't know it existed :). For the decorator I have only one thing to say, duh... I must have been sleeping... of course updating the defaults with the kwargs will work.... The last part of your answer I didn't understand, can you elaborate? – nico Jul 09 '15 at 10:01
  • At the last part the variable named "type1_search" will be a new function - just as if it was a function with the same body of `full_Search_function` decorated with `@decorator_for_type1_search` . But making it this way, the original `full_search_function` would still be undecorated and available for creating more functions with things like `type2_search = decorator_for_type2(full_search_function)` – jsbueno Jul 10 '15 at 03:57