Python Forum
ATM machine (deposits/withdrawals) using OOP
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
ATM machine (deposits/withdrawals) using OOP
#11
self is completely a convention, but conventions are important. When calling a method of an instance, Python passes the instance as the first argument. When calling a method of a class, Python passes the class as the first argument. It is convention to use "self" and "cls" for these first arguments. You could use "this" and "class_", but that would confuse others used to the conventions.

Your error is that self is an instance of Account, not an instance of TimeZone. self is an instance of Account because "deposit()" is an instance method of Account, and the first argument in deposit(self) is an instance of Account. Account does not have an attribute "transaction_time_id_format".

Question is, what should have a timezone? Right now I don't think you have any time zones. You have a class, but did you create any instances? And if you have an instance of Timezone, where is that kept? What is a time zone associated with? The Account? I don't know. I can do banking in different time zones. Is the purpose of TimeZone to convert from UTC to my local time, or is it to convert the time from where the transaction took place to UTC?
Reply
#12
Try these two functions I made to help you understand

def is_dunder(property):
    return True if property[:2] == '__' else False

def print_properties(obj):
    [print(prop) for prop in dir(obj) if not is_dunder(prop)]
Create your instances in the repl BA and time_instance in your case and pass them to print_properties() to see what sorts of properties each instance has. To explain the code, its simple is_dunder simply checks the first two characters of property to see if they are '__' and returns a boolean. print_properties(obj) takes a instance object and prints each of the properties if they are not dunders.

This may help you understand.
Apologetic Canadian, Sorry about that eh?
Reply
#13
Thank you knackwurstbagel for your help and guidance so far.

The latest iteration of my script can be found at the bottom of this post. When I import it, instantiate, and perform a few deposits in my REPL, here is the output:

$ bpython
bpython version 0.22.1 on top of Python 3.10.2 /usr/bin/python
>>> import script
>>> BA = script.Account('Winston', 'Smith')
The date and time: 2022-03-14 13:18:01 MDT-0600
The date and time condensed: 20220314131801
>>> print(BA.deposit(100))
D-1400-20220314131801-1
None
>>> print(BA.deposit(100))
D-1400-20220314131801-2
None
>>> print(BA.deposit(100))
D-1400-20220314131801-3
None
>>> print(BA.deposit(100))
D-1400-20220314131801-4
None
Next in my REPL I enter your recommended two functions:

>>> def is_dunder(property):
...     return True if property[:2] == '__' else False
...     
... 
>>> def print_properties(obj):
...     [print(prop) for prop in dir(obj) if not is_dunder(prop)]
...     
... 
>>> print_properties(BA)
account_num
balance
deposit
first_name
full_name
interest
last_name
pay_interest
transaction_id
tzone
withdraw
So far, so good. That makes sense. Your print_properties() function behaves as you describe. All the class properties that are not dunders are printed out in a list that you have achieved with list comprehension.

What comes next kind of throws me off:

>>> is_dunder(BA)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    is_dunder(BA)
  File "<input>", line 2, in is_dunder
    return True if property[:2] == '__' else False
TypeError: 'Account' object is not subscriptable
>>> is_dunder(last_name)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    is_dunder(last_name)
NameError: name 'last_name' is not defined
>>> is_dunder(self.last_name)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    is_dunder(self.last_name)
NameError: name 'self' is not defined
>>> 
When calling is_dunder() on various properties and objects, the Python REPL just says: “object not subscriptable” or “is not defined”. Is this expected?

By the way, if I am not mistaken, I believe property is a reserved keyword in Python so this partially threw me off in the beginning, but now that the print_properties() function works, I think I understand.

Here is my script:

from datetime import datetime
from pytz import timezone

class TimeZone:

    def __init__(self, locality='US/Mountain'):
        self.tz = datetime.now(timezone(locality))
        self.readable_format = '%Y-%m-%d %H:%M:%S %Z%z'
        print(f'The date and time: {self.tz.strftime(self.readable_format)}')
        self.transaction_time_id_format = '%Y%m%d%H%M%S'
        print(f'The date and time condensed: {self.tz.strftime(self.transaction_time_id_format)}')
    
    def condensed(self):
       return f'{self.tz.strftime(self.transaction_time_id_format)}'

class Account:
    
    interest = 0.005 # Percent

    def __init__(self, first_name, last_name, account_num=1400, starting_balance=0.00):
        self.first_name = first_name
        self.last_name = last_name
        self.full_name = f'{first_name} {last_name}'
        self.account_num = account_num
        self.balance = starting_balance
        self.transaction_id = 0
        self.tzone = TimeZone()

    def deposit(self, amount):
        self.balance += amount
        self.transaction_id += 1
        print(f'D-{self.account_num}-{self.tzone.condensed()}-{self.transaction_id}')
    
    def withdraw(self, amount):
        if amount > self.balance:            
            self.transaction_id += 1
            print(f'X-{self.account_num}-{self.tzone.condensed()}-{self.transaction_id}') 
            raise ValueError('Transaction declined. Insufficient funds. Please deposit some more $$$ first.')
            
        self.balance -= amount 
        self.transaction_id += 1
        print(f'W-{self.account_num}-{self.tzone.condensed()}-{self.transaction_id}') 

    def pay_interest(self):
        monthly_rate = self.interest/12
        monthly_sum = monthly_rate * self.balance
        self.transaction_id += 1
        print(f'I-{self.account_num}-{self.tzone.condensed()}-{self.transaction_id}') 
        return monthly_sum + self.balance

    def __repr__(self):
        """Return a string that represents the account."""
        return f"{self.__class__.__name__}({self.last_name}, {self.first_name}, balance={self.balance})"
Reply
#14
Thank you deanhystad for your reply.

(Mar-10-2022, 09:56 PM)deanhystad Wrote: self is completely a convention, but conventions are important. When calling a method of an instance, Python passes the instance as the first argument. When calling a method of a class, Python passes the class as the first argument. It is convention to use "self" and "cls" for these first arguments. You could use "this" and "class_", but that would confuse others used to the conventions.

Thank you, this does a better job of clarifying the point I was trying to make earlier. When calling a method of an instance, Python passes the instance of the first argument.

Quote:Your error is that self is an instance of Account, not an instance of TimeZone. self is an instance of Account because "deposit()" is an instance method of Account, and the first argument in deposit(self) is an instance of Account. Account does not have an attribute "transaction_time_id_format".

In the latest (and near final I think) iteration of my script (quoted at the end - - one post above), Account now has instantiates TimeZone which now includes a class method that calls transaction_time_id_format.

It seems to work really well now. Any further suggestions, comments, and observations on my script welcome. Dance

Quote:Question is, what should have a timezone? Right now I don't think you have any time zones. You have a class, but did you create any instances? And if you have an instance of Timezone, where is that kept? What is a time zone associated with? The Account? I don't know. I can do banking in different time zones. Is the purpose of TimeZone to convert from UTC to my local time, or is it to convert the time from where the transaction took place to UTC?

I believe these considerations are beyond the scope of the rudimentary ATM bank machine app as set out by the instructor for now, but I may return this in the future if I decide to refine my timezoning features.
Reply
#15
Quote:When calling is_dunder() on various properties and objects, the Python REPL just says: “object not subscriptable” or “is not defined”. Is this expected?

Yes, I should have clarified that is_dunder() is just a helper for my print_properties function. It was not necessary to separate into its own function but I thought it might make the list comprehension more readable if I did not tack on that extra bit of code.

The entire purpose for my helper function was to illustrate to you that the BA instance does not have a transaction_time_id_format which is why you got the traceback on post #8

Sounds like now you have amended your code to instantiate TimeZone inside Account which will solve your problem. :)
Apologetic Canadian, Sorry about that eh?
Reply
#16
property is not a reserved word. Is is a function. for is a reserved word. while is a reserved word. property is a useful function (and @decorator) in builtins. You can repurpose function names, they are just variables, but that eliminates using the original function.

If you reuse a builtin name inside a function the effect is highly localized and not likely a problem. The builtin cannot be used inside the function. If you reuse a builtin name in global scope it means you would have to use a scope specifier to use the original. This is questionable at best.
print = lambda x: __builtins__.print("I have a bad feeling about this", x)
print("Chewy")
Output:
I have a bad feeling about this Chewy
If you replace the function in the builtins scope there is a special level in hell reserved just for you. The original is no longer accessible.

is_dunder() does not work for properties, or attributes, or objects, unless that object is a str. dir() returns a list of strings, so is_dunder(x) expects x to be a str. By itself is_dunder() isn't particularly interesting, it is a helper function. I would rewrite print_properties() as:
def print_properties(obj):
    """Print non-dunder properties of obj.  Does not do bad things like using a list comprehension for it's side effect]"""
    print("\n".join([f"{p}={getattr(obj, p)}" for p in dir(obj) if not p.startswith("__")]))
I'm not sure what knackwurstbagel has against dunders. If you want to know the attributes inside an object, __dict__ works better than dir(). dir() is just attribute names, and lots of names. __dict__ is a dictionary of attributes (names and values) for the object. The stuff you are most interested in.
class Me():
    def __init__(self):
        self.x = 1
        self.y = 2

m = Me()
print(m.__dict__)  # Get this for free
print_properties(m)  # Have to remember to import
Output:
{'x': 1, 'y': 2} x 1 y 2}
I think having transactions print anything is a bad idea, and that printing the "transaction" is particularly egregious. Instead of printing, transactions should return a transaction id object. The transaction would have the account number, transaction type, balance and transaction id and transaction date/time. You can print the transaction object or not. You can add the transaction to a list to have a transaction history. Transactions should only have one side effect (at most), and that is changing the balance.
Reply
#17
Quote:I'm not sure what knackwurstbagel has against dunders.
I have nothing against dunders, there was no use printing out a bunch of dunders when I wanted to make clear the properties that were not dunders so that it can be observed the BA instance of Account does not have the property transaction_time_id_format

I do appreciate the free education about __dict__ and the rewrite of print_properties I do not appreciate being talked to like that.
Apologetic Canadian, Sorry about that eh?
Reply
#18
Was just having a bit of fun while trying to point out that if you are interested in displaying the "interesting" attributes of a class there are better ways than filtering out dunders. I apologize for insulting you. It was not my intention. I will try to be more careful in future posts to prevent this kind of thing from happening again.
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020