Reputation: 73
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:
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)
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
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
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")
Default Case should be the terminating case
Errors should never pass silently.
Upvotes: 5