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
?
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)