Program 2

Classes, Overloaded Operators, and Iterators

ICS-33: Intermediate Programming


Introduction This programming assignment is designed first to ensure that you know how to write classes that overload many of the standard Python operators by defining various double-underscore methods. It also ensures that you know how to write classes that implement iterators, by defining an __iter__ method that returns an object that we/Python can call __next__ on. These Iterators are covered near the end of the due date for this project; skip writing these functions (only in the first class) until the material is covered in class, or read ahead.

You should download the program2 project folder and unzip it to produce an Eclipse project with two modules. You will write classes in these modules, which can be tested in the script and using the standard driver using the batch self-check files that I supplied. Eventually you will submit each of these modules you write separately to Checkmate.

I recommend that you work on this assignment in pairs, and I recommend that you work with someone in your lab section (so that you have 4 hours each week of scheduled time together). These are just recommendations. Try to find someone who lives near you, with similar programming skills, and work habits/schedule: e.g., talk about whether you prefer to work mornings, nights, or weekends; what kind of commitment you will make to submit program early.

Only one student should submit all parts of the the assignment, but both students' UCInetID and name should appear in a comment at the top of each submitted .py file. A special grading program reads this information. The format is a comment starting with Submitter and Partner (when working with a partner), followed by a colon, followed by the student's UCInetID (in all lower-case), followed by the student's name in parentheses (last name, comma, first name -capitalized appropriately). If you omit this information, or do not follow this exact form, it will require extra work for us to grade your program, so we will deduct points. Note: if you are submitting by yourself, and do NOT have a partner, you should OMIT the partner line and the "...certify" sentence.

For example if Romeo Montague (whose UCInetID is romeo1) submitted a program that he worked on with his partner Juliet Capulet (whose UCInetID is jcapulet) the comment at the top of each .py file would appear as:

# Submitter: romeo1(Montague, Romeo)
# Partner  : jcapulet(Capulet, Juliet)
# We certify that we worked cooperatively on this programming
#   assignment, according to the rules for pair programming
If you do not know what the terms cooperatively and/or rules for pair programming mean, please read about Pair Programming before starting this assignment. Please turn in each program as you finish it, so that I can more accurately assess the progress of the class as a whole during this assignment.

Print this document and carefully read it, marking any parts that contain important detailed information that you find (for review before you turn in the files). The code you write should be as compact and elegant as possible, using appropriate Python idioms.


Problem #1: Bag Class

Problem Summary:

Write a class that represents and defines methods, operators, and an iterator for the Bag class. Bags are similar to sets, and have similar operations (of which we will implement just the most important) but unlike sets they can store multiple copies of items. We will store the information in bags as dictionaries (I suggest using a defaultdict) whose keys are associated with int values that specify the number of times the key occurs in the Bag. You must store Bags using this one data structure, as specified

Details

  1. Define a class named Bag in a module named bag.py

  2. Define an __init__ method that has one parameter, an iterable of values that initalize the bag. Writing Bag() constructs an empty bag. Writing Bag(['d','a','b','d','c','b','d']) construct a bag with one 'a', two 'b's, one 'c', and three 'd's. Objects in the Bag class should store only the dictionary specified above: it should not store/manipulate any other self variables.

  3. Define a __repr__ method that returns a string, which when passed to eval returns a newly constructed bag with the same value (==) to the object __repr__ was called on. For example, for the Bag in the discussion of __init__ the __repr__ method would print its result as Bag(['a', 'c', 'b', 'b', 'd', 'd', 'd']). Bags like sets are not sorted, so these 7 values can appear in any order. We might require that information in the list is sorted, but not all values we might put in a bag may be ordered (and therefore not sortable): e.g., a bag storing both string and int values, Bag(['a',1]) which is allowed.

    Note: This method is used to test several other methods/operators in the batch self-check file; so it is critical to write it correctly.

  4. Define a __str__ method that returns a string that more compactly shows a bag. For example, for the Bag in the discussion of __init__ the __str__ method would print its result as Bag(a[1], c[1], b[2], d[3]). Bags like sets are not sorted, so these 7 values can appear in any order.

  5. Define a __len__ method that returns the total number of values in the Bag. For example, for the Bag in the discussion of __init__ the __len__ method would return 7.

  6. Define a unique method that returns the number of different (unique) values in the Bag. For example, for the Bag in the discussion of __init__ the unique method would return 4, because there are four different values in the Bag; contrast this method with __len__.

  7. Define a __contains__ method that returns whether or not its argument is in the Bag (one or more times).

  8. Define a count method that returns the number of times its argument is in the Bag: 0 if the argument is not in the Bag.

  9. Define an add method that adds its argument to the Bag: if that value is already in the Bag, its count is incremented by 1; if it is not already in the Bag, it is added to the Bag with a count of 1.

  10. Define an __add__ method that unions its two Bag operands: it returns a new Bag with all the values in Bag operands. For example: str(Bag(['a','b']) + Bag(['b','c'])) should be 'Bag(a[1],b[2],c[1])' Neither Bag operand should change.

  11. Define a remove method that removes its argument from the Bag: if that value is already in the Bag, its count is decremented by 1 (and if the count reduces to 0, the value is removed from the dictionary; if it is not in the Bag, raise a ValueError exception, with an appropriate message that includes the value that could not be removed.

  12. Define __eq__/__ne__ methods that return whether one Bag is equal/not equal to another: contains the same values the same number of times. A Bag is not equal to anything whose type is not a Bag This this method should not change either Bag.

  13. Define an __iter__ method that that returns an object on which next can be called to produce every value in the Bag: all len of them. For example, for the Bag in the discussion of __init__, the following code
      for i in x:
          print(i,end='')
    would print
      acbbddd
    Bags like sets are not sorted, so these 7 values can appear in any order.

    Ensure that the iterator produces those values in the Bag at the time the iterator starts executing; so mutating the Bag during iteration will not affect what values it produces.

    Hint: Write this method as a call to a local generator, passing a copy of the dictionary (covered in Friday's lecture in Week 4).

I have shown only examples of Bags storing strings, because they are convenient to write. But bags can store any type of data. The __repr__, __str__, and __iter__/__next__ methods must be written independently: neither should call the other to get things done.

Testing

The bag.py module includes a script that calls driver.driver(). The project folder contains a bsc1.txt file (examine it) to use for batch-self-checking your class. These are rigorous but not exhaustive tests. Incrementally write and test your class; check each method as you write it.

Note that when exceptions are raised, they are printed by the driver but the Command: prompt sometimes appears misplaced.

You can write other code at the bottom of your bag.py module to test the Bag class, or type code into the driver as illustrated below. Notice the default for each command is the command previously entered.

  Driver started
  Command[!]: from bag import Bag
  Command[from bag import Bag]: b = Bag(['d','a','b','d','c','b','d'])
  Command[b = Bag(['d','a','b','d','c','b','d'])]: print(b)
  Bag(a[1], b[2], c[1], d[3])
  Command[len(b)]: print(len(b))
  7
  Command[print(len(b))]: print(b.count('d'))
  3
  Command[print(b.count('d'))]: quit
  Driver stopped

Problem #1: Dimensional Class (operators)

Problem Summary:

Write a class that represents and defines operators for Dimensional numbers, which are represented by an int or float value and 3 integers representing the dimensions: length, mass, and time. With this class we can write scripts that perform numeric calculations on values of this new numeric type, which automatically keeps track of the dimensions for each value (and prohibits operations that mix dimensions in illegal ways).

For example, to represent the standard acceleration due to gravity at the Earth's surface we might write g = Dimensional(9.8,l=1,t=-2) which represents 9.8 l/t**2: the dimensions are length per time squared (where the unstated length unit is is meters and the unstated time unit is seconds: so this really represents 9.8 m/s**2). Each of the three integers represents the power of that dimension, with positive powers in the numerator and negative powers in the denominator. There are two important rules for operating on Dimensional values.

  1. Whenever we add, subtract or relationally compare Dimensional values, the dimensions of the operands must be identical: e.g., we cannot add 10 meters to 10 seconds and get a meaningful result, and trying to do so should raise the DimensionError (whose definition is supplied in the dimensional.py module).

  2. Whenever we multiply, divide, or raise to a power Dimensional values, the result must have the correct dimensions: e.g., multiplying 9.8 l/t**2 by 10 t produces the result 98.0 l/t: the value is 9.8 * 10 = 98.0 and the dimensions are l/t**2 * t = lt/t**2 = l/t. For multiplication, as an example, the dimensions are added: 9.8(l=1,m=0,t=-2) * 10(l=0,m=0,t=1) = 98.0(l=1,m=0,t=-1) where the resulting dimensions are l = 1+0 = 1, m = 0+0 = 0, and t = -2+1 = -1.

Details

  1. Define a class named Dimensional in a module named dimensional.py

  2. Define an __init__ method that has one required parameter, and optional parameter names in the order l, m, t (whose arguments should be int and whose default values are 0). This method should raise AssertionError(s) if any argument matching one of the optional parameters is not a int.

    Objects in the Dimensional class are immutable: the methods you will write should never bind any instance/self name (except __init__, which initializes them) but exclusively returns newly constructed Dimensional objects with the correct values and dimensions. I suggest using the instance/self names l, m, and t (which mirror the parameter names).

  3. Define a __bool__ method that returns True for any non-zero value.

  4. Define a __len__ method that returns an int that is the sum of the absolute value of all the powers of the dimensions: len(g) returns 3 (2 for length + 1 for time).

  5. Define a __repr__ method that returns a string, which when passed to eval returns a newly constructed rational with the same value (==) to the object __repr__ was called on. As an additiona requirement for __repr__, include named parameters initialized by only non-0 values: repr(Dimensional(9.8,1,0,-2)) should return 'Dimensional(9.8,l=1,t=-2)'

  6. Define a __str__ method that returns a string, with the value followed by a triple of dimensions (in the order l, m, t), including those that are 0: so calling str(Dimensional(9.8,1,0,-2)) should return '9.8(1,0,-2)'.

  7. Define a __getitem__ method that that returns the value, length dimension, mass dimension, time dimension, or a tuple of all three dimensions for the string parameters 'value', 'l', 'm', 't', and 'd' respectively. If any other argument value is used as a key, raise KeyError.

    Hint: I frequently wrote code that made use of the following two facts:

    • g['d'] returns a tuple of the length, mass, and time dimensions (in that order).

    • If g['d'] returns (1,0,-2) then the call Dimension(9.8,*g['d']) is equivalent to the call Dimension(9.8,1,0,-2). That is, * applied to a tuple in a function/method call expands the tuple's value into different argument positions.

  8. Define a method names format that takes as an argument an iterable that produces 3 strings; it represents names of the units for the dimensions: 'mgs' represents meter for length, gram for mass, and second for time; we could also use the argument ['meter','gram','second']. The format function returns a string with the value of the Dimensional followed by a space and its units (if the dimensions are all 0 there is no space), such that (a) each unit appears as unit**power although **power is implicit with a power of 1. (b) if there are negative powers, there is a / followed by the negative powers (which appear in absolute value form); if there are no negative powers, there is no /. (c) if there are no positive powers, and there are negative powers, there is a 1 for the positive powers in the numerator. For example, g.format('mgs') returns 9.8 m/s**2. See the batch-self-check file for more examples.

  9. Define all the underscore methods needed to ensure the prefix +/- work correctly: + returns the same value and dimensions, while - returns the negated value with the same dimensions. Return a Dimension object, not a string.

  10. Define all the underscore methods needed to ensure that the add, subtract, multiply, and divide (/) operators produce the correct answers when their operands are any combination of Dimensional and either int or float values. If Python tries to apply an +/- operator to two Dimensional values with different dimensions, or to an int/float value and a Dimensional value with any non-0 dimensions, raise a DimensionError indicating the mismatched dimensions. See the Problem Summary section for more details. If Python tries to apply an arithmetic operator to a Dimensional and any other type of value (besides Dimensional or int or float) raise the standard TypeError exception with the standard messsage about unsupported operand types: see what 1+'a' produces. Return a Dimension object, not a string.

    Hint: (a) I wrote two static methods to check for correct types and dimensions (one used in the add/subtract methods and one used in the multiply/divide methods); each raises the appropriate exception for any violation. (b) I wrote two static methods that return the value/dimensions for their argument whose result depends on whether the argument is of type int/float or Dimensional. These four methods simplified all my arithmetic methods (which typically included just two statements: one to check the arguments; one to return the computed result (if no exceptions were raised when the arguments were checked).

  11. Define the underscore method that allows us to raise a Dimensional to any positive or negative int power or to any Dimensional power whose dimensions are all 0. Determine both the resulting value and resulting dimensions: the original dimensions multiplied by the power.

  12. Define all the underscore methods needed to ensure that we can compare two Dimensional values with the six standard relational operators, with any combination of Dimensional and int/float. Regardless of its dimensions, Dimensional values compare with any int/float values; but if Python tries to compare two Dimensional values with different dimensions, raise DimensionError (as with addition and subtraction) Note that these rules are a bit different than for +/-, because for +/- int/float would work only with Dimensional values that had all dimensions 0.

  13. Python automatically provides meanings for +=, -=, *=, /=, and **=; ensure they are correct or define the needed underscore methods.

    • Ensure the abs function that works on Dimensional values (the dimensions don't change).
    • Write a sqrt function that works on Dimensional values whose dimensional powers are all even (otherwise raise DimensionError): it should return a Dimensional object with the sqrt of the value and half of all the powers of the dimensions.

  14. Define a __setattr__ method that ensures objects in the Dimensional class cannot store new attributes. The methods you will write should never bind any instance names (except in __init__, which initializes them) but exclusively return newly constructed Dimensional objects with the correct values. If an attempt is made to add new attributes to an object (by defining a new attribute or rebinding an existing attribute), raise an AssertionError with an appropriate message.

    Important: Do not attempt to solve this part of the problem until all other parts are working correctly. If you do not write a __setattr__ method, then Python uses.

    def __setattr__(self,attribute,value):
        self.__dict__[attribute] = value
    DO NOT ever write a __setattr__ method and make its body pass! If you do, whenever you try to set an attribute, it will call this method and nothing will happen: no key in __dict__ will ever be set, so all the tests will likely fail.

Testing

The dimensional.py module includes a script that calls driver.driver(). The project folder contains a bsc2.txt file (examine it) to use for batch-self-checking your class. These are rigorous but not exhaustive tests. Incrementally write and test your class: for example, getting one arithmetic operator working correctly will create a pattern for the others.

Note that when exceptions are raised, they are printed by the driver but the Command: prompt sometimes appears misplaced.

You can write other code at the bottom of your dimensional.py module to test the Dimensional class, or type code into the driver as illustrated below. Notice the default for each command is the command previously entered.

  Driver started
  Command[!]: from dimensional import Dimensional as Dim
  Command[from dimensional import Dimensional as Dim]: g = Dim(9.8,1,0,-2)
  Command[g = Dim(9.8,1,0,-2)]: print(g)
  9.8(1,0,-2)
  Command[print(g)]: print(g.format('mgs'))
  9.8 m/s**2
  Command[print(g.format('mgs'))]: print(10*g)
  98.0(1,0,-2)
  Command[print(10*g)]: print(10+g)
  Traceback (most recent call last):
    File "C:\Users\Pattis\workspace\courselib\driver.py", line 219, in driver
    ...
  dimensional.DimensionError: incompatible dimensions for +: 'int' and '(1, 0, -2)'
  Command[print(10+g)]: quit
  Driver stopped