Search code examples
pythonkeyword-argument

Keyword argument aliases in Python


I am creating a custom 3D space from scratch for some new coders, and I am worried that perhaps my documentation isn't beginner friendly. Here is an example:

def make_point(**kwargs):
   X, Y, Z = 0, 0, 0;
   if "Xcoord" in kwargs.keys(): X = kwargs["Xcoord"];
   if "Ycoord" in kwargs.keys(): Y = kwargs["Ycoord"];
   if "Zcoord" in kwargs.keys(): Z = kwargs["Zcoord"];
   return tuple(X, Y, Z);

But the way I named the keyword arguments isn't very suitable for a new programmer with some knowledge in linear algebra, but it is necessary for me to keep track which variable is what. So in that manner I have vXcoord for the vector x coordinate, pXcoord for point, etc.

Is there a way to make keyword arguments more user friendly so if a user typed vX,vectorX or whatever it seems more logical it would still map to vXcoord?


Solution

  • 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 the user wants to make a 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 don't want an alias to repeat itself
            listNoitem = filter(lambda x: x != item, argListNames)
            listNoitem = list(listNoitem)
    
            la(item) # If returningValues omitted, use the '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 the current alias
                            cList = cnvrt(listItem)
    
                            la(conectedJoing(connecter, converted, cList)) # If returningValues is omitted, use the 'yield' statement
    
    
                    la(conectedJoing(connecter, converted, listNoitem)) # if returningValues is omitted, use the 'yield' statement
    
        # If the user wanted to make a generator, omit the 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 a function or class, so we need some argument parser.

    ## **kwargs argument parser, no error
    def argumentParser(ApprovedSequence, **kwargs):
    
        # ApprovedSequence is supposed to be a dictionary data type with {"original argument": generateAliases(originalArgumentName, somealias, somealias, ...)
    
        """
            Phrases the keyword arguments,
                for example:     argumentParser(ApprovedSequence, someArgument=somevalue, otherArgument=othervalue ...)
            Then it checks if someArgument is needed by checking in ApprovedSequence if name "someArgument" is found in the sequence:
            If "someArgument" is found in ApprovedSequence, it stores returns the dictionary of DefaultKeys: Values,
                for example: DefaultKey for someArgument: somevalue
    
            input:
                argumentParser(dict: ApprovedSequence, 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 appropriate keyword!!
            If you don't know what kind of keywords are needed for class,
                just type className.errorAttributeNames().
                For example, point.errorAttributeNames()
    
        """
        if isinstance(ApprovedSequence, dict):
    
            di = dict.items # dictionary.values(someDict)
            dk = dict.keys  # dictionary.keys(someDict)
    
            # Managing the kwargs and approved sequence data
            toLowerStr = lambda el: str(el).lower() # Conversion to lower string
            asingKey = lambda el: [ key for key in dk(ApprovedSequence) if toLowerStr(el) in ApprovedSequence[key] ][0] # Assigning key
    
            return { asingKey(k):v for k,v in di(kwargs) } # Dictionary comprehension
        else:
            raise TypeError("argumentPhraser function accepts only a dictionary for a ApprovedSequence, aka first item")
            return None
    

    Implementation

    def somefunction(**kwargs):
        aliases = {
            "val1": generateAliases("first", "1"),
            "val2": generateAliases("second", "2")
        }
        approved = argumentParser(aliases, **kwargs)
    
        if "val1" in approved.keys(): val1 = approved["val1"]
        else: val1 = 0 # Setting default value for val1
    
        if "val2" in approved.keys(): val2 = approved["val2"]
        else: val2 = 1 # Setting 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 inputed variables
        form = ["passed "]
        form += [ "{} as {} ".format(key, value) for key, value in x.items() ]
        # Implementing somefunction
        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, attribute 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 on what kind of data your function is using.

    This code has been tested on Python 2.7 and Python 3.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 a `__dict__` method or the class needs to be instanced from an 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)