Skip to content

[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

Closed
wants to merge 10 commits into from
12 changes: 12 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,18 @@

]
},
{
"uuid": "83a3ff95-c043-401c-bc2c-547d52344b02",
"slug": "bank-account",
"core": false,
"unlocked_by": null,
"difficulty": 4,
"topics": [
"classes",
"concurrency",
"conditionals"
]
},
{
"uuid": "d41238ce-359c-4a9a-81ea-ca5d2c4bb50d",
"slug": "twelve-days",
Expand Down
22 changes: 22 additions & 0 deletions exercises/bank-account/README.md
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
Copy link
Contributor

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 (##).


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.
21 changes: 21 additions & 0 deletions exercises/bank-account/bank_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@


class BankAccount(object):

def __init__(self):
pass

def getBalance(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following PEP8 we use snake_case for function and method names, so this should be get_balance.

pass

def open(self):
pass

def deposit(self, amount):
pass

def withdraw(self, amount):
pass

def close(self):
pass
125 changes: 125 additions & 0 deletions exercises/bank-account/bank_account_test.py
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):
Copy link
Contributor

Choose a reason for hiding this comment

The 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 self.lock from the example code and the tests still pass. Part of the issue is probably that the Global Interpreter Lock (GIL) already locks the BankAccount instance when a thread accesses it, and releases it once the thread ends. I'm not sure of the best way to get around this.

Currently, Python seems to be running these threads in order, depositing 10 and withdrawing 1, then moving on to the next thread. We can see this by storing a list of transaction values and using i+1 as a second argument for the increment_and_decrement function, depositing and withdrawing this value, and then printing the transaction list after all threads are done.

One possible solution to get disorder seems to be to add time.sleep(random.random()) before deposits or withdrawals, but I think the deposits and withdrawal should be decoupled and not part of the same function call - just joining a list of threads for each should be sufficient if there are random delays.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@N-Parsons (cc: @aes421, @m-a-ge)

I think the exercise would be ok without it

I disagree. It's a core part of the exercise. From the problem description:

Create an account that can be accessed from multiple threads/processes (terminology depends on your programming language).

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 parallel-letter-frequency.

Copy link
Contributor

@Dog Dog Mar 1, 2018

Choose a reason for hiding this comment

The 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)

Copy link
Contributor

@Dog Dog Mar 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also might be able to use this:

https://docs.python.org/2/library/multiprocessing.html

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
Copy link
Contributor

@N-Parsons N-Parsons Nov 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop would be better implemented using for _ in range(100):.


for thread in threadlist:
thread.start()

for thread in threadlist:
thread.join()

self.assertEqual(self.account.getBalance(), 900)


if __name__ == '__main__':
unittest.main()
45 changes: 45 additions & 0 deletions exercises/bank-account/example.py
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()