Handling aliases for kwargs keywords

95 views Asked by At

I'm trying create the simplest code to handle aliases for kwargs keywords. For example, in matplotlib, color='red', can also be tagged as c='red'. The following code works, but is there a better way?

I was considering developing a class/structure that knows the keyword, the alias, the default value, and the variable they are linked to. A list or dictionary of these would be declared in the function and a single call would parse the kwargs.

class Cylinder():
    def __init__(self,d:float,l:float):
        """ 
        d = diameter
        l = length
    
        """
        self.d:float = d
        self.l:float = l

        #Default values
        self.color:str = 'black'
        self.transparency:float = 0.0
        return

    def Plot(self, **kwargs):
        """
        Optional parameters:
            color or c          default 'black'
            transparency or tr  default  0.0
        """
        if 'color' in kwargs: 
            self.color = kwargs['color']
        elif 'c' in kwargs:
            self.color = kwargs['c']
            
        if 'transparency' in kwargs:
            self.transparency = kwargs['transparency']
        elif 'tr' in kwargs:
            self.transparency = kwargs['tr']

        print("Plotting....color = ", self.color, ' transparency = ', self.transparency)
        return
4

There are 4 answers

0
enzo On

You can create an utility function to handle that:

def parse_kwarg(kwargs, aliases, default_value):
    for alias in aliases:
        try:
            return kwargs[alias]
        except KeyError:
            pass

    return default_value

class Cylinder:
    # ...

    def plot(self, **kwargs):
        color = parse_kwarg(kwargs, ('color', 'c'), self.color) 
        transparency = parse_kwarg(kwargs, ('transparency', 'tr'), self.transparency)

        print("Plotting....color = ", color, ' transparency = ', transparency)
1
ShadowRanger On

If you're not doing it too often, you can just accept both with defaults and choose the one that isn't the default, e.g.:

# *, makes following arguments keyword only, just like accepting them only via **kwargs
# so they can't be passed positionally by accident
def Plot(self, *, color=None, c=None, transparency=None, tr=None):
    """
    Optional parameters:
        color or c          default 'black'
        transparency or tr  default  0.0
    """
    if color is not None or c is not None:
        self.color = color if c is None else c
    if transparency is not None or tr is not None:
        self.transparency = transparency if tr is None else tr
    print("Plotting....color = ", self.color, ' transparency = ', self.transparency)

It's not amazing (and you'd need to add tests for someone passing both aliases if you want to forbid that), but it's somewhat cleaner.


Purely for funsies, a one-liner could be self.color = min((color, c, self.color), key=lambda x: x is None), which would select the first value that it finds to be non-None (on ties in min, it's guaranteed to return the first value). It's rather obscure though, thus "for funsies".

0
it's jayway On

My take on this is to employ as much OCP as possible.

class Property:
    def __init__(self, type, possible_kwargs, default_value=None):
        self.name = possible_kwargs[0] # you may want to make this an __init__ param...
        self.type = type
        self.value = default_value
        self.possible_kwargs = possible_kwargs


class Cylinder:
    def __init__(self, d: float, l: float):
        """
        d = diameter
        l = length
        """
        self.d: float = d
        self.l: float = l

        # add new properties here as you see fit
        self.properties = {
        # this structure can also be better planned out
            "color": Property(str, ["color", "c"], "black"),
            "transparency": Property(float, ["transparency", "tr"], 0.0),
        }
        return

    def Plot(self, **kwargs):
        """
        Optional parameters:
            color or c          default 'black'
            transparency or tr  default  0.0
        """
        affected_properties = []

        # probably make this into a function like enzo did
        for kwarg in kwargs:
            for key, property in self.properties.items():
                if kwarg in property.possible_kwargs:
                    if isinstance(kwargs[kwarg], property.type):
                        property.value = kwargs[kwarg]
                        affected_properties.append(property)
                    else:
                        raise TypeError(
                            f"Expected {property.type}, got {type(kwargs[kwarg])}"
                        )
        print(
            "Plotting: "
            + ", ".join(
                [
                    f"{property.name}: {property.value}"
                    for property in affected_properties
                ]
            )
        )
        return


if __name__ == "__main__":
    c = Cylinder(1.0, 2.0)
    c.Plot(color="red", transparency=0.5)

Output

Plotting: color: red, transparency: 0.5
0
BLehmann On

Several suggested some sort of parser or utility. Here's what I came up with. I tried to make it handle exceptions and options, such as no alias. I've also experimented with documenting and print functions (maybe useful or not).

The main goal was to make the code within the calling function concise and hide all the working details in the key word handler. Once the handler is setup, accessing a value is the same as accessing a dictionary.

To make the implementation into a module more compact, I may eliminate the dataclass KeyWord and, instead add a function "AddKeyWord(Name,Alias,Default,DocString)" to the handler class.

from dataclasses import dataclass
import typing

@dataclass
class KeyWord():
        Alias:typing.Union[str,None]
        Default:typing.Union[typing.Any]  # at some point, add a dtype parameter for type checking
        #dtype  
        DocString:str

class KeyWordHandler():
    """
    Handle key word aliases.
       
    Get the value of the key word with code:
        value = KeyWordHandler[Name]
    """
    def __init__(self,kwargs:dict[str,typing.Any], OptionalKeyWords:dict[KeyWord]):
        self.kwargs:dict[str,typing.Any] = kwargs   # passed in function call
        self.OptionalKeyWords:dict[KeyWord] = OptionalKeyWords   
        
        self.AllowableKeyWords = []
        for name in self.OptionalKeyWords:
            self.AllowableKeyWords.append(name)
            alias = self.OptionalKeyWords[name].Alias
            if alias is not None:
                self.AllowableKeyWords.append(alias)

        for k in self.kwargs:
            if k not in self.AllowableKeyWords:
                print("KeyWordHandler: Warning...parameter ",k," not recognized: will be ignored")
        return
        
    def __getitem__(self,KW:str): 
        """
        return value of keyword
        v = KeyWordHandler[KW]      (behaves like a dictionary, at least for access)
        """
        if KW in self.AllowableKeyWords:
            for name in self.OptionalKeyWords.keys():
                alias = self.OptionalKeyWords[name].Alias
                if KW == name or KW == alias:
                    if name in self.kwargs:
                        v = self.kwargs[name]                    
                    elif alias in self.kwargs:
                        v = self.kwargs[alias]
                    else:
                        v =  self.OptionalKeyWords[name].Default
            return v
        else:
            raise KeyError("KeyWordHandler: ", KW +" Not found")
            return

    def PrintValues(self):
        for kw,kwi in self.OptionalKeyWords.items():
            print(kwi.DocString + ' = ', self[kw])
        return

    def PrintDocStrings(self):
        for kw,kwi in self.OptionalKeyWords.items():
            print(kwi.DocString, "Name:",kw, "Alias:",kwi.Alias,"Default:",kwi.Default  )
        return

The code below shows how the keyword handler is setup and used.

class Cylinder():
    def __init__(self,d:float,l:float):
        """ 
        d = diameter
        l = length
        """
        self.d:float = d
        self.l:float = l
        return

    def Plot(self, **kwargs):
        """
        Optional parameters:
            - color or c          default 'black'
            - transparency or tr  default  0.0
            - unrecognized parameters in kwargs will be ignored
        """
        #Optional plotting parameters   
        kwh = KeyWordHandler(kwargs, \
              {'color':       KeyWord(Alias='c',  Default='black', DocString="Line color"),\
               'width':       KeyWord(Alias=None, Default=0.2    , DocString="Line width"),\
               'markers':     KeyWord(Alias='m',  Default=None   , DocString="Point markers"),\
               'blinking':    KeyWord(Alias='bl', Default=False  , DocString="Line blinking active" ),\
               'transparency':KeyWord(Alias='tr', Default=0.0    , DocString="Line transparency" )} )
        
        # access the values
        color        = kwh['color']
        transparency = kwh['transparency']
        width        = kwh['width']
        blinking     = kwh['blinking']
        markers      = kwh['markers']

        # test of unknown parameter
        try:    
            x = kwh['x']
        except KeyError as e:
            print(e)
            
        kwh.PrintDocStrings()
        print('\n')
        kwh.PrintValues()
        print("Plotting ...\n")
        return

The output, testing several options/exceptions, is:

cyl = Cylinder(2,3)
cyl.Plot()
cyl.Plot(c = 'red', tr=0.5, width=0.1, blinking=True, m=[1,3])
cyl.Plot(foo='bar', c = "purple", tr=100.0, w=0.2)
('KeyWordHandler: ', 'x Not found')
Line color Name: color Alias: c Default: black
Line width Name: width Alias: None Default: 0.2
Point markers Name: markers Alias: m Default: None
Line blinking active Name: blinking Alias: bl Default: False
Line transparency Name: transparency Alias: tr Default: 0.0


Line color =  black
Line width =  0.2
Point markers =  None
Line blinking active =  False
Line transparency =  0.0
Plotting ...

('KeyWordHandler: ', 'x Not found')
Line color Name: color Alias: c Default: black
Line width Name: width Alias: None Default: 0.2
Point markers Name: markers Alias: m Default: None
Line blinking active Name: blinking Alias: bl Default: False
Line transparency Name: transparency Alias: tr Default: 0.0


Line color =  red
Line width =  0.1
Point markers =  [1, 3]
Line blinking active =  True
Line transparency =  0.5
Plotting ...

KeyWordHandler: Warning...parameter  foo  not recognized: will be ignored
KeyWordHandler: Warning...parameter  w  not recognized: will be ignored
('KeyWordHandler: ', 'x Not found')
Line color Name: color Alias: c Default: black
Line width Name: width Alias: None Default: 0.2
Point markers Name: markers Alias: m Default: None
Line blinking active Name: blinking Alias: bl Default: False
Line transparency Name: transparency Alias: tr Default: 0.0


Line color =  purple
Line width =  0.2
Point markers =  None
Line blinking active =  False
Line transparency =  100.0
Plotting ...