2
\$\begingroup\$

Q. Write three similar functions, each of which takes as an argument a sequence of intervals and returns the sum of the square of each interval that does not contain 0.

  • Using a for statement containing an if statement.

  • Using map and filter and reduce.

  • Using generator expression and reduce.

Hint: Square is a special case of quadratic, but you can also use the simpler square_interval function below for intervals that do not contain 0:

def non_zero(x):
    """Return whether x contains 0."""
    return lower_bound(x) > 0 or upper_bound(x) < 0

def square_interval(x):
    """Return the interval that contains all squares of values in x, where x
    does not contain 0.
    """
    assert non_zero(x), 'square_interval is incorrect for x containing 0'
    return mul_interval(x, x)

# The first two of these intervals contain 0, but the third does not.
seq = (interval(-1, 2), make_center_width(-1, 2), make_center_percent(-1, 50))

zero = interval(0, 0)

def sum_nonzero_with_for(seq):
    """Returns an interval that is the sum of the squares of the non-zero
    intervals in seq, using a for statement.

    >>> str_interval(sum_nonzero_with_for(seq))
    '0.25 to 2.25'
    """
    "*** YOUR CODE HERE ***"

from functools import reduce
def sum_nonzero_with_map_filter_reduce(seq):
    """Returns an interval that is the sum of the squares of the non-zero
    intervals in seq, using using map, filter, and reduce.

    >>> str_interval(sum_nonzero_with_map_filter_reduce(seq))
    '0.25 to 2.25'
    """
    "*** YOUR CODE HERE ***"

def sum_nonzero_with_generator_reduce(seq):
    """Returns an interval that is the sum of the squares of the non-zero
    intervals in seq, using using reduce and a generator expression.

    >>> str_interval(sum_nonzero_with_generator_reduce(seq))
    '0.25 to 2.25'
    """
    "*** YOUR CODE HERE ***"

Below is the solution:

# Representation 1
def make_center_width(c, w):
    """Construct an interval from center and width."""
    return interval(c - w, c + w)

def center(x):
    """Return the center of interval x."""
    return (upper_bound(x) + lower_bound(x)) / 2


def width(x):
    """Return the width of interval x."""
    return (upper_bound(x) - lower_bound(x)) / 2

#Representation 2
def make_center_percent(c, p):
    """ Construct an interval from center and tolerance.

    >>> str_interval(make_center_percent(2, 50))
    '1.0 to 3.0'
    """
    w = c * (p/100)
    return interval(c - w, c + w)

def percent(x):
   """Retunr the percentage tolerance of interval x.

   >>> percent(interval(1, 3))
   50.0
   """
   w  = ((upper_bound(x) - lower_bound(x))/2)
   c  = ((upper_bound(x) + lower_bound(x))/2)
   return (w/c)*100.0

#Representation 3
def interval(a, b):
    """Construct an interval from a to b. """
    return (a, b)

def lower_bound(x):
    """Return the lower bound of interval x. """
    return x[0]

def upper_bound(x):
    """Return the upper bound of interval x. """
    return x[1]
# Representation 3 - end

#Usage - starts
def str_interval(x):
    """Return a string representation of interval x.
    >>> str_interval(interval(-1, 2))
    '-1 to 2'
    """
    return '{0} to {1}'.format(lower_bound(x), upper_bound(x))

def mul_interval(x, y):
    """Return the interval that contains the product of any value in x and any
    value in y.

    >>> str_interval(mul_interval(interval(-1, 2), interval(4, 8)))
    '-8 to 16'
    """
    p1 = lower_bound(x) * lower_bound(y)
    p2 = lower_bound(x) * upper_bound(y)
    p3 = upper_bound(x) * lower_bound(y)
    p4 = upper_bound(x) * upper_bound(y)
    return interval(min(p1, p2, p3, p4), max(p1, p2, p3, p4))

def add_interval(x, y):
    """Return an interval that contains the sum of any value in interval x and
    any value in interval y.
    >>> str_interval(add_interval(interval(-1, 2), interval(4, 8)))
    '3 to 10'
    """
    lower = lower_bound(x) + lower_bound(y)
    upper = upper_bound(x) + upper_bound(y)
    return interval(lower, upper)

def non_zero(x):
    """Return whether x contains 0."""
    return lower_bound(x) > 0 or upper_bound(x) < 0

def square_interval(x):
    """Return the interval that contains all squares of values in x, where x
    does not contain 0.
    """
    assert non_zero(x), 'square_interval is incorrect for x containing 0'
    return mul_interval(x, x)

# The first two of these intervals contain 0, but the third does not.
seq = (interval(-1, 2), make_center_width(-1, 2), make_center_percent(-1, 50))

zero = interval(0, 0)

def sum_nonzero_with_for(seq):
    """Returns an interval that is the sum of the squares of the non-zero
    intervals in seq, using a for statement.

    >>> str_interval(sum_nonzero_with_for(seq))
    '0.25 to 2.25'
    """
    result = zero
    for interval in seq:
        if  non_zero(interval):
            result = add_interval(result, square_interval(interval))
    if non_zero(result):
        return result

from functools import reduce
def sum_nonzero_with_map_filter_reduce(seq):
    """Returns an interval that is the sum of the squares of the non-zero
    intervals in seq, using using map, filter, and reduce.

    >>> str_interval(sum_nonzero_with_map_filter_reduce(seq))
    '0.25 to 2.25'
    """
    return reduce(add_interval, map(square_interval, filter(non_zero, seq)))

def sum_nonzero_with_generator_reduce(seq):
    """Returns an interval that is the sum of the squares of the non-zero
    intervals in seq, using using reduce and a generator expression.

    >>> str_interval(sum_nonzero_with_generator_reduce(seq))
    '0.25 to 2.25'
    """
    return reduce(add_interval, (square_interval(interval) for interval in seq if non_zero(interval)))

Can we improve this solution?

\$\endgroup\$
2
  • \$\begingroup\$ "improvise" -> "improve"? And, if so, do you have some criteria for what constitutes better? \$\endgroup\$
    – jonrsharpe
    Commented May 1, 2015 at 10:55
  • \$\begingroup\$ DO I need to check extra condition here if non_zero(result). I mean removing unnecessary code apart from 3 representations. \$\endgroup\$ Commented May 1, 2015 at 11:59

1 Answer 1

1
\$\begingroup\$

A few general comments first:

  • Use more descriptive variable names. It’s harder to discern the meaning of single-letter variables. Better names will make it easier to read and follow your code.

    Here’s a simple example:

    def make_center_width(center, width):
        """Construct an interval from center and width."""
        return interval(center - width, center + width)
    

    It’s almost exactly the same, but using the full words make it easier to follow than using c and w.

    If the variable is an interval, I suggest the variable name intvl. You probably don't want to use the name interval, to avoid clashing with the function, and int clashes as well.

  • Use spaces around binary operators. In particular, you need some more spaces in the final line of percent(). (PEP 8: Other recommendations)

  • In your docstrings, I wouldn’t apply str_interval in your examples. The output is just a tuple, e.g.:

    >>> add_interval(interval(-1, 2), interval(4, 8))
    (3, 10)
    

    The output is just as easy to read, but the example is slightly cleaner.

  • Lines should always be 79 characters or less; the final line of sum_nonzero_with_generator_reduce() breaks this rule. (PEP 8: Maximum Line Length)

  • In the docstring of make_center_percent(), you should remove the first space in the docstring. In the docstring of percent(), the word "Return" is misspelt.

It’s not clear which code comes from the assignment, and which is stuff you’ve added independently. I think (if I’ve interpreted correctly) that these are design decisions made in the teacher’s code, but worth mentioning anyway:

  • You have a module import (functools) near the bottom of your code. Module imports should always be at the top of the file; see PEP 8: Imports.

  • Rather than making an interval a tuple, I'd consider creating an Interval class, making upper and lower class properties, and making your functions class methods. I’m not sure if it’s better, but it’s my gut instinct.

And now some comments on your specific functions:

  • In make_center_percent(), you calculate the width and are given the center, so why not use make_center_width() to create the returned interval?

  • In interval(), you do nothing to check that the interval makes sense. What if I enter strings, or have a > b, or drop in some nested tuples? You should do some basic checking of the numbers entered here.

  • Your str_interval() function doesn’t cover the case in which the interval is a singleton: e.g. str_interval(interval(0, 0)) returns 0 to 0. It should really return just 0.

  • In mul_interval(), rather than those four lines of multiplications, which are hard to read, you could instead use itertools.product. Given two tuples, it can compute their Cartesian product as an iterator.

    For example:

    from itertools import product
    
    x = (1, 3)
    y = (2, 4)
    print min(product(x, y)) # (1, 2)
    print max(product(x, y)) # (3, 4)
    

    There’s nothing particularly wrong with your approach; this is just an alternative.

  • The docstring for non_zero() is quite confusing, and it took several attempts to wrap my head around it. In fact, the function tells me if the interval does not contain 0.

    I would create a new function contains_zero(), which tells me whether the function contains 0, then replace all the calls to non_zero() by not contains_zero() instead. Much easier to follow.

    Or, if you take the class approach, then you can define the __contains__ method in your class and use 0 in x. For example:

    >>> x = Interval(-1, 5)
    >>> 0 in x
    True
    >>> 6 in x
    False
    
  • Your sum_nonzero_with_for() relies on the definition of a zero variable outside the function. That’s a bit fragile: it would be better if the zero interval is defined within the function.

    I’m also not sure why you have an if non-zero statement at the end of the function; mathematically, it should be impossible for an interval to contain zero at the end of this function. I’d put an assert here rather than an if statement. You want to spot the impossible behaviour immediately, not return None and wait for that to cause a problem elsewhere.

  • In both _with_map_filter_reduce() and _with_generator_reduce(), you cram some quite complicated expressions into a single line. To make it easier to read, I’d suggest breaking these into several lines with intermediary variables.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.