OOP: How to write pythonic interaction between classes? - python

I would like to model the interaction between two classes where one of the classes takes the other one as an argument in one of its methods. Which class should be in the argument of the method of the other class?
I have written two alternative solutions to the problem but I am not sure which one of them is considered to be the correct way of handling this issue. Maybe there is even a better way but here are my two alternatives:
class BankAccount:
def __init__(self, balance):
self._balance = balance
def transaction(self, cash):
self._balance += cash._value
cash._value = 0
class Cash:
def __init__(self, value):
self._value = value
def transfer(self, bank_account):
bank_account._balance += self._value
self._value = 0
if __name__ == "__main__":
# First alternative
acc = BankAccount(balance=100)
cash = Cash(value=10)
print('-' * 30)
print('First alternative')
print(f'Account balance before: {acc._balance}')
print(f'Cash value before: {cash._value}')
acc.transaction(cash=cash)
print(f'Account balance after: {acc._balance}')
print(f'Cash value after: {cash._value}')
# Second alternative
acc = BankAccount(balance=100)
cash = Cash(value=10)
print('-' * 30)
print('Second alternative')
print(f'Account balance before: {acc._balance}')
print(f'Cash value before: {cash._value}')
cash.transfer(bank_account=acc)
print(f'Account balance after: {acc._balance}')
print(f'Cash value after: {cash._value}')
As you can see, both alternatives show the same results but I'd be happy to get a recommendation for the pythonic way of modelling this kind of class interaction. Thanks.

The example is ill-formed which prevents us from concentrating on actual OOP. It does not make sense for some cash to lose its value because it is tranfered. Does a $20 bill lose its value because you deposit it at the bank?
A new example
Instead let's consider the problem of representing money transfer between two bank accounts.
A key concept of OOP is that attributes of an instance should not be directly updated. Instead, the instance should provide an API via methods which provide some control over its state.
We can accomplish that by defining the methods deposit and withdraw for a BankAccount. This way, we can then define the method transfer_to which only makes use of this API.
class BankAccount:
def __init__(self):
self.balance = 0
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount >= self.balance:
self.balance -= amount
else:
raise ValueError('Insufficient funds')
def transfer_to(self, target, amount):
if isinstance(target, BankAccount):
self.withdraw(amount)
target.deposit(amount)
else:
raise ValueError("'target' must be another BankAccount")
Advantage
By encapsulating the logic of withdraw, deposit and tranfer_to, we allow for simple implementation of more complex types of accounts through inheritance.
Here let's give the example of a new type of BankAccount which allows for negative balance.
class CreditAccount(BankAccount):
def withdraw(self, amount):
self._balance -= amount
By fully delegating the responsability of withdrawing to instances of BankAccount, we allow external agents to manipulate an account without having to be aware of the inner logic of withdrawing and depositing.

Related

How to sum a list of sets? with strings and integers in a class?

I'm new to programming with python and I'm working on the python 4 everybody course. For that I need to build a budget app. In specific, this is the first two tasks:
Complete the Category class in budget.py. It should be able to instantiate objects based on different budget categories like food, clothing, and entertainment. When objects are created, they are passed in the name of the category. The class should have an instance variable called ledger that is a list. The class should also contain the following methods:
A deposit method that accepts an amount and description. If no description is given, it should default to an empty string. The method should append an object to the ledger list in the form of {"amount": amount, "description": description}.
A withdraw method that is similar to the deposit method, but the amount passed in should be stored in the ledger as a negative number. If there are not enough funds, nothing should be added to the ledger. This method should return True if the withdrawal took place, and False otherwise.
My attempt for now is the following:
class Category:
def __init__(self):
self.ledger = []
Total = 0
def deposit(self, amount, *description):
self.ledger.append({"amount": amount, "description": description})
return self.ledger
def withdraw(self, withdrawal):
for x in self.ledger:
if x == int():
return sum
self.ledger.append({"withdrawal": withdrawal})
return self.ledger
I think I have multiple questions:
What is a list with {} as one Item? Is it a 5.4. "Set"?
How can I now implement the requirement of the withdraw method "If there are not enough funds, nothing should be added to the ledger." I think I need to sum up all the integers of the list self.ledger, but idk how to grab those from it and sum it up. I tried as you can see a for loop, but I think that is incorrect syntax?
I really appreciate every help and am grateful for also some background knowledge!
Thanks in advance!
Lukas
{} is en empty dict. An empty set is set()
This should do for the withdraw function :
def withdraw(self, amount, description):
balance = sum(transaction["amount"] for transaction in self.ledger)
if balance >= withdrawal :
self.ledger.append({"amount": -amount, "description": description})
return True
else :
return False
return self.ledger
This use a generator as an argument for the builtin sum function. This may be a bit advanced if you are just starting python so you can also compute the balance with more beginner friendly code :
balance = 0
for transaction in ledger:
balance = balance + transaction["amount"]
# you can shorten the line above to
# balance += transaction["amount"]
This is roughly equivalent to what the sum and the generator syntax do.
Out of curiosity, is the * in front of description argument in your deposit function a typo ?
So, some things for clarification.
self.ledger = []: The [] makes the ledger a list, and the self. part makes it an instance variable, only applicable to a specific instance of the class Category
As stated in the comments, {"key": "value"} is a dict object. Using self.ledger.append({"key": "value"}) adds the dict object {"key": "value"} to the ledger list.
Normally if you want to keep track of a number inside a class, you create an instance variable and update it when it gets changed. See the usage of self.total below.
Recalculating the total can also be done, see the method update_total() below.
I added some testing below.
class Category:
def __init__(self):
self.ledger = []
# total also needs the "self." to make it an instance variable, just
# as the ledger above
# if you omit the "self."" it's a localized variable only available
# to the __init__ method itself.
self.total = 0
def deposit(self, amount, *description):
self.ledger.append({"amount": amount, "description": description})
# add the amount to the total
self.total += amount
return self.ledger
def withdraw(self, withdrawal):
# make the withdrawal negative
if abs(withdrawal) == withdrawal:
withdrawal = 0 - withdrawal
# check if there's enough in the total
if abs(withdrawal) <= self.total:
# update the total
self.total += withdrawal
# add to the ledger
self.ledger.append({"withdrawal": withdrawal})
else:
# you could return an error message here
pass
return self.ledger
def update_total(self):
total = 0
for item in self.ledger:
# need to check if the amount key is in the dict object
if "amount" in item:
total += item["amount"]
# this check below is not needed but it ensures future compatability
elif "withdrawal" in item:
total += item["withdrawal"]
# just because, you can check if the totals match
if self.total != total:
print(
f"""Difference in totals found. Someone is cheating :-|
Instance total: {self.total}
Calculated total: {total}"""
)
# set the instance variable to the local variable
self.total = total
return self.total
from pprint import pprint
nr1 = Category()
nr2 = Category()
for i in range(10):
nr1.deposit(i, f"deposit - {i}")
pprint(nr1.ledger)
print(f"Total: {nr1.total}")
nr1.withdraw(10)
print(f"Total: {nr1.total}")
nr1.withdraw(-10)
print(f"Total: {nr1.total}")
nr1.withdraw(99999)
print(f"Total: {nr1.total}")
pprint(nr1.ledger)
print(nr1.update_total())
nr1.total = 123
print(nr1.update_total())
# just to show that the above only updated the values inside the nr1 instance.
print(f"Total for nr2: {nr2.total}")

How to calculate variable declared in a method from inside python class function

need help about my code (python):
class Account:
def __init__(self, money):
self.money= money
def __str__(self):
return f'Money in the bank: {self.money} dollar'
def withdraw(self,withdraw):
self.withdraw = withdraw
money = self.money-self.withdraw
return money
print(Account.withdraw(20000,1000))
What I want from code above is the code will print my remaining money (19000), but I always got error
'int' object has no attribute 'withdraw'.
I have tried a lot of things for 4 hours but got no satifying result.
This is my first question in this forum, i am sorry if the formatting is not right.
Thank you in advance :)
Below I have made some small changes in your code and it is working as you expect.
Defined new variable withdrawMoney to track the withdraw amount.
Some changes in string format.
Returned the updated amount every time.
class Account:
def __init__(self, money):
self.money= money
self.withdrawMoney = 0
def __str__(self):
return "Money in the bank: {} dollar".format(self.money)
def withdraw(self,withdrawMoney):
self.withdrawMoney = withdrawMoney
self.money = self.money-self.withdrawMoney
return self.money
acc = Account(20000)
print(acc.withdraw(1000)) # 19000
print(acc.withdraw(1000)) # 18000

Python Inheritance - What am I doing wrong?

I am trying to run a example of inheritance and don't know why my output is not correct. Below is the code.
class BankAccount():
def __init__(self):
self.balance=0
def deposit(self,amount):
self.balance += amount
print(self.balance)
def withdraw(self,amount):
self.balance -= amount
print(self.balance)
class MinimumBalance(BankAccount):
def __init__(self,min_bal):
BankAccount.__init__(self)
self.min_balance=min_bal
def withdraw(self,amount):
if self.balance - amount < self.min_balance:
print("Balance is LOW")
else:
BankAccount.withdraw(self,amount)
a=BankAccount()
b=MinimumBalance(50)
a.deposit(100)
b.withdraw(40)
The output is:
100
Balance is LOW
Your MinimumBalance(50) instance has a balance of 0. The a object is independent, it is a separate instance, it doesn't matter what balance that entry has. The MinimumBalance() class has all the functionality of Balance, with some tweaks, so you just interact with the b instance entirely.
Deposit a balance on b instead:
b = MinimumBalance(50)
b.deposit(100)
b.withdraw(40)
Your inheritance construction is right, however you have made two objects. MinimumBalance is now also a BankAccount but with a different withdraw function. You should call it BankAccountWithMinimumBalance instead which is what it is. Then instead of making two objects, you just make a BankAccountWithMinimumBalance and use that.

TypeError: unorderable types: atm() >= int()

I have 3 classes, ATM (main class), atmFees (subclass of ATM) and transaction. I want to have my class atmFees inherit methods from the parent class ATM.
The atmFees class takes the atm object as a parameter, initializing with atm.__init__(self, balance)
I want to override the parent/super class's "withdrawal" method, modifying one of the parameters -- subtracting 50 cents from amount -- and then accessing the super method in atm with the new amount.
Doing so returns a TypeError: unorderable types: atm() >= int()
I have absolutely no idea what to do from here, I've changed almost everything but I can't seem to get it to work.
import transaction
import random
class atm(object):
def __init__(self, bal):
self.__balance = bal
self.transactionList = []
def deposit(self, name, amount):
self.__balance += amount
ndt = transaction.Transaction(name, amount)
self.transactionList.append(ndt)
def withdraw(self, name, amount):
if self.__balance >= amount:
self.__balance -= amount
nwt = transaction.Transaction(name, amount)
self.transactionList.append(nwt)
else:
print('Uh oh, not enough money!')
def get_balance(self):
return self.__balance
def __str__(self):
string_return = ""
for transaction in self.transactionList:
string_return += str(transaction) + "\n"
string_return = '\n' + 'The balance is $' + format(self.__balance, ',.2f')
return string_return
class atmFee(atm):
def __init__(self, balance):
atm.__init__(self, balance)
def widthrawal(cls, name, amount):
amount = amount - .50
atm.widthrawal(cls, name, amount)
def deposit():
pass
def main():
myATM = atm.atm(75)
fees = atm.atmFee(myATM)
fees.withdraw("2250",30)
fees.withdraw("1000",20)
myATM.deposit("3035",10)
print("Let's withdraw $40")
if myATM.withdraw("Amazon Prime",40) == 0:
print ("Oh noes! No more money!")
print()
print("Audit Trail:")
print(myATM)
main();
The full code is posted here:
https://gist.github.com/markbratanov/e2bd662d7ff83ca5ef61
Any guidance / help would be appreciated.
The error message means just what it says - you can't order an object and an integer. This is possible (for some reason) in Python 2, where the ordering is essentially arbitrary (for example, an empty dict {} is always greater than an integer, no matter how large...), but it is not in Python 3, because the comparison is meaningless.
You create your ATM object like this:
myATM = atm.atm(75)
fees = atm.atmFee(myATM)
So myATM, itself an ATM object, gets passed in to atmFee.__init__ as the balance. In withdraw, you expect the balance to be a number and not an ATM object (if the comparison worked, the arithmetic you do on it would then fail). You almost certainly meant to set the balance to a number by creating the object like this:
fees = atm.atmFee(75)
Note that atmFee takes exactly the same constructor signature as the superclass (this isn't a rule, but it is how you've set it up here), so you should use it in the same way.
You are also switching between using fees and myATM in the rest of your code, which seems odd. It looks like you mean to be using fees in all cases, and don't actually need myATM at all.

Errors in minor details in a class output

I have two files :
class Account:
def __init__(self,id=0,balance=100.0,AIR=0.0):
self.__id = id
self.__balance = balance
self.__AIR = AIR
def getd(self):
return self.__id
def getbalance(self):
return self.__balance
def getAnnualInterest(self):
return self.__AIR
def setid(self,newid):
self.__id = newid
def setbalance(self,newbalance):
self.__balance = newbalance
def setAnnualInterestRate(self,newrate):
self.__AIR = newrate
def getMonthlyInterestRate(self):
return self.__AIR/12
def getMonthlyInterest(self):
return self.__balance*self.getMonthlyInterestRate()
def withdraw(self,amount):
if amount<=self.__balance:
self.__balance -= amount
def deposit(self,amount):
self.__balance += amount
def __str__(self):
return "Account ID : {0.setid} Account Balance : {0.setbalance} Annual Interest Rate : {0.setAnnualInterestRate}".format(self)
and Test:
from Account import Account
def main():
accountA = Account(0,100,0)
accountA.setid = 1234
accountA.setbalance = 20500
accountA.setAnnualInterestRate = 0.375
print(accountA)
accountA.withdraw(500)
accountA.deposit(1500)
print(accountA)
print(accountA.getMonthlyInterest())
main()
My output is mostly correct but there are two minor deatils which I have gotten wrong and I am not sure where in the code the problem is from.
Account ID : 1234 Account Balance : 20500 Annual Interest Rate : 0.375
Account ID : 1234 Account Balance : 20500(This is supposed to be 21500) Annual Interest Rate : 0.375
0.0(And this is supposed to be 671.875 but somehow I got it wrong)
accountA.setbalance = 20500 doesn't call the setbalance method. It changes the value of the setbalance attribute to 20500 (that is, after this line, accountA.setbalance is no longer a method but an int). Instead, you want accountA.setbalance(20500).
However, what you're doing is profoundly un-pythonic in the first place (you're a Java/C#/C++ programmer, aren't you?). Getters and setters are an anti-pattern in Python: just access and change the id, balance et al. attributes, and make them properties if (and only if) you need to perform computations/checks when setting/accessing them.
In addition, __attribute is not a private attribute in Python. The pythonic way to mark an attribute as "private" is a single leading underscore. However, it's just a convention, and the attribute itself will still be public (everything always is in Python -- it has no concept of visibility modifiers).
This:
accountA.setid = 1234
accountA.setbalance = 20500
accountA.setAnnualInterestRate = 0.375
doesn't call the functions. You actually change functions into variables this way. To call the functions use this notation:
accountA.setid(1234)
accountA.setbalance(20500)
accountA.setAnnualInterestRate(0.375)

Categories