0

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)
Harley
  • 1,305
  • 1
  • 13
  • 28
  • 2
    This inheritance structure doesn't make sense. How is a transaction supposed to be a kind of account? How is a "Combo" supposed to be a kind of transaction, loan, *and* account? – user2357112 May 17 '23 at 04:16
  • 2
    Why is a transaction a kind of account? Why is a loan a kind of account? Why isn't a loan a kind of transaction? What does "it fails here" mean? – kaya3 May 17 '23 at 04:16
  • They are loan and transaction accounts. – Harley May 17 '23 at 04:18
  • The combo account means the loan account has a card attached and is also the transaction account. – Harley May 17 '23 at 04:19
  • 2
    That still doesn't make sense. What would it even mean to call `withdraw` on a transaction? And withdrawing money from a loan? You could withdraw from a line of credit, but withdrawing from a *loan* is bizarre. – user2357112 May 17 '23 at 04:27
  • The Transaction object is not a transaction, it is a transaction account, an account you use for common transactions. A loan... often you have credit available because you've paid off more than you had to, so you can redraw to buy a new car or something. – Harley May 17 '23 at 04:37
  • What does "it fails here" mean? It means I can't figure out how to write the constructor for the Combo class. – Harley May 17 '23 at 05:15
  • I think [this](https://stackoverflow.com/questions/9575409/calling-parent-class-init-with-multiple-inheritance-whats-the-right-way) may help you here. – Matthew Towers May 17 '23 at 06:14

2 Answers2

0

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)

Harley
  • 1,305
  • 1
  • 13
  • 28
0

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.

jsbueno
  • 99,910
  • 10
  • 151
  • 209