Reputation: 5686
I'm sure this will be a duplicate question, but I can't seem to find the words to locate one.
I have a set of very similar models I'd like to code up. The models are all the same, apart from a single function / line of code. I'd like to avoid any code repetition. Let' see an MWE:
import numpy as np
class SinModel:
def __init__(self):
self.x = np.linspace(-np.pi, np.pi)
def run(self):
# Computations which are invariant of the function we use later
self.y = np.sin(self.x)
# More computations which are invariant of which funcion was used
Our second model will involve the same series of computations, but will use a different function mid way though (here, cosine instead of sine):
class CosModel:
def __init__(self):
self.x = np.linspace(-np.pi, np.pi)
def run(self):
# Computations which are the same as in SinModel
self.y = np.cos(self.x)
# More computations which are the same as in SinModel
Here I have lots of code repetition. Is there a better way to implement these models? I was hoping it would be possible to create a class Model
which could inherit the differing function from an arbitrary class.
An important note is that the function which changes between models may take different arguments from self
depending on the model.
Upvotes: 1
Views: 140
Reputation: 77902
The words you're looking for are inheritance (allowing a class to inherit and extends / specialize a parent class) and the "template method" design pattern (which is possibly the most common design pattern - the one everyone discovers by itself long before reading about design patterns).
Expanding on your MWE:
import numpy as np
class ModelBase(object):
def __init__(self):
self.x = np.linspace(-np.pi, np.pi)
def run(self):
# Computations which are invariant of the function we use later
self.y = self.compute_y()
# More computations which are invariant of which funcion was used
def compute_y(self):
raise NotImplementedError("class {} must implement compute_y()".format(type(self).__name__))
class SinModel(ModelBase):
def compute_y(self):
return np.sin(self.x)
class CosModel(ModelBase):
def compute_y(self):
return np.cos(self.x)
This being said, creating instance attributes outside the initializer (the __init__
method) is considered bad practice - an object should be fully initialized (have all it's attributes defined) when the initializer returns, so it might be better to move the self.y = self.compute_y()
line to the initializer if possible, or, if self.y
always only depends on self.x
, make it a computed attribute:
class ModelBase(object):
def __init__(self):
self.x = np.linspace(-np.pi, np.pi)
@property
def y(self):
return self._compute_y()
def _compute_y(self):
raise NotImplementedError("class {} must implement _compute_y()".format(type(self).__name__))
def run(self):
# Computations which are invariant of the function we use later
# no need to explicitely set self.y here, just use `self.y`
# and it will delegate to self._compute_y()
#(you can't set it anymore anyway since we made it a readonly propery)
# More computations which are invariant of which funcion was used
class SinModel(ModelBase):
def _compute_y(self):
return np.sin(self.x)
class CosModel(ModelBase):
def _compute_y(self):
return np.cos(self.x)
Also at this point you don't necessarily need subclasses anymore, at least if that's the only thing that changes - you can just pass the proper function as a callback to your model class ie:
class Model(object):
def __init__(self, compute_y):
self.x = np.linspace(-np.pi, np.pi)
self._compute_y = compute_y
@property
def y(self):
return self._compute_y(self)
def run(self):
# code here
cos_model = Model(lambda obj: np.cos(obj.x))
cos_model.run()
sin_model = Model(lambda obj: np.sin(obj.x))
sin_model.run()
Upvotes: 2
Reputation: 24691
Yes, and there's even a name for it: Inheritance is the idea that child classes can "inherit" behaviors and attributes from parent classes, and Polymorphism is the idea that two child classes, sharing similar behavior, can have different implementations of the same method - so that you can call a method on an object without knowing explicitly what type it is, and still have it do the right thing.
Here's how you'd do that in python:
class TrigModel:
def __init__(self):
self.x = np.linspace(-np.pi, np.pi)
def run(self):
raise NotImplementedError("Use subclasses SinModel or CosModel")
class SinModel(TrigModel):
@override
def run(self):
self.y = np.sin(self.x)
class CosModel(TrigModel):
@override
def run(self):
self.y = np.cos(self.x)
Unless you explicitly specify otherwise (by declaring a method like run()
that overrides the parent class's method of the same name), SinModel
and CosModel
will call TrigModel
's methods on themselves (in this case, they both call TrigModel
's constructor, but then display different behavior when you call run()
on them).
If you then do:
model.run()
then model
will behave differently depending on whether it's a SinModel
or a CosModel
, depending on what you set it to beforehand.
The @override
decorator isn't strictly necessary, but it's good practice to lessen ambiguity.
Upvotes: 0