Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pythonic way of writing a library function which accepts multiple types?

Tags:

python

If, as a simplified example, I am writing a library to help people model populations I might have a class such as:

class Population:
    def __init__(self, t0, initial, growth):
        self.t0 = t0,
        self.initial = initial
        self.growth = growth

where t0 is of type datetime. Now I want to provide a method to determine the population at a given time, whether that be a datetime or a float containing the number of seconds since t0. Further, it would be reasonable for the caller to provide an array of such times (if so, I think it reasonable to assume they will all be of the same type). There are at least two ways I can see to accomplish this:

  1. Method for each type

    def at_raw(self, t):
        if not isinstance(t, collections.Iterable):
            t = numpy.array([t])
        return self.initial*numpy.exp(self.growth*t)
    def at_datetime(self, t):
        if not isinstance(t, collections.Iterable):
            t = [t]
        dt = numpy.array([(t1-self.t0).total_seconds() for t1 in t])
        return self.at_raw(dt)
    
  2. Universal method

    def at(self, t):
        if isinstance(t, datetime):
            t = (t-self.t0).total_seconds()
        if isinstance(t, collections.Iterable):
            if isinstance(t[0], datetime):
                t = [(t1-self.t0).total_seconds() for t1 in t]
        else:
            t = np.array([t])
        return self.initial*numpy.exp(self.growth*t)
    

Either would work, but I'm not sure which is more pythonic. I've seen some suggestions that type checking indicates bad design which would suggest method 1 but as this is a library intended for others to use, method 2 would probably be more useful.

Note that it is necessary to support times given as floats, even if only the library itself uses this feature, for example I might implement a method which root finds for stationary points in a more complicated model where the float representation is clearly preferable. Thanks in advance for any suggestions or advice.

like image 448
Sam C Avatar asked Oct 14 '13 17:10

Sam C


People also ask

Are nested functions Pythonic?

Conclusion. So, in Python, nested functions have direct access to the variables and names that you define in the enclosing function. It provides a mechanism for encapsulating functions, creating helper solutions, and implementing closures and decorators.

Can Python function have multiple return types?

Python functions can return multiple values. To return multiple values, you can return either a dictionary, a Python tuple, or a list to your main program.

How do you type multiple types of hints?

If you need to allow multiple types, you can use typing. Union , e.g. Union[str, int] which tells the reader that a variable can be either a string or an integer. This might be useful whenever it is not possible to require a single type or you want to provide some convenience to the users of your function or class.

Which is used to maintain multiple values of different types in Python?

In Python, a list is a way to store multiple values together.


1 Answers

I believe you can simply stick with the Python's Duck Typing Philosophy here

def at(self, t):
    def get_arr(t):
        try: # Iterate over me
            return [get_arr(t1)[0] for t1 in t]
        except TypeError:
            #Opps am not Iterable
            pass
        try: # you can subtract datetime object
            return [(t-self.t0).total_seconds()]
        except TypeError:
            #Opps am not a datetime object
            pass
        # I am just a float
        return [t]
    self.initial*numpy.exp(self.growth*np.array(get_arr(t)))

Its important, how you order the cases

  1. Specific Cases should precede generic cases.

    def foo(num):
        """Convert a string implementation to
           Python Object"""
        try: #First check if its an Integer
            return int(num)
        except ValueError:
            #Well not an Integer
            pass
        try: #Check if its a float
            return float(num)
        except ValueError:
            pass
        #Invalid Number
        raise TypeError("Invalid Number Specified")
    
  2. Default Case should be the terminating case

  3. If successive cases, are mutually exclusive, order them by likeliness.
  4. Prepare for the Unexpected by raising Exception. After all Errors should never pass silently.
like image 137
Abhijit Avatar answered Oct 18 '22 23:10

Abhijit