Danilo
Danilo

Reputation: 1028

Keyword arguments aliases in python

It always seemed odd to me that there are keyword arguments (or arguments) that can be passed to functions or __init__ methods of classes. How do you prevent user who is not familiar to your code from making a mistake? How to get user instantly ( almost instinctively ) familiar to your code without badly written or long winded documentation or to many trials and errors preventing user from quickly and comfortably use your code or module ?

In python we are lucky since we have help and dir functions that can often guide us to better understanding what some function parameters are. But sometimes there is poorly written __doc__ string that explains nothing.

Let me give you some examples of what i mean:

    >>> help(str.lower)
    Help on method_descriptor:

        lower(...)
        S.lower() -> string

    Return a copy of the string S converted to lowercase

. For example here we have some function that have ... input parameters. What does this parameter stands for, for complete newbie ( as was I when i first dived into python ) this was confusing and often i just skipped this section.

Some of websites that offer advice or tutorial just print-out help function file or just implement 1 of many functionalities of sed function.

1 functionality of example function

or directly from python.org

str.lower()
Return a copy of the string with all the cased characters [4] converted to lowercase. For 8-bit strings, this method is locale-dependent.

Now for someone who just started programing and don't ( or can't ) dive into bits and bytes , addresses and etc, this is some ancient spell that only master warlock can perform, don't even let me started on why this is not helping for non-english speaking people.

For this particular example function i can figure out 2-3 additional examples where it can do its job in different way, also i had to find out that this example function could be used by imputing string into str.lower(here) part.

The big problem here ( as i see it of course ) is that str with little googling can be descriptive by itself, and its functionality can be logically concluded only via wikipedia.

If i summarize the questions is simple, is there a way to modify the keywords when using as parameters to accept more names than we define so user would not have to rip his/her hair of on first step of the introduction tutorial?

Now i know some of you guys will say something along the lines : "if you dont understand it, don't do it" or "i ain't yo mama to teach you stuff"... to that i have to say "sharing is caring" and "You sometimes need help coding too otherwise you would not be on this website ".

Upvotes: 2

Views: 6732

Answers (2)

Alexander Wolf
Alexander Wolf

Reputation: 81

I made this decorator to alias kwargs. Works like a charm

def alias_param(param_name: str, param_alias: str) -> Callable:
    """
    Decorator for aliasing a param in a function

    Args:
        param_name: name of param in function to alias
        param_alias: alias that can be used for this param
    Returns:
    """
    def decorator(func: Callable):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            alias_param_value = kwargs.get(param_alias)
            if alias_param_value:
                kwargs[param_name] = alias_param_value
                del kwargs[param_alias]
            result = func(*args, **kwargs)
            return result
        return wrapper

it then can be used like so

@alias_param("param_1", alias='p1')
def function(param_1=None):
    return param_1

function(p1='value')

Upvotes: 5

Danilo
Danilo

Reputation: 1028

This is mine solution if someone has better please comment or correct.

the idea:
The idea is that every class could have a list of aliases to specific attributes so user could (based on class name logic :> point needs x,y,z,name attributes, dog needs breed,name,age,sex attributes and so on ) based on its own internal logic to call attributes without need for knowing exactly what attribute name sed attribute have.

the logic:
If function or class has for input some keyword arguments, then I would need minimal list of common words associated with sed argument. Synonyms and idioms can be googled easily but i would advise against the big list of synonyms, keep it small 2-3 + attribute name . Then all we need is to map those aliases to the original attribute since we as codes need to know how to call attribute without calling getattr(self,someattributestring)

code:
Chronologically we must first define a function to generate aliases.

# generate aliases for attributes
def generateAliases(*argListNames):

    returningValues = [] # this could be omitted if user wants to make generator
    la = returningValues.append # this could be omitted also 

    #dominated argListNames
    argListNames = map( str, argListNames ) #for simplicity convert to strings
    argListNames = map( str.lower , argListNames ) #for simplicity convert to lower string
    argListNames = list(argListNames) # back to list

    # small nameless lambda functions
    getFirstChr = lambda element: element[0] # getting first character
    conectedJoing= lambda connector,item,args: connecter.join([ item, args if not __isTL__(args) else connecter.join(args) ]) # generating joined string

    # list of string convertors used to generate aliases
    convertorList= [ lambda x: x , getFirstChr , str.title , str.upper , lambda x: getFirstChr( str.upper(x) ) ]

    for item in argListNames:
        ## since we dont want alias to repeat itself
        listNoitem = filter( lambda x: x!=item , argListNames )
        listNoitem = list(listNoitem)

        la(item) # if returningValues omitted use yield statement

        for conversion in convertorList: ##1 keeping up with for loops
            converted = conversion(item)

            for connecter in "_,".split(","):

                for listItem in listNoitem:

                    for cnvrt in convertorList: ##2 cnvrt is converted second stage : used to convert the whole list of items without current alias
                        cList = cnvrt(listItem)

                        la( conectedJoing(connecter,converted,cList) )# if returningValues omitted use yield statement


                la( conectedJoing(connecter,converted,listNoitem) )# if returningValues omitted use yield statement

    # if user wanted to make generator omit next lines
    returningValues = [ x.replace("_","") if x.endswith("_") else x for x in returningValues ]
    returningValues = sorted(set(returningValues)) 
    return list( map(str,returningValues) )

Now we need to map and check those arguments inside function or class so we need some argument parser.

## **kwargs argument parser , no error
def argumentParser(AprovedSequence,**kwargs):

    # AprovedSequence is suposed to be dictionary data type with {"original argument": generateAliases(originalArgumentName,somealias,somealias,...)

    """
        phrases the keyword arguments, 
            for example :     argumentParser(AprovedSequence,someArgument=somevalue,otherArgument=othervalue ... )
        then it checks if someArgument is needed by checking in AprovedSequence if name "someArgument" is found in sequence: 
        If "someArgument" is found in AprovedSequence it stores returns dictionary of DefaultKeys : Values 
            for example: DefaultKey for someArgument: somevalue

        input: 
            argumentParser(dict: AprovedSequence, kwargs)
        returns:
            dictionary of found attributes and their values

        !!important!! kwargs are not case sensitive in this case , so go crazy as long as you get the apropriate keyword!!
        if you dont know what kind of keywords are needed for class
            just type className.errorAttributeNames()
            for example point.errorAttributeNames()

    """
    if isinstance(AprovedSequence,dict):

        di = dict.items # dictionary.values(someDict)
        dk = dict.keys  # dictionary.keys(someDict)

        # managing the kwargs and aprooved sequence data
        toLowerStr = lambda el: str(el).lower() # conversion to lower string
        asingKey = lambda el: [ key for key in dk(AprovedSequence) if toLowerStr(el) in AprovedSequence[key] ][0] # asigning key 

        return { asingKey(k):v for k,v in di(kwargs) } # dictionary comprehension
    else: 
        raise TypeError("argumentPhraser function accepts only dictionary for a AprovedSequence aka first item")
        return None

implementation

def somefunction(**kwargs):
    aliases = {
        "val1":generateAliases("first","1"),
        "val2":generateAliases("second","2")
    }
    aproved = argumentParser(aliases,**kwargs)

    if "val1" in aproved.keys(): val1 = aproved["val1"]
    else: val1 = 0 # seting default value for val1

    if "val2" in aproved.keys(): val2 = aproved["val2"]
    else: val2 = 1 # seting default value for val2

    #do something or your code here

    return val1,val2

# for testing purposes 
for x in [ {"first":1} , {"second":2,"first":3} , {"f1":4,"s2":5} , {"f_1":6,"2_s":7} ]:
    # displaying imputed variables 
    form = ["passed "]
    form += [ "{} as {} ".format(key,value) for key,value in x.items() ]
    # implementing somefunciton
    print( "".join(form), somefunction(**x) )

output

python27 -m kwtest
Process started >>>
passed first as 1  (1, 1)
passed second as 2 first as 3  (3, 2)
passed f1 as 4 s2 as 5  (4, 5)
passed 2_s as 7 f_1 as 6  (6, 7)
<<< Process finished. (Exit code 0)

python35 -m kwtest
Process started >>>
passed first as 1  (1, 1)
passed first as 3 second as 2  (3, 2)
passed f1 as 4 s2 as 5  (4, 5)
passed f_1 as 6 2_s as 7  (6, 7)
<<< Process finished. (Exit code 0)

If implemented in classes, the process is similar in __init__ but __getitem__ , __setitem__and __delitem__ must be coded so they could search for attribute names in aliases as well. Also attribtribute names could be generated with self.attributes = list( aliases.keys()) or something like that.Default values could be stored in classes with __kwdefaults__ or 'defaults' depending what kind of data your function is using.

this code has been tested on py2.7 and py3.5 as you can see.

further explanation if needed
You can define aliases within class global attributes or within __init__.
Explaining further __getitem__:

def __getitem__(self,item):
    if item in self.aliases.keys():
         return getattr(self,item)
    if any( item in value for value in self.aliases.values() ):
         item = [ key for key in self.aliases.keys() if item in self.aliases[key] ] [0]
         return getattr(self,item)
    if item in range( len( self.aliases.keys() ) ): 
         item = list( self.aliases.keys() )[item]
         return getattr(self,item)

Explaining further __setitem__:

def __setitem__(self,item,value):
    item = self.__getitem__(self,item)
    #? must have `__dict__` method or class needs to be instanced from object like class someclass(object) 
    item = [ key for key in vars(self).items() if self[key] == item] [0]
    if item != None:
        setattr(self,item,value) 

Upvotes: 0

Related Questions