-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[WIP] Implement exercise bank-account #757
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6bf3092
5a41971
9c5a2de
97e823a
fd061cb
8335b9e
a80f196
34d7f5a
4550401
a1b8599
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Bank Account | ||
|
||
Simulate a bank account supporting opening/closing, withdrawals, and deposits of money. Watch out for concurrent transactions! | ||
|
||
A bank account can be accessed in multiple ways. Clients can make deposits and withdrawals using the internet, mobile phones, etc. Shops can charge against the account. | ||
|
||
Create an account that can be accessed from multiple threads/processes (terminology depends on your programming language). | ||
|
||
It should be possible to close an account; operations against a closed account must fail. | ||
|
||
### Submitting Exercises | ||
|
||
Note that, when trying to submit an exercise, make sure the solution is in the `exercism/python/<exerciseName>` directory. | ||
|
||
For example, if you're submitting `bob.py` for the Bob exercise, the submit command would be something like `exercism submit <path_to_exercism_dir>/python/bob/bob.py`. | ||
|
||
|
||
For more detailed information about running tests, code style and linting, | ||
please see the [help page](http://exercism.io/languages/python). | ||
|
||
## Submitting Incomplete Solutions | ||
It's possible to submit an incomplete solution so you can see how others have completed the exercise. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
|
||
|
||
class BankAccount(object): | ||
|
||
def __init__(self): | ||
pass | ||
|
||
def getBalance(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Following PEP8 we use |
||
pass | ||
|
||
def open(self): | ||
pass | ||
|
||
def deposit(self, amount): | ||
pass | ||
|
||
def withdraw(self, amount): | ||
pass | ||
|
||
def close(self): | ||
pass |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import unittest | ||
import threading | ||
|
||
from bank_account import BankAccount | ||
|
||
|
||
class BankAccountTests(unittest.TestCase): | ||
|
||
def test_newly_opened_account_has_zero_balance(self): | ||
self.account = BankAccount() | ||
self.account.open() | ||
|
||
self.assertEqual(self.account.getBalance(), 0) | ||
|
||
def test_get_balance_0(self): | ||
self.account = BankAccount() | ||
self.account.open() | ||
|
||
self.assertEqual(self.account.getBalance(), 0) | ||
|
||
def test_get_balance_with_amount(self): | ||
self.account = BankAccount() | ||
self.account.open() | ||
self.account.deposit(100) | ||
|
||
self.assertEqual(self.account.getBalance(), 100) | ||
|
||
def test_deposit_into_account(self): | ||
self.account = BankAccount() | ||
self.account.open() | ||
self.account.deposit(100) | ||
|
||
self.assertEqual(self.account.getBalance(), 100) | ||
|
||
def test_two_deposits_in_a_row(self): | ||
self.account = BankAccount() | ||
self.account.open() | ||
self.account.deposit(100) | ||
self.account.deposit(50) | ||
|
||
self.assertEqual(self.account.getBalance(), 150) | ||
|
||
def test_can_withdraw(self): | ||
self.account = BankAccount() | ||
self.account.open() | ||
self.account.deposit(100) | ||
self.account.withdraw(50) | ||
|
||
self.assertEqual(self.account.getBalance(), 50) | ||
|
||
def test_checking_balance_of_closed_account_throws_error(self): | ||
self.account = BankAccount() | ||
self.account.open() | ||
self.account.close() | ||
|
||
with self.assertRaises(ValueError): | ||
self.account.getBalance() | ||
|
||
def test_deposit_into_closed_account(self): | ||
self.account = BankAccount() | ||
self.account.open() | ||
self.account.close() | ||
|
||
with self.assertRaises(ValueError): | ||
self.account.deposit(50) | ||
|
||
def test_withdraw_from_closed_account(self): | ||
self.account = BankAccount() | ||
self.account.open() | ||
self.account.close() | ||
|
||
with self.assertRaises(ValueError): | ||
self.account.withdraw(50) | ||
|
||
def test_cannot_withdraw_more_than_deposited(self): | ||
self.account = BankAccount() | ||
self.account.open() | ||
self.account.deposit(25) | ||
|
||
with self.assertRaises(ValueError): | ||
self.account.withdraw(50) | ||
|
||
def test_cannot_withdraw_negative(self): | ||
self.account = BankAccount() | ||
self.account.open() | ||
self.account.deposit(100) | ||
|
||
with self.assertRaises(ValueError): | ||
self.account.withdraw(-50) | ||
|
||
def test_cannot_deposit_negative(self): | ||
self.account = BankAccount() | ||
self.account.open() | ||
|
||
with self.assertRaises(ValueError): | ||
self.account.deposit(-50) | ||
|
||
def test_can_handle_concurrent_transactions(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @aes421 (cc: @cmccandless, @m-a-ge) I'm not sure that this test is truly testing for concurrency - depositing and then immediately withdrawing inherently safe values within one function is never going to break as far as I can see. Further, I can remove all instances of Currently, Python seems to be running these threads in order, depositing One possible solution to get disorder seems to be to add Concurrency was discussed a little in #1106. If we want to keep concurrency in this exercise (I think the exercise would be ok without it, but only as an early exercise about classes), I think that some consideration needs to be made for how to make the tests only pass if it is safe concurrent implementation. Ultimately, for concurrency to make sense here, we need to be able to make it so that the concurrency matters in some way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @N-Parsons (cc: @aes421, @m-a-ge)
I disagree. It's a core part of the exercise. From the problem description:
If we marked another exercise as foregone simply because of the challenges of testing parallelism in Python, then I think we might need to consider foregoing this exercise as well. Either that, or we need to come up with another solution and apply it both to this exercise and the foregone
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we want to be pedantic with this exercise, we may be able to do something similar to NumPy and write an extension in C that runs outside of the GIL. However we can only safely do that if we use no Python C API functions. I also want to quickly put a note here because I don't think concurrency and parallelism have been discussed much in this repo. There is a difference between concurrency and parallelism. "In programming, concurrency is the composition of independently executing processes, while parallelism is the simultaneous execution of (possibly related) computations." - from golang.org https://vimeo.com/49718712 (Video of Rob Pike talking about the difference at Heroku)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also might be able to use this: |
||
def increment_and_decrement(self): | ||
self.account.deposit(10) | ||
self.account.withdraw(1) | ||
|
||
self.account = BankAccount() | ||
self.account.open() | ||
|
||
threadlist = [] | ||
threads = 100 | ||
i = 0 | ||
while (i < threads): | ||
thread = threading.Thread(target=increment_and_decrement, | ||
args=(self, )) | ||
threadlist.append(thread) | ||
i += 1 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This loop would be better implemented using |
||
|
||
for thread in threadlist: | ||
thread.start() | ||
|
||
for thread in threadlist: | ||
thread.join() | ||
|
||
self.assertEqual(self.account.getBalance(), 900) | ||
|
||
|
||
if __name__ == '__main__': | ||
unittest.main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import threading | ||
|
||
|
||
class BankAccount(object): | ||
def __init__(self): | ||
self.isOpen = False | ||
self.lock = threading.Lock() | ||
|
||
def getBalance(self): | ||
self.lock.acquire() | ||
if (self.isOpen): | ||
self.lock.release() | ||
return self.balance | ||
else: | ||
self.lock.release() | ||
raise ValueError | ||
|
||
def open(self): | ||
self.lock.acquire() | ||
self.balance = 0 | ||
self.isOpen = True | ||
self.lock.release() | ||
|
||
def deposit(self, amount): | ||
self.lock.acquire() | ||
if (self.isOpen and amount > 0): | ||
self.balance += amount | ||
self.lock.release() | ||
else: | ||
self.lock.release() | ||
raise ValueError | ||
|
||
def withdraw(self, amount): | ||
self.lock.acquire() | ||
if (self.isOpen and self.balance >= amount and amount > 0): | ||
self.balance -= amount | ||
self.lock.release() | ||
else: | ||
self.lock.release() | ||
raise ValueError | ||
|
||
def close(self): | ||
self.lock.acquire() | ||
self.isOpen = False | ||
self.lock.release() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be at heading level 2 (
##
).