Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Elegant pattern for mutually exclusive keyword args?

Sometimes in my code I have a function which can take an argument in one of two ways. Something like:

def func(objname=None, objtype=None):
    if objname is not None and objtype is not None:
        raise ValueError("only 1 of the ways at a time")
    if objname is not None:
        obj = getObjByName(objname)
    elif objtype is not None:
        obj = getObjByType(objtype)
    else:
        raise ValueError("not given any of the ways")

    doStuffWithObj(obj)

Is there any more elegant way to do this? What if the arg could come in one of three ways? If the types are distinct I could do:

def func(objnameOrType):
    if type(objnameOrType) is str:
        getObjByName(objnameOrType)
    elif type(objnameOrType) is type:
        getObjByType(objnameOrType)
    else:
        raise ValueError("unk arg type: %s" % type(objnameOrType))

But what if they are not? This alternative seems silly:

def func(objnameOrType, isName=True):
    if isName:
        getObjByName(objnameOrType)
    else:
        getObjByType(objnameOrType)

cause then you have to call it like func(mytype, isName=False) which is weird.

like image 851
Claudiu Avatar asked Mar 03 '11 17:03

Claudiu


4 Answers

How about using something like a command dispatch pattern:

def funct(objnameOrType):
   dispatcher = {str: getObjByName,
                 type1: getObjByType1,
                 type2: getObjByType2}
   t = type(objnameOrType)
   obj = dispatcher[t](objnameOrType)
   doStuffWithObj(obj)

where type1,type2, etc are actual python types (e.g. int, float, etc).

like image 168
JoshAdel Avatar answered Nov 03 '22 00:11

JoshAdel


Sounds like it should go to https://codereview.stackexchange.com/

Anyway, keeping the same interface, I may try

arg_parsers = {
  'objname': getObjByName,
  'objtype': getObjByType,
  ...
}
def func(**kwargs):
  assert len(kwargs) == 1 # replace this with your favorite exception
  (argtypename, argval) = next(kwargs.items())
  obj = arg_parsers[argtypename](argval) 
  doStuffWithObj(obj)

or simply create 2 functions?

def funcByName(name): ...
def funcByType(type_): ...
like image 34
kennytm Avatar answered Nov 02 '22 22:11

kennytm


One way to make it slightly shorter is

def func(objname=None, objtype=None):
    if [objname, objtype].count(None) != 1:
        raise TypeError("Exactly 1 of the ways must be used.")
    if objname is not None:
        obj = getObjByName(objname)
    else: 
        obj = getObjByType(objtype)

I have not yet decided if I would call this "elegant".

Note that you should raise a TypeError if the wrong number of arguments was given, not a ValueError.

like image 24
Sven Marnach Avatar answered Nov 02 '22 23:11

Sven Marnach


For whatever it's worth, similar kinds of things happen in the Standard Libraries; see, for example, the beginning of GzipFile in gzip.py (shown here with docstrings removed):

class GzipFile:
    myfileobj = None
    max_read_chunk = 10 * 1024 * 1024   # 10Mb
    def __init__(self, filename=None, mode=None,
                 compresslevel=9, fileobj=None):
        if mode and 'b' not in mode:
            mode += 'b'
        if fileobj is None:
            fileobj = self.myfileobj = __builtin__.open(filename, mode or 'rb')
        if filename is None:
            if hasattr(fileobj, 'name'): filename = fileobj.name
            else: filename = ''
        if mode is None:
            if hasattr(fileobj, 'mode'): mode = fileobj.mode
            else: mode = 'rb'

Of course this accepts both filename and fileobj keywords and defines a particular behavior in the case that it receives both; but the general approach seems pretty much identical.

like image 43
senderle Avatar answered Nov 02 '22 22:11

senderle