diff --git a/README.md b/README.md index 98c75d84..b2dff291 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ There are two ways of running the modules: - Abstract class: [Abstract definition](ultimatepython/classes/abstract_class.py) - Exception class: [Exception definition](ultimatepython/classes/exception_class.py) - Iterator class: [Iterator definition | yield](ultimatepython/classes/iterator_class.py) (:exploding_head:) + - Encapsulation: [Encapsulation definition](ultimatepython/classes/encapsulation.py) (:pill:) 5. **Advanced** - Decorator: [Decorator definition | wraps](ultimatepython/advanced/decorator.py) (:exploding_head:) - Context manager: [Context managers](ultimatepython/advanced/context_manager.py) (:exploding_head:) diff --git a/ultimatepython/classes/encapsulation.py b/ultimatepython/classes/encapsulation.py new file mode 100644 index 00000000..818a5b5d --- /dev/null +++ b/ultimatepython/classes/encapsulation.py @@ -0,0 +1,130 @@ +""" +Encapsulation is a feature of OOP that allows you to hide the +implementation details of a class from its users. +Encapsulation allows us to limit the access of certain attributes +within a class. This prevents users from directly accessing and modifying such +attributes from outside the class. Instead, users must use methods to access and +modify attributes. +""" +import secrets + +# Module-level constanct +_INVALID_AMOUNT_MESSAGE = "Invalid amount." +_INSUFFICIENT_BALANCE_MESSAGE = "Insufficient balance." + + +class BankAccount: + def __init__(self, account_holder_name): + """ + In Python, a class attribute can be made private by prefixing it with two underscores. + This makes it inaccessible to users outside the class. + By default, class attributes are public. Therefore, they can be accessed and modified + outside of the class. + + Here, account_number and balance are private while account_holder_name is public. + """ + self.account_holder_name = account_holder_name + """ + The account number is generated automatically using the randbelow function from + the random module when a new instance of the class is created. + The balance is set to 0 by default. + """ + self.__account_number = secrets.randbelow(10**10) # generate a random account number of 10 digits. + self.__balance = 0 + + def deposit(self, balance): + """ + The deposit function is used to add new balance to the account. + The provided balance is added to the existing balance. + """ + self.__balance += int(balance) + + def withdraw(self, balance): + """ + The withraw method is used to deduct the balance from the account. + In case there is insufficient balance, or the input is invalid, + a value error is raised. + """ + if balance <= 0: + raise ValueError(_INVALID_AMOUNT_MESSAGE) + if balance > self.__balance: + raise ValueError(_INSUFFICIENT_BALANCE_MESSAGE) + + self.__balance -= balance + + def get_balance(self): + """ + This function returs the available balance in the account. + """ + return self.__balance + + def get_account_number(self): + """ + The account number is generated randomly when a new instance of the class is created. + Since the attribute is also private, it cannot be accessed directly from outside the class. + The get_account_number method allows you to access the account number outside of the class. + But since we do not define a setter method for this variable, we cannot modify it outside the class. + Therefore, the account number generated while creating an object of the BankAccount class cannot be changed + but can only be read using this function. + """ + return self.__account_number + + def __set_account_number(self, number): + """ + This is a private method. Similar to private variables, + private methods also cannot be accessed outside the class. + """ + self.__account_number = number + + def remove_account_details(self): + """ + This method is used to reset the account details. + Here, the __set_account_number function is private. + This, it cannot be called from outside the class. + However, the remove_account_details calls the function from + inside the class and as it is a public method, it can be called from + outside the class. + """ + self.__balance = 0 + self.__set_account_number(0) + self.account_holder_name = "" + + +def main(): + # Account names constants. + USER1 = "John Doe" + USER2 = "Jane Doe" + + # Account instances. + account1 = BankAccount(USER1) + account2 = BankAccount(USER2) + + assert account1.account_holder_name == USER1 + assert account2.account_holder_name == USER2 + + # Deposit amount and check if the balance is updated. + account1.deposit(100) + assert account1.get_balance() == 100 + + # Withdraw amount and check if the balance is updated. + account1.withdraw(50) + assert account1.get_balance() == 50 + + # validating invalid amounts. + error_inputs = [-10, 0, 150] + for input in error_inputs: + try: + account1.withdraw(input) + except ValueError as e: + assert str(e) in {_INSUFFICIENT_BALANCE_MESSAGE, _INVALID_AMOUNT_MESSAGE} + + # Remove account details and assert values. + account1.remove_account_details() + + assert account1.get_balance() == 0 + assert account1.get_account_number() == 0 + assert account1.account_holder_name == "" + + +if __name__ == "__main__": + main()