Python Multiple Inheritance Diamond Problem

208 views Asked by At

I have a diamond inheritance scenario. The two middle classes inherit okay, but the Combo class, I can't quite figure out. I want the Combo class to inherit all attributes with the overridden methods coming from the Loan class.

I can't figure out how to write the constructor for the Combo class.

This is the database, the data is coming from:

ERD

Business Rules:

  • One customer can have zero or many accounts
  • One account belongs to one customer
  • One account is optionally a loan account
  • One loan account is one account
  • One account is optionally a transaction account
  • One transaction account is one account

An account can be both a loan account and a transaction account

The combo account means the loan account has a card attached and is also the transaction account.

And this is the class diagram:

UML Diagram

class Account:
    def __init__(self, account_id, customer_id, balance, interest_rate):
        self.account_id = account_id
        self.customer_id = customer_id
        self.balance = float(balance)
        self.interest_rate = float(interest_rate)

    def deposit(self):
        try:
            amount = float(input('Enter deposit amount: '))
            self.balance = self.balance + amount
            print('Account Number:', self.account_id)
            print('Deposit Amount:', amount)          
            print('Closing Balance:', self.balance)
            print('Deposit successful.\n')
            return True
        except ValueError as e:
            print('Error: please enter a valid amount.\n')
            return False

    def withdraw(self):
        try:
            amount = float(input('Enter withdrawal amount: '))
            if amount > self.balance:
                print('Account Number: ', self.account_id)
                print('Withdrawal Amount: ', amount)
                print('Account Balance: ', self.balance)
                print('Error: withdrawal amount greater than balance.\n')
                return False
            else:
                self.bal = self.balance - amount
                print('Account Number: ', self.account_id)
                print('Withdraw Amount: ', amount)
                print('Closing Balance: ', self.balance)
                print('Withdrawal Successful.\n')
                return True
        except ValueError as e:
            print('Error: please enter a valid amount.\n')
            return False

    def __str__(self):
        return (f'\nAccount Number: {self.account_id}\n'
    f'Balance: {self.balance:.2f}\n'
    f'Interest Rate: {self.interest_rate:.2f}\n')


class Transaction(Account):
    def __init__(self, account_id, customer_id, balance, interest_rate, card_no, cvn, pin):
        super().__init__(account_id, customer_id, balance, interest_rate)
        self.card_no = card_no
        self.cvn = cvn
        self.pin = pin

    
class Loan(Account):
    def __init__(self, account_id, customer_id, balance, interest_rate, duration, frequency, payment, limit):
        super().__init__(account_id, customer_id, balance, interest_rate)
        self.duration = int(duration)
        self.frequency = frequency
        self.payment = float(payment)
        self.limit = float(limit)

    # override inherited method
    def withdraw(self):
        try:
            amount = float(input('Enter withdrawal amount: '))
            if self.balance - amount < self.limit:
                print('Account Number: ', self.account_id)
                print('Withdrawal Amount: ', amount)
                print('Account Balance: ', self.balance)
                print('Error: withdrawal amount greater than limit.\n')
                return False
            else:
                self.balance = self.balance - amount
                print('Account Number: ', self.account_id)
                print('Withdraw Amount: ', amount)
                print('Closing Balance: ', self.balance)
                print('Withdrawal Successful.\n')
                return True
        except ValueError as e:
            print('Error: please enter a valid amount.\n')
            return False

    def __str__(self):
        return super().__str__() + (f'Duration: {self.account_id} years\n'
    f'Payment Frequency: {self.frequency}\n'
    f'Limit: {self.limit:.2f}\n')


class Combo(Loan, Transaction):
    def __init__(self, account_id, customer_id, balance, interest_rate, duration, frequency, payment, limit, card_no, cvn, pin):
        # what goes here?

Test Data...

from account import Account, Transaction, Loan, Combo

account_id = '1'
customer_id = '1'
balance = 0
interest_rate = 0.06
duration = 20
frequency = 'week'
payment = 500
limit = 500000
card_no = '5274728372688376'
cvn = '234'
pin = '9876'

loan = Loan(account_id, customer_id, balance, interest_rate, duration, frequency, payment, limit)
print(loan)

transaction = Transaction(account_id, customer_id, balance, interest_rate, card_no, cvn, pin)
print(transaction)

# whatever I do, it fails here
combo = Combo(account_id, customer_id, balance, interest_rate, duration, frequency, payment, limit, card_no, cvn, pin)
print(combo)
2

There are 2 answers

0
Harley On

I got it working not using super(). I'm not sure if this is ideal or not.

class Account:
    def __init__(self, account_id, customer_id, balance, interest_rate):
        self.account_id = account_id
        self.customer_id = customer_id
        self.balance = float(balance)
        self.interest_rate = float(interest_rate)

    def deposit(self):
        try:
            amount = float(input('Enter deposit amount: '))
            self.balance = self.balance + amount
            print('Account Number:', self.account_id)
            print('Deposit Amount:', amount)          
            print('Closing Balance:', self.balance)
            print('Deposit successful.\n')
            return True
        except ValueError as e:
            print('Error: please enter a valid amount.\n')
            return False

    def withdraw(self):
        try:
            amount = float(input('Enter withdrawal amount: '))
            if amount > self.balance:
                print('Account Number: ', self.account_id)
                print('Withdrawal Amount: ', amount)
                print('Account Balance: ', self.balance)
                print('Error: withdrawal amount greater than balance.\n')
                return False
            else:
                self.bal = self.balance - amount
                print('Account Number: ', self.account_id)
                print('Withdraw Amount: ', amount)
                print('Closing Balance: ', self.balance)
                print('Withdrawal Successful.\n')
                return True
        except ValueError as e:
            print('Error: please enter a valid amount.\n')
            return False

    def __str__(self):
        return (f'\nAccount Number: {self.account_id}\n'
    f'Balance: {self.balance:.2f}\n'
    f'Interest Rate: {self.interest_rate:.2f}\n')


class Transaction(Account):
    def __init__(self, account_id, customer_id, balance, interest_rate, card_no, cvn, pin):
        Account.__init__(self, account_id, customer_id, balance, interest_rate)
        self.card_no = card_no
        self.cvn = cvn
        self.pin = pin    

    
class Loan(Account):
    def __init__(self, account_id, customer_id, balance, interest_rate, duration, frequency, payment, limit):
        Account.__init__(self, account_id, customer_id, balance, interest_rate)
        self.duration = int(duration)
        self.frequency = frequency
        self.payment = float(payment)
        self.limit = float(limit)

    # override inherited method
    def withdraw(self):
        try:
            amount = float(input('Enter withdrawal amount: '))
            if self.balance - amount < self.limit:
                print('Account Number: ', self.account_id)
                print('Withdrawal Amount: ', amount)
                print('Account Balance: ', self.balance)
                print('Error: withdrawal amount greater than limit.\n')
                return False
            else:
                self.balance = self.balance - amount
                print('Account Number: ', self.account_id)
                print('Withdraw Amount: ', amount)
                print('Closing Balance: ', self.balance)
                print('Withdrawal Successful.\n')
                return True
        except ValueError as e:
            print('Error: please enter a valid amount.\n')
            return False

    def __str__(self):
        return Account.__str__(self) + (f'Duration: {self.account_id} years\n'
    f'Payment Frequency: {self.frequency}\n'
    f'Limit: {self.limit:.2f}\n')


class Combo(Loan, Transaction):
    def __init__(self, account_id, customer_id, balance, interest_rate, duration, frequency, payment, limit, card_no, cvn, pin):
        Loan.__init__(self, account_id, customer_id, balance, interest_rate, duration, frequency, payment, limit)
        Transaction.__init__(self, account_id, customer_id, balance, interest_rate, card_no, cvn, pin)

Test Data

from account import Account, Transaction, Loan, Combo

account_id = '1'
customer_id = '1'
balance = 0
interest_rate = 0.06
duration = 20
frequency = 'week'
payment = 500
limit = 500000
card_no = '5274728372688376'
cvn = '234'
pin = '9876'


account = Account(account_id, customer_id, balance, interest_rate)
print(account)

loan = Loan(account_id, customer_id, balance, interest_rate, duration, frequency, payment, limit)
print(loan)

transaction = Transaction(account_id, customer_id, balance, interest_rate, card_no, cvn, pin)
print(transaction)

combo = Combo(account_id, customer_id, balance, interest_rate, duration, frequency, payment, limit, card_no, cvn, pin)
print(combo)

0
jsbueno On

The key there is not "not using super" - is that, if you are using multiple inheritance, methods must be ready for parameters they know nothing about, and just forward those for the next super-class in the chain.

In Python that is done with the **: named argument packing mechanism. So, only changing your Loan and Transaction and Combo classes enough so that they work, you need this:

class Account:
    def __init__(self, account_id, customer_id, balance, interest_rate):
        self.account_id = account_id
        self.customer_id = customer_id
        self.balance = float(balance)
        self.interest_rate = float(interest_rate)

    ...
    

class Transaction(Account):
    def __init__(self, account_id, customer_id, balance, interest_rate, *, card_no, cvn, pin, **kwargs):
        super().__init__(account_id, customer_id, balance, interest_rate, **kwargs)
        self.card_no = card_no
        self.cvn = cvn
        self.pin = pin

    
class Loan(Account):
    def __init__(self, account_id, customer_id, balance, interest_rate, *, duration, frequency, payment, limit. **kwargs):
        super().__init__(account_id, customer_id, balance, interest_rate, **kwargs)
        self.duration = int(duration)
        self.frequency = frequency
        self.payment = float(payment)
        self.limit = float(limit)

    ...

class Combo(Loan, Transaction):
    def __init__(self, account_id, customer_id, balance, interest_rate, duration, frequency, payment, limit, card_no, cvn, pin):
        super().__init__(account_id, customer_id, balance, interest_rate, duration=duration. frequency=frequency, payment=payment, limit=limit. card_no=card_no, cvn=cvn, pin=pin)
        
        

By using the * in the parameter argument names for a method, you indicate that from that point on, only named arguments are accepted, so that there is no confusion about the order the arguments are passed. And any named arguments not recognized (or required, in this case) by the current method, are packed as a dictionary in the "kwargs" argument (the name "kwargs" is just a convention - sometimes "kw" is used - what makes up for the feature is the ** prefix to the argument).

Here, each method won't do anything with the arguments it does not know about, just use the converse part of the ** syntax on calling the next method in the inheritance chaing: the ** prefix to a dictionary unpacks all of the dicts items as pairs of "parameter_name=value" in the call.

Doing this even allow you to introduce more classes in the design without having to make any changes to the intermediary classes.