Search code examples
pythonclassderived-class

Aligning Python Class & Super-Class


Consider the following Python code snippet where we define a Portfolio, Company and Deposit class. A Portfolio object simply acts as a union of companies & deposits. And we can run metrics on the portfolio like Profit. Questions:

  1. For every new metric I include in the Company or Deposit class I need to manually add a corresponding function in the Portfolio class; despite the fact that their behaviour is always the same: sum across all investments. Is there a way to improve this logic/construction of classes? What if we need to add 100 other metrics...

  2. The Deposit class only has a Profit function, but not Loss (interest in a bank account is assumed to be guaranteed). Is there a way to treat "undefined" metrics as always returning 0? Or is there a cleaner/more correct to define these metrics? What if we need to cover 100 different investment types that may or may not have different metrics...


class Company():
    def __init__(self, ItemsSold, ItemPrice, Expenses, Fines):
        self.ItemsSold = ItemsSold
        self.ItemPrice = ItemPrice
        self.Expenses  = Expenses
        self.Fines     = Fines

    def Profit(self):
        return self.ItemsSold * self.ItemPrice

    def Loss(self):
        return self.Expenses + self.Fines

    def ProfitAndLoss(self):
        return self.Profit() - self.Loss()

class Portfolio():
    def __init__(self, Investments):
        self.Investments = Investments

    def Profit(self):
        return sum([inv.Profit() for inv in self.Investments])

    def Loss(self):
        return sum([inv.Loss() for inv in self.Investments])

    def ProfitAndLoss(self):
        return sum([inv.ProfitAndLoss() for inv in self.Investments])

class Deposit():
    def __init__(self, Notional, InterestRate, TimeHorizon):
        self.Notional     = Notional
        self.InterestRate = InterestRate
        self.TimeHorizon  = TimeHorizon

    def Profit(self):
        return self.Notional * self.InterestRate * self.TimeHorizon        

myComp1 = Company(100,2,50,20)
myComp2 = Company(200,2,100,80)
myDepos = Deposit(100,0.02,3)
myPortf = Portfolio([myComp1,myComp2,myDepos])

print(myPortf.Profit())          # Works fine
print(myPortf.ProfitAndLoss())   # Throws an error

Solution

  • The second question is easy: all you have to do is to create a Base class where each metrics is defined as a method returning 0. Then derive all your Invest classes (Company, Deposit, etc) from the Base class, so as all undefined metrics will call the corresponding method in the Base class.

    The first question is a bit tougher as it requires some meta-programming. Your Portfolio class can also be derived from the Base class, then it looks in the method dictionary of the Base class (Base.__dict__) to retrieve all metrics names. Afterwards, for all these metrics, it creates a specific lambda method that calls this metrics for each item in your Investments list and sums up the results. Here is a skeleton code for this:

    class Base(object):
        def f1(self):
            return 0
        def f2(self):
            return 0
    
    class InvestA(Base):
        def f2(self):
            return 2
    
    class InvestB(Base):
        def f1(self):
            return 1
    
    class Portfolio(Base):
        def __init__(self, invest):
            self.invest = invest
            for name in [n for n in Base.__dict__ if n[:2] != '__']:
               self.__dict__[name] = lambda name=name: self.sum(name)
        def sum(self, name):
            return sum([i.__class__.__dict__[name](i) for i in self.invest
                       if name in i.__class__.__dict__])
    
    A = InvestA()
    print("A.f1 = %s, A.f2 = %s" % (A.f1(), A.f2()))
    B = InvestB()
    print("B.f1 = %s, B.f2 = %s" % (B.f1(), B.f2()))
    P = Portfolio([A,A,B])
    print('P.f1 = A.f1 + A.f1 + B.f1 =', P.f1())
    print('P.f2 = A.f2 + A.f2 + B.f2 =', P.f2())
    

    which produces the following output:

    A.f1 = 0, A.f2 = 2
    B.f1 = 1, B.f2 = 0
    P.f1 = A.f1 + A.f1 + B.f1 = 1
    P.f2 = A.f2 + A.f2 + B.f2 = 4
    

    As you can see, A.f1, B.f2, P.f1 and P.f2 are not explicitely defined as methods, but they can be called thanks to inheritance and meta-programming