I tried to collect some of my formulas inside classes so that I didn't need to browser the names of the functions anymore but only determine which Quantity I want to calculate and then check which methods are avaiable for determining it. This is based on the astropy.units
module. Just to make a small example:
import astropy.units as u
class Length(object):
unit = u.m
@classmethod
#@quantitydecorator # I'll explain this later
def from_velocity_time(cls, velocity, time):
"""
Calculate length from a given velocity and time.
Parameters
----------
velocity: `astropy.units.Quantity`
The velocity of the object.
time: `astropy.units.Quantity`
The time the object moved at this velocity
Returns
-------
length: `astropy.units.Quantity`
The determined length based on velocity * time
"""
return velocity * time
This works great, I tried:
Length.from_velocity_time(100*u.km/u.h, 100*u.h)
# gives 10000 km
but has some downsides when the units are equivalent but different:
Length.from_velocity_time(100*u.km/u.s, 100*u.h)
# 10000 h*km/s
so I decided to create a general decorator that converts the results unit to the unit defined in the class or have an additional parameter result_unit
for every method call:
def quantitydecorator(f):
"""
Allows for an extra argument "result_unit" for all methods
and converts the result to the unit specified there or if not
given to the unit specified in the class.
"""
@wraps(f)
def wrapper(*args, **kwds):
# Check if result_unit is given otherwise use the class default
# (args[0] is the class)
if 'result_unit' in kwds:
result_unit = u.Unit(kwds.pop('result_unit'))
else:
result_unit = args[0].unit
# Calculate the result
result = f(*args, **kwds)
# Convert to result unit if the result has a unit
if not hasattr(result, 'unit'):
# No unit attribute so we have a plain numpy array
# or number, only let it pass when target unit is
# dimensionless
if args[0].unit != u.dimensionless_unscaled:
raise ValueError('Got dimensionless quantity but needed'
' quantity with unit: {0}'.format(args[0].unit))
# Result has a different unit than wanted, convert it to wanted unit
elif result.unit != result_unit:
result = result.to(result_unit)
return result
return wrapper
This works fine (this time the decorator must be uncommented in the class):
Length.from_velocity_time(100*u.km/u.s, 100*u.h)
# 3.6e10m
Length.from_velocity_time(100*u.km/u.s, 100*u.h, result_unit=u.AU)
# 0.24064514AU
but I can't help suspecting that this is rather awkward. I'm using approximately 300 functions but there are only around 30-40 parameters (like Length in the example but more sophisticated like "free-fall-time", "half-light-radius". "redshift", etc. everything that I at some point needed for any calculation and there are typically 2-10 ways to calculate such a Quantity) so it would be a great gain to collect them into classes and don't have duplicate logic with the result_unit
. Would you consider this "good coding" style or should I dismiss this attempt and choose a different approach (if so, any idea which)?