Extra Answers

File Exercises

Country Capitals CSV

This is the country_capitals.py exercise in the modules directory. Create the file country_capitals.py in the modules sub-directory of the exercises directory. To test it, run python test.py country_capitals.py from your exercises directory.

Download this country capitals file.

Write a program country_capitals.py that opens the file and extracts country name and capital city from each row, and write a new file to disk in the following format:

country,capital,population
China,Beijing,1330044000
India,New Delhi,1173108018
United States,Washington,310232863

The country rows should be sorted by largest population first.

Answers

import csv
import sys
from urllib.request import urlopen


def get_capital_data():
    """Return parsed CSV data from capitals API."""
    url = "http://api.geonames.org/countryInfoCSV?username=truthfultechnology"
    with urlopen(url) as response:
        raw_data = response.read().decode('utf-8').splitlines()
        reader = csv.DictReader(raw_data, delimiter="\t")
        return list(reader)


def rearrange_capital_data(old_rows):
    """Sort CSV data by population and rename country column."""
    def by_population(row): return int(row['population'])
    new_rows = [{
        'country': row['name'],
        'capital': row['capital'],
        'population': row['population'],
    } for row in old_rows]
    return sorted(new_rows, reverse=True, key=by_population)


def write_capital_file(file_name, csv_rows):
    """Write capital rows CSV file."""
    headers = ["country", "capital", "population"]
    with open(file_name, mode='wt') as out_file:
        writer = csv.DictWriter(out_file, delimiter=",",
                                fieldnames=headers)
        writer.writeheader()
        writer.writerows(csv_rows)


def main(out_file_name):
    original_data = get_capital_data()
    new_data = rearrange_capital_data(original_data)
    write_capital_file(out_file_name, new_data)


if __name__ == "__main__":
    main(sys.argv[1])

GZip Fetch

This is the gzip_fetch.py exercise in the modules directory. Create the file gzip_fetch.py in the modules sub-directory of the exercises directory. To test it, run python test.py gzip_fetch.py from your exercises directory.

Write a program that downloads gzipped data from the Internet, extracts it, and saves it on disk all without using a temporary file.

You can use this gzipped response: https://httpbin.org/gzip

Example:

$ python gzip_fetch.py https://httpbin.org/gzip data.json

Answers

import gzip
from urllib.request import urlopen
import sys


def save_gzipped_data(url, filename):
    with urlopen(url) as response:
        with gzip.GzipFile(fileobj=response, mode='rb') as extracted:
            with open(filename, mode='wb') as data_file:
                data_file.write(extracted.read())


if __name__ == "__main__":
    url, filename = sys.argv[1:]
    save_gzipped_data(url, filename)

Decorator Exercises

JSONify

This is the jsonify exercise in decorators.py.

Make a decorator jsonify that will take the return value of the function and return a JSON-encoded version of the return value.

>>> @jsonify
... def get_thing():
...     return {'trey': "red", 'diane': "purple"}
...
>>> get_thing()
'{"diane": "purple", "trey": "red"}'

Hint

You’ll want to use json.dumps

Answers

import json
from functools import wraps


def jsonify(func):
    """Return JSON-encoded value returned by decorated function."""
    @wraps(func)
    def new_func(*args, **kwargs):
        return json.dumps(func(*args, **kwargs))
    return new_func

Groot

This is the groot exercise in decorators.py.

Write a decorator that ignores the function it’s decorating and returns a function that prints “Groot”.

>>> @groot
... def greet(name):
...     print(f"Hello {name}")
...
>>> greet("Trey")
Groot

Answers

from functools import wraps


def groot(func):
    """Return function which prints 'Groot' (ignore decoratee)."""
    @wraps(func)
    def new_func(*args, **kwargs):
        print("Groot")
    return new_func

Four

This is the four exercise in decorators.py.

Write a decorator that ignores the function it’s decorating and returns the number 4. In other words, it doesn’t return a new function like you might expect from a decorator.

>>> @four
... def greet(name):
...     print(f"Hello {name}")
...
>>> greet
4
>>> type(greet)
<class 'int'>

Answers

def four(func):
    """Return 4 (ignore decorated function)."""
    return 4

Hide Errors

This is the hide_errors exercise in decorators.py.

Make a decorator hide_errors that will suppress all non-system exiting exceptions raised by the target function, returning None if an exception was caught.

The decorator should act like this:

>>> @hide_errors
... def divide(x, y):
...     return x / y
...
>>> divide(1, 0)
>>> divide(1, 2)
0.5

Answers

from functools import wraps


def hide_errors(func):
    """Trap and ignore non-exiting errors."""
    @wraps(func)
    def new_func(*args):
        try:
            return func(*args)
        except Exception:
            return
    return new_func

Prompt on Error

This is the catch_all exercise in decorators.py.

Make a decorator catch_all that will catch all exceptions raised by a function, prompt the user to ask whether the program should be exited, and then act accordingly.

The decorator should act like this:

>>> @catch_all
... def divide(x, y):
...     return x / y
...
>>> divide(1, 0)
Exception occurred: division by zero
Should we ignore this exception (Y/n)? y
>>> divide(1, 2)
0.5
>>> divide(1, 0)
Exception occurred: division by zero
Should we ignore this exception (Y/n)? n
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in decor
File "<stdin>", line 3, in divide
ZeroDivisionError: division by zero

Answers

from functools import wraps


def catch_all(func):
    """Trap non-exiting errors and ask user if we should ignore."""
    @wraps(func)
    def new_func(*args):
        try:
            return func(*args)
        except Exception as error:
            print(f"Exception occurred: {error}")
            answer = input("Should we ignore this exception (Y/n)? ")
            if answer.lower() == "n":
                raise
    return new_func

Make Partial

This is the make_partial exercise in decorators.py.

Make a decorator make_partial which makes a function return a new partially-evaluated function.

Example:

>>> @make_partial
... def add(x, y):
...     return x + y
...
>>> add(1)
functools.partial(<function add at 0x7f4d086b4950>, 1)
>>> add(1)(2)
3
>>> add(1)(3)
4

Hint

Use functools.partial.

Answers

from functools import partial, wraps


def make_partial(func):
    """Returns a new partially-evaluated function."""
    @wraps(func)
    def new_func(*args):
        return partial(func, *args)
    return new_func

Count Calls

This is the count_calls exercise in decorators.py.

Make a decorator count_calls that keeps track of the number of times a function is called and passes the call count in as the first argument to the function.

Example usage:

>>> @count_calls
... def add(count, x, y):
...     print(f"called {count} times")
...     return x + y
...
>>> add(1, 2)
called 1 times
3
>>> add(1, 2)
called 2 times
3
>>> add(1, 3)
called 3 times
4

Tip

You may need to use nonlocal when making the inner function in your decorator.

Answers

from functools import wraps


def count_calls(func):
    """Pass call counts into first argument to the function."""
    call_count = 0
    @wraps(func)
    def new_func(*args):
        nonlocal call_count
        call_count += 1
        return func(call_count, *args)
    return new_func

Object Exercises

Inverse Filter

This is the exclude exercise in objects.py.

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):
    """Only keep items which fail a given predicate test"""
    return [
        var
        for var in iterable
        if not condition(var)
    ]

Call

This is the call exercise in objects.py.

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):
    """Call the function provided with the given arguments."""
    return func(*args)

Call Later

This is the call_later exercises in objects.py.

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):
    """Return a function to call given function with provided arguments."""
    def new_func():
        return func(*args)
    return new_func

Call Again

This is the call_again exercise in objects.py.

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):
    """Return function return value and a function to call again."""
    def new_func():
        return func(*args)
    return func(*args), new_func

Only Once

This is the only_once exercise in objects.py.

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(f"doing something with {x} and {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 new_func
ValueError: You can't call this function twice!

Answers

def only_once(func):
    """Return new version of the function that can only be called once."""
    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

This is the cache exercise in objects.py.

Write a cache function which 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(f"doing something with {x} and {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):
    """Return a cache-ing version of the given function."""
    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

This is the partial exercise in objects.py.

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):
    """Partially evaluate the given function with the given arguments."""
    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

Property Exercises

Point with Magnitude

This is the Point exercise in properties.py.

Make a Point class that has an auto-calculated magnitude attribute. The magnitude should be calculated by sqrt(x**2 + y**2 + z**2).

Note

To test these changes, you should modify the PointTests class in properties_test.py to comment out the @unittest.skip for “magnitude property”.

Example:

>>> from properties import Point
>>> p = Point(2, 3, 6)
>>> p.magnitude
7.0
>>> p.y = 9
>>> p.magnitude
11.0

Answers

import math


class Point:

    """Three-dimensional point."""

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

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

    @property
    def magnitude(self):
        """Return the magnitude of the Point"""
        return math.sqrt(self.x**2 + self.y**2 + self.z**2)

Full Name

This is the Person exercise in properties.py.

Create the Person class so that it has a first_name and last_name attribute and a name property which is the combination first_name and last_name, with a space between.

>>> from properties import Person
>>> trey = Person("Trey", "Hunner")
>>> trey.name
'Trey Hunner'
>>> trey.last_name = "Smith"
>>> trey.name
'Trey Smith'

Answers

class Person:

    """Person with first and last name."""

    def __init__(self, first, last):
        self.first_name = first
        self.last_name = last

    @property
    def name(self):
        """Return first and last name joined together."""
        return " ".join((self.first_name, self.last_name))

Circle

This is the Circle exercise in properties.py.

Create the Circle class so that it:

  • can be constructed with an explicit radius or the default radius of 1

  • has an area property which auto-updates based on radius changes

  • has a diameter property which auto-updates based on radius changes

  • auto-updates the radius property based on diameter changes

Example usage:

>>> from properties import Circle
>>> circle1 = Circle()
>>> circle.radius
1
>>> circle.area
3.141592653589793
>>> circle.diameter
>>> circle.radius = 2
>>> circle.area
12.566370614359172
>>> circle.diameter
4
>>> circle.diameter = 10
>>> circle.radius
5.0
>>> circle.area
78.53981633974483

Answers

import math

class Circle:

    """Circle with radius, area, etc."""

    def __init__(self, radius=1):
        self.radius = radius

    @property
    def area(self):
        return math.pi * self.radius ** 2

    @property
    def diameter(self):
        return self.radius * 2

    @diameter.setter
    def diameter(self, diameter):
        self.radius = diameter / 2.0

Log Radius Changes

This is the Circle exercise in properties.py.

Modify the Circle class we defined previously to use a property for the radius and record changes to the radius in a radius_changes attribute.

Note

To test these changes, you should modify the CircleTests class in properties_test.py to comment out the @unittest.skip for the lines from the appropriate test methods.

Hint

You will need to store the actual radius somewhere. You may want to use a _radius attribute when converting the radius to a property.

Example usage:

>>> from properties import Circle
>>> circle = Circle()
>>> circle.radius
1
>>> circle.radius_changes
[1]
>>> circle.radius = 2
>>> circle.radius = 3
>>> circle.radius_changes
[1, 2, 3]

Answers

import math

class Circle(object):

    """Circle with radius, area, etc."""

    def __init__(self, radius=1):
        self.radius_changes = []
        self.radius = radius

    @property
    def area(self):
        return math.pi * self.radius ** 2

    @property
    def diameter(self):
        return self.radius * 2

    @diameter.setter
    def diameter(self, diameter):
        self.radius = diameter / 2.0

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, radius):
        self.radius_changes.append(radius)
        self._radius = radius

Set Radius Error

This is the Circle exercise in properties.py.

Edit the Circle class and modify it again to raise a ValueError when the radius or diameter are set to a negative number.

Note

To test these changes, you should modify the CircleTests class in properties_test.py to comment out the @unittest.skip for the lines from the appropriate test methods.

Example usage:

>>> from properties import Circle
>>> circle = Circle()
>>> circle.radius = -10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "shapes.py", line 18, in radius
    raise ValueError("Radius cannot be negative")
ValueError: Radius cannot be negative

Answers

import math

class Circle(object):

    """Circle with radius, area, etc."""

    def __init__(self, radius=1):
        self.radius_changes = []
        self.radius = radius

    @property
    def area(self):
        return math.pi * self.radius ** 2

    @property
    def diameter(self):
        return self.radius * 2

    @diameter.setter
    def diameter(self, diameter):
        self.radius = diameter / 2.0

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, radius):
        if radius < 0:
            raise ValueError("Radius cannot be negative!")
        self.radius_changes.append(radius)
        self._radius = radius

Dunder Exercises

Pythonic Points

Edit the Point class in the properties.py file to:

  1. Support using the + operator between two points

  2. Support using the * operator between two a point and a number

  3. Support the == and != operators between two points

Note

To test these changes, you should modify the PointTests class in properties_test.py to comment out the @unittest.skip for “Pythonic Points”.

>>> p1 = Point(1, 2, 3)
>>> p2 = Point(4, 5, 6)
>>> p1 + p2
Point(5, 7, 9)
>>> p1 * 2
Point(2, 4, 6)
>>> p1 == p2
False
>>> p1 + Point(3, 3, 3) == p2
True

Answers

import math


class Point:

    """Three-dimensional point."""

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

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

    def __eq__(self, other):
        """Return True if our point is equal to the other point."""
        return (self.x, self.y, self.z) == (other.x, other.y, other.z)

    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)

    @property
    def magnitude(self):
        """Return the magnitude of the Point"""
        return math.sqrt(self.x**2 + self.y**2 + self.z**2)

Truthy Account

Edit the BankAccount class in the classes.py file to make account objects truthy when the bank account has money in it.

Note

To test these changes, you should modify the BankAccountTests class in classes_test.py to comment out the lines with “Comment this line for truthy accounts exercise.”

Example usage:

>>> from classes import BankAccount
>>> trey_account = BankAccount(100)
>>> bool(trey_account)
True
>>> trey_account.withdraw(100)
>>> bool(trey_account)
False
>>> trey_account.withdraw(1)
>>> bool(trey_account)
False

You can use this class:

class BankAccount:

    """Bank account that supports truthiness."""

    def __init__(self, balance=0):
        self.balance = balance

    def __repr__(self):
        return f"{type(self).__name__}(balance={self.balance})"

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        self.balance -= amount

    def transfer(self, other_account, amount):
        self.withdraw(amount)
        other_account.deposit(amount)

Hint

Use the __bool__ method

Answers

class BankAccount:

    """Bank account that supports truthiness."""

    def __init__(self, balance=0):
        self.balance = balance

    def __repr__(self):
        return f"{type(self).__name__}(balance={self.balance})"

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        self.balance -= amount

    def transfer(self, other_account, amount):
        self.withdraw(amount)
        other_account.deposit(amount)

    def __bool__(self):
        return self.balance > 0

Comparable Account

Edit the BankAccount class in the classes.py file to allow accounts to be compared using arithmetic comparison operators.

Note

To test these changes, you should modify the BankAccountTests class in classes_test.py to comment out the line with “Comment this line for BankAccount comparison exercise.”

Example:

>>> from classes import BankAccount
>>> account1 = BankAccount(balance=100)
>>> account2 = BankAccount(balance=200)
>>> account1 == account2
False
>>> account1 < account2
True
>>> account1 >= account2
False

Answers

class BankAccount:

    """Bank account that supports truthiness."""

    def __init__(self, balance=0):
        self.balance = balance

    def __repr__(self):
        return f"{type(self).__name__}(balance={self.balance})"

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        self.balance -= amount

    def transfer(self, other_account, amount):
        self.withdraw(amount)
        other_account.deposit(amount)

    def __eq__(self, rhs):
        return self.balance == rhs.balance

    def __lt__(self, rhs):
        return self.balance < rhs.balance

    def __gt__(self, rhs):
        return self.balance > rhs.balance

    def __le__(self, rhs):
        return self.balance <= rhs.balance

    def __ge__(self, rhs):
        return self.balance >= rhs.balance

Caseless String

This is the CaselessString exercise in dunder.py.

Use UserString to make a CaselessString class which does comparisons in a case insensitive manner.

Example:

>>> from dunder import CaselessString
>>> s1 = CaselessString("Hello there")
>>> s2 = CaselessString("hello there")
>>> s3 = CaselessString("HELLO THERE")
>>> s1 == s2 == s3
True
>>> s1.lower() == s2.upper()
True

Answers

We are using the decorator functools.total_ordering. It allows you to define __eq__ or __ne__ and one of the other comparison operators __lt__ or __gt__. It takes care of all the rest!

from collections import UserString
from functools import total_ordering


@total_ordering
class CaselessString(UserString):

    """Class whose strings are compared caselessly."""

    def __eq__(self, other):
        return self.data.lower() == other.lower()

    def __lt__(self, other):
        return self.data.lower() < other.lower()

Easy Dict

Edit the EasyDict class in the dunder.py file to make an EasyDict class that can be used with both attribute and item syntax.

Run python test.py EasyDict in your exercises directory.

Example:

>>> from dunder import EasyDict
>>> a = EasyDict()
>>> a['shoe'] = "blue"
>>> a.shoe
"blue"
>>> a['shoe']
"blue"
>>> a.car = "green"
>>> a['car']
"green"

Hint

Use the __getitem__ and __setitem__ methods

Answers

class EasyDict:

    """Class which allows both attribute and get/set item syntax."""

    def __getitem__(self, key):
        return getattr(self, key)

    def __setitem__(self, key, value):
        setattr(self, key, value)

Cyclic List

This is the CyclicList exercise in dunder.py.

Create a list-like data structure that loops in a cyclic manner:

>>> from dunder import CyclicList
>>> numbers = CyclicList([1, 2, 3, 4])
>>> numbers[1]
2
>>> numbers[2]
3
>>> numbers[12]
1
>>> [x for x, _ in zip(numbers, range(10))]
[1, 2, 3, 4, 1, 2, 3, 4, 1, 2]
>>> numbers[35]
4

Answers

from collections import UserList

class CyclicList(UserList):

    """Class with list-like structure that loops cyclicly."""

    def __getitem__(self, i):
        return super().__getitem__(i % len(self))

    def __setitem__(self, i, v):
        return super().__setitem__(i % len(self), v)