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 ‣ -
-❝ 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
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 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.)
0 in Roman numerals.
... + +
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. + +
+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) ③
+test_too_large() method has not changed since the previous step. I'm including it here to show where the new code fits.
+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.
+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. + +
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
+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.
+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+ +
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'+
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
+isinstance() function tests whether a variable is a particular type (or, technically, any descendant type).
+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+ + +