Sam C
Sam C

Reputation: 73

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

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.

Upvotes: 6

Views: 154

Answers (1)

Abhijit
Abhijit

Reputation: 63777

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.

Upvotes: 5

Related Questions