Classes Answers

Class Exercises

Record Transactions

Modify our class to record transactions for deposits and withdrawals.

A transactions attribute should be added to our object that maintains a list of all transactions in our account. Each transaction should be a tuple containing an action name (as a string), the amount the account changed, and the account balance after the change.

Example usage:

>>> from bank_account import BankAccount
>>> my_account = BankAccount()
>>> my_account.deposit(100)
>>> my_account.withdraw(40)
>>> my_account.deposit(95)
>>> my_account.transactions
[('OPEN', 0, 0), ('DEPOSIT', 100, 100), ('WITHDRAWAL', -40, 60), ('DEPOSIT', 95, 155)]

Answers

class BankAccount:

    def __init__(self, balance=0):
        self.balance = balance
        self.transactions = [("OPEN", balance, balance)]
        # print("Account opened.")
        self.print_balance()

    def deposit(self, amount):
        self.balance += amount
        self.transactions.append(("DEPOSIT", amount, self.balance))
        # print("${} deposited.".format(amount))
        self.print_balance()

    def withdraw(self, amount):
        self.balance -= amount
        self.transactions.append(("WITHDRAWAL", -amount, self.balance))
        # print("${} withdrawn.".format(amount))
        self.print_balance()

    # ...

Points

Create a Point class that has the following methods:

  • distance: accepts another Point as an argument and returns the distance between the two points (using the Pythagorean Theorem)
  • shift: accepts another Point an argument and returns a new point that is the sum of the two points
  • scale: accepts a number as an argument and returns a new point that is scaled by that number
>>> p1 = Point(1, 2, 3)
>>> p2 = Point(4, 5, 6)
>>> p2.shift(p1)
Point(5, 7, 9)
>>> p1.scale(2)
Point(2, 4, 6)
>>> p1.distance(p2)
5.196152422706632

Answers

import math


class Point:

    """Three-dimensional point."""

    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

    def shift(self, other):
        """Return copy of our point, shifted by other."""
        return Point(self.x+other.x, self.y+other.y, self.z+other.z)

    def scale(self, value):
        """Return new copy of our point, scaled by given value."""
        return Point(value*self.x, value*self.y, value*self.z)

    def distance(self, other):
        """Return distance between two points."""
        x, y, z = (self.x-other.x), (self.y-other.y), (self.z-other.z)
        return math.sqrt(x**2 + y**2 + z**2)

    def __repr__(self):
        """Return dev-readable representation of Point."""
        return "Point({}, {}, {})".format(self.x, self.y, self.z)

Pythonic Points

Modify the Point class you implemented in the last exercise:

  • remove shift and add support for using the + operator between two points (hint: use __add__)
  • remove scale and add support for using the * operator between two points (hint: use __mul__)
>>> p1 = Point(1, 2, 3)
>>> p2 = Point(4, 5, 6)
>>> p1 + p2
Point(5, 7, 9)
>>> p1 * 2
Point(2, 4, 6)

Answers

import math


class Point:

    """Three-dimensional point."""

    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

    def distance(self, other):
        """Return distance between two points."""
        x, y, z = (self.x-other.x), (self.y-other.y), (self.z-other.z)
        return math.sqrt(x**2 + y**2 + z**2)

    def __add__(self, other):
        """Return copy of our point, shifted by other."""
        cls = self.__class__
        return cls(self.x+other.x, self.y+other.y, self.z+other.z)

    def __mul__(self, value):
        """Return new copy of our point, scaled by given value."""
        cls = self.__class__
        return cls(value*self.x, value*self.y, value*self.z)

    def __rmul__(self, value):
        """Return new copy of our point, scaled by given value."""
        return self.__mul__(value)

    def __repr__(self):
        """Return dev-readable representation of Point."""
        return "{cls}({x}, {y}, {z})".format(
            cls=self.__class__.__name__,
            x=self.x,
            y=self.y,
            z=self.z,
        )

Object Exercises

Inverse Filter

Create an exclude function that only keeps items which fail a given predicate test. The function should accept a function and an iterable as its arguments and should return an iterable containing all items which yielded a falsey return value from the predicate function. This is basically the opposite of the built-in filter function.

>>> exclude(bool, [False, True, False])
[False, False]
>>> exclude(lambda x: len(x) > 3, ["red", "blue", "green"])
['red']

Answers

def exclude(condition, iterable):
    return [var for var in iterable if not condition(var)]

Call

Write a function call which calls a given function with any given positional and keyword arguments and returns the value returned by the function call.

>>> call(int)
0
>>> call(int, "5")
5
>>> call(len, "hello")
5
>>> list(call(zip, [1, 2], [3, 4]))
[(1, 3), (2, 4)]

Answers

def call(func, *args):
    return func(*args)

Call Later

Write a function call_later which accepts a function and a list of arguments and returns a new function that, when called, will call the function with the given arguments.

>>> names = []
>>> append_name = call_later(names.append, "Trey")
>>> append_name()
>>> names
['Trey']
>>> append_name()
>>> names
['Trey', 'Trey']
>>> call_zip = call_later(zip, [1, 2], [3, 4])
>>> list(call_zip())
[(1, 3), (2, 4)]

Answers

def call_later(func, *args):
    def new_func():
        return func(*args)
    return new_func

Call Again

Write a function call_again which accepts a function and a list of arguments and returns a tuple. The first item in the tuple should be the return value from calling the given function with the given arguments. The second item in the tuple should be a function that, when called, will call the function again with the given arguments.

>>> names = []
>>> response, names_as_str = call_again(str, names)
>>> response
'[]'
>>> names.append("Diane")
>>> names_as_str()
"['Diane']"

Answers

def call_again(func, *args):
    def new_func():
        return func(*args)
    return func(*args), new_func

Only Once

Make a function only_once that accepts a function as an argument and returns a new function. The returned function should be identical to the original function except that when you try to call the function more than once, it shouldn’t let you.

>>> def do_something(x, y):
...     print("doing something with {} and {}".format(x, y))
...     return x * 2 + y ** 2
...
>>> do_something_once = only_once(do_something)
>>> do_something_once(1, 2)
doing something with 1 and 2
6
>>> do_something_once(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in wrapped
ValueError: You can't call this function twice!

Answers

def only_once(func):
    called_before = set()
    def new_func(*args):
        if args in called_before:
            raise ValueError("You can't call this function twice!")
        called_before.add(args)
        return func(*args)
    return new_func

Cache

Write a cache function which accepts takes a function as its argument and returns a function that is identical to the original function except that it caches its return values based on any positional arguments given.

>>> def do_something(x, y):
...     print("doing something with {} and {}".format(x, y))
...     return x * 2 + y ** 2
...
>>> do_something_cached = cache(do_something)
>>> do_something_cached(1, 2)
doing something with 1 and 2
6
>>> do_something_cached(1, 2)
6

Answers

def cache(func):
    cached_calls = {}
    def new_func(*args):
        if args in cached_calls:
            return cached_calls[args]
        response = func(*args)
        cached_calls[args] = response
        return response
    return new_func

Partial

Make a partial function (like the one we already made) which accepts positional arguments and keyword arguments.

Answers

def partial(func, *first_args, **first_kwargs):
    def new_func(*args, **kwargs):
        new_args = first_args + args
        new_kwargs = first_kwargs.copy()
        new_kwargs.update(kwargs)
        return func(*new_args, **new_kwargs)
    return new_func

Inheritance Exercises

Minimum Balance

Create a class MinimumBalanceAccount which subclasses BankAccount. This new class should raise an exception whenever the user attempts to withdraw so much money that their account goes below 0.

>>> from bank_account import MinimumBalanceAccount
>>> my_account = MinimumBalanceAccount()
Account opened.
Account balance is $0.
>>> my_account.deposit(100)
$100 deposited.
Account balance is $100.
>>> my_account.withdraw(200)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "bank_account.py", line 45, in withdraw
    raise ValueError("Balance cannot be less than $0!")
ValueError: Balance cannot be less than $0!

Answers

class MinimumBalanceAccount(BankAccount):

    """Bank account which does not allow balance to drop below zero."""

    def withdraw(self, amount):
        if self.balance - amount < 0:
            raise ValueError("Balance cannot be less than $0!")
        return super().withdraw(amount)