diff --git a/advanced-iterators.html b/advanced-iterators.html index 6554828..d56e10b 100644 --- a/advanced-iterators.html +++ b/advanced-iterators.html @@ -10,9 +10,9 @@ body{counter-reset:h1 6}
  

You are here: Home Dive Into Python 3 -

Iterators & Generators

+

Advanced Iterators

-

FIXME
— FIXME +

My ambition is to live to see all of physics reduced to a formula so elegant and simple that it will fit easily on the front of a t-shirt.
Leon Lederman

 

Diving In

diff --git a/dip3.css b/dip3.css index 95c3a23..f35772b 100644 --- a/dip3.css +++ b/dip3.css @@ -65,7 +65,7 @@ abbr{font-variant:small-caps;text-transform:lowercase;letter-spacing:0.1em} p,ul,ol{margin:1.75em 0;font-size:medium} /* basics */ -html{background:white;color:darkslategray} +html{background:#fff;color:#333} body{margin:1.75em 28px} form div{float:right} .c{text-align:center;margin:2.154em 0} diff --git a/mobile.css b/mobile.css index 285bf22..51be0c9 100644 --- a/mobile.css +++ b/mobile.css @@ -58,5 +58,4 @@ h1:before{content:""} /* overrides */ .nm,.w,aside,form,form+p,.note span,.q span{display:none} -ol,ul{margin:0;padding:0 0 0 24px} dd{margin:0 0 0 1.75em} diff --git a/strings.html b/strings.html index c8c31dd..58b0b1c 100644 --- a/strings.html +++ b/strings.html @@ -50,7 +50,7 @@ My alphabet starts where your alphabet ends!
— Dr

Unicode is a system designed to represent every character from every language. Unicode represents each letter, character, or ideograph as a 4-byte number, from 0–4294967295. (That's 232−1.) Each 4-byte number represents a unique character used in at least one of the world's languages. Not all the numbers are used, but more than 65535 of them are, so 2 bytes wouldn't be sufficient. Characters that are used in multiple languages generally have the same number, unless there is a good etymological reason not to. Regardless, there is exactly 1 number per character, and exactly 1 character per number. Every number always means just one thing; there are no “modes” to keep track of. U+0041 is always 'A', even if your language doesn't have an 'A' in it. -

Right away, problems leap out at you. 4 bytes? For every single character That seems awfully wasteful, especially for English and Spanish, which need less than 256 numbers to express every possible character. [FIXME incomplete paragraph] +

Right away, the obvious question should leap out at you. Four bytes? For every single character That seems awfully wasteful, especially for languages like English and Spanish, which need less than 256 numbers to express every possible character. [FIXME incomplete paragraph]

Of course, there is still the matter of all those legacy encoding systems. [FIXME incomplete paragraph] diff --git a/unit-testing.html b/unit-testing.html index 68f923d..fed8eb6 100644 --- a/unit-testing.html +++ b/unit-testing.html @@ -5,6 +5,7 @@ @@ -19,8 +20,8 @@ body{counter-reset:h1 8}

In this chapter, you're going to write and debug a set of utility functions to convert to and from Roman numerals. You saw the mechanics of constructing and validating Roman numerals in “Case study: roman numerals”. Now step back and consider what it would take to expand that into a two-way utility.

The rules for Roman numerals lead to a number of interesting observations:

    -
  1. There is only one correct way to represent a particular number as Roman numerals. -
  2. The converse is also true: if a string of characters is a valid Roman numeral, it represents only one number (that is, it can only be read one way). +
  3. There is only one correct way to represent a particular number as a Roman numeral. +
  4. The converse is also true: if a string of characters is a valid Roman numeral, it represents only one number (that is, it can only be interpreted one way).
  5. There is a limited range of numbers that can be expressed as Roman numerals, specifically 1 through 3999. (The Romans did have several ways of expressing larger numbers, for instance by having a bar over a numeral to represent that its normal value should be multiplied by 1000, but you're not going to deal with that. For the purposes of this chapter, let's stipulate that Roman numerals go from 1 to 3999.)
  6. There is no way to represent 0 in Roman numerals.
  7. There is no way to represent negative numbers in Roman numerals. @@ -329,8 +330,196 @@ OK
    1. Hooray! Both tests pass. Because you worked iteratively, bouncing back and forth between testing and coding, you can be sure that the two lines of code you just wrote were the cause of that one test going from “fail” to “pass.” That kind of confidence doesn't come cheap, but it will pay for itself over the lifetime of your code.
    +

    More Halting, More Fire

    -

    ... + +

    Along with testing numbers that are too large, you need to test numbers that are too small. As we noted in our functional requirements, Roman numerals cannot express 0 or negative numbers. + +

    +>>> import roman2
    +>>> roman2.to_roman(0)
    +''
    +>>> roman2.to_roman(-1)
    +''
    + +

    Well that's not good. Let's add tests for each of these conditions. + +

    [download romantest3.py] +

    
    +class ToRomanBadInput(unittest.TestCase):
    +    def test_too_large(self):
    +        """to_roman should fail with large input"""
    +        self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 4000)  
    +
    +    def test_zero(self):
    +        """to_roman should fail with 0 input"""
    +        self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0)     
    +
    +    def test_negative(self):
    +        """to_roman should fail with negative input"""
    +        self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1)    
    +
      +
    1. The test_too_large() method has not changed since the previous step. I'm including it here to show where the new code fits. +
    2. Here's a new test: the test_zero() method. Like the test_too_large() method, it tells the assertRaises() method defined in unittest.TestCase to call our to_roman() function with a parameter of 0, and check that it raises the appropriate exception, OutOfRangeError. +
    3. The test_negative() method is almost identical, except it passes -1 to the to_roman() function. If either of these new tests does not raise an OutOfRangeError (either because the function returns an actual value, or because it raises some other exception), the test is considered failed. +
    + +

    Now check that the tests fail: + +

    +you@localhost:~$ python3 romantest3.py -v
    +to_roman should give known result with known input ... ok
    +to_roman should fail with negative input ... FAIL
    +to_roman should fail with large input ... ok
    +to_roman should fail with 0 input ... FAIL
    +
    +======================================================================
    +FAIL: to_roman should fail with negative input
    +----------------------------------------------------------------------
    +Traceback (most recent call last):
    +  File "romantest3.py", line 86, in test_negative
    +    self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1)
    +AssertionError: OutOfRangeError not raised by to_roman
    +
    +======================================================================
    +FAIL: to_roman should fail with 0 input
    +----------------------------------------------------------------------
    +Traceback (most recent call last):
    +  File "romantest3.py", line 82, in test_zero
    +    self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0)
    +AssertionError: OutOfRangeError not raised by to_roman
    +
    +----------------------------------------------------------------------
    +Ran 4 tests in 0.000s
    +
    +FAILED (failures=2)
    + +

    Excellent. Both tests failed, as expected. Now let's switch over to the code and see what we can do to make them pass. + +

    [download roman3.py] +

    def to_roman(n):
    +    """convert integer to Roman numeral"""
    +    if not (0 < n < 4000):                                              
    +        raise OutOfRangeError("number out of range (must be 0..3999)")  
    +
    +    result = ""
    +    for numeral, integer in roman_numeral_map:
    +        while n >= integer:
    +            result += numeral
    +            n -= integer
    +    return result
    +
      +
    1. This is a nice Pythonic shortcut: multiple comparisons at once. This is equivalent to if not ((0 < n) and (n < 4000)), but it's much easier to read. This one line of code should catch inputs that are too large, negative, or zero. +
    2. If you change your conditions, make sure to update your human-readable error strings to match. The unittest framework won't care, but it'll make it difficult to do manual debugging if your code is throwing incorrectly-described exceptions. +
    + +

    I could show you a whole series of unrelated examples to show that the multiple-comparisons-at-once shortcut works, but instead I'll just run the unit tests and prove it. + +

    +you@localhost:~$ python3 romantest3.py -v
    +to_roman should give known result with known input ... ok
    +to_roman should fail with negative input ... ok
    +to_roman should fail with large input ... ok
    +to_roman should fail with 0 input ... ok
    +
    +----------------------------------------------------------------------
    +Ran 4 tests in 0.016s
    +
    +OK
    + +

    And One More Thing…

    + +

    There was one more functional requirement for converting numbers to Roman numerals: dealing with non-integers. + +

    +>>> import roman3
    +>>> roman3.to_roman(0.5)  
    +''
    +>>> roman3.to_roman(1.5)  
    +'I'
    +
      +
    1. Oh, that's bad. +
    2. Oh, that's even worse. Both of these cases should raise an exception. Instead, they give bogus results. +
    + +

    Testing for non-integers is not difficult. First, define a NonIntegerError exception. + +

    # roman4.py
    +class OutOfRangeError(ValueError): pass
    +class NotIntegerError(ValueError): pass
    + +

    Next, write a test case that checks for the NonIntegerError exception. + +

    class ToRomanBadInput(unittest.TestCase):
    +    .
    +    .
    +    .
    +    def test_non_integer(self):
    +        """to_roman should fail with non-integer input"""
    +        self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5)
    + +

    Now check that the test fails properly. + +

    +you@localhost:~$ python3 romantest4.py -v
    +to_roman should give known result with known input ... ok
    +to_roman should fail with negative input ... ok
    +to_roman should fail with non-integer input ... FAIL
    +to_roman should fail with large input ... ok
    +to_roman should fail with 0 input ... ok
    +
    +======================================================================
    +FAIL: to_roman should fail with non-integer input
    +----------------------------------------------------------------------
    +Traceback (most recent call last):
    +  File "romantest4.py", line 90, in test_non_integer
    +    self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5)
    +AssertionError: NotIntegerError not raised by to_roman
    +
    +----------------------------------------------------------------------
    +Ran 5 tests in 0.000s
    +
    +FAILED (failures=1)
    + +

    Write the code that makes the test pass. + +

    def to_roman(n):
    +    """convert integer to Roman numeral"""
    +    if not (0 < n < 4000):
    +        raise OutOfRangeError("number out of range (must be 0..3999)")
    +    if not isinstance(n, int):                                          
    +        raise NotIntegerError("non-integers can not be converted")      
    +
    +    result = ""
    +    for numeral, integer in roman_numeral_map:
    +        while n >= integer:
    +            result += numeral
    +            n -= integer
    +    return result
    +
      +
    1. The built-in isinstance() function tests whether a variable is a particular type (or, technically, any descendant type). +
    2. If the argument n is not an int, raise our newly minted NotIntegerError exception. +
    + +

    Finally, check that the code does indeed make the test pass. + +

    +you@localhost:~$ python3 romantest4.py -v
    +to_roman should give known result with known input ... ok
    +to_roman should fail with negative input ... ok
    +to_roman should fail with non-integer input ... ok
    +to_roman should fail with large input ... ok
    +to_roman should fail with 0 input ... ok
    +
    +----------------------------------------------------------------------
    +Ran 5 tests in 0.000s
    +
    +OK
    + + +