diff --git a/.gitignore b/.gitignore index 053ae06c..e6c77b9d 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,7 @@ instance/ # Sphinx documentation docs/_build/ +source/_build/ # PyBuilder target/ @@ -104,4 +105,7 @@ ENV/ .mypy_cache/ # emacs -*.*~ \ No newline at end of file +*.*~ + +# vscode +.vscode/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..b858b1ac --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +dist: trusty + +language: python + +python: "3.6" + +cache: pip + +install: "pip install -r requirements.txt" + +script: + - make html + +deploy: + provider: pages + skip-cleanup: true + github-token: $GITHUB_TOKEN + keep-history: true + local-dir: build/html + on: + branch: master + target-branch: gh-pages diff --git a/README.rst b/README.rst index eea28014..9f08477b 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ Building the docs for uploading to gh-pages The html docs are published by gitHub's gh-pages. This is accomplished by putting all the html in the ``gh-pages`` branch of the repo. -YOu can do that by hand, by copying any new html to the branch, but if you want to do it more often, it's easier to automate it. +You can do that by hand, by copying any new html to the branch, but if you want to do it more often, it's easier to automate it. There is a bash script that will do it for you: ``build_gh_pages.sh``. @@ -54,7 +54,7 @@ It requires a bit of setup: git pull -* Once this is setup, you can run the build_gh_pages script from the name repo, and it should: +* Once this is setup, you can run the build_gh_pages script from the main repo, and it should: - build the html docs - copy that build docs over to the other clone diff --git a/build_gh_pages.sh b/build_gh_pages.sh index d5d3047c..6a8a84c8 100755 --- a/build_gh_pages.sh +++ b/build_gh_pages.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # simple script to build and push to gh-pages # designed to be run from master diff --git a/requirements.txt b/requirements.txt index 29a37631..1b6df6a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -sphinx +sphinx==1.7.7 sphinx-rtd-theme ipython diff --git a/source/class_schedule/session_1_01.rst b/source/class_schedule/session_1_01.rst index 7d924862..85598b2e 100644 --- a/source/class_schedule/session_1_01.rst +++ b/source/class_schedule/session_1_01.rst @@ -40,8 +40,6 @@ In-class Activities ../modules/Git - ../modules/lightning_talks - ../modules/BasicPython ../modules/Py2vsPy3 diff --git a/source/class_schedule/session_1_03.rst b/source/class_schedule/session_1_03.rst index 8e635f7a..3b6d9bce 100644 --- a/source/class_schedule/session_1_03.rst +++ b/source/class_schedule/session_1_03.rst @@ -68,7 +68,7 @@ Mailroom Exercises You've now got the basics of the language down -- enough to write the first full "program": -:ref:`exercise_mailroom` +:ref:`exercise_mailroom_part1` Post-class Activites diff --git a/source/class_schedule/session_1_04.rst b/source/class_schedule/session_1_04.rst index bbf2d141..0f07b0e7 100644 --- a/source/class_schedule/session_1_04.rst +++ b/source/class_schedule/session_1_04.rst @@ -49,7 +49,7 @@ Exercises: * :ref:`exercise_file_lab` - * Update mailroom with dicts and files: :ref:`exercise_mailroom_plus` + * Update mailroom with dicts and files: :ref:`exercise_mailroom_part2_dict_files` * :ref:`exercise_trigrams` diff --git a/source/class_schedule/session_1_05.rst b/source/class_schedule/session_1_05.rst index 7f364b83..c4171b8a 100644 --- a/source/class_schedule/session_1_05.rst +++ b/source/class_schedule/session_1_05.rst @@ -36,7 +36,7 @@ Exercises: 3. Optional: :ref:`exercise_exceptions_lab` -4. Update mailroom with Exceptions: :ref:`exercise_mailroom_exceptions` +4. Update mailroom with Exceptions: :ref:`exercise_mailroom_part3_exceptions` Post-class Activities diff --git a/source/class_schedule/session_1_06.rst b/source/class_schedule/session_1_06.rst index 7fefaa75..5d6a037a 100644 --- a/source/class_schedule/session_1_06.rst +++ b/source/class_schedule/session_1_06.rst @@ -53,7 +53,7 @@ Exercises: Testing mailroom: ................. -:ref:`exercise_mailroom_testing` +:ref:`exercise_mailroom_part4_testing` Write a complete set of unit tests for your mailroom program. diff --git a/source/conf.py b/source/conf.py index b27ab256..ee5d2fbe 100644 --- a/source/conf.py +++ b/source/conf.py @@ -63,10 +63,10 @@ "Christy Heaton", "Jon Jacky", "Maria McKinley", - "Andy Miles" + "Andy Miles", "Rick Riehle", "Joseph Schilz", - "Joseph Sheedy" + "Joseph Sheedy", "Hosung Song" ] diff --git a/source/examples/file_exercise/students.txt b/source/examples/file_exercise/students.txt index dc8508d5..a85853a8 100644 --- a/source/examples/file_exercise/students.txt +++ b/source/examples/file_exercise/students.txt @@ -23,8 +23,8 @@ Mitchell, Joni: php, mysql, python Ramone, John: Johnny, rex, db King, Carol: r Waters, Muddy: perl, python -Star, Richard: Ringo, Tom, shell, python, vb -Smith, Patricia: Patti, Gene, python +Star, Richard: Ringo, shell, python, vb +Smith, Patricia: Patti, python Morrison, Jim: fortran, perl, sql, python Marley, Robert: Bob, c, c++, lisp Simon, Paul: bash, python, sql diff --git a/source/examples/testing/cigar_party.py b/source/examples/testing/cigar_party.py deleted file mode 100644 index e6863f46..00000000 --- a/source/examples/testing/cigar_party.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python - -""" -When squirrels get together for a party, they like to have cigars. -A squirrel party is successful when the number of cigars is between -40 and 60, inclusive. Unless it is the weekend, in which case there -is no upper bound on the number of cigars. - -Return True if the party with the given values is successful, -or False otherwise. -""" - - -def cigar_party(cigars, is_weekend): - pass diff --git a/source/examples/testing/test_cigar_party.py b/source/examples/testing/test_cigar_party.py deleted file mode 100644 index 260d5f47..00000000 --- a/source/examples/testing/test_cigar_party.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python - -""" -When squirrels get together for a party, they like to have cigars. -A squirrel party is successful when the number of cigars is between -40 and 60, inclusive. Unless it is the weekend, in which case there -is no upper bound on the number of cigars. - -Return True if the party with the given values is successful, -or False otherwise. -""" - - -# you can change this import to test different versions -from cigar_party import cigar_party -# from cigar_party import cigar_party2 as cigar_party -# from cigar_party import cigar_party3 as cigar_party - - -def test_1(): - assert cigar_party(30, False) is False - - -def test_2(): - assert cigar_party(50, False) is True - - -def test_3(): - assert cigar_party(70, True) is True - - -def test_4(): - assert cigar_party(30, True) is False - - -def test_5(): - assert cigar_party(50, True) is True - - -def test_6(): - assert cigar_party(60, False) is True - - -def test_7(): - assert cigar_party(61, False) is False - - -def test_8(): - assert cigar_party(40, False) is True - - -def test_9(): - assert cigar_party(39, False) is False - - -def test_10(): - assert cigar_party(40, True) is True - - -def test_11(): - assert cigar_party(39, True) is False diff --git a/source/examples/testing/test_random_pytest.py b/source/examples/testing/test_random_pytest.py index 6250d1b4..b9a65afd 100644 --- a/source/examples/testing/test_random_pytest.py +++ b/source/examples/testing/test_random_pytest.py @@ -8,32 +8,46 @@ import pytest -seq = list(range(10)) +example_seq = list(range(10)) + + +def test_choice(): + """ + A choice selected should be in the sequence + """ + element = random.choice(example_seq) + assert (element in example_seq) + + +def test_sample(): + """ + All the items in a sample should be in the sequence + """ + for element in random.sample(example_seq, 5): + assert element in example_seq def test_shuffle(): - # make sure the shuffled sequence does not lose any elements + """ + Make sure a shuffled sequence does not lose any elements + """ + seq = list(range(10)) random.shuffle(seq) - seq.sort() # IFyou comment this out, it will fail, so you can see output + # seq.sort() # If you comment this out, it will fail, so you can see output print("seq:", seq) # only see output if it fails assert seq == list(range(10)) def test_shuffle_immutable(): + """ + Trying to shuffle an immutable sequence raises an Exception + """ with pytest.raises(TypeError): random.shuffle((1, 2, 3)) - -def test_choice(): - element = random.choice(seq) - assert (element in seq) - - -def test_sample(): - for element in random.sample(seq, 5): - assert element in seq - - def test_sample_too_large(): + """ + Trying to sample more than exist should raise an error + """ with pytest.raises(ValueError): - random.sample(seq, 20) + random.sample(example_seq, 20) diff --git a/source/examples/testing/test_random_unitest.py b/source/examples/testing/test_random_unitest.py index f825be5b..b8a1b712 100644 --- a/source/examples/testing/test_random_unitest.py +++ b/source/examples/testing/test_random_unitest.py @@ -8,7 +8,9 @@ def setUp(self): self.seq = list(range(10)) def test_shuffle(self): - # make sure the shuffled sequence does not lose any elements + """ + make sure the shuffled sequence does not lose any elements + """ random.shuffle(self.seq) self.seq.sort() self.assertEqual(self.seq, list(range(10))) diff --git a/source/examples/testing/test_walnut_party.py b/source/examples/testing/test_walnut_party.py new file mode 100644 index 00000000..62f226dc --- /dev/null +++ b/source/examples/testing/test_walnut_party.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +""" +test code for the walnut party example + +Adapted from the "coding bat" site: https://codingbat.com/python + +When squirrels get together for a party, they like to have walnuts. +A squirrel party is successful when the number of walnuts is between +40 and 60, inclusive. Unless it is the weekend, in which case there +is no upper bound on the number of walnuts. + +Return True if the party with the given values is successful, +or False otherwise. +""" + + +# you can change this import to test different versions +from walnut_party import walnut_party +# from walnut_party import walnut_party2 as walnut_party +# from walnut_party import walnut_party3 as walnut_party + + +def test_1(): + assert walnut_party(30, False) is False + + +def test_2(): + assert walnut_party(50, False) is True + + +def test_3(): + assert walnut_party(70, True) is True + + +def test_4(): + assert walnut_party(30, True) is False + + +def test_5(): + assert walnut_party(50, True) is True + + +def test_6(): + assert walnut_party(60, False) is True + + +def test_7(): + assert walnut_party(61, False) is False + + +def test_8(): + assert walnut_party(40, False) is True + + +def test_9(): + assert walnut_party(39, False) is False + + +def test_10(): + assert walnut_party(40, True) is True + + +def test_11(): + assert walnut_party(39, True) is False diff --git a/source/examples/testing/walnut_party.py b/source/examples/testing/walnut_party.py new file mode 100644 index 00000000..9b195b28 --- /dev/null +++ b/source/examples/testing/walnut_party.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +""" +When squirrels get together for a party, they like to have walnuts. +A squirrel party is successful when the number of walnuts is between +40 and 60, inclusive. Unless it is the weekend, in which case there +is no upper bound on the number of walnuts. + +Return True if the party with the given values is successful, +or False otherwise. +""" + + +def walnut_party(walnuts, is_weekend): + pass diff --git a/source/examples/unicode/ICanEatGlass.utf16.txt b/source/examples/unicode/ICanEatGlass.utf16.txt new file mode 100644 index 00000000..24a0858d Binary files /dev/null and b/source/examples/unicode/ICanEatGlass.utf16.txt differ diff --git a/source/examples/unicode/ICanEatGlass.utf8.txt b/source/examples/unicode/ICanEatGlass.utf8.txt new file mode 100644 index 00000000..9ecba2b9 --- /dev/null +++ b/source/examples/unicode/ICanEatGlass.utf8.txt @@ -0,0 +1,23 @@ +I Can Eat Glass: + +And from the sublime to the ridiculous, here is a certain phrase in an assortment of languages: + +Sanskrit: काचं शक्नोम्यत्तुम् । नोपहिनस्ति माम् ॥ + +Sanskrit (standard transcription): kācaṃ śaknomyattum; nopahinasti mām. + +Classical Greek: ὕαλον ϕαγεῖν δύναμαι· τοῦτο οὔ με βλάπτει. + +Greek (monotonic): Μπορώ να φάω σπασμένα γυαλιά χωρίς να πάθω τίποτα. + +Greek (polytonic): Μπορῶ νὰ φάω σπασμένα γυαλιὰ χωρὶς νὰ πάθω τίποτα. + +Latin: Vitrum edere possum; mihi non nocet. + +Old French: Je puis mangier del voirre. Ne me nuit. + +French: Je peux manger du verre, ça ne me fait pas mal. + +Provençal / Occitan: Pòdi manjar de veire, me nafrariá pas. + +Québécois: J'peux manger d'la vitre, ça m'fa pas mal. \ No newline at end of file diff --git a/source/examples/unicode/exception_test.py b/source/examples/unicode/exception_test.py new file mode 100755 index 00000000..975f1df8 --- /dev/null +++ b/source/examples/unicode/exception_test.py @@ -0,0 +1,16 @@ +#!/usr/bin/python + +""" +example for what happens when you pass non-ascii unicode to a Exception +""" + +msg = u'This is an ASCII-compatible unicode message' + +#msg = u'This is an non ASCII\N{EM DASH}compatible unicode message' + +print "\nDo you see this message in the Exception report?\n" +print msg +print + +raise ValueError(msg) + diff --git a/source/examples/unicode/hello_unicode.py b/source/examples/unicode/hello_unicode.py new file mode 100644 index 00000000..6bbad1de --- /dev/null +++ b/source/examples/unicode/hello_unicode.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +hello = 'Hello ' +world = u'世界' + +print hello + world + +print u"It was nice weather today: it reached 80\u00B0" + +print u"Maybe it will reach 90\N{degree sign}" + +print u"It is extremely rare for it ever to reach 100° in Seattle" diff --git a/source/examples/unicode/latin1_test.py b/source/examples/unicode/latin1_test.py new file mode 100644 index 00000000..3990078f --- /dev/null +++ b/source/examples/unicode/latin1_test.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +""" +An example of using latin-1 as a universal encoding + +latin-1 is a superset of ASCII that is suitable for western european languages. + +Is the most common, and a good default, if you need a one-byte per char encoding +for European text. + +It also has a nice property: + : every byte value from 0 to 255 is avalid charactor + +Thus you will never get an UnicodeDecodeError if +you try to decode arbitrary bytes with latin-1. + +And it can "round-trip" trhough a unicode object. + +This can be useful is you don't know the encoding -- at least it won't break. +It's also useful if you need to work with cobined text+binary data. + + + +""" + +# all the byte values in a bytes (str) object: +all_bytes = ''.join( [chr(i) for i in range(255)] ) + +print type(all_bytes) +print len(all_bytes) + +print "Example value: 20" +print ord(all_bytes[20]) == 20 +print "Example high value: 245" +print ord(all_bytes[245]) == 245 + +# now decode it to a unicode object: +try: + uni = all_bytes.decode() +except UnicodeDecodeError: + print "OOPS: can't decode with default encoding" + +# latin-1 works: +try: + all_uni = all_bytes.decode('latin-1') + print "Yup -- that worked" + print all_uni + print "note that the ASCII subset is the same..." +except UnicodeDecodeError: + print "OOPS: This should have worked!!" + raise + +## now show that it round-trips: +all_bytes2 = all_uni.encode('latin-1') + +if all_bytes2 == all_bytes: + print "yup -- that worked...the values are preserved on the round trip." +else: + print "Hey, that should have worked" + + + + + + + diff --git a/source/examples/unicode/text.utf16 b/source/examples/unicode/text.utf16 new file mode 100644 index 00000000..b80b2efc Binary files /dev/null and b/source/examples/unicode/text.utf16 differ diff --git a/source/examples/unicode/text.utf32 b/source/examples/unicode/text.utf32 new file mode 100644 index 00000000..c5295310 Binary files /dev/null and b/source/examples/unicode/text.utf32 differ diff --git a/source/examples/unicode/text.utf8 b/source/examples/unicode/text.utf8 new file mode 100644 index 00000000..9de18890 --- /dev/null +++ b/source/examples/unicode/text.utf8 @@ -0,0 +1,17 @@ +Origin (in native language) Name (in native language) +Հայաստան Արամ Խաչատրյան + Australia Nicole Kidman + Österreich Johann Strauß + Azərbaycan Vaqif Səmədoğlu + Азәрбајҹан Вагиф Сәмәдоғлу + Azərbaycan Heydər Əliyev + Азәрбајҹан Һејдәр Әлијев + België René Magritte + Belgique René Magritte + Belgien René Magritte + বাংলা সুকুমার রায় + འབྲུག་ཡུལ། མགོན་པོ་རྡོ་རྗེ། + ប្រទេស​​​កម្ពុជា ព្រះ​ពុទ្ឋឃោសាចារ‌្យ​ជួន​ណាត +Canada Céline Dion + ᓄᓇᕗᒻᒥᐅᑦ ᓱᓴᓐ ᐊᒡᓗᒃᑲᖅ + \ No newline at end of file diff --git a/source/examples/unicode/unicodify.py b/source/examples/unicode/unicodify.py new file mode 100644 index 00000000..15683ee6 --- /dev/null +++ b/source/examples/unicode/unicodify.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +''' +Decorators to convert all arguments passed to a function or method to +unicode or str, including default arguments + +From: http://axialcorps.com/2014/03/20/unicode-str/ + +''' + + +import sys +import functools +import inspect + +def _convert_arg(arg, from_, conv, enc): + '''Safely convert unicode to string or string to unicode''' + return getattr(arg, conv)(encoding=enc) if isinstance(arg, from_) else arg + +def _wrap_convert(from_type, fn, encoding=None): + '''Decorate a function converting all str arguments to unicode or + vice-versa''' + conv = 'decode' if from_type is str else 'encode' + encoding = encoding or sys.getdefaultencoding() + + # override string defaults using partial + aspec, dflts = inspect.getargspec(fn), {} + if aspec.defaults: + for k,v in zip(aspec.args[-len(aspec.defaults):],aspec.defaults): + dflts[k] = _convert_arg(v, from_type, conv, encoding) + fn = functools.partial(fn, **dflts) + + @functools.wraps(fn.func if isinstance(fn, functools.partial) else fn) + def converted(*args, **kwargs): + args = [_convert_arg(a, from_type, conv, encoding) for a in args] + for k,v in kwargs.iteritems(): + kwargs[k] = _convert_arg(v, from_type, conv, encoding) + return fn(*args, **kwargs) + + return converted + +def unicodify(fn=None, encoding=None): + '''Convert all str arguments to unicode''' + if fn is None: + return functools.partial(unicodify, encoding=encoding) + return _wrap_convert(str, fn, encoding=encoding) + +def stringify(fn=None, encoding=None): + '''Convert all unicode arguments to str''' + if fn is None: + return functools.partial(stringify, encoding=encoding) + return _wrap_convert(unicode, fn, encoding=encoding) + +__all__ = ['unicodify', 'stringify'] \ No newline at end of file diff --git a/source/exercises/args_kwargs_lab.rst b/source/exercises/args_kwargs_lab.rst index 155dc188..0273c287 100644 --- a/source/exercises/args_kwargs_lab.rst +++ b/source/exercises/args_kwargs_lab.rst @@ -1,7 +1,7 @@ .. _exercise_args_kwargs_lab: -Args and Kwargs LAB -=================== +``args`` and ``kwargs`` LAB +=========================== Goal: ----- @@ -12,33 +12,31 @@ If this is all confusing -- you may want to review this: http://stupidpythonideas.blogspot.com/2013/08/arguments-and-parameters.html -Note: +.. note:: -This is not all that clearly specified -- the goal is for you to -experiment with various ways to define and call functions, so you -can understand what's possible, and what happens with each call. -It is also entirely silly, since the function does not do anything -at all, but it will teach you about using parameters effectively. + This is not all that clearly specified -- the goal is for you to + experiment with various ways to define and call functions, so you + can understand what's possible, and what happens with each call. + It is also entirely silly, since the function does not do anything + at all, but it will teach you about using parameters effectively. Test Driven Development? ------------------------ -Since this code isn't really going to do anything, it doesn't make a lot of sense to test it. However, you need to run the code somehow anyway. So this is a good chance to practice test-driven development anyway. +Since this code isn't really going to do anything, it doesn't make a lot of sense to test it. However, you need to run the code somehow. So this is a good chance to practice test-driven development -- even if only as a way to run your code as you write it. -So for each step of the exercise, write a test that calls your function in a particular way, and test that it returns what you expect. In this case, what you will be testing is not really the code -- but rather your own expectations of what the results should be. +For each step of the exercise, write a test that calls your function in a particular way, and test that it returns what you expect. In this case, what you will be testing is not really the code -- but rather your own expectations of what the results should be. You will also be testing Python's argument handling, which you can be pretty sure DOES work correctly. -So while these won't be useful tests in the usual sense, this is a chance to practice test driven development. +So while these won't be useful tests in the usual sense, this is a chance to get used to test driven development. Procedure --------- -We are going to do this as test driven development. So your first task for -each assignment below is to write a test that will ensure your code does what -we are telling you it should do. +We are going to do this as test driven development: Your first task for each step below is to write a test that will ensure your code does whatvwe are telling you it should do. **Keyword arguments:** @@ -49,15 +47,15 @@ we are telling you it should do. - `link_color` - `visited_color` -* Have it return the colors (use strings for the colors) +* Have it return the colors (use strings for the colors, e.g. "blue", "red", etc.) -* Call it with a couple different parameters set. IOW, write tests that verify that all of the following work as advertised: +* Call it with a couple different parameters set. That is, write tests that verify that all of the following work as advertised: - - using just positional arguments: + - Using just positional arguments: - ``func('red', 'blue', 'yellow', 'chartreuse')`` - - using just keyword arguments: + - Using just keyword arguments: - ``func(link_color='red', back_color='blue')`` @@ -80,10 +78,11 @@ we are telling you it should do. ``*args`` and ``**kwargs`` -* Have it return the colors (use strings for the colors) +* Have it return the colors (use strings for the colors again) * Call it with the same various combinations of arguments used above. -* Also have it print `args` and `kwargs` directly, so you can be sure you understand what's going on. +* Also have it print ``args`` and ``kwargs`` directly, so you can be sure you understand what's going on. * Note that in general, you can't know what will get passed into ``**kwargs`` So maybe adapt your function to be able to do something reasonable with any keywords. + diff --git a/source/exercises/circle_class.rst b/source/exercises/circle_class.rst index a94310d5..b2c47af9 100644 --- a/source/exercises/circle_class.rst +++ b/source/exercises/circle_class.rst @@ -32,8 +32,7 @@ You will use: - properties. - a bunch of "magic methods". - - a classmethod (after you've learned about them...). - + - a classmethod. General Instructions: --------------------- @@ -121,25 +120,25 @@ The user should not be able to set the area: Step 5: ------- -**NOTE:** wait on this one 'till we learn about class methods.. - Add an "alternate constructor" that lets the user create a Circle directly with the diameter: .. code-block:: python >> c = Circle.from_diameter(8) - >> print c.diameter + >> print(c.diameter) 8 - >> print c.radius + >> print(c.radius) 4 +Hint: This is a good use case for a ``classmethod`` + Step 6: ------- Every class should have a nice way to print it out... -Add __str__ and __repr__ methods to your Circle class. +Add ``__str__`` and ``__repr__`` methods to your Circle class. Now you can print it: @@ -147,7 +146,7 @@ Now you can print it: In [2]: c = Circle(4) - In [3]: print c + In [3]: print(c) Circle with radius: 4.000000 In [4]: repr(c) @@ -214,9 +213,6 @@ Once the comparing is done, you should be able to sort a list of circles: In [18]: print circles [Circle(6), Circle(7), Circle(8), Circle(4), Circle(0), Circle(2), Circle(3), Circle(5), Circle(9), Circle(1)] - In [19]: circl - circle circle.py circle.pyc circles - In [19]: circles.sort() In [20]: print circles diff --git a/source/exercises/context-managers-exercise.rst b/source/exercises/context-managers-exercise.rst index 2c826466..67e2b505 100644 --- a/source/exercises/context-managers-exercise.rst +++ b/source/exercises/context-managers-exercise.rst @@ -22,7 +22,7 @@ run all the code inside the context: ...: This code took 0.206805 seconds -NOTE: the time module has what you need: +NOTE: the ``time`` module has what you need: .. code-block:: python @@ -30,7 +30,7 @@ NOTE: the time module has what you need: start = time.clock() # some code here - elapsed = time.clock() = start + elapsed = time.clock() - start ``time.clock()`` returns the number of seconds that this process has been running. You can also use ``time.time()``, which gives the "wall time", rather than the process time. ``time()`` will vary more depending on how busy the system is. But you may want to use it if you want to measure how long it takes to download something, for instance. @@ -38,13 +38,13 @@ Extra Credit ------------ Allow the ``Timer`` context manager to take a file-like -object as an argument (the default should be sys.stdout). The results of the +object as an argument (the default should be ``sys.stdout``). The results of the timing should be printed to the file-like object. You could also pass in a name for this particular context, so the message in the file-like object is labeled -- kind of a poor man's logging system. Extra Extra Credit ------------------ -Implement this a a generator, wrapped by the: +Implement this as a generator, wrapped by the: ``contextlib.contextmanager`` @@ -101,7 +101,7 @@ tests fail when an assert fails: assert some_expression, "a message" -you get a failure when some_expression evaluates as false. +you get a failure when ``some_expression`` evaluates as false. This is more-or-less the same as this code: @@ -110,7 +110,7 @@ This is more-or-less the same as this code: if some_expression: raise AssertionError("a message") -The reason it exists is not so much to save a bit of typing (though that's nice), but that assertions are designed for tests, ans thus can be turned of for an entire python process -- and, indeed are turned off when you turn on optimization. +The reason it exists is not so much to save a bit of typing (though that's nice), but that assertions are designed for tests, and thus can be turned off for an entire python process -- and, indeed are turned off when you turn on optimization. So in your context manager, you can raise an AssertionError, or force one with an assert: diff --git a/source/exercises/dict_lab.rst b/source/exercises/dict_lab.rst index 12ae4ab4..9c863c54 100644 --- a/source/exercises/dict_lab.rst +++ b/source/exercises/dict_lab.rst @@ -15,7 +15,7 @@ Learn the basic ins and outs of Python dictionaries and sets. Procedure --------- -In your student dir in the class repo, create a ``session04`` dir and put in a new ``dict_lab.py`` file. +In your student dir in the class repo, create a ``lesson04`` dir and put in a new ``dict_lab.py`` file. The file should be an executable Python script. That is to say that one should be able to run the script directly like so: @@ -28,6 +28,8 @@ should be able to run the script directly like so: To make this work you, make sure you include the 'shebang' on the first line of your file. +.. code-block:: bash + #!/usr/bin/env python3 @@ -74,11 +76,18 @@ Dictionaries 2 * Using the dictionary from item 1: Make a dictionary using the same keys but with the number of 't's in each value as the value (consider upper and lower case?). + The result should look something like:: + + {"name": 0 + "city": 2 + "cake": 2 + } + Sets ---- * Create sets s2, s3 and s4 that contain numbers from zero through twenty, - divisible by 2, 3 and 4. + divisible by 2, 3 and 4 (figure out a way to compute those -- don't just type them in). * Display the sets. @@ -93,5 +102,5 @@ Sets 2 * Create a frozenset with the letters in 'marathon'. -* display the union and intersection of the two sets. +* Display the union and intersection of the two sets. diff --git a/source/exercises/except_exercise.rst b/source/exercises/except_exercise.rst index 62104ccf..c4b0fdc1 100644 --- a/source/exercises/except_exercise.rst +++ b/source/exercises/except_exercise.rst @@ -9,7 +9,7 @@ This is a little exercise that shows you how to handle exceptions in a way that Procedure ========= -Here are two files that you should put in your ``session05`` directory in the class repo. +Here are two files that you should put in your ``lesson05`` directory in the class repo. :download:`except_exercise.py` diff --git a/source/exercises/fib_and_lucas.rst b/source/exercises/fib_and_lucas.rst index 189aaa29..b0243936 100644 --- a/source/exercises/fib_and_lucas.rst +++ b/source/exercises/fib_and_lucas.rst @@ -12,12 +12,15 @@ Goal: The `Fibonacci Series`_ is a numeric series starting with the integers 0 and 1. -In this series, the next integer is determined by summing the previous two. +In this series, the next integer is determined by summing the previous two + This gives us:: 0, 1, 1, 2, 3, 5, 8, 13, ... +.. note: 0+1 is 1; 1+1 is 2; 1+2 is 3; 2+3 is 5; 3+5 is 8; and so on forever... + We will write a function that computes this series -- then generalize it. .. _Fibonacci Series: http://en.wikipedia.org/wiki/Fibbonaci_Series @@ -25,18 +28,17 @@ We will write a function that computes this series -- then generalize it. Step 1 ------ -* Create a new module ``series.py`` in the ``session02`` folder in your student folder. +* Create a new module ``series.py`` in the ``lesson02`` folder in your student folder. - In it, add a function called ``fibonacci``. - The function should have one parameter ``n``. - - The function should return the ``nth`` value in the fibonacci series - (starting with zero index). + - The function should return the ``nth`` value in the fibonacci series (starting with zero index). * Ensure that your function has a well-formed ``docstring`` -Note that the fibinacci series is naturally recursive -- the value is +Note that the fibonacci series is naturally recursive -- the value is defined by previous values: fib(n) = fib(n-2) + fib(n-1) @@ -58,50 +60,58 @@ In your ``series.py`` module, add a new function ``lucas`` that returns the Ensure that your function has a well-formed ``docstring`` +YOu should find it's *very* similar to the ``fibonacci()`` function. + Generalizing ------------ -Both the *fibonacci series* and the *lucas numbers* are based on an identical -formula. +Both the *fibonacci series* and the *lucas numbers* are based on an identical formula: + +fib(n) = fib(n-2) + fib(n-1) + +That's why the code is so similar. + +This formula creates a class of series that are all related -- each with a different two starting numbers. + +Add a third function called ``sum_series`` that can compute all of these related series. + +It should have one required parameter and two optional parameters. +The required parameter will determine which element in the +series to print. +The two optional parameters will have default values of 0 and 1 and will determine the first two values for the series to be produced. + +Calling this function with no optional parameters will produce numbers from the *fibonacci series* (because 0 and 1 are the defaults). -Add a third function called ``sum_series`` with one required parameter and two -optional parameters. The required parameter will determine which element in the -series to print. The two optional parameters will have default values of 0 and -1 and will determine the first two values for the series to be produced. +Calling it with the optional arguments 2 and 1 will +produce values from the *lucas numbers*. -Calling this function with no optional parameters will produce numbers from the -*fibonacci series*. Calling it with the optional arguments 2 and 1 will -produce values from the *lucas numbers*. Other values for the optional -parameters will produce other series. +Other values for the optional parameters will produce other series. **Note:** While you *could* check the input arguments, and then call one of the functions you wrote, the idea of this exercise is to make a general -function, rather than one specialized. So you should reimplement the code +function, rather than one specialized. So you should re-implement the code in this function. -In fact, you could go back and reimplement your fibonacci and lucas -functions to call this one with particular arguments. +In fact, you could go back and re-implement your fibonacci and lucas +functions to call ``sum-series`` with particular arguments. Ensure that your function has a well-formed ``docstring`` Tests... -------- -Add a block of code to the end of your ``series.py`` -module. Use the block to write a series of ``assert`` statements that +Add a block of code to the end of your ``series.py`` module. +Use the block to write a series of ``assert`` statements that demonstrate that your three functions work properly. Use comments in this block to inform the observer what your tests do. -We have created a template for you to use, to clarify what we mean by these -tests: +We have created a template for you to use to clarify what we mean by these asserts: :download:`series_template.py <../exercises/series_template.py>` -Add your new module to your git clone and commit frequently while working on -your implementation. Include good commit messages that explain concisely both -*what* you are doing and *why*. +Add your new module to your personal git repo and commit frequently while working on your implementation. +Include good commit messages that explain concisely both *what* you are doing and *why*. -When you are finished, push your changes to your fork of the class repository -in GitHub and make a pull request. +When you are finished, push your changes to your fork of the class repository in GitHub and make a pull request. diff --git a/source/exercises/file_lab.rst b/source/exercises/file_lab.rst index a7b26bb3..9aa4dfd9 100644 --- a/source/exercises/file_lab.rst +++ b/source/exercises/file_lab.rst @@ -16,20 +16,22 @@ Paths and File Processing ========================= * Write a program which prints the full path for all files in the current - directory, one per line + directory, one per line. Use either the ``os`` module or ``pathlib``. * Write a program which copies a file from a source, to a destination - (without using shutil, or the OS copy command). - - - Advanced: make it work for any size file: i.e. don't read the entire - contents of the file into memory at once. + (without using shutil, or the OS copy command (you are essentially writing a simple version of the OS copy command)). - This should work for any kind of file, so you need to open the files in binary mode: ``open(filename, 'rb')`` (or ``'wb'`` for writing). Note that for binary files, you can't use ``readline()`` -- lines don't have any meaning for binary files. - - Test it with both text and binary files (maybe jpeg or something of your choosing). + - Test it with both text and binary files (maybe a jpeg or something of your choosing). + + - Advanced: make it work for any size file: i.e. don't read the entire + contents of the file into memory at once. + + - This should only be a few lines of code :-) File reading and parsing @@ -41,9 +43,9 @@ Download this text file: In it, you will find a list of names and what programming languages they have used in the past. This may be similar to a list generated at the beginning of this class. -Write a little script that reads that file, and generates a list of all the languages that have been used. +Write a little script that reads that file and generates a list of all the languages that have been used. -What might be the best data structure to use to keep track of bunch of values without duplication? +What might be the best data structure to use to keep track of bunch of values (the languages) without duplication? The file format: ---------------- @@ -60,5 +62,7 @@ So a colon after the name, then the nickname, and then one or more languages. However, like real data files, the file is NOT well-formed. Only some lines have nicknames, and other small differences, so you will need to write some code to make sure you get it all correct. +How can you tell the difference between a nickname and a language? + Extra challenge: keep track of how many students specified each language. diff --git a/source/exercises/grid_printer.rst b/source/exercises/grid_printer.rst index 861869ff..87804fdc 100644 --- a/source/exercises/grid_printer.rst +++ b/source/exercises/grid_printer.rst @@ -128,7 +128,7 @@ One of the points of writing functions is so you can write code that does simila Write a function ``print_grid(n)`` that takes one integer argument and prints a grid just like before, *BUT* the size of the grid is given by the argument. -For example, ``print_grid(11)`` prints the grid at the top of this page. +For example, ``print_grid(9)`` prints the grid at the top of this page. ``print_grid(3)`` would print a smaller grid:: diff --git a/source/exercises/html_renderer.rst b/source/exercises/html_renderer.rst index 8175b30b..8761e90d 100644 --- a/source/exercises/html_renderer.rst +++ b/source/exercises/html_renderer.rst @@ -30,7 +30,7 @@ If you don't know html -- just look at the example and copy that. And you can re The exercise is broken down into a number of steps -- each requiring a few more OO concepts in Python. -The goal of the code is render html. The goal of the *exercise* is to build up a simple object hierarchy with: +The goal of the code is to render html. The goal of the *exercise* is to build up a simple object hierarchy with: * classes * class attributes @@ -219,7 +219,7 @@ Part B: Now it gets fun! -Now that you have multipel types of elements, it's worth looking a bit at how html works. A given element can hold text, but it can *also* hold other elements. So we need to update our ``Element`` classes to support that. +Now that you have multiple types of elements, it's worth looking a bit at how html works. A given element can hold text, but it can *also* hold other elements. So we need to update our ``Element`` classes to support that. Extend the ``Element.render()`` method so that it can render other elements inside the tag in addition to strings. A recursion-like approach should do it. i.e. it can call the ``render()`` method of the elements it contains. diff --git a/source/exercises/html_renderer_tutorial.rst b/source/exercises/html_renderer_tutorial.rst index d2459ffa..762688c0 100644 --- a/source/exercises/html_renderer_tutorial.rst +++ b/source/exercises/html_renderer_tutorial.rst @@ -252,7 +252,7 @@ In this case, the "html" part is stored in a class attribute. So how would you m "<{}>".format(self.tag) -and +and:: "".format(self.tag) @@ -441,7 +441,7 @@ Darn! Something is wrong here. And this time it errored out before it even got r It failed when we tried to write to the file. We're trying to write a piece of content, and we got a ``NoneType``. How in the world did a ``NoneType`` (which is the type of None) get in there? -Where does the ``self.contents`` list get created? In the ``__init__``. Let's do a little print debugging here. Add a print to the __init__: +Where does the ``self.contents`` list get created? In the ``__init__``. Let's do a little print debugging here. Add a print to the ``__init__``: .. code-block:: python @@ -460,9 +460,9 @@ And run the tests again:: ====================== 1 failed, 3 passed in 0.06 seconds ====================== -Same failure -- but pytest does a nice job of showing you what was printed (stdout) when a test fails. So in this case, at the end of the ``__init__`` method, the contents list looks like ``[None]`` -- a list with a single None object in it. No wonder it failed later when we tried to write that None to a file! +Same failure -- but pytest does a nice job of showing you what was printed (stdout) when a test fails. So in this case, at the end of the ``__init__`` method, the contents list looks like ``[None]`` -- a list with a single None object in it. No wonder it failed later when we tried to write that ``None`` to a file! -But why? Well, looking at the ``__init__``, it looks like content gets set to None by default:: +But why? Well, looking at the ``__init__``, it looks like content gets set to None by default: .. code-block:: python @@ -561,13 +561,17 @@ OK, did you do something as simple as this? That's it! But what does that mean? This line: -``class Body(Element):`` +.. code-block:: python + + class Body(Element): -means: make a new subclass of the ``Element`` tag called "Body". +means: make a new subclass of the ``Element`` tag called ``Body``. and this line: -`` tag = 'body'`` +.. code-block:: python + + tag = 'body' means: set the ``tag`` class attribute to ``'body'``. Since this class attribute was set on the ``Element`` class already, this is called "overriding" the tag attribute. @@ -585,7 +589,7 @@ Let's run the tests and see if this worked:: =========================== 7 passed in 0.02 seconds =========================== -Success!. We now have three different tags. +Success! We now have three different tags. .. note:: Why the ``Html`` element? Doesn't the ``Element`` class already use the "html" tag? @@ -681,7 +685,7 @@ So we need a way to write an element to a file. How might we do that? Inside the Well, elements already know how to render themselves. This is what is meant by a recursive approach. In the ``render`` method, we want to make use of the ``render`` method itself. -Looking at the signature of the render method:: +Looking at the signature of the render method: .. code-block:: python @@ -827,7 +831,7 @@ Now we are getting a little more interesting. This is easy; you know how to do that, yes? -But the training wheels are off -- you are going to need to write your own tests now. So before you create the ``Head`` element class, write a test for it. You should be able to copy and paste one of the previous tests, and just change the name of the class and the tag value. Remember to give yo test a new name, or it will simply replace the previous test. +But the training wheels are off -- you are going to need to write your own tests now. So before you create the ``Head`` element class, write a test for it. You should be able to copy and paste one of the previous tests, and just change the name of the class and the tag value. Remember to give your test a new name, or it will simply replace the previous test. I like to run the tests as soon as I make a new one. If nothing else, I can make sure I have one more test. @@ -1057,7 +1061,6 @@ OK, I've got 11 tests passing now. How about you? Time for the next step. .. _render_tutorial_4: - Step 4. ------- @@ -1170,7 +1173,7 @@ So we need to render the ``<``, then the ``p``, then a bunch of attribute name=v out_file.write("".join(open_tag)) ... -OK, the rest of the tests are still passing for me; I haven't broken anything else. Now to add code to render the attributes. You'll need to write some sort of loop to loop through each attribute, probably looping through the keys and the values:: +OK, the rest of the tests are still passing for me; I haven't broken anything else. Now to add code to render the attributes. You'll need to write some sort of loop to loop through each attribute, probably looping through the keys and the values: .. code-block:: python @@ -1255,7 +1258,13 @@ Step 5: Create a ``SelfClosingTag`` subclass of Element, to render tags like:: -
and
(horizontal rule and line break)." +
+ +and:: + +
+ +(horizontal rule and line break) Including with attributes:: @@ -1591,7 +1600,9 @@ You've gotten to the end of the project. By going through this whole procedure, * overriding: - class attributes + - class attributes with instance attributes + - methods * calling a superclass' method from an overridden method. diff --git a/source/exercises/kata_fourteen.rst b/source/exercises/kata_fourteen.rst index a2ad4f5f..999084a7 100644 --- a/source/exercises/kata_fourteen.rst +++ b/source/exercises/kata_fourteen.rst @@ -212,7 +212,7 @@ Building the Trigrams dict So you've got a list of words, and you need to build up a dict like one of the above. -It time to create a python file and start writting some code! +It's time to create a python file and start writting some code! .. code-block:: python @@ -244,7 +244,7 @@ So how do you actually build up that dict? That's kind of the point of the exerc **Looping through the words** -Obviously you need to loop through all the words, so a ``for loop`` makes sense. However, this is a bit tricky. Usually in Python you loop through all the items in a list, and don't worry about the indices: +Obviously you need to loop through all the words, so a ``for`` loop makes sense. However, this is a bit tricky. Usually in Python you loop through all the items in a list, and don't worry about the indices: .. code-block:: python @@ -256,7 +256,7 @@ So contrary to the usual practice, an index can be helpful here: .. code-block:: python - for i in len(words)-2: # why -2 ? + for i in range(len(words)-2): # why -2 ? pair = words[i:i + 2] follower = words[i + 2] @@ -264,7 +264,7 @@ So contrary to the usual practice, an index can be helpful here: For each pair in the text, you need to add it to the dict. But: -- words[i:i + 2] is a list with two words in it. Can that be used as a key in a dict? (Try it.) If not, how can you make a valid key out of it? +- ``words[i:i + 2]`` is a list with two words in it. Can that be used as a key in a dict? (Try it.) If not, how can you make a valid key out of it? - As you loop through the text, you will collect pairs of words. Each time, a given pair may already be in the dict. @@ -272,7 +272,7 @@ For each pair in the text, you need to add it to the dict. But: ("may", "I"): ["wish"] - - If the pair already is in the dict, then you want to add the follower (the second word in the pair) to the list that's already there + - If the pair already is in the dict, then you want to add the follower (the second word in the pair) to the list that's already there:: ("wish", "I"): ["may", "might"] @@ -294,7 +294,7 @@ Using the Trigrams dict This is the fun part. Once you have a mapping of word pairs to following words, you can build up some new "fake" text. Re-read the previous sections again to remind yourself of the procedure. Here are a couple of additional hints and questions to consider: -- The ```random`` module `_ is your friend here: +- The ``random`` module is your friend here: .. code-block:: python @@ -357,7 +357,7 @@ Do get the full trigrams code working first, then play with some of the fancier Code Structure -------------- -Break your code down into a handful of separate functions. This way you can test each on its own, and it's easier to refactor one part without messing with the others. For instance, your __main__ block might look something like: +Break your code down into a handful of separate functions. This way you can test each on its own, and it's easier to refactor one part without messing with the others. For instance, your ``__main__`` block might look something like: .. code-block:: python diff --git a/source/exercises/list_lab.rst b/source/exercises/list_lab.rst index 3e787fb9..f4c78606 100644 --- a/source/exercises/list_lab.rst +++ b/source/exercises/list_lab.rst @@ -14,7 +14,7 @@ Goal: Learn the basic ins and outs of Python lists. -hint +Hint ---- to query the user for info at the command line, you use: @@ -29,7 +29,7 @@ to query the user for info at the command line, you use: Procedure --------- -In your student dir in the class repo, create a ``session03`` dir and put in a new ``list_lab.py`` file. +In your student dir in the class repo, create a ``lesson03`` dir and put in a new ``list_lab.py`` file. The file should be an executable Python script. That is to say that one should be able to run the script directly like so: @@ -114,6 +114,6 @@ Series 4 Once more, using the list from series 1: -- Make a copy of the list and reverse the letters in each fruit in the copy. -- Delete the last item of the original list. Display the original list and the - copy. +- Make a new list with the contents of the original, but with all the letters in each item reversed. + +- Delete the last item of the original list. Display the original list and the copy. diff --git a/source/exercises/mailroom-oo.rst b/source/exercises/mailroom-oo.rst index fc377b80..9df501ad 100644 --- a/source/exercises/mailroom-oo.rst +++ b/source/exercises/mailroom-oo.rst @@ -1,21 +1,22 @@ .. _exercise_mailroom_oo: +########################## Mailroom - Object Oriented -========================== +########################## Making Mailroom Object Oriented. -Goal: Refactor the mailroom program using classes to help organize the code. +**Goal:** Refactor the mailroom program using classes to help organize the code. The functionality is the same as the earlier mailroom: -:ref:`exercise_mailroom` +:ref:`exercise_mailroom_part1` But this time, we want to use an OO approach to better structure the code to make it more extensible. It was quite reasonable to build the simple mailroom program using a single module, a simple data structure, and functions that manipulate -that data structure. +that data structure. In fact, you've already done that :-) But if one were to expand the program with additional functionality, it would start to get a bit unwieldy and hard to maintain. So it's a pretty good candidate for an object-oriented approach. @@ -23,21 +24,21 @@ would start to get a bit unwieldy and hard to maintain. So it's a pretty good ca As you design appropriate classes, keep in mind these three guidelines for good code structure: -1) Encapsulation: You have a data structure that holds your data, and functions that manipulate that data; you want data and methods "bundled up" in a neat package so that they that work on that data are within one unit. The rest of the code doesn't need to know about the data structure you are using. +1) Encapsulation: You have a data structure that holds your data, and functions that manipulate that data; you want data and methods "bundled up" in a neat package so that everything that works with that data structure are within one unit. The rest of the code doesn't need to know about the data structure you are using. 2) Separation of Concerns: The user-interaction code should be cleanly separated from the data handling code. https://en.wikipedia.org/wiki/Separation_of_concerns - There should be no ``input`` functions in the classes that hold the data! + There should be no use of the ``input()`` function in the classes that hold the data! Nor any use of ``print()`` -- these are for user interaction, and you want the data handling code to be potentially usable with totally different user interaction -- such as a desktop GUI or Web interface. 3) As always: **DRY** (Don't Repeat Yourself): Anywhere you see repeated code; refactor it! The Program ------------ +=========== -See: :ref:`exercise_mailroom` to remind yourself what the program needs to do. +See: :ref:`exercise_mailroom_part1` to remind yourself what the program needs to do. Suggestions @@ -47,23 +48,24 @@ One of the hardest parts of OO design (particularly in Python) is to know how "l There are no hard and fast rules, but here are some guidelines: -For this assignment it's OK to go with simple tuples. However, in order for the code to be more flexible in the future, for example, if new "fields" were added to the donor object, it's probably better to use a more structured data type, so you don't have to worry about changing the order or number of fields. +For this simple problem, simple tuples could work fine. However, in order for the code to be more flexible in the future: for example, if new "fields" were added to the donor object, it's probably better to use a more structured data type, so you don't have to worry about changing the order or number of fields. -So now you have to think about using a dict or class. Again for flexibility, I think a dict is a bit easier; you can add fields to it very easily. However, with a class, you can build some functionality in there, too. This is where encapsulation comes in. For instance, one thing you might want to do is get the total of all donations a donor has made in the past. If you add a method to compute that (or a property!), then the rest of the code doesn't need to know how the donations are stored. +So now you have to think about using a dict or class. Again for flexibility, a dict is a bit easier; you can add fields to it very easily. However, with a class, you can build some functionality in there, too. This is where encapsulation comes in. For instance, one thing you might want to do is get the total of all donations a donor has made in the past. If you add a method to compute that (or a property!), then the rest of the code doesn't need to know how the donations are stored. -Consider ``data[0]`` vs ``data["first_name"]`` vs ``data.first_name``. Which one is more readable? Keep in mind that another benefit of using OO for data encapsulation is ability of modern IDE to provide auto-completion, which reduces number of bugs and helps to produce code faster. +Consider ``data[0]`` vs ``data["name"]`` vs ``data.name``. Which one is more readable? Keep in mind that another benefit of using OO for data encapsulation is ability of modern IDEs to provide auto-completion, which reduces the number of bugs and helps to produce code faster. Below are more detailed suggestions on breaking down your existing code into multiple modules that will be part of a single mailroom program. -Modules vs. Classes +Modules and Classes ................... You may organize your code to your preference and keep it simple by having all of the code in a single file. Optionally, you could organize your code into modules, which helps to keep code organized and re-usable. -What is a module? A module is a python file that can be imported in other files. +What is a module? A module is a python file with a collection of code that can be imported into other python files. + Modules can contain functions, classes, and even variables (constants). Here is an example file structure for ``mailroom_oo`` package that contains 3 modules: @@ -71,28 +73,25 @@ Here is an example file structure for ``mailroom_oo`` package that contains 3 mo .. code-block:: bash └── mailroom_oo - ├── __init__.py ├── cli_main.py ├── donor_models.py └── test_mailroom_oo.py - The module ``donor_models.py`` can contain the ``Donor`` and ``DonorCollection`` classes. The module ``cli_main.py`` would include all of your user interaction functions and main program flow. -Note that ``__init__.py`` is an empty file that tells Python that this directory should be treated as a package so that you can import modules. - -Donor Class -........... +``Donor`` Class +............... **Class responsible for donor data encapsulation** -This class will hold all the information about a single donor, and have attributes, properties, and methods to provide access to the donor-specific information that is needed. -Remember, if you are writing code that only accesses information about a single donor, then it should most likely live in this class. +This class will hold all the information about a *single* donor, and have attributes, properties, and methods to provide access to the donor-specific information that is needed. +Any code that only accesses information about a single donor should be part of this class. -DonorCollection Class -..................... + +``DonorCollection`` Class +......................... **Class responsible for donor collection data encapsulation** @@ -100,24 +99,35 @@ This class will hold all of the donor objects, as well as methods to add a new d Your class for the collection of donors will also hold the code that generates reports about multiple donors. +In short: if the functionality involves more than one donor -- it belongs in this class. + +Note that the ``DonorCollection`` class should be holding, and working with, ``Donor`` objects -- it should NOT work directly with a list of donations, etc. + +**Examples:** + +Generating a thank you letter to a donor only requires knowledge of that one donor -- so that code belongs in the ``Donor`` class. + +Generating a report about all the donors requires knowledge of all the donors, so that code belongs in the ``DonorCollection`` class. + Command Line Interface ....................... **Module responsible for main program flow (CLI - Command Line Interface)** -Let's call this module ``cli_main.py`` to represent the entry point for the mailroom program. This module will be using the classes we defined: ``Donor`` and ``DonorCollection``. It will also handle interaction with the user via the ``input`` function calls that gather user input and to provide the output to the console. +Let's call this module ``cli_main.py`` to represent the entry point for the mailroom program. +This module will be using the classes we defined: ``Donor`` and ``DonorCollection``. +It will also handle interaction with the user via the ``input`` function calls that gather user input and to provide the output to the console. What should go into this module? -* main "switch dictionary" to map user selection to the program features; in general, you will have a method for each of the mailroom functions. -* ``input`` function calls to gather user input -* ``print`` statements to print to console +A set of user-interaction menu functions -- to handle each of the modes of the program. -.. note:: Technically, console print statements don't belong in your data classes. However, for some features of this program, such as "send letters," we are simply printing instead of "sending," so it is ok for this feature to reside in the data class. But do keep integration of console print statements with data classes to a minimum. Ideally, the data class methods return a string, and the UI code does the printing. +These will include ``input()`` function calls to gather user input, and ``print()`` functions to print results to console. +.. note:: Console print statements don't belong in your data classes. So for features such as "send letters," in which we are simply printing instead of "sending", the data class methods should return a string, and let the UI code do the printing. This will mean there may be very simple functions in the UI code that simply call a method and print the results -- but that does keep flexibility for other ways of handling user interaction. -Why is this separation of data and method so important? +.. rubric:: Why is this separation of data and method so important? The idea here is that we should be able to fairly easy replace this CLI program with a different type of interface, such as a GUI (Graphical User Interface), without having to make any changes to our data classes. @@ -129,9 +139,99 @@ Test-Driven Development At this point we have done a great job refactoring the more complex code out of data-holding classes and we are left with simple classes that are more straightforward to unit test. As you build your classes, update the tests you already have to the logic code to the new API. Ideally, update the tests first, then the code. -The ``Donor`` and ``DonorCollection`` classes should now have close to 100 percent code coverage. +The ``Donor`` and ``DonorCollection`` classes should now have 100 percent code coverage, which means that every line of code in your ``donor_models.py`` file will be run at least once when your tests are run. + +For the moment, don't worry about testing most of the command line interface code. That requires simulating user input, which is an advanced testing topic. But you can (hopefully) see some of the benefits of separating the user-interaction code from the logic code; your logic code is much easier to test with no user-interaction involved. + +.. rubric:: refactoring non-OO code + +In this case, you already have working code without an OO structure. You should be able to re-use a fair bit of your existing code. +However, you should still start with the OO structure/design. +That is, rather than take a non-OO function and try to make it a method of a class, decide what method you need, and what it's API should be, and then see if you have code you can use to fill in that function. + +You should expect to re-use a lot of the command line interface code, while refactoring most of the logic code. + +If you are not sure at the start what functionality you data classes will need, you can start with the CLI code, and as you find the need for a function, add it to your data classes (after writing a test first, of course). + + +Exercise Guidelines +=================== + +OO mailroom is the final project for the class. + +So this is your chance to really do things "right". Strive to make this code as good, by every definition, as you can. + +With that in mind: + +Functionality +------------- + +* The logic is correct -- i.e. the program works :-) + +* The logic is robust -- you are handling obvious expected errors reasonably: + + - User inputting a non-number as a donation + + - Trying to make a negative donation + + - User getting capitalization or spacing or ??? wrong with a name. + + - Maybe add logic where you tell them that the name is not in the DB, and do they want to create it, rather than simply creating a new record for a typo in a donor name. + +.. rubric:: Code structure + +* Classes should have clear purpose and encapsulation: only the code within a class should know exactly how the data are stored, for instance. + +* Anything that only needs to know about one donor should be in the ``Donor`` class + +* Anything that needs to know about the collection should be in a ``DonorCollection`` class. + +* Any user interaction should be outside the "logic" code. (Sometimes called the "Model", or "Business logic") + + - You should be able to re-use all the logic code with a different UI -- Web App, GUI, etc. + + - There should be no ``input()`` or ``print`` functions in the logic code. + + - The logic code should be 100% testable (without mocking input() or any fancy stuff like that) + +.. rubric:: Testing + +* All logic code should be tested. + +* Tests should be isolated to test one thing each + +* Tests should (reasonably) check for handling of weird input. + +* Tests should be isolated -- that is, they will work if run by themselves, and in any order. + + - This means they should not rely on any global state. + + - you'll probably find this easier with a well structured OO approach -- that is, you can test an individual donor functionality without knowing about the rest of the donors. + + +.. rubric:: The "soft" stuff: + +Style: + - conform to PEP8! (or another consistent style) + + - You can use 95 or some other reasonable number for line length + +Docstrings: + Functions and classes should all have good docstrings. They can be very short if the function does something simple. + +Naming: + All classes, functions, methods, attributes, variables should have appropriate names: meaningful, but not too detailed. + +Extra Ideas: +------------ + +In case you are bored -- what features can you add? + +* How about an html report using your html_render code? + +* Fancier reporting -For the moment, don't worry about testing most of the command line interface code. That requires simulating use input, which is an advanced testing topic. But you can (hopefully) see some of the benefits of separating the user-interaction code from the logic code; your logic code is much easier to test with no user-interaction involved. +* The sky's the limit diff --git a/source/exercises/mailroom-part2.rst b/source/exercises/mailroom-part2.rst index 7e94ccdb..36a1a600 100644 --- a/source/exercises/mailroom-part2.rst +++ b/source/exercises/mailroom-part2.rst @@ -50,7 +50,7 @@ In the first version of mailroom, you generated a letter to a donor who had just In this version of your program, add a function (and a menu item to invoke it), that goes through all the donors in your donor data structure, generates a thank you letter for each donor, and writes each letter to disk as a text file. -Your main menu may look something like: +Your main menu may look something like:: Choose an action: diff --git a/source/exercises/mailroom-part3.rst b/source/exercises/mailroom-part3.rst index b2be6384..71701d2a 100644 --- a/source/exercises/mailroom-part3.rst +++ b/source/exercises/mailroom-part3.rst @@ -31,10 +31,17 @@ with [print(donor) for donor in donors] -That's not the intended use of comprehensions. Because ``print`` function does not have a value, this code will allocate a space for an "empty" result list filled with None values: +That's not the intended use of comprehensions. Because ``print`` function does not return a value, this code will allocate a space for an "empty" result list filled with None values: >>> [print(donor) for donor in donors] jane wendy [None, None] >>> + +List comprehensions are designed for a very specific use case: + +*Processing a sequence of items to create another sequence.* + +They are not designed to replace all for loops. + diff --git a/source/exercises/oo_intro.py b/source/exercises/oo_intro.py new file mode 100644 index 00000000..522e62a0 --- /dev/null +++ b/source/exercises/oo_intro.py @@ -0,0 +1,103 @@ +import math +import operator +from uuid import uuid4 + + +class Row: + """This class represents a single row with ID, first name, last name and state attributes""" + def __init__(self, fname: str, lname: str, state: str): + self.row_id = str(uuid4()) # randomly generated unique ID + self.fname = fname + self.lname = lname + self.state = state + + def __str__(self): + return f"| {self.row_id} | {self.fname + ' ' + self.lname:<15} | {self.state} |" + + +class Report: + def __init__(self, limit: int): + self.limit = limit + self.rows = [] + + def add_row(self, row: Row) -> None: + """Add a row object to the list""" + pass + + def remove_row(self, row_id: str) -> None: + """Remove a row object by the row ID""" + pass + + def size(self) -> int: + """Return how many total rows the report has""" + pass + + def get_number_of_pages(self) -> int: + """Get how many pages the report has; this will be based on limit variable. + If your limit=4 and rows list has 6 records then there are two pages: page1 has 4 records, page2 has 2 records + hint: you'll want to round up + """ + pass + + def get_paged_rows(self, sort_field: str, page: int) -> list: + """Return a list of rows for a specific page number + :param sort_field: field to sort on, "name" or "-name" (descending) + :param page: specific page for returning data + :return: list of row objects for specific page + + Hints: + 1. you'll want to determine if sort is reversed or not (remember that sorted() takes in param for that) + this is based on if the fields start with a minus sign for DESCENDING sort + 2. when sorting on passed in field you can use handy `operator` library with `attrgetter` method (look up official docs) + 3. to actually determine what rows belong on the specific page you'll be using list slicing (remember lesson 03?) + here is an illustration to help with the code logic: + our list has 6 rows => [, , , , , ] + for page=2 we expect to get => [, ] + with slicing you'll want to offset your list by 4 in this case + (extra hint: we can define offset as `offset = (page - 1) * self.limit`) + + """ + pass + + +if __name__ == "__main__": + + report = Report(4) + + report.add_row(Row("natasha", "smith", "WA")) + report.add_row(Row("devin", "lei", "WA")) + report.add_row(Row("bob", "li", "CA")) + report.add_row(Row("tracy", "jones", "OR")) + report.add_row(Row("johny", "jakes", "WA")) + report.add_row(Row("derek", "wright", "WA")) + + + def run_report(sort_field): + print(f"... PAGED REPORT SORTED BY: '{sort_field}'...") + page = 1 + while True: + rows = report.get_paged_rows(sort_field, page=page) + + if not rows: + break + + input(f"Press ENTER to see page {page}") + + print(f"PAGE: {page} of {report.get_number_of_pages()}") + print("---------------------------------------------------------------") + + for row in rows: + print(row) + + print("---------------------------------------------------------------") + + page += 1 + + + run_report("fname") + + print(f"\n\nRemoving student: {report.rows[1].fname} [{report.rows[1].row_id}]... \n\n") + + report.remove_row(report.rows[1].row_id) + + run_report("-fname") diff --git a/source/exercises/oo_intro.rst b/source/exercises/oo_intro.rst new file mode 100644 index 00000000..703b3df3 --- /dev/null +++ b/source/exercises/oo_intro.rst @@ -0,0 +1,30 @@ +.. _oo_intro: + +######################## +OO Intro - Report Class +######################## + +This assignment uses Object Oriented Programming to design a class that can be used to manage data reporting. + +We have done some reporting in our mailroom program, but that report was pretty simple and used simple functions to accomplish the work. + +We will explore here, how one can utilize OO to improve and enhance reporting capabilities. + + +Procedure +========= + +You will use :download:`oo_intro.py` file as a starting point for your code. + +You will notice that ``Report`` class will have attributes and methods defined for you, including input parameters (and their types) as well as expected output. You will need to fill out the code for each defined method and docstrings contain additional information on what is expected. + +The ``Report`` class uses another class that is fully defined for you, the ``Row`` class. This class represents a single row in your report, and the report class will hold a list of the row instances. +There are big advantages to using a class like ``Row`` in contrast to a simple dictionary, this design creates a clear contract on what's expected to be as part of a row, where with a dictionary it is easy to misspell or miss a key. +A class like ``Row`` is often called a dataclass (since it only holds "data" and doesn't actually have any logic to it). + +This is such a popular pattern that python introduced dataclasses to make this even simpler and you can read more about them here: +https://realpython.com/python-data-classes/. You do not use them in this assignment but you should know that they exist and why. + + + +And at last, you will of course want to include unit tests covering all of your class methods. \ No newline at end of file diff --git a/source/exercises/python_pushups.rst b/source/exercises/python_pushups.rst index 146ef766..7586521b 100644 --- a/source/exercises/python_pushups.rst +++ b/source/exercises/python_pushups.rst @@ -11,8 +11,8 @@ Task 1: Explore Errors * Create a new directory in your working dir for the class:: - $ mkdir session01 - $ cd session01 + $ mkdir lesson01 + $ cd lesson01 * Add a new file to it called ``break_me.py`` @@ -38,3 +38,8 @@ exercises at "Coding Bat": http://codingbat.com/python There are 8 sets of puzzles. Do as many as you can, but try to get to at least the "Warmups". +While the codingbat site has a great interface for submitting your solution and see if it works, we suggest you write your code in your regular text editor and get it to run on your machine first. + +Put at least one solution in the ``lesson1`` folder you created in git and submit it via pull request. + + diff --git a/source/exercises/rot13.rst b/source/exercises/rot13.rst index 8000d19b..26dbcc47 100644 --- a/source/exercises/rot13.rst +++ b/source/exercises/rot13.rst @@ -22,7 +22,7 @@ circle, so it wraps around). The task -------- -Add a python module named ``rot13.py`` to the session03 dir in your student dir. This module should provide at least one function called ``rot13`` that takes any amount of text and returns that same text encrypted by ROT13. +Add a python module named ``rot13.py`` to the lesson03 dir in your student dir. This module should provide at least one function called ``rot13`` that takes any amount of text and returns that same text encrypted by ROT13. This function should preserve whitespace, punctuation and capitalization. diff --git a/source/exercises/series_template.py b/source/exercises/series_template.py index 8af982d0..fb161d70 100644 --- a/source/exercises/series_template.py +++ b/source/exercises/series_template.py @@ -22,9 +22,15 @@ def sum_series(n, n0=0, n1=1): :param n0=0: value of zeroth element in the series :param n1=1: value of first element in the series - if n0 == 0 and n1 == 1, the result is the Fibbonacci series + This function should generalize the fibonacci() and the lucas(), + so that this function works for any first two numbers for a sum series. + Once generalized that way, sum_series(n, 0, 1) should be equivalent to fibonacci(n). + And sum_series(n, 2, 1) should be equivalent to lucas(n). - if n0 == 2 and n1 == 1, the result is the Lucas series + sum_series(n, 3, 2) should generate antoehr series with no specific name + + The defaults are set to 0, 1, so if you don't pass in any values, you'll + get the fibonacci sercies """ pass @@ -44,9 +50,19 @@ def sum_series(n, n0=0, n1=1): assert lucas(4) == 7 + # test that sum_series matches fibonacci assert sum_series(5) == fibonacci(5) + assert sum_series(7, 0, 1) == fibonacci(7) # test if sum_series matched lucas assert sum_series(5, 2, 1) == lucas(5) + # test if sum_series works for arbitrary initial values + assert sum_series(0, 3, 2) == 3 + assert sum_series(1, 3, 2) == 2 + assert sum_series(2, 3, 2) == 5 + assert sum_series(3, 3, 2) == 7 + assert sum_series(4, 3, 2) == 12 + assert sum_series(5, 3, 2) == 19 + print("tests passed") diff --git a/source/exercises/sparse_array.rst b/source/exercises/sparse_array.rst index 23b1a742..939ebcb9 100644 --- a/source/exercises/sparse_array.rst +++ b/source/exercises/sparse_array.rst @@ -48,7 +48,7 @@ Some ideas of how to do that: len(my_array) - This will give its "virtual" length -- with the zeros +This will give its "virtual" length -- with the zeros * It should support getting and setting particular elements via indexing: diff --git a/source/exercises/string_formatting.rst b/source/exercises/string_formatting.rst index 5af98291..9879cb58 100644 --- a/source/exercises/string_formatting.rst +++ b/source/exercises/string_formatting.rst @@ -10,11 +10,7 @@ In this exercise we will reinforce the important concepts of string formatting, Procedure ========= -Be sure to follow all the steps described in the Procedure section at: - -https://canvas.uw.edu/courses/1200526/assignments/3970003?module_item_id=7963708 - -but this time, creating a new file called strformat_lab.py in your student dir in the class repo. +Create a new file called ``strformat_lab.py`` in your student dir in the class repo. When the empty script is available and runnable, complete the following four tasks. @@ -124,7 +120,7 @@ It will look like: def formatter(in_tuple): do_something_here_to_make_a_format_string - return form_string.format(in_tuple) + return form_string.format(*in_tuple) Task Four @@ -226,7 +222,7 @@ https://pyformat.info/ A nice "Cookbook": -https://mkaz.tech/python-string-format.html +https://mkaz.blog/code/python-string-format-cookbook/ Submitting Your Work diff --git a/source/exercises/unit_testing.rst b/source/exercises/unit_testing.rst index c1a7834b..b71653dc 100644 --- a/source/exercises/unit_testing.rst +++ b/source/exercises/unit_testing.rst @@ -4,51 +4,93 @@ Introduction To Unit Testing ############################ +Preparation +----------- + +In order to do unit testing, you need a framework in which to write and run your tests. +Earlier in this class, you've been adding "asserts" to your modules -- perhaps in the ``__name__ == "__main__"`` block. These are, in fact, a kind of unit test. +But as you build larger systems, you'll want a more structured way to write and run your tests. + +We will use the pytest testing system for this class. + +If you have not already done so -- install pytest like so: + +.. code-block:: bash + + $ python3 -m pip install pytest + +Once this is complete, you should have a ``pytest`` command you can run +at the command line: + +.. code-block:: bash + + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/temp/DrMartins, inifile: + plugins: cov-2.6.0 + collected 0 items + + ========================= no tests ran in 0.01 seconds ========================= + +If you already HAVE some tests -- you may see something different! + + Test Driven Development ----------------------- -Download this module: - -:download:`cigar_party.py ` +Download these files, and save them in your own students directory in the class repo: -(This is the `"cigar party" `_ problem from the codingbat site) +:download:`test_walnut_party.py <../examples/testing/test_walnut_party.py>` -and this test file: +and: -:download:`test_cigar_party.py ` +:download:`walnut_party.py <../examples/testing/walnut_party.py>` -Put them in the same directory, and make that directory your working directory. +(This is the adapted from the codingbat site: http://codingbat.com/prob/p195669) -Then then try running the test file with pytest: +In the directory where you put the files, run: .. code-block:: bash - $ pytest test_cigar_party + $ pytest test_walnut_party.py + +You will get a LOT of test failures! What you've done here is the first step in what is called: - **Test Driven Development**. + **Test Driven Development** A bunch of tests exist, but the code to make them pass does not yet exist. The red you see in the terminal when we run our tests is a goad to us to write the code that fixes these tests. -Let's do that next! +The tests all failed because currently ``walnut_party()`` looks like: -Test Driven development ------------------------ +.. code-block:: python + + def walnut_party(walnuts, is_weekend): + pass + +A totally do nothing function. + + +Making tests pass +----------------- Open: -``test_cigar_party.py`` +``test_walnut_party.py`` and: -``cigar_party.py`` +``walnut_party.py`` In your editor. -Now edit ``cigar_party.py``, and each time you make a change, run teh tests again. Continue until all the tests pass. +Now edit the function in ``walnut_party.py``, and each time you make a change, run the tests again. Continue until all the tests pass. + +When the tests pass -- you are done! That's the beauty of test-driven development. Doing your own: --------------- @@ -59,14 +101,23 @@ Pick another example from codingbat: Do a bit of test-driven development on it: - * run something on the web site. - * write a few tests using the examples from the site. +* Run something on the web site. +* Write a few tests using the examples from the site. +* Then write the function, and fix it 'till it passes the tests. + +These tests should be in a file named ``test_something.py`` -- I usually name the test file the same as the module it tests, +with ``test_`` prepended. -These tests should be in a file names ``test_something.py`` -- I usually name the test file the same as the module it tests, with ``test_`` prepended. +.. note:: + Technically, you can name your test files anything you want. But there are two reasons to use standard naming conventions. + One is that it is clear to anyone looking at the code what is and isn't a test module. The other is that pytest, and other testing systems, use `naming conventions `_ to find your test files. + If you name your test files: ``test_something.py`` then pytest will find them for you. And if you use the name of the module being tested: + ``test_name_of_tested_module.py`` then it will be clear which test files belong to which modules. - * then write the function, and fix it 'till it passes the tests. Do at least two of these to get the hang of the process. -Also -- once you have the tests passing, look at your solution -- is there a way it could be refactored to be cleaner? Give it a shot -- you'll know if it still works if the tests still pass! +Also -- once you have the tests passing, look at your solution -- is there a way it could be refactored to be cleaner? + +Give it a shot -- you'll know if it still works if the tests still pass! diff --git a/source/for_instructors/code_review.rst b/source/for_instructors/code_review.rst index 776f9bea..ff1f1e32 100644 --- a/source/for_instructors/code_review.rst +++ b/source/for_instructors/code_review.rst @@ -2,7 +2,7 @@ Reviewing Students' Code ######################## -One of the most important services the program provides is code review. While for teh most part the students will know if they have solved the problem (i.e. their code works) there is al ot they can learn about clean style, robust logic, appropriate data structures, pythonicity, etc. +One of the most important services the program provides is code review. While for the most part the students will know if they have solved the problem (i.e. their code works) there is a lot they can learn about clean style, robust logic, appropriate data structures, Pythonicity, etc. And they can only learn that if their code is reviewed and critiqued. @@ -11,7 +11,7 @@ Scoring Assignments Exactly how to score the exercises is up to the individual instructor. But we do need to provide some score, so there is a clear requirement to passing the class. -As the course is pass/fail, I don't think it's important to distinguish between decent, good, and excellent work. So I tend to give full credit for a good-faith effort at an exercise, and let me review be the feedback they need. +As the course is pass/fail, I don't think it's important to distinguish between decent, good, and excellent work. So I tend to give full credit for a good-faith effort at an exercise, and let the review be the feedback they need. Reviewing Rubric ================ @@ -38,16 +38,18 @@ Is the code Pythonic? - looping through a sequence rather than indexing - EAFTP Exception handling - - ... + - iterating rather that making copies + - no mutables in default arguments Style Issues: ------------- - PEP 8 compliance - variable names: + - meaningful names -- not "key", "value", "foo" - one letter names only for index variables etc. - - verbs for functions. methods + - verbs for functions and methods - nouns for objects - - plural for sequences of objects. - - ... + - plural for sequences of objects / singular for single items + diff --git a/source/for_instructors/github.rst b/source/for_instructors/github.rst index b46fde41..b5bc4bbb 100644 --- a/source/for_instructors/github.rst +++ b/source/for_instructors/github.rst @@ -9,6 +9,36 @@ gitHub provides a good platform for code review, and it also provides the studen The class repo ============== +There is a gitHub org for the class repos here: + +https://github.com/UWPCE-PythonCert-ClassRepos + +And a template here: + +https://github.com/UWPCE-PythonCert-ClassRepos/ClassRepoTemplate + +Before the class starts, the instructor should make a ocpy of that TEmplate repo, and crate a new one labeled something like: + +``Fall2018-PY210A`` or ``Sp2018-Online`` + +Make sure your students know which repo to fork and use for your class -- probably by updating Canvas or EdX for your instance. + +Class Repo Structure +-------------------- + +The class repo has three directories in it:: + + students + examples + solutions + +The ``students`` dir is where each student should create a directory for their own work. They can then submit a gitHub PR to add their dir to the class repo, and to have their work reviewed when it is ready to be turned in. + +The ``examples`` dir can be used to put examples of various sorts -- either from the main PythonCertDevel repo -- or anything you work up for class. It is a good place to put code developed during class as well, so everyone can have access in the future. + +The ``solutions`` dir can be used to post solutions to the exercises. How and when (and if) you do that is up the the individual instructor, and may be different depending on the modality. i.e. for self-paced inline there may no appropriate time to post solutions. + + Example Code ============ diff --git a/source/for_instructors/solutions.rst b/source/for_instructors/solutions.rst index b98b888f..86a15b64 100644 --- a/source/for_instructors/solutions.rst +++ b/source/for_instructors/solutions.rst @@ -4,7 +4,7 @@ Exercise Solutions A complete set of solutions is available in the PythonCertDevel Repo. -I've found that students really like to see clean, nicely written solutions to the exercises -- when I started teaching, I would go over the students' code in class, and correct and critique it. But I got a lot of requests for how **I** would have written the code. So I ended up developing a full set of solutions to share with the class. +I've found that students really like to see clean, nicely written solutions to the exercises -- when I started teaching, I would go over the students' code in class, and correct and critique it. But I got a lot of requests for how *I* would have written the code. So I ended up developing a full set of solutions to share with the class. When / How to Share Solutions ============================= diff --git a/source/lesson_templates/course1_lesson03.rst b/source/lesson_templates/course1_lesson03.rst index ac830266..fc61c077 100644 --- a/source/lesson_templates/course1_lesson03.rst +++ b/source/lesson_templates/course1_lesson03.rst @@ -57,7 +57,7 @@ Mailroom Exercise You've now got the basics of the language down -- enough to write your first full "program": -:ref:`exercise_mailroom` (10 points) +:ref:`exercise_mailroom_part1` (10 points) Ungraded exercises ------------------ diff --git a/source/lesson_templates/course1_lesson04.rst b/source/lesson_templates/course1_lesson04.rst index 8e5b1c75..a1a14ecf 100644 --- a/source/lesson_templates/course1_lesson04.rst +++ b/source/lesson_templates/course1_lesson04.rst @@ -43,7 +43,7 @@ Graded Assignments * :ref:`exercise_file_lab` - * Update mailroom with dicts :ref:`exercise_mailroom_plus` + * Update mailroom with dicts :ref:`exercise_mailroom_part2_dict_files` * :ref:`exercise_trigrams` diff --git a/source/modules/Booleans.rst b/source/modules/Booleans.rst index 0667806a..26208e77 100644 --- a/source/modules/Booleans.rst +++ b/source/modules/Booleans.rst @@ -45,8 +45,10 @@ This is similar to making an integer out of another value, such a float or a str In [2]: int("345") Out[2]: 345 +``bool(something)`` will always evaluate to either ``True`` or ``False``. -What is False? + +What is Falsy? -------------- * ``None`` @@ -59,7 +61,7 @@ What is False? - Any empty sequence, for example, ``"", (), []``. - - Any empty mapping, for example, ``{}`` . + - Any empty mapping, for example, ``dict()``. - Instances of user-defined classes: @@ -71,8 +73,8 @@ What is False? (Don't worry about that last one -- what that means is that user-defined types can control their truthiness behavior). -What is True? -------------- +What is Truthy? +--------------- Everything else. @@ -141,7 +143,7 @@ of this operand: Shortcutting ------------ -If you think about it, what ``and`` and ``or`` are doing is as little work as possible. They will only evaluate as much as they need to get the answer. +``and`` and ``or`` returning teh first value that determines the result is known as "shortcutting". If you think about it, what ``and`` and ``or`` are doing is as little work as possible. They will only evaluate as much as they need to get the answer. Think about ``and``: it is testing if *both* the operands are True. If the first one is False, there is no need to bother checking the second. @@ -171,21 +173,20 @@ In this case, the second expression needs to be evaluated -- so it DID raise an This can be exploited to provide compact logic -- but it can also hide bugs! - Because of the return value of the boolean operators, you can write concise -statements: +statements, rather than a full ``if -- else`` block like so: :: - if x is False: + if bool(x) is False: x or y return y else: return x - if x is False: + if bool(x) is False: x and y return x else: return y - if x is False: + if bool(x) is False: not x return True else: return False diff --git a/source/modules/Class_introduction.rst b/source/modules/Class_introduction.rst index 80b788b9..363e0421 100644 --- a/source/modules/Class_introduction.rst +++ b/source/modules/Class_introduction.rst @@ -9,7 +9,7 @@ In which you are introduced to this class, your instructors, your environment an .. image:: /_static/python.png :align: center - :width: 38% + :width: 80% `xkcd.com/353`_ @@ -18,14 +18,21 @@ In which you are introduced to this class, your instructors, your environment an Who are we? ------------ +=========== +Introduction to your instructors. Who are you? ------------ +Despite the common myth of the lone programmer, most software development is a collaborative activity. As such, we encourage students in this program to work together whenever possible. - Tell us a tiny bit about yourself: +As you will be working with your fellow students for the rest of the program +Take a couple minutes now to get to know your neighbors -- what is their favorite coffee shop or bar? + +Then we'll go around the room and introduce ourselves: + +Tell us a tiny bit about yourself: * name * programming background: what languages have you used? @@ -36,9 +43,9 @@ Who are you? Introduction to This Class ========================== -The overall class is managed by a learning managment system -- Canvs or EdX. +The overall class is managed by a learning management system -- Canvas or EdX. -You have gotten a link to the instance for the class sent to you. IN fact, you probably found this page with a link from there. +You have gotten a link to the instance for the class sent to you. Class Structure --------------- @@ -49,7 +56,7 @@ for this program. This means that the "homework" will be reading, watching videos, coding, etc. -And class time will be spent primarly coding: +And class time will be spent primarily coding: * Still some lecture -- as little as possible * Lots of demos @@ -58,20 +65,20 @@ And class time will be spent primarly coding: - In small groups - Instructor led. -This means that you are expected to complete the reading BEFORE each class. That way, we don't have to take class time introducing the basic material and can focus on questions and applying what you've read about. +This means that you are expected to complete the reading (and video watching) BEFORE each class. That way, we don't have to take class time introducing the basic material and can focus on questions and applying what you've read about. -Interrupt with questions -- please! +Interrupt us with questions -- please! (Some of the best learning prompted by questions) Homework: --------- -* Homework will primarily be reading, a handful of videos, and links to optional external materials -- videos, blog posts, etc. +* Homework will be reading: a handful of videos, and links to optional external materials -- videos, blog posts, etc. -* Exercises will be started in class -- but you can finish them at home. +* Exercises will be started in class -- but you can finish them at home (and you will need time to do that!) -* You are adults -- it's up to you to do the homework. But if you dont code, you won't learn to code... +* You are adults -- it's up to you to do the homework. But if you don't code, you won't learn to code. And we can't give you a certificate if you haven't demonstrated that you've done the work. * To submit your work, you can do a gitHub "pull request" to the class repo. @@ -80,17 +87,14 @@ There is a video about that, and we will show you in the first class as well. Communication ------------- -**Mailing list:** - -You should have been subscribed to the mailing list, if not talk to your instructors. +**Mailing List** -We will be using this list to communicate with you. If you have not gotten notes from the list -- make sure to let your instructors know, and we'll make sure to add you. +There should have been a Mailing List set up for this class. You should have been invited to join -- if not, let your instructors know. Also let them know if you would prefer a different email address. -Slack may set up a slack channel for discussions. Anything python related is fair game. We'll poll the class in the first session to decide if that would be helpful. +Anything Python related is fair game. Questions and discussion about the assignments are encouraged. -We highly encourage you to work together. You will learn at a much deeper level if you work together, and it gets you ready to collaborate with colleagues. If you have never used slack before, you may want to look at their intro: +We highly encourage you to work together. You will learn at a much deeper level if you work together, and it gets you ready to collaborate with colleagues. -https://get.slack.help/hc/en-us/articles/115004071768 Office Hours ------------ @@ -102,3 +106,37 @@ Please feel free to attend even if you do not have a specific question. It is an What are good times for you? And what locations? + +.. _lightning_talks: + +Lightning Talks +=============== + +"Lightning Talks" are a tradition in open-source technical conferences (and maybe others?). The idea is that people can do a quick talk about a topic of their choice -- much lower pressure than a "real" talk -- but gives folks a chance to show off something they have worked on. + +For this class, it's a chance to us to learn a bit about each-other and maybe something new about Python. + +Each of you will be required to give one lightning talk at some point during the course. + +**Lightning Talks Requirements** + + * 5 minutes each (including setup) - no kidding! + * Every student will give one + * Purposes: introduce yourself, share interests, show Python applications + * Any topic you like that is related to Python -- according to you! + +Schedule the lightning talks: +----------------------------- + +We need to schedule your lightning talks. + +**Let's use Python for that !** + +There is a class list in the class repo here: + +``examples/session01/students.txt`` + +Let's write a script to generate a random talk schedule... + + + diff --git a/source/modules/Closures.rst b/source/modules/Closures.rst index 74ef6359..17e70703 100644 --- a/source/modules/Closures.rst +++ b/source/modules/Closures.rst @@ -14,14 +14,16 @@ In order to get a handle on all this, it's important to understand variable scop "Scope" is the word for where the names in your code are accessible. Another word for a scope is namespace. -global ------- +Global Scope +------------ -The simplest is the global scope. This is where all the names defined right in your code file are (or in the interpreter). +The simplest is the global scope. This is where all the names defined right in your code file (module) are. When running in an interactavive interpreter, it is in the global namespace as well. You can get the global namespace with the ``globals()`` function, but ... -The Python interpreter defines a handful of names when it starts up, and iPython defines a whole bunch more. Most of those start with an underscore, so you can filter them out for a more reasonable result: +The Python interpreter defines a handful of names when it starts up, and iPython defines a whole bunch more. +Recall that a convention in Python is that names that start with an underscore are "special" in some way -- double underscore names have a special meaning to Python, and single underscore names are considered "private". +Most of the extra names defined by the Python interpreter or iPython that are designed for internal use start with an underscore. These can really "clutter up" the namespace, but they can be filtered out for a more reasonable result: .. code-block:: python @@ -31,7 +33,7 @@ The Python interpreter defines a handful of names when it starts up, and iPython if not (name.startswith("_") or name in ipy_names): print(name) -And run that in a raw interpreter: +Try running that in a newly started interpreter: .. code-block:: ipython @@ -47,6 +49,8 @@ And run that in a raw interpreter: The only name left is "print_globals" itself -- created when we defined the function. +.. note:: Try running ``globals()`` by itself to see all the cruft iPython adds. Also note that ``globals`` returns not just the names, but a dictionary, where the keys are the names, and the items are the values bound to those names. + If we add a name or two, they show up in the global scope: .. code-block:: ipython @@ -60,7 +64,7 @@ If we add a name or two, they show up in the global scope: x this -names are created by assignment, and by ``def`` and ``class`` statements. we already saw a ``def``. +names are created by assignment, and by ``def`` and ``class`` statements. We already saw a ``def``, here is a ``class`` definition. .. code-block:: ipython @@ -75,14 +79,14 @@ names are created by assignment, and by ``def`` and ``class`` statements. we alr test TestClass -local ------ +Local Scope +----------- So that's the global scope -- what creates a new scope? A new, "local" scope is created by a function or class definition: -And there is a built-in function to get the names in the local scope, too, so we can use it to show us the names in a function's local namespace. There isn't a lot of cruft in the local namespace, so we don't need a special function to print it. +There is a built-in function to get the names in the local scope, too, so we can use it to show us the names in a function's local namespace. There isn't a lot of cruft in the local namespace, so we don't need a special function to print it. Note that ``locals()`` and ``globals()`` returns a dict of the names and the objects they are bound to, so we can print the keys to get the names: @@ -136,10 +140,32 @@ Turns out that this holds true for functions defined within functions also: outer scope: dict_keys(['inner', 'y', 'x']) inner scope: dict_keys(['z', 'w']) +Function Parameters +------------------- + +The other way you can define names in a function's local namespace is with function parameters: + + +.. code-block:: ipython + + In [14]: def fun_with_parameters(a, b=0): + ...: print("local names are:", locals().keys()) + ...: + ...: + + In [15]: fun_with_parameters(4) + local names are: dict_keys(['a', 'b']) + +Notice that no other names have been defined in the function, but both of the parameters (positional and keyword) are local names. + + Finding Names ------------- -So there are multiple scopes in play at any point -- the local scope, and all the surrounding scopes. When you use a name, python checks in the local scope first, then moves out one by one until it finds the name. So if you define a new name inside a function, it "overrides" the name in any of the outer scopes. But the outer one will be found. +At any point, there are multiple scopes in play: the local scope, and all the surrounding scopes. +When you use a name, python checks in the local scope first, then moves out one by one until it finds the name. +If you define a new name inside a function, it "overrides" the name in any of the outer scopes. +But any names not defined in an inner scope will be found by looking in the enclosing scopes. .. code-block:: ipython @@ -162,12 +188,12 @@ So there are multiple scopes in play at any point -- the local scope, and all th this is in outer this is in inner -Look carefully to see where each of those names came from. All the print statements are in the inner function, so its local scope is searched first, and then the outer function's scope, and then the global scope. name1 is only defined in the global scope, so that one is found. +Look carefully to see where each of those names came from. All the print statements are in the inner function, so its local scope is searched first, and then the outer function's scope, and then the global scope. ``name1`` is only defined in the global scope, so that one is found. -The global keyword ------------------- +The ``global`` keyword +---------------------- -global names can be accessed from within functions, but not if that same name is created in the local scope. So you can't change an immutable object that is outside the local scope: +Global names can be accessed from within functions, but not if that same name is created in the local scope. So you can't change an immutable object that is outside the local scope: .. code-block:: ipython @@ -192,7 +218,7 @@ global names can be accessed from within functions, but not if that same name is The problem here is that ``x += 5`` is the same as ``x = x + 5``, so it is creating a local name, but it can't be incremented, because it hasn't had a value set yet. -The global keyword tells python that you want to use the global name, rather than create a new, local name: +The ``global`` keyword tells python that you want to use the global name, rather than create a new, local name: .. code-block:: ipython @@ -207,10 +233,10 @@ The global keyword tells python that you want to use the global name, rather tha In [42]: x Out[42]: 10 -**NOTE:** the use of global is frowned upon -- having global variables manipulated in arbitrary other scopes makes for buggy, hard to maintain code! +**NOTE:** The use of ``global`` is frowned upon -- having global variables manipulated in arbitrary other scopes makes for buggy, hard to maintain code! -nonlocal keyword ----------------- +``nonlocal`` keyword +-------------------- The other limitation with ``global`` is that there is only one global namespace, so what if you are in a nested scope, and want to get at the value outside the current scope, but not all the way up at the global scope: @@ -229,7 +255,7 @@ That's not going to work as the inner x hasn't been initialized: ``UnboundLocalError: local variable 'x' referenced before assignment`` -But if we use ``global``, we'll get the global x: +But if we use ``global``, we'll get the global ``x``: .. code-block:: ipython @@ -257,7 +283,7 @@ But if we use ``global``, we'll get the global x: In [9]: x Out[9]: 15 -so the global x is getting changed, but not the one in the outer scope. +This indicates that the global ``x`` is getting changed, but not the one in the ``outer`` scope. This is enough of a limitation that Python 3 added a new keyword: ``nonlocal``. What it means is that the name should be looked for outside the local scope, but only as far as you need to go to find it: @@ -275,9 +301,9 @@ This is enough of a limitation that Python 3 added a new keyword: ``nonlocal``. In [11]: outer() x in outer is: 15 -So the x in the outer function scope is the one being changed. +So the ``x`` in the ``outer`` function scope is the one being changed. -While using ``global`` is discouraged, ``nonlocal`` is safer -- as long as it is referring to a name in a scope that is closely defined like the above example. In fact, nonlocal will not go all the way up to the global scope to find a name: +While using ``global`` is discouraged, ``nonlocal`` is safer -- as long as it is referring to a name in a scope that is closely defined like the above example. In fact, ``nonlocal`` will not go all the way up to the global scope to find a name: .. code-block:: ipython @@ -295,7 +321,7 @@ While using ``global`` is discouraged, ``nonlocal`` is safer -- as long as it is But it will go up multiple levels in nested scopes: -.. code-block: ipython +.. code-block:: ipython In [16]: def outer(): ...: x = 10 @@ -311,20 +337,6 @@ But it will go up multiple levels in nested scopes: In [17]: outer() x in outer is: 20 -function parameters -------------------- - -A side note: function parameters are in a function's local scope, just as though they were created there: - -.. code-block:: ipython - - In [28]: def fun(x, y, z): - ...: print(locals().keys()) - ...: - - In [29]: fun(1,2,3) - dict_keys(['z', 'y', 'x']) - Closures ======== @@ -354,7 +366,7 @@ So after we define a function within a function, we can actually return that fun return count return incr -So this looks a lot like the previous examples, but we are returning the function that was defined inside the function. +This looks a lot like the previous examples, but we are returning the function that was defined inside the function. Which means is can be used elsewhere. What's going on here? ..................... @@ -404,9 +416,9 @@ But what happens if we call ``counter()`` multiple times? In [44]: c2() Out[44]: 11 -So each time ``counter()`` is called, a new ``incr`` function is created. But also, a new namespace is created, that holds the count name. So the new ``incr`` function is holding a reference to that new count name. +So each time ``counter()`` is called, a new ``incr`` function is created. Along with the new function, a new namespace is created that holds the ``count`` name. So the new ``incr`` function is holding a reference to that new ``count`` name. -This is what makes in a "closure" -- it carries with it the scope in which it was created. +This is what makes it a "closure" -- it carries with it the scope in which it was created (or enclosed - I guess that's where the word closure comes from). The returned ``incr`` function is a "curried" function -- a function with some parameters pre-specified. @@ -414,8 +426,6 @@ Let's experiment a bit more with these ideas: :download:`play_with_scope.py <../examples/closures_currying/play_with_scope.py>` -.. :download:`capitalize.zip <../examples/packaging/capitalize.zip>` - Currying ======== @@ -423,7 +433,7 @@ Currying `Currying on Wikipedia `_ -The idea behind currying is that you may have a function with a number of parameters, and you want to make a specialized version of that function with a couple parameters pre-set. +The idea behind currying is that you may have a function with a number of parameters, and you want to make a specialized version of that function with a couple of parameters pre-set. Real world Example @@ -442,7 +452,9 @@ So I wanted a function that would compute how much the concentration would reduc The trick is, how much the concentration would be reduced depends on both time and the half life. And for a given material, and given flow conditions in the river, that half life is pre-determined. Once you know the half-life, the scale is given by: -scale = 0.5 ** (time / (half_life)) +.. code-block:: python + + scale = 0.5 ** (time / (half_life)) So to compute the scale, I could pass that half-life in each time I called the function: @@ -451,7 +463,7 @@ So to compute the scale, I could pass that half-life in each time I called the f def scale(time, half_life): return 0.5 ** (time / (half_life)) -But this is a bit klunky -- I need to keep passing that half_life around, even though it isn't changing. And there are places, like ``map`` that require a function that takes only one argument! +But this is a bit klunky -- I need to keep passing that ``half_life`` around, even though it isn't changing. And there are places, like ``map`` that require a function that takes only one argument! What if I could create a function, on the fly, that had a particular half-life "baked in"? @@ -527,7 +539,7 @@ https://docs.python.org/3.5/library/functools.html Creating a curried function turns out to be common enough that the ``functools.partial`` function provides an optimized way to do it: -What functools.partial does is: +What ``functools.partial`` does is: * Makes a new version of a function with one or more arguments already filled in. * The new version of a function documents itself. diff --git a/source/modules/CollectionsModule.rst b/source/modules/CollectionsModule.rst index c23897cc..842e2fa6 100644 --- a/source/modules/CollectionsModule.rst +++ b/source/modules/CollectionsModule.rst @@ -4,7 +4,7 @@ The Collections Module ###################### -Python has a very complete set of built in standard types that support most programming tasks. These include strings and numbers, and also types that can be used to hold other objects -- or "collection" types" +Python has a very complete set of built in standard types that support most programming tasks. These include strings and numbers, and also types that can be used to hold other objects -- or "collection" types. * tuples * lists diff --git a/source/modules/Comprehensions.rst b/source/modules/Comprehensions.rst index f48a569a..397933f4 100644 --- a/source/modules/Comprehensions.rst +++ b/source/modules/Comprehensions.rst @@ -14,7 +14,7 @@ The concept of "functional programming" is clearly defined in some contexts, but In general, code is considered "Pythonic" that uses functional paradigms where they are natural, but not when they have to be forced in. -We will cover functional programming concepts more clearly later in the program, but for now, we'll talk about the syntax for a common functional paradigm: applying an expression to all the members of a sequence to produce another sequence: +We will cover functional programming concepts more clearly later in the program, but for now, we'll talk about the syntax for a common functional paradigm: applying an expression to all the members of a sequence to produce another sequence. Consider this common ``for`` loop structure: @@ -26,7 +26,7 @@ Consider this common ``for`` loop structure: This is such a common pattern that python added syntax to directly support it. This syntax is known as "comprehensions". The most common of which is a list comprehension, used to build up a new list. There are a couple others, which we will get too later, but they all share a similar structure. -The above structure can be expressed with a single line using a "list comprehension" as so: +The above structure can be expressed with a single line using a "list comprehension" like so: .. code-block:: python @@ -34,7 +34,7 @@ The above structure can be expressed with a single line using a "list comprehens Nice and clear and compact, and the use of the "list" brackets (``[...]``) makes it clear you are making a list. -Recall what an expression is in Python: a bit of code (names and operators) that evaluates to a value. So in the beginning of a comprehension, you can put anything that evaluates to a value -- and that value is what gets added to the list. +Recall what an expression is in Python: a bit of code (names and operators) that evaluates to a value. So in the beginning of a comprehension, you can put anything that evaluates to a value -- and that value is what gets added to the new list. This can be a simple (or complex) math operation: ``x * 3``, or a function or method call: ``a_string.upper()``, ``int(x)``, etc. But it can not contain any statements: code that does not return a value, such as assignment (``x = 5``), or ``for`` loops, or ``if`` blocks. @@ -88,7 +88,40 @@ Comprehensions and map() Comprehensions are another way of expressing the "map" pattern from functional programming. -Python does have a ``map()`` function, which pre-dates comprehensions. But it does much of the same things -- and most folks think comprehensions are the more "Pythonic" way to do it. +Python does have a ``map()`` function, which pre-dates comprehensions. But it does much of the same things -- and most folks think comprehensions are the more "Pythonic" way to do it. And there is nothing that can be expressed with ``map()`` that cannot be done with a comprehension. If you are not familiar with ``map()``, you can safely skip this, but if you are: + +.. code-block:: python + + map(a_function, an_iterable) + +is the same as: + +.. code-block:: python + + [a_function(item), for item in an_iterable] + +In this case, the comprehension is a tad wordier than ``map()``. But comprehensions really shine when you don't already have a handy function to pass to map: + +.. code-block:: python + + [x**2 for x in an_iterable] + +To use ``map()``, you need a function: + +.. code-block:: python + + def square(x): + return x**2 + + map(square, an_iterable) + +There are shortcuts of course, including ``lambda`` (stay tuned for more about that): + +.. code-block:: python + + map(lambda x: x**2, an_iterable) + +But is that easier to read or write? What about filter? @@ -111,7 +144,7 @@ This kind of "filtering" loop can be achieved by adding a conditional to the com new_list = [expr for var in a_list if something_is_true] -This is expressing the "filter" pattern and the "map" pattern at the same time -- one reason I like the comprehension sytax so much. +This is expressing the "filter" pattern and the "map" pattern at the same time -- one reason I like the comprehension syntax so much. .. rubric:: Examples: @@ -130,6 +163,8 @@ This is expressing the "filter" pattern and the "map" pattern at the same time - Get creative.... +How do I see all the built in Exceptions? + .. code-block:: python [name for name in dir(__builtin__) if "Error" in name] @@ -164,7 +199,9 @@ This results in the same set as this for loop: or, indeed, the same as passing a list comp to ``set()``. -new_set = set([expression_with_variable for variable in a_sequence]) +.. code-block:: python + + new_set = set([expression_with_variable for variable in a_sequence]) **Example:** Finding all the vowels in a string... @@ -180,20 +217,20 @@ new_set = set([expression_with_variable for variable in a_sequence]) .. note:: - Why did I use ``set('aeiou')`` rather than just `aeiou` ? ... ``in`` works with strings as well, but is it efficient? + Why did I use ``set('aeiou')`` rather than just ``'aeiou'`` ? ... ``in`` works with strings as well, but is it efficient? Dict Comprehensions ------------------- -Also with dictionaries +You can also build up a dictionary with a comprehension: .. code-block:: python new_dict = {key: value for variable in a_sequence} -Same as this for loop: +Which is the same as this for loop: .. code-block:: python @@ -201,7 +238,7 @@ Same as this for loop: for key in a_list: new_dict[key] = value -A dict comprehension also uses curly brackets -- Python knows it's a dict comprehension due to the ``key: value`` construct. +A dict comprehension also uses curly brackets like the set comprehension -- Python knows it's a dict comprehension due to the ``key: value`` construct. **Example:** @@ -267,11 +304,13 @@ But there is also a ``dict()`` constructor (actually the type object for dict): in the keyword argument list. For example: dict(one=1, two=2) Type: type -So the first one is an empty dict -- simple enough +``dict()`` can take different types of arguments, and will do something different with each one. + +The first option (no argument) is an empty dict -- simple enough. -The second makes a dict from the contents of another dict (or similar object) +The option makes a dict from the contents of another dict or similar object (called a "mapping"). -The third one is of interest here -- it makes a dict from an iterable of key, value pairs -- exactly what ``zip()`` gives you. +The options is of interest here -- it makes a dict from an iterable of key, value pairs -- exactly what ``zip()`` gives you. So we can create a dict from data like so: @@ -294,10 +333,10 @@ dict comps are still nice if you need to filter the results, though: Out[17]: {1: 'fred', 2: 'john'} -Generator Expressions ---------------------- +Generator Comprehensions +------------------------ -There is yet another type of comprehension: generator comprehensions, technically known as "generator expressions". They are very much like a list comprehension, except that they evaluate to an lazy-evaluated "iterable", rather than a list. That is, they *generate* the items on the fly. +There is yet another type of comprehension: generator comprehensions, technically known as "generator expressions". They are very much like a list comprehension, except that they evaluate to a lazy-evaluated "iterable", rather than a list. That is, they *generate* the items on the fly. This is useful, because we often create a comprehension simply to loop over it right away: @@ -306,7 +345,7 @@ This is useful, because we often create a comprehension simply to loop over it r for x in [y**2 for y in a_sequence]: outfile.write(f"The number is: {x}") -In this case, the comprehension: ``[y**2 for y in a_sequence]`` iterates over ``a_sequence``, computes the square of each item, and creates a whole new list with the new values. +In this case, the list comprehension: ``[y**2 for y in a_sequence]`` iterates over ``a_sequence``, computes the square of each item, and creates a whole new list with the new values. All this, just so it can be iterated over again right away. If the original sequence is large (or is itself a lazy-evaluated iterable), then the step of creating the extra list can be expensive and unnecessary. Generator comprehensions, on the other hand, create an iterable that evaluates the items as they are iterated over, rather than all at once ahead of time -- so the entire collection is never stored. @@ -351,7 +390,7 @@ A generator is an object that can be iterated over with a for loop, and it will 4 9 -You will learn more about generators, and other ways to make them, in future lessons. +You will learn more about generators and other ways to make them in future lessons. Let's use a little function to make this clear: @@ -362,6 +401,11 @@ Let's use a little function to make this clear: ...: return x ** 2 It simply returns the square of the passed-in value, but prints it as it does so, so we can see when it is called. + +.. note:: + Having a "print" in a function is a example of a "side effect" -- something that is an effect of the function being called that is not reflected in the return value of that function. + As a rule, it's not a good idea to use functions with side effects in comprehensions. We're only doing it here as a debugging aid -- so we can clearly see when the function is being called. + If we use it in a list comp: .. code-block:: ipython @@ -372,7 +416,7 @@ If we use it in a list comp: test called with: 2 Out[10]: [0, 1, 4] -We see that test gets called for all the values, and then a list is returned with all the results. +We see that ``test()`` gets called for all the values, and then a list is returned with all the results. But if we use it in a generator comprehension: .. code-block:: ipython @@ -409,7 +453,7 @@ You usually don't assign a generator expression to a variable, but rather, loop test called with: 2 4 -When to use What +When to Use What ................ It's pretty simple: @@ -422,3 +466,15 @@ If you are going to immediately loop through the items created by the comprehens The "official" term is "generator expression" -- that is what you will see in the Python docs, and a lot of online discussions. I've used the term "generator comprehension" here to better make clear the association with list comprehensions. +References +---------- + +This is a nice intro to comprehensions from Trey Hunner: + +https://treyhunner.com/2015/12/python-list-comprehensions-now-in-color/ + +Once you've got the hang of it, you may want to read this so you don't overdo it :-) + +https://treyhunner.com/2019/03/abusing-and-overusing-list-comprehensions-in-python/ + +Trey writes a lot of good stuff -- I recommend browsing his site. diff --git a/source/modules/ContextManagers.rst b/source/modules/ContextManagers.rst index d0784ba7..60e5d8c0 100644 --- a/source/modules/ContextManagers.rst +++ b/source/modules/ContextManagers.rst @@ -104,9 +104,9 @@ If the resource in questions has a ``.close()`` method, then you can simply use # and here, it will be closed automatically But what if the thing doesn't have a ``close()`` method, or you're creating -the thing and it shouldn't have a close() method? +the thing and it shouldn't have a ``close()`` method? -(full confession: urlib.request was not a context manager in py2 -- but it is in py3 -- but the issue still comes up with third-party packages and your own code!) +(full confession: ``urlib.request`` was not a context manager in py2 -- but it is in py3 -- but the issue still comes up with third-party packages and your own code!) Do It Yourself -------------- @@ -158,12 +158,10 @@ clarify the order in which things happen: .. code-block:: ipython - In [2]: %paste - In [46]: with Context(True) as foo: - ....: print('This is in the context') - ....: raise RuntimeError('this is the error message') - - ## -- End pasted text -- + In [46]: with Context(True) as foo: + ....: print('This is in the context') + ....: raise RuntimeError('this is the error message') + ....: __init__(True) __enter__() This is in the context diff --git a/source/modules/Decorators.rst b/source/modules/Decorators.rst index 60299383..4234f114 100644 --- a/source/modules/Decorators.rst +++ b/source/modules/Decorators.rst @@ -186,7 +186,7 @@ And we can apply it with the regular calling and rebinding syntax: In [6]: other_func Out[6]: .inner> -Notice that other_func is now the "inner" function, which lives in the "my_decorator" namespace... +Notice that ``other_func`` is now the "inner" function, which lives in the "my_decorator" namespace... And this is the same with the decoration syntax: @@ -203,7 +203,7 @@ And this is the same with the decoration syntax: In [9]: other_func Out[9]: .inner> -Notice that other_func is the "inner" function here as well. +Notice that ``other_func`` is the "inner" function here as well. Decorators have the power to replace the decorated function with a different one! @@ -411,7 +411,7 @@ The ``classmethod()`` builtin can do the same thing: property() ----------- -Remember the property() built in? +Remember the ``property()`` builtin? Perhaps most commonly, you'll see the ``property()`` builtin used this way. @@ -502,8 +502,8 @@ A decorator that wraps an html `

` tag around the output of any decorated func @p_decorate - def get_fullname(first_name, last_name): - return f"{first_name} {last_name}" + def get_fullname(first_name, last_name): + return f"{first_name} {last_name}" In [124]: get_fullname('Chris', 'Barker') Out[124]: '

Chris Barker

' @@ -532,8 +532,8 @@ Can you make a version that will wrap any other tag -- specified as a parameter .. code-block:: ipython @add_tag('p') - def get_fullname(first_name, last_name): - return f"{first_name} {last_name}" + def get_fullname(first_name, last_name): + return f"{first_name} {last_name}" In [124]: get_fullname('Chris', 'Barker') Out[124]: '

Chris Barker

' @@ -545,21 +545,21 @@ But: .. code-block:: ipython @add_tag('div') - def get_fullname(first_name, last_name): - return f"{first_name} {last_name}" + def get_fullname(first_name, last_name): + return f"{first_name} {last_name}" In [124]: get_fullname('Chris', 'Barker') Out[124]: '
Chris Barker
' and you could pass any tag in. -This can be ackomplished either with a closure --nesting antoher level of functions in the decorator, or with a callable class, like the memoize example. Maybe try both, and decide which you like better. +This can be accomplished either with a closure --nesting another level of functions in the decorator, or with a callable class, like the memoize example. Maybe try both, and decide which you like better. Further Reading: ---------------- -*Fluent Python* by Luciano Ramalho, chapter 7. +*Fluent Python* by Luciano Ramalho, Chapter 7. Another good overview: diff --git a/source/modules/DictsAndSets.rst b/source/modules/DictsAndSets.rst index 70c2e373..4191fdcc 100644 --- a/source/modules/DictsAndSets.rst +++ b/source/modules/DictsAndSets.rst @@ -57,7 +57,7 @@ Calling the dict type object constructor: >>> dict(key1=3, key2= 5) {'key1': 3, 'key2': 5} - # creating and empty dict, and then populating it: + # creating an empty dict, and then populating it: >>> d = {} >>> d['key1'] = 3 >>> d['key2'] = 5 @@ -181,7 +181,7 @@ Traditionally, dictionaries have had no defined order. See this example from Pyt Note how I defined the dict in a natural order, but when it gets printed, or you display the keys, they are in a different order. However, In cPython 3.6, the internal implementation was changed, and it *does* happen to preserve order. In cPython 3.6, that is considered an implementation detail -- and you should not count on it! However, as of cPython 3.7, dictionaries preserving order will be part of the language specification. This was declared by Guido on the python-dev mailing list on -`Dec 15, 2017 ` +Dec 15, 2017 . .. code-block:: ipython @@ -213,7 +213,7 @@ When new items are added to a dict, they go on the "end": and ``dict.popitem()`` will remove the "last" item in the dict. -**CAUTION** This is new behavior in cPython 3.6 -- older versions of Python (notably including Python 2) do not preserve order. In older versions, there is a special version of a dict in the collections module: ``Collections.OrderedDict`` which preserves order in all versions of Python, and has a couple extra features. +**CAUTION** This is new behavior in cPython 3.6 -- older versions of Python (notably including Python 2) do not preserve order. In older versions, there is a special version of a dict in the collections module: ``collections.OrderedDict`` which preserves order in all versions of Python, and has a couple extra features. Dictionary Iterating @@ -442,8 +442,8 @@ If you want a copy, use the explicit copy method to get a copy: 'something': 'a value', 'something else': 'another value'} - In [54]: item_copy - Out[54]: {'something': 'a value', 'something else': 'another value'} + In [54]: item_copy + Out[54]: {'something': 'a value', 'something else': 'another value'} Sets diff --git a/source/modules/Documentation.rst b/source/modules/Documentation.rst index b1e370cf..9b471f91 100644 --- a/source/modules/Documentation.rst +++ b/source/modules/Documentation.rst @@ -60,7 +60,7 @@ This is not useful: # apply soap to each sponge worker.apply_soap(sponge) -Note: Nothing special about Python here -- basic good programing practice. Note that you will need a lto fewer comments if you choose your names well! +Note: Nothing special about Python here -- basic good programing practice. Note that you will need a lot fewer comments if you choose your names well! Docstrings ---------- @@ -89,9 +89,9 @@ A Function Docstring Should: * Be a complete sentence in the form of a command describing what the function does. - * """Return a list of values based on blah blah""" is a good docstring + * ``"""Return a list of values based on blah blah"""`` is a good docstring - * """Returns a list of values based on blah blah""" is *not* as good.. + * ``"""Returns a list of values based on blah blah"""`` is *not* as good.. * Have a useful single line. diff --git a/source/modules/EnvironmentOverview.rst b/source/modules/EnvironmentOverview.rst index 90f8dcfc..ecf09e0d 100644 --- a/source/modules/EnvironmentOverview.rst +++ b/source/modules/EnvironmentOverview.rst @@ -40,7 +40,6 @@ If you are not sure, then read on ... The Command Line (cli) ---------------------- - Having some facility on the command line is important for a software developer. We won't cover this much in class, so if you are not comfortable, @@ -54,7 +53,6 @@ Most of the demos in class, etc., will be done using the "bash" command line she Windows provides the "DOS" command line, which is OK, but pretty old and limited, or "Power Shell," a more modern, powerful, flexible command shell. - If you are comfortable with either of these, go for it. If not, you can use the "git Bash" shell, which is much like the bash shell on OS-X and Linux. Or, on Windows 10, look into the "bash shell for Windows," otherwise known as the "Windows Subsystem for Linux." More info is available here: @@ -140,7 +138,6 @@ The Editor Typing code in an interpreter is great for exploring. - But for anything "real," you'll want to save the work you are doing in a more permanent fashion. This is where a "programmer's text editor" fits in. @@ -206,9 +203,6 @@ You may want to try the educational edition of PyCharm, which some people like a https://www.jetbrains.com/pycharm-edu/ - - - .. _testing_your_setup: Testing Your setup diff --git a/source/modules/Exceptions.rst b/source/modules/Exceptions.rst index 3d5b3fd1..cb314857 100644 --- a/source/modules/Exceptions.rst +++ b/source/modules/Exceptions.rst @@ -166,7 +166,7 @@ This is really important if your code does anything before the exception occurre In this case, the file will be properly closed regardless. And many other systems, like database managers, etc. can also be used with ``with``. -This is known as a "context manager", and was added to Python specifically to handle the common cases that required finally clauses. But if your use case does not already have a context manager that handles the cleanup you may need. +This is known as a "context manager", and was added to Python specifically to handle the common cases that required ``finally`` clauses. But if your use case does not already have a context manager that handles the cleanup you may need. Exceptions -- ``else`` ---------------------- @@ -185,11 +185,11 @@ Yet another flow control option: So the ``else`` block only runs if there was no exception. That was also the case in the previous code, so what's the difference? -**Advantage of ``else``:** +**Advantage of** ``else`` **:** Using the ``else`` block lets you catch the exception as close to where it occurred as possible -- always a good thing. -Why? -- because maybe the "process(f)" could raise an exception, too? Then you don't know if the exception came from the ``open()`` call or in some code after that. +Why? -- because maybe the ``process(f)`` could raise an exception, too? Then you don't know if the exception came from the ``open()`` call or in some code after that. This bears repeating: @@ -250,16 +250,16 @@ For an example -- try running this code: .. code-block:: ipython -In [34]: try: - ...: f = open("blah") - ...: except IOError as err: - ...: print(err) - ...: print(dir(err)) - ...: the_err = err + In [34]: try: + ...: f = open("blah") + ...: except IOError as err: + ...: print(err) + ...: print(dir(err)) + ...: the_err = err The ``print(dir(err))`` will print all the names (attributes) in the error object. A number of those are ordinary names that all objects have, but a few are specific to this error. -the ``the_err`` = err line is there so that we can keep a name bound to the err after the code is run. ``err`` as bound by the except line only exists inside the following block. +the ``the_err = err`` line is there so that we can keep a name bound to the ``err`` after the code is run. ``err`` as bound by the except line only exists inside the following block. Now that we have a name to access it, we can look at some of its attributes. The name of the file that was attempted to be opened: @@ -268,14 +268,14 @@ Now that we have a name to access it, we can look at some of its attributes. The In [35]: the_err.filename Out[35]: 'blah' -The message that will be pronted is usually in the "args" attribute: +The message that will be printed is usually in the ``.args`` attribute: .. code-block:: ipython In [37]: the_err.args Out[37]: (2, 'No such file or directory') -the ``.__traceback__`` attribute hold the actual traceback object -- all the information about the context the exception was raised in. That can inpsected to get all sorts of info. tHat is very advanced stuff, but you can investigate the ``inspect`` module if you want to know how. +the ``.__traceback__`` attribute hold the actual traceback object -- all the information about the context the exception was raised in. That can be inspected to get all sorts of info. That is very advanced stuff, but you can investigate the ``inspect`` module if you want to know how. Multiple Exceptions ------------------- diff --git a/source/modules/Files.rst b/source/modules/Files.rst index 55c189b6..622e1abb 100644 --- a/source/modules/Files.rst +++ b/source/modules/Files.rst @@ -21,7 +21,7 @@ Text Files ``secret_data`` is a string -NOTE: these days, you probably need to use Unicode for text -- we'll get to that later... +.. note:: In Python 3, files are opened by default in text mode, and the default encoding is UTF-8. This means that in the usual case, you get a proper Unicode string to work with, as UTF-8 is the most common encoding for text. Also, it is ASCII compatible, so ASCII Files with "just work". IF "Unicode" and "ASCII" mean nothing to you -- don't worry about it, just know that things will usually work for text, even non-English text. And if you get odd characters or an ``EncodingError``, then your file is not UTF-8, and it's time to Google "Python Unicode". (more info here: :ref:`unicode`) Binary Files @@ -55,14 +55,14 @@ in the Python docs. But these BSD docs make it pretty clear: http://www.manpagez.com/man/3/fopen/ -**Gotcha** -- 'w' modes always clear the file. +**Gotcha** -- 'w' modes always clear the file if it already exists! Text File Notes --------------- Text is default: - * Newlines are translated: ``\r\n -> \n`` + * Newlines are translated: ``\r\n`` -> ``\n`` * -- reading and writing! * Use \*nix-style in your code: ``\n`` @@ -95,7 +95,7 @@ Common Idioms for line in open('secrets.txt'): print(line) -(the file object is an iterator!) +(The file object is an iterable that iterates through the lines in a text file.) .. code-block:: python @@ -107,8 +107,7 @@ Common Idioms do_something_with_line() -We will learn more about the keyword with later, but for now, just understand -the syntax and the advantage over the try-finally block: +We will learn more about the keyword ``with`` later (it creates a "context manager"), but for now, just understand the syntax and the advantage over simply opening the file: .. code-block:: python @@ -117,6 +116,9 @@ the syntax and the advantage over the try-finally block: f.closed True +You use ``with`` to open the file, and assign it a name (``f`` in this case). +The file remains open while in the ``with`` block. +At the end of the ``with`` block, the file is unconditionally closed, even if an Exception is raised. You code will (mostly) work without it, but it's a good habit to get into to always use ``with`` to open a file. File Writing ------------ @@ -148,27 +150,34 @@ Commonly Used Methods: f.close() -StringIO --------- +``StringIO`` +------------ + +A ``StringIO`` method is a "file like" object that stores the content in memory. +That is, it has all the methods of a file, and behaves the same way, but never writes anything to disk. .. code-block:: python - In [417]: import io - In [420]: f = io.StringIO() - In [421]: f.write("somestuff") - In [422]: f.seek(0) - In [423]: f.read() - Out[423]: 'somestuff' - Out[424]: stuff = f.getvalue() - Out[425]: f.close() + In [6]: import io -(This can be handy for testing file handling code...) + In [7]: f = io.StringIO() -There is also cStringIO -- a bit faster. + In [8]: f.write("some stuff") + Out[8]: 10 -.. code-block:: python + In [9]: f.seek(0) + Out[9]: 0 + + In [10]: f.read() + Out[10]: 'some stuff' + + In [11]: f.getvalue() + Out[11]: 'some stuff' + + In [12]: f.close() + +(This can be handy for testing file handling code...) - from cStringIO import StringIO Paths and Directories ===================== @@ -176,7 +185,7 @@ Paths and Directories Paths ----- -Paths are generally handled with simple strings (or Unicode strings). +Paths are generally handled with simple strings. Relative paths: @@ -192,19 +201,17 @@ Absolute paths: '/home/chris/secret.txt' -Either work with ``open()`` , etc. +Either works with ``open()`` , etc. -(A working directory only makes sense with command-line programs.) +Relative paths are relative to the current working directory, which is only relevant to command-line programs. -os module ----------- +``os`` module +------------- .. code-block:: python os.getcwd() os.chdir(path) - os.path.abspath() - os.path.relpath() ``os.path`` module @@ -217,6 +224,8 @@ os module os.path.basename() os.path.dirname() os.path.join() + os.path.abspath() + os.path.relpath() (all platform independent) @@ -243,17 +252,20 @@ All the stuff in os.path and more: .. code-block:: ipython - In [64]: import pathlib - In [65]: pth = pathlib.Path('./') - In [66]: pth.is_dir() - Out[66]: True - In [67]: pth.absolute() - Out[67]: PosixPath('/Users/Chris/PythonStuff/UWPCE/IntroPython2015') - In [68]: for f in pth.iterdir(): - print(f) - junk2.txt - junkfile.txt - ... + In [14]: import pathlib + + In [15]: pth = pathlib.Path('./') + + In [16]: pth.is_dir() + Out[16]: True + + In [17]: pth.absolute() + Out[17]: PosixPath('/Users/Chris/PythonStuff/UWPCE/Fall2018-PY210A/examples/Session02') + + In [18]: for f in pth.iterdir(): + ...: print(f) + ...: + ...: And it has a really nifty way to join paths, by overloading the "division" operator: @@ -303,7 +315,7 @@ What this means to you Unless you are writing a path manipulation library, or a library that deals with paths other than with the stdlib packages (like ``open()``), all you need to know is that you can use ``Path`` objects most places you need a path. -I expect we will see expanded use of pathlib as python 3.6 becomes widely used. +I expect we will see expanded use of pathlib as python 3.6 and 3.7 becomes widely used. Some added notes: ================= @@ -323,8 +335,7 @@ When working with files, unless you have a good reason not to, use ``with``: # now done with out file -- it will be closed, regardless of errors, etc. do_other_stuff -``with`` invokes a context manager -- which can be confusing, but for now, -just follow this pattern -- it really is more robust. +``with`` invokes a context manager -- which can be confusing, but for now, just follow this pattern -- it really is more robust. And you can even do two at once: @@ -348,7 +359,6 @@ All data in all files is binary -- that's how computers work. So in Python3, "te But this too is complicated -- there are multiple ways that binary data can be mapped to Unicode text, known as "encodings". In Python, text files are by default opened with the "utf-8" encoding. These days, that mostly "just works". - But if you read a binary file as text, then Python will try to interpret the bytes as utf-8 encoded text -- and this will likely fail: .. code-block:: ipython diff --git a/source/modules/Functions.rst b/source/modules/Functions.rst index 27b73c0c..c2503909 100644 --- a/source/modules/Functions.rst +++ b/source/modules/Functions.rst @@ -233,6 +233,12 @@ B) If not, then it does a computation using the same function with another value It is critical that the first check is there, or the function will never terminate. +Further Reading +--------------- + +Here's a nice blog post about writting better functions: + +https://jeffknupp.com/blog/2018/10/11/write-better-python-functions/ diff --git a/source/modules/Git.rst b/source/modules/Git.rst index 069f85da..0ee378ac 100644 --- a/source/modules/Git.rst +++ b/source/modules/Git.rst @@ -26,7 +26,7 @@ That last one is a bit tricky, and is not necessary to understand right out of t There are other versioning systems, such as Mercurial and Subversion (and commercial offerings), but Git is the most popular. -It is incredibly important to learn and understand versioning control to work as a developer today, so we have incorporated Git into our work flow for submitting students' work in this class. +It is incredibly important to learn and understand version control to work as a developer today, so we have incorporated Git into our work flow for submitting students' work in this class. Setting up Git @@ -86,7 +86,7 @@ Repositories A repository is just a collection of files that 'belong together'. Since ``git`` is a *distributed* versioning system, there is no **primary** -repository that serves as the one to rule them all. This simply means that all repositories should look the same. +repository that serves as the one to rule them all. This simply means that all repositories on each users machine should look the same. However, to keep things sane, there is generally one "central" repository chosen that users check with for changes. For us this is the one hosted on GitHub in the UWPCE-PythonCert-ClassRepos organization. @@ -124,9 +124,11 @@ The class repositories are on *GitHub* in the *UWPCE-PythonCert-ClassRepos* orga :width: 50% :class: center +https://github.com/UWPCE-PythonCert-ClassRepos + Each class will have a repository created specifically for it, called something like: "Wi2018-Online". -In examples below it is called YourClassRepoNameHere, so replace that in your head with the name of your class's repository. +In examples below it is called YourClassRepoNameHere, so replace that in your head with the name of your class' repository. This class repository will include examples and relevant materials (and some exercise solutions) will be added throughout the class. @@ -149,13 +151,15 @@ Once you are logged in to your gitHub account, go to the appropriate class repos https://github.com/UWPCE-PythonCert-ClassRepos +Make sure you find the right repo for YOUR class! + Once in the repo for your class, click on the "fork" button in the upper right of the page to create a fork in your gitHub account. You will now have a copy of the class repo, and can then set up your personal machine to connect to that copy. .. figure:: /_static/remotes_fork.png :width: 50% :class: center -Everyone should now have a copy of the class repository in their account on the GitHub website. +Yoy should now have a copy of the class repository in your account on the GitHub website. The next step is to make a *clone* of your fork on your own computer, which means that **your fork** in GitHub is the *origin*: @@ -172,7 +176,7 @@ From that directory, run $ git clone https://github.com/your_github_id/YourClassRepoNameHere -Be sure to replace "YourClassRepoNameHere" with the name of your class repository (you can get the url by going to the class repo on gitHub and clicking “clone or download”). +Be sure to replace "YourClassRepoNameHere" with the name of your class repository (you can get the url by going to the class repo on gitHub and clicking "clone or download"). This will download the repository from your GitHub account into the local directory that you ran the git *clone* command from. @@ -181,20 +185,28 @@ Adding a remote Since you are working on a repository that you do not own, you will need to make a git shortcut to the original repository, so that you can get changes made by other contributors (i.e. the instructors and other students) before you start working. -You can add *remotes* at will, to connect your *local* repository to other -copies of it in different remote locations. +You can add *remotes* at will, to connect your *local* repository to other copies of it in different remote locations. This allows you to grab changes made to the repository in these other locations. For this class, you will add an *upstream* remote to our local copy that points to the original copy of the material in the -``UWPCE-PythonCert-ClassRepos`` account, and we will call it, appropriately, "upstream". Change directories into your local version of the class -repository and run (remembering to use the name of your class): +``UWPCE-PythonCert-ClassRepos`` account, and we will call it, appropriately, "upstream". +Change directories into your local version of the class repository: + +.. code-block:: bash + + $ cd YourClassRepoNameHere + +and run (remembering to use the name of your class): + .. code-block:: bash $ git remote add upstream https://github.com/UWPCE-PythonCert-ClassRepos/YourClassRepoNameHere +You can get that full url by going to GitHub, finding the repo, and copying the "clone" url. then you can type the command, and simply paste the url. + Your local setup should now look something like this: .. code-block:: bash diff --git a/source/modules/IteratorsAndGenerators.rst b/source/modules/IteratorsAndGenerators.rst index 61b0cb2d..f0298ea0 100644 --- a/source/modules/IteratorsAndGenerators.rst +++ b/source/modules/IteratorsAndGenerators.rst @@ -115,7 +115,7 @@ https://docs.python.org/3/library/stdtypes.html#iterator-types Iterables --------- -To make an object iterable, you simply have to implement the __getitem__ method. +To make an object iterable, you simply have to implement the ``__getitem__`` method. .. code-block:: python @@ -131,8 +131,8 @@ To make an object iterable, you simply have to implement the __getitem__ method. How do you get the iterator object from an "iterable"? -The iter function will make any iterable an iterator. It first looks for the __iter__ -method, and if none is found, uses get_item to create the iterator. +The ``iter`` function will make any iterable an iterator. It first looks for the ``__iter__`` +method, and if none is found, uses ``__getitem__`` to create the iterator. The ``iter()`` function: @@ -199,7 +199,7 @@ It works, and is fairly efficient, but what about: for triple in zip(words[:-2], words[1:-1], words[2:]): -zip() returns an iterable -- it does not build up the whole list. +``zip()`` returns an iterable -- it does not build up the whole list. So this is quite efficient. but we are still slicing: ([1:]), which produces a copy -- so we are creating three copies of @@ -323,7 +323,7 @@ An "iterator" is anything that conforms to the "iterator protocol": - Has a ``__next__()`` method that returns objects. - Raises ``StopIteration`` when their are no more objects to be returned. - Has a ``__iter__()`` method that returns an iterator -- usually itself. - - sometimes the __iter__() method re-sets the iteration... + - sometimes the ``__iter__()`` method re-sets the iteration... https://docs.python.org/3/glossary.html#term-iterator @@ -430,7 +430,7 @@ Really just a shorthand for an iterator class that does the book keeping for you To master yield, you must understand that when you call the function, the code you have written in the function body does not run. The function only returns the generator object. The actual code in the function is run -when next() is called on the generator itself. +when ``next()`` is called on the generator itself. And note that each time you call the "generator function" you get a new instance of a generator object that saves state separately from other instances. @@ -473,7 +473,7 @@ Note: A generator function can also be a method in a class In fact, this is a nice way to provide different ways to iterate over the data in a class in multiple ways. -This is done by the dict protocol with dict.keys() and dict.values(). +This is done by the dict protocol with ``dict.keys()`` and ``dict.values()``. More about iterators and generators: @@ -509,11 +509,11 @@ Keep in mind -- if all you need to do with the results is loop over it Other uses for ``yield`` ------------------------ -The yield keyword and generator functions were designed with classic "generators" in mind. +The ``yield`` keyword and generator functions were designed with classic "generators" in mind. That is -- objects that generate values on the fly. -But, as we alluded to earlier, yield can be used for other things as well. +But, as we alluded to earlier, ``yield`` can be used for other things as well. Anytime you want to return a value, and then hold state until later, ``yield`` can be used. @@ -530,7 +530,7 @@ Anytime you want to return a value, and then hold state until later, # do the teardown something_with(value) -In this case, the yield isn't in any sort of loop or anything. +In this case, the ``yield`` isn't in any sort of loop or anything. It will only get run once. But the generator will maintain state, so the value can be used after the yield to do the teardown. diff --git a/source/modules/MetaProgramming.rst b/source/modules/MetaProgramming.rst index d23d7a3c..efe62c0e 100644 --- a/source/modules/MetaProgramming.rst +++ b/source/modules/MetaProgramming.rst @@ -497,7 +497,7 @@ In Python 2, instead of the keyword argument, a special class attribute: Otherwise it's the same. -The __metaclass__ attribute is part of determining that function. If __metaclass__ is a key in the body dictionary then the value of that key is used. This value could be anything, although if not callable an exception will be raised. +The __metaclass__ attribute is part of determining that function. If __metaclass__ is a key in the body dictionary then the value of that key is used. This value could be anything, although if not callable an exception will be raised. from http://jfine-python-classes.readthedocs.io/en/latest/decorators-versus-metaclass.html Why use metaclasses? @@ -764,7 +764,7 @@ This also happens at compile time, rather than run time, just like metaclasses. class decorators were actually introduced AFTER metaclasses -- maybe they are a clearer solution to some problems? -As an example, in Python 3.7 (which is in beta release as of this writing), there is a new feature in the standard library: ``Data Classes``, introduced in +As an example, in Python 3.7, there is a new feature in the standard library: ``Data Classes``, introduced in `PEP 557 `_ They are a quick way to make a simple class whose prime purpose is to store a set of fields -- kind of like a database record. What the new tool provides is auto-generation of all the boilerplate code for the ``__init__``, etc. They could have been implemented with a metaclass, but it was decided to use a class decorator instead. From the PEP: @@ -785,6 +785,11 @@ And another one: `A Study of Python's More Advanced Features Part III: Classes and Metaclasses `_ +And this is a argument for class decorators by the author or the patch that enabled them (in Python 2.6): + +`Jack Diederich: Class Decorators: Radically Simple `_ + + NameMangler Decorator Edition ----------------------------- diff --git a/source/modules/Modules.rst b/source/modules/Modules.rst index 314d9d82..865a781e 100644 --- a/source/modules/Modules.rst +++ b/source/modules/Modules.rst @@ -126,7 +126,7 @@ Packages A package is a module with other modules in it. -On a filesystem, this is represented as a directory that contains one or more``.py`` files, one of which **must** be called ``__init__.py``. +On a filesystem, this is represented as a directory that contains one or more ``.py`` files, one of which **must** be called ``__init__.py``. When you have a package, you can import only the package, or any of the modules inside it. When a package is imported, the code in the ``__init__.py`` file is run, and any names defined in that file are available in the *package namespace*. @@ -155,7 +155,7 @@ Save another file in your my_package dir called ``a_module.py``, and put this in def a_function(): print("a_function has been called") -You now have about the simplest package you can have. If make sure your current working dir is the dir that ``my_package`` is in, and start python or iPython. Then try this code: +You now have about the simplest package you can have. Make sure your current working dir is the dir that ``my_package`` is in, and start python or iPython. Then try this code: .. code-block:: ipython @@ -204,7 +204,7 @@ Note that you can also put a package inside a package. So you can create arbitra "Flat is better than nested." -So don't overdue it -- only go as deep as you really need to to keep the your code organized. +So don't overdo it -- only go as deep as you really need to to keep your code organized. Importing modules ----------------- diff --git a/source/modules/NamingThings.rst b/source/modules/NamingThings.rst index b343b353..e0cc5a8b 100644 --- a/source/modules/NamingThings.rst +++ b/source/modules/NamingThings.rst @@ -54,7 +54,7 @@ Naming Guidelines ----------------- Whenever possible, use strong, unambiguous names that relate to a concept in the business area applicable for your program. -For example, cargo_weight is probably better than item_weight, current_fund_price is better than value. +For example, ``cargo_weight`` is probably better than ``item_weight``, ``current_fund_price`` is better than ``value``. But all of those are better than ``item``, or ``x``, or ... Only use single-letter names for things with limited scope: indexes and the like: @@ -91,7 +91,49 @@ And then singular for a single item in that collection: line = line.replace(",", " ") .... +What about Hungarian Notation? +------------------------------ + +`Hungarian Notation `_ +is a naming system where the data type is part of the name: + +.. code-block:: python + + strFirstName = "Chris" + + listDonations = [400.0, 125.0, 1000.0] + + int_num_days = 30 + +This method is not recommended nor widely used in the Python community. + +One reason is Python's dynamic typing -- it really isn't important what type a value is, but rather, what it means. +And you may end up refactoring the code to use a different type, and then do you want to have to rename everything? +Or worse, the type in the name no longer matches the actual type in the code -- and that's really bad. I have seen code like this: + +.. code-block:: python + + strNumber = input("How many would you like?") + strNumber = int(strNumber) + + for i in range(strNumber): + ... + +So you have a name used for a string, then it gets converted to an integer, and the data type no longer matches the name. Wouldn't you be better off if that had never been named with the type in the first place? + +While widely used in some circles, it is generally considered bad style in the Python community -- so: + + **Do not use Hungarian Notation** + +More About Naming Things +------------------------ + Here's a nice talk about naming: -http://pyvideo.org/video/3792/name-things-once-0 +`Jack Diederich: Name things Once `_ + +One note about that talk -- Jack is mostly encouraging people to not use names that are too long and unnecessarily specific. +However, with beginners, it's often tempting to use names that are too *short* and *non-specific*, like "x" and "item" -- so you need to strike a balance. + + diff --git a/source/modules/ObjectOrientationOverview.rst b/source/modules/ObjectOrientationOverview.rst index 792a2da3..1e695674 100644 --- a/source/modules/ObjectOrientationOverview.rst +++ b/source/modules/ObjectOrientationOverview.rst @@ -31,10 +31,9 @@ What is Object Oriented Programming? within a set of functions designed to ensure that the data are used appropriately, and to assist in that use" - | -http://en.wikipedia.org/wiki/Object-oriented_programming +- http://en.wikipedia.org/wiki/Object-oriented_programming **Even simpler:** @@ -47,7 +46,8 @@ This is the core of "encapsulation" The Dominant Model ------------------ -OO is the dominant model for the past couple decades, but it is not the only model, and languages such as Python increasingly mix and blend models. +OO is the dominant model for the past couple decades, but it is not the only model, and languages such as Python increasingly mix and blend models (such as Procedural, Object Oriented, Functional). In Python, it is best to choose the approach that best solves your problem. + Object Oriented Concepts ------------------------ @@ -56,7 +56,7 @@ These are all terms you will hear when talking about Object Oriented Programming Class - A category of objects: particular data and behavior: for example, a "circle" (same as a type in Python). + A category of objects: particular data and behavior: for example, a "circle" (same as a "type" in Python). Instance A particular object of a class: a specific circle. @@ -66,18 +66,18 @@ Object Attribute Something that belongs to an object (or class): generally thought of - as a variable, or single object, as opposed to a ... + as a simple value, variable, or single object, as opposed to a ... Method - A function that belongs to a class. In Python, functions *are* semantically the same as any other type -- so all methods are "attributes", but not all attributes are methods --methods are the functions -- or more strictly speaking the 'callable' attributes. + A function that belongs to a class. In Python, functions *are* semantically the same as any other type -- so all methods are "attributes", but not all attributes are methods. Methods are the functions, or more strictly speaking: the 'callable' attributes. Encapsulation The approach where the details of the structure are "hidden" in a class -- the user of the class does not need to know how the data is stored (and may not be able to know...) Data Protection - This is the cocept that classes can hide data from outside access (sometimes called "private" attributes. Python does not strictly support data protection. + This is the concept that classes can hide data from outside access (sometimes called "private" attributes. Python does not strictly support data protection. -Class vs Instance attributes +Class vs. Instance Attributes Attributes can be attached to a class -- that is, shared by all instances of that class, or they can be attached to only that instance. Subclassing @@ -99,20 +99,16 @@ Is Python a "True" Object-Oriented Language? What are its strengths and weaknesses vis-a-vis OO? -Python does support all of the above concepts (except data protection). - But it does not support *full* encapsulation, i.e., it does not require classes, and classes don't have "private" attributes. **but ...** Folks can't even agree on what OO "really" means. -See: The Quarks of Object-Oriented Development +See: `The Quarks of Object-Oriented Development `_ - Deborah J. Armstrong -http://ontheturingtest.blogspot.com/2013/11/the-quarks-of-objected-orientation-la.html - Object Oriented Design ---------------------- @@ -122,7 +118,8 @@ This may be a good approach for a "pure" OO language, but with Python it tends t So my recommendation is to think in terms of what makes sense for your project: - -- there is no single paradigm for software design. +.. centered:: **There is no single best paradigm for software design** + Python's roots @@ -137,13 +134,12 @@ Python's roots You can do OO in C ------------------ -Which today is not considered an OO Language. - -See the GTK+ project. +Which today is not considered an OO Language. See the GTK+ project. So OO is really a design approach -- putting the data together with the functions that manipulate that data. It isn't defined by language features. -That being said: OO languages give you some handy tools to make it easier (and safer): +That being said: OO languages give you some handy tools to make it easier +(and safer): * Polymorphism (duck typing gives you this) * Inheritance @@ -178,10 +174,4 @@ As you learn what is possible, this will all start to make more sense. So time to move on to how to actually **do** OO in Python! - - - - - - - +Here's how to do it in Python: :ref:`python_classes` diff --git a/source/modules/Packaging.rst b/source/modules/Packaging.rst index 02753d94..492beb9b 100644 --- a/source/modules/Packaging.rst +++ b/source/modules/Packaging.rst @@ -37,25 +37,25 @@ of python files or other package directories:: module_b.py The ``__init__.py`` can be totally empty -- or it can have arbitrary python code in it. -The code will be run when the package is imported -- just like a module, +The code will be run when the package is imported -- just like a module. -modules inside packages are *not* automatically imported. So, with the above sgructure:: +Modules inside packages are *not* automatically imported. So, with the above structure:: import a_package -will run the code in ``a_package/__init__.py``. Any names defined in the -``__init__.py`` will be available in:: +will run the code in ``a_package/__init__.py``. Any names defined in the ``__init__.py`` will be available in:: a_package.a_name -but:: +But:: a_package.module_a -will not exist. To get submodules, you need to explicitly import them: +will not exist. To get submodules, you need to explicitly import them like so: import a_package.module_a + More on Importing ----------------- @@ -77,11 +77,11 @@ or a few names from a package:: x, y) -And you can rename stuff as you import it:: +You also can optionally rename stuff as you import it:: import numpy as np -This is a common pattern for using large packages and not having to type a lot... +This is a common pattern for using large packages (maybe with long names) and not having to type a lot. ``import *`` @@ -91,18 +91,16 @@ This is a common pattern for using large packages and not having to type a lot.. from something import * -means: "import all the names in the module" +Means: "import all the names in the module, "something". -You really don't want to do that! It is an old pattern that is now an anti-pattern +You really don't want to do that! It is an old pattern that is now an anti-pattern. -But if you do encounter it, it doesn't actually import all the names -- -it imports the ones defined in the module's ``__all__`` variable. +But if you do encounter it, it doesn't actually import all the names -- it imports the ones defined in the module's ``__all__`` variable. -``__all__`` is a list of names that you want import * to import -- so -the module author can control it, and not expect all sorts of build ins -and other modules. +``__all__`` is a list of names that you want ``import *`` to import. +So the module author can control it, and not accidentally override builtins or bring a lot of extraneous names into your namespace. -But really -- don't use it! +But really -- **don't use ``import *``** Relative imports @@ -120,7 +118,7 @@ This gets confusing! There is a good discussion on Stack Overflow here: http://stackoverflow.com/questions/14132789/relative-imports-for-the-billionth-time -Relative imports allow you to refer to other modules relative to where the existing module is in the package hierarchy, rather than in the whole thing. For instance, with the following package structure:: +Relative imports allow you to refer to other modules relative to where the existing module is in the package hierarchy, rather than in the entire python module namespace. For instance, with the following package structure:: package/ __init__.py @@ -151,7 +149,7 @@ Similarly to \*nix shells: ".." means "the package above this one" -Note that you have to use the "from" form when using relative imports. +Note that you have to use the ``from`` form of import when using relative imports. **Caveats:** @@ -187,8 +185,13 @@ There is debate about which is the "one way to do it" -- a bit unpythonic, but y sys.modules ----------- +``sys.modules`` is simply a dictionary that stores all teh already imported modules. +The keys are the module names, and the values are the module objects themselves: + .. code-block:: ipython + In [3]: import sys + In [4]: type(sys.modules) Out[4]: dict @@ -207,7 +210,7 @@ sys.modules '__file__', '__builtins__'] -you can access the module through the modules dict: +you can access the module through the ``sys.modules`` dict: .. code-block:: ipython @@ -233,8 +236,7 @@ So, more or less, when you import a module, the interpreter: * Looks to see if the module is already in ``sys.modules``. -* If it is, it binds a name to the existing module in the current - module's namespace. +* If it is, it binds a name to the existing module in the current module's namespace. * If it isn't: @@ -251,15 +253,14 @@ Implications of module import process: * Every place your code imports a module it gets the *same* object - You can use this to share "global" state where you want to. -* If you change the code in a module while the program is running -- the - change will **not** show up, even if re-imported. +* If you change the code in a module while the program is running -- the change will **not** show up, even if re-imported. - That's what ``imp.reload()`` is for. The module search path ---------------------- -The interpreter keeps a list of all the places that it looks for modules or packages when you do an import: +The interpreter keeps a list (``sys.path``) of all the places that it looks for modules or packages when you do an import: .. code-block:: python @@ -267,11 +268,11 @@ The interpreter keeps a list of all the places that it looks for modules or pack for p in sys.path: print p -you can manipulate that list to add or remove paths to let python find modules on a new place. +you can manipulate that list to add or remove paths to let python find modules in a new place. -And every module has a ``__file__`` name that points to the path it lives in. This lets you add paths relative to where you are, etc. +Every module has a ``__file__`` name that points to the path it lives in. This lets you add paths relative to where you are, etc. -*NOTE* it's usually better to use setuptools' "develop" mode instead -- see below. + .. note:: It's usually better to use setuptools' "develop" mode (or ``pip install -e``) instead -- see below. Reloading --------- @@ -311,7 +312,8 @@ So far in this class, we've used the Python from python.org. It works great, and But there are also a few "curated" distributions. These provide python and a package management system for hard-to-build packages. -Widely used by the scipy community +Widely used by the scipy community: + (lots of hard to build stuff that needs to work together...) * Anaconda (https://store.continuum.io/cshop/anaconda/) @@ -328,9 +330,9 @@ If you are doing data science or scientific development -- I recommend you take Installing Packages =================== -Every Python installation has its own stdlib and ``site-packages`` folder +Every Python installation has its own stdlib and ``site-packages`` folder. -``site-packages`` is the default place for third-party packages +``site-packages`` is the default place for third-party packages. From source @@ -453,7 +455,7 @@ Sometimes simpler: 1) A lot of packages have Windows wheels now. - - often installable with pip (pip will install a wheel for you if it exists) + - Often installable with pip (pip will install a wheel for you if it exists) - Usually for python.org builds - Excellent source: http://www.lfd.uci.edu/~gohlke/pythonlibs/ - Make sure you get 32 or 64 bit consistent @@ -1049,7 +1051,7 @@ I personally like the simplest one with the least magic: setup( ... package_data={'pkg_name': ['data/datatfile1', - 'data/datafile2']}, + 'data/datafile2']}, ... ) @@ -1135,15 +1137,12 @@ LAB: A Small Example Package - ``at least one working test`` -* If you are ready -- it can be the start of your project package. +* If you have some code of your own ready to go -- use that. -Start with the silly code in: +* If you don't have any code of your own to package, start with the silly code in: :download:`capitalize.zip <../examples/packaging/capitalize.zip>` -Or go straight to making a package our of mailroom project. - - - +Or go straight to making a package our of your mailroom project. diff --git a/source/modules/PersistanceAndSerialization.rst b/source/modules/PersistanceAndSerialization.rst index 19ba8d93..f564b512 100644 --- a/source/modules/PersistanceAndSerialization.rst +++ b/source/modules/PersistanceAndSerialization.rst @@ -315,7 +315,7 @@ The CSV module Reading ``CSV`` files: -(uses: :download:`eggs.csv <../Examples/persistence/eggs.csv>`) +(uses: :download:`eggs.csv <../examples/persistence/eggs.csv>`) .. code-block:: ipython diff --git a/source/modules/Properties.rst b/source/modules/Properties.rst index 7668cf30..dbe72873 100644 --- a/source/modules/Properties.rst +++ b/source/modules/Properties.rst @@ -64,15 +64,14 @@ Properties .. code-block:: ipython - class C: - _x = None - @property - def x(self): - return self._x - @x.setter - def x(self, value): - self._x = value - + In [27]: class C: + ....: _x = None + ....: @property + ....: def x(self): + ....: return self._x + ....: @x.setter + ....: def x(self, value): + ....: self._x = value In [28]: c = C() In [30]: c.x = 5 In [31]: print(c.x) @@ -150,4 +149,4 @@ what you want. Play around with some properties code: -:download:`properties_example.py <../examples/Properties/properties_example.py>` +:download:`properties_example.py <../examples/properties/properties_example.py>` diff --git a/source/modules/Py2vsPy3.rst b/source/modules/Py2vsPy3.rst index 02d537be..555b38d1 100644 --- a/source/modules/Py2vsPy3.rst +++ b/source/modules/Py2vsPy3.rst @@ -38,7 +38,7 @@ So -- if you get this error, simply add the parentheses: .. code-block:: ipython - In [16]: print ("this") + In [16]: print("this") this Division @@ -76,6 +76,19 @@ And in Python2, you can get the behavior of Python3 with "true division": For the most part, you just need to be a bit careful with the rare cases where Python2 code counts on integer division. +Iterators vs Lists +------------------ + +In Python2, a number of functions returned a full list of the contents. But most of the time, you didn't need a list -- you only needed a way to loop through all the items returned. Such an object is called an "iterable" -- more about that later in the class. But for now, if you get an error like:: + + TypeError: 'dict_keys' object does not support indexing + +Then you likely got an iterator, rather than a "proper" list. You can fix this by making a list out of it:: + + list(an_iterator) + +the list constructor will make a list out of any iterable. So you can now index it, etc. + Other Python2 / Python3 differences ----------------------------------- @@ -85,4 +98,4 @@ Most of the other differences are essentially implementation details, like getti There are also a few syntax differences with more advanced topics: Exceptions, ``super()``, etc. -We'll talk about all that when we cover those topics. +We'll talk about all that when we cover those topics as well. diff --git a/source/modules/PythonClasses.rst b/source/modules/PythonClasses.rst index 803eaa5f..eebd5856 100644 --- a/source/modules/PythonClasses.rst +++ b/source/modules/PythonClasses.rst @@ -238,12 +238,57 @@ Example: In [10]: c1.y is c2.y # does each instance see the same y? Out[10]: False +But what are the consequences of this? It's a **very** important distinction. watch what happens if we change something in these objects, adding a new item to both the lists in ``c1``: + +.. code-block:: ipython + + # add an item to c1's x list + In [5]: c1.x.append(100) + + In [6]: c1.x + Out[6]: [1, 2, 3, 100] + + In [7]: c2.x + Out[7]: [1, 2, 3, 100] + +Note that adding something to ``c1.x`` also changed ``c2.x`` that is because they are the *same* list -- ``.x`` is a *class attribute* -- c1 and c2 share the same class, so they share the same class attributes. + +But if we change ``y``, an instance attribute: + +.. code-block:: ipython + + In [8]: c1.y.append(200) + + In [9]: c1.y + Out[9]: [4, 5, 6, 200] + + In [10]: c2.y + Out[10]: [4, 5, 6] + +appending to ``c1.y`` did not change ``c2.y`` -- ``y`` in this case is a an *instance* attribute -- each instance has its own version -- changing one will not affect the others. + +So when you are deciding where to "put" something, you need to think about whether all instances are the same, or if they each need their own version of the attribute. As a class attribute, you can access it from the class namespace as well, and it will affect all instances of that class: + +.. code-block:: python + + In [11]: C.x.append(2222) + + In [12]: c1.x + Out[12]: [1, 2, 3, 100, 2222] + + In [13]: c2.x + Out[13]: [1, 2, 3, 100, 2222] + +So here we changed ``x`` on the *class* object, ``C``, and the change showed up in all the instances, ``c1`` and ``c2``. + Typical methods --------------- .. code-block:: python + import math + class Circle: color = "red" @@ -252,12 +297,25 @@ Typical methods def expand(self, factor=2): self.diameter = self.diameter * factor + return None # note that if you leave that off, it will still return None + + def area(self): + area = (self.diameter / 2)**2 * math.pi + return area -Methods take some parameters, manipulate the attributes in ``self``. +Methods take some parameters, and possibly manipulate the attributes in ``self``. + +Remember that classes are about encapsulating the data and the functions that act on that data -- the methods are the functions that act on the data. They may or may not return something useful. +.. note:: + + It is convent in Python that methods that change the internal state of an object return None, whereas methods that return a new object, or some calculated result without changing the state return that value. + + You can see examples of this in the python built ins -- methods of lists like ``append`` or ``sort`` return None. + Gotcha ! -------- @@ -278,13 +336,49 @@ Huh???? I only gave 2: ``self`` is implicitly passed in for you by Python. so it actually *did* get three! -Functions (methods) are First Class ------------------------------------ +Functions (methods) are First Class Objects +------------------------------------------- Note that in Python, functions are first class objects, so a method *is* an attribute. -All the same rules apply about attribute access: note that the methods are defined in the class -- so they are class attributes. All the instances share the same methods. +All the same rules apply about attribute access: note that the methods are defined in the class -- so they are class attributes. +All the instances share the same methods. + +But each method gets its own namespace when it is actually called, so there is no confusion -- just like when you call a regular function multiple times. + +Manipulating Attributes +----------------------- + +Python makes it very easy to manipulate object's attributes -- you can access them with the "dot" notation, and simply set them like any other variable. With the Circle class above: + +.. code-block:: python + + In [15]: c = Circle(2) + + In [16]: c.area() + Out[16]: 3.141592653589793 + + In [17]: c.diameter = 4 + + In [18]: c.area() + Out[18]: 12.566370614359172 + +Note that after I changed the diameter attribute, when I called the ``area()`` method --it used the new diameter. Simple attribute access changed the state of the object. + +So you now know how to: + + * Define a class + * Give the class shared (class) attributes + * Add an initializer to set up it's initial state + * Add methods to manipulate that state. + * Add methods that return the results of calculations of the current state + +You can do a lot with this simple functionality -- but all it's done is put everything together in a neat package -- useful, but the real power of OO comes when you can subclass. So time to move on: + +:ref:`subclassing_inheritance` + + + -But each method gets its own namespace when it is actually called, so there is no confusion-- just like when you call a regular function multiple times. diff --git a/source/modules/Recursion.rst b/source/modules/Recursion.rst index 9d7e0bc9..b3e91ec0 100644 --- a/source/modules/Recursion.rst +++ b/source/modules/Recursion.rst @@ -13,7 +13,6 @@ With recursion, if you are not careful, this stack can get *very* deep. Python has a maximum limit to how much it can recurse. This is intended to save your machine from running out of RAM. - Recursion is especially useful for a particular set of problems. For example, take the case of the *factorial* function. @@ -25,4 +24,137 @@ integer by every integer smaller than it down to 1. 5! == 5 * 4 * 3 * 2 * 1 -We can use a recursive function nicely to model this mathematical function +We can use a recursive function nicely to model this mathematical function: + +:: + + 1! = 1 + 2! = 2 * 1 = 2 * 1! + 3! = 3 * 2 * 1 = 3 * 2! + +So we have a pattern here -- each value can be defined in terms of the previous value. + +So generically:: + + 1! = 1 + n! = n * (n-1)! + +How would we put that in code? Pretty straightforward translation: + +.. code-block:: python + + def factorial(n): + return n * factorial(n-1) + +That was pretty easy -- what happens when we run it? + +.. code-block:: ipython + + In [2]: factorial(3) + --------------------------------------------------------------------------- + RecursionError Traceback (most recent call last) + in () + ----> 1 factorial(3) + + in factorial(n) + 1 def factorial(n): + ----> 2 return n * factorial(n-1) + + ... last 1 frames repeated, from the frame below ... + + in factorial(n) + 1 def factorial(n): + ----> 2 return n * factorial(n-1) + + RecursionError: maximum recursion depth exceeded + +OOPS! that didn't work -- why not? Let's add a print... + +.. code-block:: python + + def factorial(n): + print("factorial called with", n) + return n * factorial(n-1) + +And call it: + +.. code-block:: ipython + + In [5]: factorial(3) + factorial called with 3 + factorial called with 2 + factorial called with 1 + factorial called with 0 + factorial called with -1 + factorial called with -2 + factorial called with -3 + factorial called with -4 + factorial called with -5 + ... + in factorial(n) + 1 def factorial(n): + 2 print("factorial called with", n) + ----> 3 return n * factorial(n-1) + + RecursionError: maximum recursion depth exceeded while calling a Python object + +Now it's clear what's going on -- each time you call the function, it calls itself with a value one less -- but then it just keeps going into the deep negative numbers, and only stops because Python reaches its recursion limit. + +This makes clear a core requirement of recursive functions: + + **Recursive functions must have a termination criteria!** + +That is, there must be a case (or more than one) for which they return a direct value. What should that be for factorial? Well, it's part of the definition that 1! == 1 -- so let's put that in our function: + +.. code-block:: python + + def factorial(n): + print("factorial called with", n) + if n == 1: + return 1 + return n * factorial(n-1) + +and try that: + +.. code-block:: ipython + + In [7]: factorial(3) + factorial called with 3 + factorial called with 2 + factorial called with 1 + Out[7]: 6 + +Much better! Try it out now with various values, and maybe without the print: + +.. code-block:: ipython + + In [14]: factorial(1) + Out[14]: 1 + + In [15]: factorial(2) + Out[15]: 2 + + In [16]: factorial(3) + Out[16]: 6 + + In [17]: factorial(4) + Out[17]: 24 + +Looking good! + +Exercise for the reader: What happens if you pass in a negative number? +Think about it first, before you try it. Hint -- it won't work! +How would you change your code to make it more robust? + +Summary +------- + +* Whenever you have a function that can be defined in terms of itself, you have a use case for recursion. It can make for nice compact, clear code. + +* Python will create a new "stack frame" for each call to the function -- so each call is kept separate, with separate local variables. + +But: + +* Python has a limited recursion depth -- so it can't be used for "big" problems. + +* You do need to make sure the calls will terminate. diff --git a/source/modules/StaticAndClassMethods.rst b/source/modules/StaticAndClassMethods.rst index ff8b7a76..5ef64ca4 100644 --- a/source/modules/StaticAndClassMethods.rst +++ b/source/modules/StaticAndClassMethods.rst @@ -139,9 +139,9 @@ implements an alternate constructor that *can*. self[key] = value return self -(This is actually from the OrderedDict implementation in ``collections.py``). +(This is actually from the ``OrderedDict`` implementation in ``collections.py``). -See also datetime.datetime.now(), etc.... +See also ``datetime.datetime.now()``, etc.... Properties, Static Methods and Class Methods are powerful features of Python's @@ -157,4 +157,9 @@ well. .. _Here is a low level look: https://docs.python.org/2/howto/descriptor.html -For the Circle Lab: use a class method to make an alternate constructor that takes the diameter instead. +For the Circle Excercise: use a class method to make an alternate constructor that takes the diameter instead. + +Ultimately, make a subclass of ``Circle``, called ``Sphere``. Check and see if the ``.from_diameter`` alternate consructor still works! + + + diff --git a/source/modules/Strings.rst b/source/modules/Strings.rst index d476e186..cafbfcc7 100644 --- a/source/modules/Strings.rst +++ b/source/modules/Strings.rst @@ -9,7 +9,26 @@ Strings Strings ======= -A "String" is a computerese word for a piece of text -- a "string" of characters. +.. admonition:: Joke + + A piece of string is new in town, and looking for a drink. He sees a local bar, walks in, sits down, and orders a beer. The bartender looks at him askance, and says: "wait a minute, are you a piece of string?". "Why yes", the string replies. The bartender growls back: "We don't serve your kind in here -- you get out!". + + Disappointed, the piece of string leaves and heads down the street to the next bar, but this time, he barely gets to a seat before being yelled at -- "we don't want any string in here -- you get out!" + + A third bar, and he has a similar encounter. Now he's getting pretty distraught and confused -- "what have they got against string in this town?", he asks himself. But he's also tired and thirsty, so he gets an idea. + + The piece of string twists himself all up, winding himself around and around. Then he reaches up and fluffs up the top of his head. + + Thus prepared, he heads into yet another bar. This time is different. He walks in, sits down, the bartender takes his order -- all good. But then just as the bartender is putting his beer down he stops, and looks hard at him: "wait a minute! you're a piece of string, aren't you?". + + Full of confidence, the string replies: "Nope, I'm a frayed knot." + +A "String" is a computerese word for a piece of text -- a "string" of characters. Why "string"? "String" can be used to mean "(a linear sequence (as of characters, words, proteins, etc.)" +(`definition of string `_) + +So a string is a sequence of individual letters or characters. + +In Python, each character can be a `Unicode `_ character -- that is, any character in any language in the world. Having this built in by default in Python (3) means that you can get very far simply ignoring it -- anything you can type on your computer can be used in strings in Python. If you do need to work with non-English characters, or data encoded in non-utf-8, particularly on Python 2, here are some notes about that: :ref:`unicode`. But for the most part, in Python3 -- strings are text, and text is string, and that's that. Creating strings: ----------------- @@ -18,6 +37,8 @@ A string literal creates a string type. (we've seen this already...) +A literal can be delineated with single or double quotes, alone or in triples. + :: "this is a string" @@ -26,45 +47,52 @@ A string literal creates a string type. """and this also""" - '''and even this''' + '''and even this + triple quotes preserve newlines + so this is three lines''' -You can also use ``str()`` +You can also use call the string object (``str()``) to "make" a string out of other data types. .. code-block:: ipython In [256]: str(34) Out[256]: '34' -And strings can be read from files or other sources of I/O. +Strings can also be read from files or other sources of I/O. String Methods =============== -String objects have a lot of methods. +The python string object is very powerful with lots of methods for common text manipulation. "methods" are functions defined on an object itself (more on that when we get to OO programming). But it means that you have many ways to manipulate text built right into the string objects themselves. -Here are just a few: +Note that strings are "immutable" --they can not be changed once they have been created. So the string methods all return new objects, rather than change the string in place. + +Here are just a few of the more common string methods: Splitting and Joining Strings ----------------------------- -``split`` and ``join``: +``split`` and ``join`` can be used to break up a string into pieces, or make one big string out of multiple smaller pieces: .. code-block:: ipython - In [167]: csv = "comma, separated, values" - In [168]: csv.split(', ') + In [167]: csv = "comma,separated,values" + + In [168]: csv.split(',') Out[168]: ['comma', 'separated', 'values'] - In [169]: psv = '|'.join(csv.split(', ')) + + In [169]: psv = '|'.join(csv.split(',')) + In [170]: psv Out[170]: 'comma|separated|values' -It may seem odd at first that ``.join()`` is a string method, rather than, say, a method on lists. But, in fact it makes a lot of sense. Lists (and tuples, and other sequences) can hold any type of data -- and "joining" arbitrary data types doesn't make any sense. Joining is strictly a string activity. +It may seem odd at first that ``.join()`` is a string method, rather than, say, a method on lists. But in fact, it makes a lot of sense. Lists (and tuples, and other sequences) can hold any type of data -- and "joining" arbitrary data types doesn't make any sense. Joining is strictly a string activity. And you need a string so you can join the parts -- therefore, we need a string object in there somewhere anyway. -Lastly, having join() be a string method means that it can join strings in ANY iterable object -- not just the built-in sequence types. +Lastly, having join() be a string method means that it can join strings in ANY iterable object -- not just lists or other built-in sequence types. -So it does make sense -- but even if not, that's the way it is. +So it does make sense -- but even if doesn't make sense to you, that's the way it is -- so remember that you call ``.join()`` on the string you want to join things with. So to be clear: if you have a bunch of strings in a sequence and you want to put them together, you create a string with the character (or characters) you want to join them with, and call join() on that object: @@ -80,10 +108,17 @@ So to be clear: if you have a bunch of strings in a sequence and you want to put In [23]: "".join(["these", "are", "some", "strings"]) Out[23]: 'thesearesomestrings' -Building up a long string. +Maybe not very common, but you can join with a longer string as well: + +.. code-block:: ipython + + In [5]: " --#-- ".join(["these", "are", "some", "strings"]) + Out[5]: 'these --#-- are --#-- some --#-- strings' + +Building up a Long String. -------------------------- -The obvious thing to do is something like: +An obvious thing to do is something like: .. code-block:: python @@ -91,7 +126,7 @@ The obvious thing to do is something like: for piece in list_of_stuff: msg += piece -But: strings are immutable -- Python needs to create a new string each time you add a piece -- not efficient: +But: strings are immutable -- Python needs to create a new string each time you add a piece, which is not very efficient. So it's better to gather all the pieces together in a list, and then join them together: .. code-block:: python @@ -100,8 +135,7 @@ But: strings are immutable -- Python needs to create a new string each time you msg.append(piece) " ".join(msg) -appending to lists is efficient -- and so is the join() method of strings. - +appending to lists is efficient -- and so is the ``join()`` method of strings. Case Switching -------------- @@ -109,29 +143,38 @@ Case Switching .. code-block:: ipython In [171]: sample = 'A long string of words' + In [172]: sample.upper() Out[172]: 'A LONG STRING OF WORDS' + In [173]: sample.lower() Out[173]: 'a long string of words' + In [174]: sample.swapcase() Out[174]: 'a LONG STRING OF WORDS' + In [175]: sample.title() Out[175]: 'A Long String Of Words' -Testing --------- +Testing for certain classes of characters +----------------------------------------- .. code-block:: ipython In [181]: number = "12345" + In [182]: number.isnumeric() Out[182]: True + In [183]: number.isalnum() Out[183]: True + In [184]: number.isalpha() Out[184]: False + In [185]: fancy = "Th!$ $tr!ng h@$ $ymb0l$" + In [186]: fancy.isalnum() Out[186]: False @@ -139,6 +182,10 @@ Testing String Literals ----------------- +Sometimes when you are creating a string, you want to put an non-normal character in there --one that isn't strictly a letter or symbol, such as newlines, etc. + +To do that, python support a set of "escape" sequences -- when a character follows a backslash, it gets interpreted as having a particular meaning. + Common Escape Sequences:: \\ Backslash (\) @@ -149,8 +196,10 @@ Common Escape Sequences:: \t ASCII Horizontal Tab (TAB) \ooo Character with octal value ooo \xhh Character with hex value hh + \uxxxx Character with Unicode code point value xxxx + \N{char-name} Character with Unicdoe name char_name -for example -- for tab-separated values: +For example -- for tab-separated values: .. code-block:: ipython @@ -160,11 +209,14 @@ for example -- for tab-separated values: these are separated by tabs https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals + https://docs.python.org/3/library/stdtypes.html#string-methods Raw Strings ------------ +There are times when you want a literal backslash in your string: Windows file paths, regular expressions. To make this easy, Python support "raw" strings -- string literals where the backslash does not have special meaning: + Add an ``r`` in front of the string literal: **Escape Sequences Ignored** @@ -174,6 +226,7 @@ Add an ``r`` in front of the string literal: In [408]: print("this\nthat") this that + In [409]: print(r"this\nthat") this\nthat @@ -184,10 +237,33 @@ Add an ``r`` in front of the string literal: In [415]: r"\" SyntaxError: EOL while scanning string literal -putting a backslash right before the end quote confuses the interpreter! +Putting a backslash right before the end quote confuses the interpreter! + +Raw strings can be very handy for things like regular expressions that need embedded backslashes. + +Building Long String Literals +----------------------------- + +If you put two string literals next to each other in the code, Python will join them into one when compiling: -This can be very handy for things like regular expressions that need embedded backslashes. +.. code-block:: ipython + In [6]: "this" "that" + Out[6]: 'thisthat' + +(note: no comma in between!) +THis may not look useful, but when combined with the fact that Python joins together lines when inside a parentheses, it can be a nice way to make larger string literals: + +.. code-block:: ipython + + In [7]: print("This is the first line\n" + ...: "And here is another line\n" + ...: "If I don't put in a newline" + ...: "I can get an very long line in, without making the" + ...: "line of code too long.") + This is the first line + And here is another line + If I don't put in a newlineI can get an very long line in, without making the line of code too long. Ordinal values -------------- @@ -196,27 +272,33 @@ Characters in strings are stored as numeric values: * "ASCII" values: 1-127 -* Unicode values -- 1 - 1,114,111 (!!!) +* Unicode "code points" -- 1 - 1,114,111 (!!!) -Unicode supports a LOT of characters --every character in every language known to man -- and then some :-) +Unicode supports a LOT of characters -- every character in every language known to man -- and then some :-). The Unicode code poitns for the characters in the ASCII character set are the same as ASCII -- so handy for us English speakers. -To get the value: +To get the value, use ``ord()``: .. code-block:: ipython In [109]: for i in 'Chris': .....: print(ord(i), end=' ') 67 104 114 105 115 + +To get the character from the code point, use ``chr()``: + +.. code-block:: ipython + In [110]: for i in (67,104,114,105,115): .....: print(chr(i), end='') Chris -For the English language, stick with ASCII, otherwise use full Unicode: it's easy with Python3 -- more on that in a later lesson. - +For the English language, stick with ASCII, otherwise use, full Unicode: it's easy with Python3 -Building Strings from data +Building Strings from Data -------------------------- +We often have some data in Python variables -- maybe strings, maybe numbers -- and we often want to combine that data with text to make a custom message of some sort. + You could, but please don't(!), do this: .. code-block:: python @@ -225,11 +307,12 @@ You could, but please don't(!), do this: (I know -- we did that in the grid_printing exercise) -Do this instead: +Why not? It's slow and not very flexible. Python provides a few ways to "format" text, so you can do this instead: -.. code-block:: python +.. code-block:: ipython - 'Hello {}!'.format(name) + In [11]: 'Hello {}!'.format(name) + Out[11]: 'Hello Chris!' It's much faster and safer, and easier to modify as code gets complicated. @@ -249,12 +332,12 @@ This is very similar to C-style string formatting (`sprintf`). It's still around, and handy --- but ... -The "new" ``format()`` method is more powerful and flexible, so we'll focus on that in this class. +The "new" ``format()`` method is more powerful and flexible, so we'll focus on that in this class. And there is now the newer "f-strings" (see below) which provide a lot of that "quick and dirty" convenience, while using the same formatting codes as ``.format()`` String Formatting ----------------- -The string ``format()`` method: +The string ``.format()`` method: .. code-block:: ipython @@ -292,7 +375,7 @@ The counts must agree: IndexError: tuple index out of range -Named placeholders: +Named Placeholders: ------------------- .. code-block:: ipython @@ -320,7 +403,7 @@ The format operator works with string variables, too: In [82]: s.format(a, b, a/b) Out[82]: '12 / 3 = 4.000000' -So you can dynamically build a format string, and then use it in multiple places in the code. +So you can save a format string, or even built it up dynamically, and then use it in multiple places in the code. Complex Formatting @@ -390,13 +473,13 @@ For this most simple example:: f"some text: {expression}" -`expression` is any valid python expression(remember that an expression is a combination of values and operators and names that produces a value). +`expression` is any valid python expression (remember that an expression is a combination of values and operators and names that produces a value). The expression is evaluated, and then, if it is not a string, it is converted to one, so it's really:: f"some text: {str(expression)}" -Let's see how that works in practice: +Let's see how this works in practice: .. code-block:: ipython @@ -439,14 +522,14 @@ And it has to be an expression, not a statement -- so you can't put a for loop o You can see how this can be a very powerful and quick way to get things done. -f-string use +F-string Use ------------ -F-strings are a very new Python feature. They will cause a syntax error in any Python version older than 3.6 -- and 3.6 was first released on December 23, 2016 -- less than a year from this writing. +F-strings are a fairly new Python feature. They will cause a syntax error in any Python version older than 3.6 -- 3.6 was first released on December 23, 2016 -- only a couple years from this writing. So there is not much out there in the wild, and I have yet to see it in production code. -They are not currently used in any of the examples in this course. +They are not currently used in many of the examples in this course. Nevertheless, they are a nifty feature that could be very useful, so feel free to use them where it makes you code cleaner and clearer. diff --git a/source/modules/SubclassingAndInheritance.rst b/source/modules/SubclassingAndInheritance.rst index 1bc3247b..220b19a9 100644 --- a/source/modules/SubclassingAndInheritance.rst +++ b/source/modules/SubclassingAndInheritance.rst @@ -22,11 +22,11 @@ The resulting classes are known as derived classes or subclasses. Subclassing ----------- -A subclass "inherits" all the attributes (methods, etc) of the parent class. +A subclass "inherits" all the attributes (methods, etc) of the parent class. This means that a subclass will have everything that its "parents" have. -You can then change ("override") some or all of the attributes to change the behavior. +You can then change ("override") some or all of the attributes to change the behavior. You can also add new attributes to extend the behavior. -You can also add new attributes to extend the behavior. +You create a subclass by passing the superclass to the ``class`` statement. The simplest subclass in Python: @@ -35,33 +35,74 @@ The simplest subclass in Python: class A_subclass(The_superclass): pass -``A_subclass`` now has exactly the same behavior as ``The_superclass`` +``A_subclass`` now has exactly the same behavior as ``The_superclass`` -- all the same attributes and methods. Overriding attributes --------------------- Overriding is as simple as creating a new attribute with the same name: -.. code-block:: python +.. code-block:: ipython - class Circle: - color = "red" + In [1]: class Circle: + ...: color = "red" + ...: - ... +We now have a class with a class attribute, ``color``, with the value: "red". All instances of ``Circle`` will be red: - class NewCircle(Circle): - color = "blue" - >>> nc = NewCircle - >>> print(nc.color) - blue +.. code-block:: ipython + + In [2]: c = Circle() + + In [3]: c.color + Out[3]: 'red' + +If we create a subclass of Circle, and set that same class attribute: + +.. code-block:: ipython + + In [4]: class NewCircle(Circle): + ...: color = "blue" + ...: + + In [5]: nc = NewCircle() + + In [6]: nc.color + Out[6]: 'blue' + +We now have a class that is all the same, except that its instances have the color blue. + +Note that any methods that refer to that attribute, will get the new value, even if the methods themselves have not changed: + +.. code-block:: ipython + + In [10]: class Circle: + ...: color = "red" + ...: + ...: def describe(self): + ...: return f"I am a {self.color} circle" + ...: + + In [11]: class NewCircle(Circle): + ...: color = "blue" + ...: + + In [12]: c = Circle() + In [13]: c.describe() + Out[13]: 'I am a red circle' -all the ``self`` instances will have the new attribute. + In [14]: nc = NewCircle() + + In [15]: nc.describe() + Out[15]: 'I am a blue circle' + +Note that this is *why* self is passed in to every method -- when you write the method, you don't know exactly what class ``self`` will be -- it is an instance of the class at the time the method is called. Overriding methods ------------------ -Same thing, but with methods (remember, a method *is* an attribute in Python) +Overriding methods is exactly the same thing, but with methods (remember, a method *is* an attribute in Python -- one that happens to be a function) .. code-block:: python @@ -79,21 +120,24 @@ Same thing, but with methods (remember, a method *is* an attribute in Python) self.diameter = self.diameter * math.sqrt(2) -all the instances will have the new method. +all the instances of the new class will have the new method -- similar, but different, behavior. Note that both these methods are requiring that the class instance has a ``diameter`` attribute. + +**Here's a program design suggestion:** -Here's a program design suggestion: + Whenever you override a method, the interface of the new method should be the same as the old. It should take the same parameters, return the same type, and obey the same preconditions and postconditions. -Whenever you override a method, the interface of the new method should be the same as the old. It should take the same parameters, return the same type, and obey the same preconditions and postconditions. + If you obey this rule, you will find that any function designed to work with an instance of a superclass, like a Deck, will also work with instances of subclasses like a Hand or PokerHand. If you violate this rule, your code will collapse like (sorry) a house of cards. -If you obey this rule, you will find that any function designed to work with an instance of a superclass, like a Deck, will also work with instances of subclasses like a Hand or PokerHand. If you violate this rule, your code will collapse like (sorry) a house of cards. +-- from *Think Python* -Overriding \_\_init\_\_ + +Overriding ``__init__`` ----------------------- ``__init__`` is a common method to override. -You often need to call the super class ``__init__`` as well. +You often need to call the super class ``__init__`` as well, so that any initialization required is performed: .. code-block:: python @@ -110,11 +154,32 @@ You often need to call the super class ``__init__`` as well. Exception to: "don't change the method signature" rule. +Often when you override ``__init__``, the new class may take an extra parameter or two. In this case, you will want to keep the signature as similar as possible, and cleanly define what is part of the subclass. A common idiom in this case is this: + +.. code-block:: python + + class A_Subclass(A_Superclass): + + def __init__(self, param1, param2, *args, **kwargs): + self.param1 = param1 + self.init_something(param2) + super().__init__(*args, **kwargs) + +That is: -Using the superclasses' methods + * Put the extra parameters in the beginning of the list -- usually as required positional parameters. + + * Accept ``*args`` and ``**kwargs`` + + * Pass everything else on to the superclass' __init__ + +Using ``*args`` and ``**kwargs`` is a way to make it clear that the rest is simply the signature of the superclass. It is also flexible if the superclass (or others up in the hierarchy) changes -- it could completely change its signature, and this subclass would still work. + + +Using the superclass' methods ------------------------------- -You can also call the superclass' other methods: +In a subclass, you can access everything in the superclass: all attributes and other methods: .. code-block:: python @@ -130,13 +195,13 @@ You can also call the superclass' other methods: return Circle.get_area(self, self.radius*2) -Note that there is nothing special about ``__init__`` except that it gets called automatically when you instantiate an instance. Otherwise, it is the same as any other method -- it gets ``self`` as the first argument, it can or can not call the superclasses methods, etc. +Note that there is nothing special about ``__init__`` except that it gets called automatically when you instantiate an instance. Otherwise, it is the same as any other method -- it gets ``self`` as the first argument, it can or can not call the superclass' methods, etc. "Favor Object Composition Over Class Inheritance" ------------------------------------------------- -That is a quotation from the "Design Patterns" book -- kind of one of the gospels of OO programming. +That is a quotation from the "Design Patterns" book -- one of the gospels of OO programming. But what does it mean? @@ -178,6 +243,8 @@ You only want to subclass list if your class could be used anywhere a list can b Attribute Resolution Order -------------------------- +Once there is a potentially large hierarchy of subclasses, how do you know which one will be used? + When you access an attribute: ``an_instance.something`` @@ -192,6 +259,8 @@ Python looks for it in this order: It can get more complicated, particularly when there are multiple superclasses (multiple inheritance), but when there is a simple inheritance structure (the usual case) -- it's fairly straightforward. +This is often referred to as "method resolution order" (MRO), because it's more complicated with methods, and in some languages, methods and attributes are more distinct than in Python. In Python, it can be thought of as "name resolution" -- everything in Python is about names and namespaces. + If you want to know more of the gory details -- here's some reading: https://www.python.org/download/releases/2.3/mro/ @@ -222,7 +291,7 @@ That's about it -- really! Type-Based Dispatch ------------------- -You'll see code that looks like this: +Occasionally you'll see code that looks like this: .. code-block:: python @@ -231,12 +300,12 @@ You'll see code that looks like this: else: Do_something_else -When it's called for, Python provides these utilties: +When it's called for, Python provides these utilities: * ``isinstance()`` * ``issubclass()`` -But it is very rarely called for! Between Duck typing, polymorphism, and EAFP, you rarely need to check for type directly. +But it is *very* rarely called for! Between Duck Typing, polymorphism, and EAFP, you rarely need to check for type directly. Wrap Up ------- @@ -254,3 +323,5 @@ OO can be a very powerful approach, but don't be a slave to what OO is *supposed Let OO work for you, not *create* work for you. +And the biggest way to do that is to support code re-use. + diff --git a/source/modules/Testing.rst b/source/modules/Testing.rst index a6ce481d..f9baa7e6 100644 --- a/source/modules/Testing.rst +++ b/source/modules/Testing.rst @@ -26,9 +26,9 @@ block. * You can't do anything else when the file is executed without running tests. - This is not optimal. +This is not optimal. - Python provides testing systems to help. +Python provides testing systems to help. Standard Library: ``unittest`` @@ -38,11 +38,9 @@ The original testing system in Python. ``import unittest`` -More or less a port of ``JUnit`` from Java +More or less a port of `JUnit `_ from Java -A bit verbose: you have to write classes & methods - -(And we haven't covered that yet!) +A bit verbose: you have to write classes & methods (And we haven't covered that yet!) But here's a bit of an introduction, as you will see this in others' code. @@ -108,32 +106,31 @@ in ``test_my_mod.py``: Advantages of ``unittest`` -------------------------- +The ``unittest`` module is pretty full featured - The ``unittest`` module is pretty full featured - - It comes with the standard Python distribution, no installation required. +It comes with the standard Python distribution, no installation required. - It provides a wide variety of assertions for testing all sorts of situations. +It provides a wide variety of assertions for testing all sorts of situations. - It allows for a setup and tear down workflow both before and after all tests and before and after each test. +It allows for a setup and tear down workflow both before and after all tests and before and after each test. - It's well known and well understood. +It's well known and well understood. Disadvantages of ``unittest`` ----------------------------- - It's Object Oriented, and quite "heavyweight". +It's Object Oriented, and quite "heavyweight". - - modeled after Java's ``JUnit`` and it shows... + - modeled after Java's ``JUnit`` and it shows... - It uses the framework design pattern, so knowing how to use the features means learning what to override. +It uses the framework design pattern, so knowing how to use the features means learning what to override. - Needing to override means you have to be cautious. +Needing to override means you have to be cautious. - Test discovery is both inflexible and brittle. +Test discovery is both inflexible and brittle. - And there is no built-in parameterized testing. +And there is no built-in parameterized testing. Other Options @@ -141,22 +138,24 @@ Other Options There are several other options for running tests in Python. -* `Nose`: https://nose.readthedocs.org/ +* **Nose2**: https://github.com/nose-devs/nose2 -* `pytest`: http://pytest.org/latest/ +* **pytest**: http://pytest.org/latest/ * ... (many frameworks supply their own test runners: e.g. django) -Nose was the most common test runner when I first started learning testing, but it has been in maintaince mode for a while. +Nose was the most common test runner when I first started learning testing, but it has been in maintenance mode for a while. Even the nose2 site recommends that you consider pytest. pytest has become the defacto standard test runner for those that want a more "pythonic" test framework. -It is very capable and widely used. +pytest is very capable and widely used. For a great description of the strengths of pytest, see: `The Cleaning Hand of Pytest `_ +So we will use pytest for the rest of this class. + Installing ``pytest`` --------------------- @@ -173,7 +172,7 @@ at the command line: $ pytest -If you have any tests in your repository, that will find and run them. +If you have any tests in your repository, that command will find and run them (If you have followed the proper naming conventions). **Do you?** @@ -182,12 +181,18 @@ Pre-existing Tests Let's take a look at some examples. -in ``IntroPython-2017\Examples\Session06`` +Create a directory to try this out, and download: + +:download:`test_random_unitest.py <../examples/testing/test_random_unitest.py>` + +In the directory you created for that file, run: .. code-block:: bash $ pytest +It should find that test file and run it. + You can also run pytest on a particular test file: .. code-block:: bash @@ -205,17 +210,20 @@ Take a few minutes to look these files over. What is Happening Here? ----------------------- -You should have gotten results that look something like this:: +You should have gotten results that look something like this: + +.. code-block:: bash - MacBook-Pro:Session06 Chris$ pytest test_random_unitest.py + $ pytest ============================= test session starts ============================== - platform darwin -- Python 3.6.2, pytest-3.2.3, py-1.4.34, pluggy-0.4.0 - rootdir: /Users/Chris/PythonStuff/UWPCE/IntroPython-2017/examples/Session06, inifile: + platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/temp/test_temp, inifile: + plugins: cov-2.6.0 collected 3 items - test_random_unitest.py ... + test_random_unitest.py ... [100%] - =========================== 3 passed in 0.02 seconds =========================== + =========================== 3 passed in 0.06 seconds =========================== When you run the ``pytest`` command, ``pytest`` starts in your current @@ -246,60 +254,204 @@ It will run ``unittest`` tests for you, so can be used as a test runner. But in addition to finding and running tests, it makes writing tests simple, and provides a bunch of nifty utilities to support more complex testing. +Now download this file: -Test Driven Development ------------------------ +:download:`test_random_pytest.py <../examples/testing/test_random_pytest.py>` -Download these files, and save them in your own students directory in the class repo: +And run pytest again: -:download:`test_cigar_party.py <../examples/testing/test_cigar_party.py>` -and: -:download:`cigar_party.py <../examples/testing/cigar_party.py>` +.. code-block:: bash -then, in dir where you put the files, run:: + $ pytest + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/temp/test_temp, inifile: + plugins: cov-2.6.0 + collected 8 items - $ pytest test_cigar_party.py + test_random_pytest.py ..... [ 62%] + test_random_unitest.py ... [100%] -You will get a LOT of test failures! + =========================== 8 passed in 0.07 seconds =========================== -What we've just done here is the first step in what is called: +Note that it ran the tests in both the test files. - **Test Driven Development**. +Take a look at ``test_random_pytest.py`` -- It is essentially the same tests -- but written in native pytest style -- simple test functions. -The idea is that you write the tests first, and then write the code that passes the tests. In this case, the writing the tests part has been done for you: +pytest tests +------------ -A bunch of tests exist, but the code to make them pass does not yet exist. +The beauty of pytest is that it takes advantage of Python's dynamic nature -- you don't need to use any particular structure to write tests. -The red you see in the terminal when we run the tests is a goad to you to write the code that fixes these tests. +Any function named appropriately is a test. -The tests all failed because ``cigar_party()`` looks like: +If the function doesn't raise an error or an assertion, the test passes. It's that simple. + +Let's take a look at ``test_random_pytest.py`` to see how this works. .. code-block:: python - def cigar_party(cigars, is_weekend): - pass + import random + import pytest + +The ``random`` module is imported becasue that's what we are testing. +``pytest`` only needs to be imported if you are using its utilities -- more on this in a moment. -A totally do nothing function! +.. code-block:: python -Put real code in ``cigar_party.py`` until all the tests pass. + seq = list(range(10)) -When the tests pass -- you are done! That's the beauty of test-driven development. +Here we create a simple little sequence to use for testing. We put it in the global namespace so other functions can access it. -Trying it yourself ------------------- +Now the first tests -- simply by naming it ``test_something``, pytest will run it as a test: + +.. code-block:: python + + def test_choice(): + """ + A choice selected should be in the sequence + """ + element = random.choice(example_seq) + assert (element in example_seq) + +This is pretty straightforward. We make a random choice from the sequence, +and then assert that the selected element is, indeed, in the original sequence. + +.. code-block:: python + + def test_sample(): + """ + All the items in a sample should be in the sequence + """ + for element in random.sample(example_seq, 5): + assert element in example_seq + +And this is pretty much the same thing, except that it loops to make sure that every item returned by ``.sample`` is in the original sequence. + +Note that this will result in 5 separate assertions -- that is fine, you can have as many assertions as you like in one test function. But the test will fail on the first failed assertion -- so you only want to have closely related assertions in each test function. + +.. code-block:: python + + def test_shuffle(): + """ + Make sure a shuffled sequence does not lose any elements + """ + seq = list(range(10)) + random.shuffle(seq) + seq.sort() # If you comment this out, it will fail, so you can see output + print("seq:", seq) # only see output if it fails + assert seq == list(range(10)) + +This test is designed to make sure that ``random.shuffle`` only re-arranges the items, but doesn't add or lose any. + +In this case, the global ``example_seq`` isn't used, because ``shuffle()`` will change the sequence -- tests should never rely on or alter global state. So a new sequence is created for the test. This also allows the test to know exactly what the results should be at the end. + +Then the "real work" -- calling ``random.shuffle`` on the sequence -- this should re-arrange the elements without adding or losing any. + +Calling ``.sort()`` again should put the elements back in the order they started + +So we can then test that after shuffling and re-sorting, we have the same sequence back: -Try it a bit more, writing the tests yourself: +.. code-block:: python + + assert seq == list(range(10)) + +If that assertion passes, the test will pass. + +``print()`` and test failures +............................. + +Try commenting out the sort line: + +.. code-block:: python + + # seq.sort() # If you comment this out, it will fail, so you can see output + +And run again to see what happens. This is what I got: + +.. code-block:: bash + + $ pytest test_random_pytest.py + ============================= test session starts ============================== + platform darwin -- Python 3.7.0, pytest-3.10.1, py-1.5.4, pluggy-0.7.1 + rootdir: /Users/Chris/PythonStuff/UWPCE/PythonCertDevel/source/examples/testing, inifile: + plugins: cov-2.6.0 + collected 5 items + + test_random_pytest.py F.... [100%] + + =================================== FAILURES =================================== + _________________________________ test_shuffle _________________________________ + + def test_shuffle(): + """ + Make sure a shuffled sequence does not lose any elements + """ + seq = list(range(10)) + random.shuffle(seq) + # seq.sort() # If you comment this out, it will fail, so you can see output + print("seq:", seq) # only see output if it fails + > assert seq == list(range(10)) + E assert [4, 8, 9, 3, 2, 0, ...] == [0, 1, 2, 3, 4, 5, ...] + E At index 0 diff: 4 != 0 + E Use -v to get the full diff + + test_random_pytest.py:22: AssertionError + ----------------------------- Captured stdout call ----------------------------- + seq: [4, 8, 9, 3, 2, 0, 7, 5, 6, 1] + ====================== 1 failed, 4 passed in 0.40 seconds ====================== + +You get a lot of information when test fails. It's usually enough to tell you what went wrong. -Pick an example from codingbat: +Note that pytest didn't print out the results of the print statement when the test passed, but when it failed, it printed it (under "Captured stdout call"). This means you can put diagnostic print calls in your tests, and they will not clutter up the output when they are not needed. - `codingbat `_ +Testing for Exceptions +...................... -Do a bit of test-driven development on it: +One of the things you might want to test about your code is that it raises an exception when it should -- and that the exception it raises is the correct one. + +In this example, if you try to call ``random.shuffle`` with an immutable sequence, such as a tuple, it should raise a ``TypeError``. Since raising an exception will generally stop the code (and cause a test to fail), we can't use an assertion to test for this. + +pytest provides a "context manager", ``pytest.raises``, that can be used to test for exceptions. The test will pass if and only if the specified Exception is raised by the enclosed code. You use it like so: + +.. code-block:: python + + def test_shuffle_immutable(): + """ + Trying to shuffle an immutable sequence raises an Exception + """ + with pytest.raises(TypeError): + random.shuffle((1, 2, 3)) + +The ``with`` block is how you use a context manager -- it will run the code enclosed, and perform various actions at the end of the code, or when an exception is raised. +This is the same ``with`` as used to open files. In that case, it is used to assure that the file is properly closed when you are done with it. In this case, the ``pytest.raises`` context manager captures any exceptions, and raises an ``AssertionError`` if no exception is raised, or if the wrong exception is raised. + +In this case, the test will only pass if a ``TypeError`` is raised by the call to ``random.shuffle`` with a tuple as an argument. + +The next test: + +.. code-block:: python + + def test_sample_too_large(): + """ + Trying to sample more than exist should raise an error + """ + with pytest.raises(ValueError): + random.sample(example_seq, 20) + +is very similar, except that this time, a ValueError has to be raised for the test to pass. + +pytest provides a number of other features for fixtures, parameterized tests, test classes, configuration, shared resources, etc. +But simple test functions like this will get you very far. + + +Test Driven Development +----------------------- - * run something on the web site. - * write a few tests using the examples from the site. - * then write the function, and fix it 'till it passes the tests. +Test Driven Development or "TDD", is a development process where you write tests to assure that your code works, *before* you write the actual code. -Do at least two of these... +This is a very powerful approach, as it forces you to think carefully about exactly what your code should do before you start to write it. It also means that you know when you code is working, and you can refactor it in the future with assurance that you haven't broken it. +Give this exercise a try to get the idea: +:ref:`exercise_unit_testing` diff --git a/source/modules/Tutorial.rst b/source/modules/Tutorial.rst index 1f9a33e3..e5eb5740 100644 --- a/source/modules/Tutorial.rst +++ b/source/modules/Tutorial.rst @@ -39,7 +39,7 @@ There are a number of ways to run python code: - At the interpreter, often referred to as a REPL (Read, Evaluate, Print Loop) - At an enhanced interpreter such as iPython - In a browser-based interactive system such as the Jupyter Notebook -- From and IDE, such as IDLE or PyCharm +- From an IDE, such as IDLE or PyCharm - Calling python from the command line to run a file. While working with an interactive interpreter can be an excellent way to explore Python (and I highly recommend it), For this tutorial, to get you used to "real" production development, you will write, edit, and save your code in a programmer's text editor, and run it from the command line. @@ -47,7 +47,8 @@ While working with an interactive interpreter can be an excellent way to explore A Programmer's Text Editor -------------------------- -A bit here about an editor, and recommendations on selecting one, with pointers to documentation about editor configuration. +See These notes for getting set up with an editor and Python itself: :ref:`setting_up_dev_environment` + The Python Interpreter ---------------------- @@ -66,19 +67,19 @@ These each have their own special uses for interaction the the Java VM or CLR, o [link to setting up your environment here] -For this tutorial, you will need cPython version 3.6, installed and running so that when you type "python" at your command line, it starts up: +For this tutorial, you will need cPython version 3.7, installed and running so that when you type "python" at your command line, it starts up: .. code-block:: bash MacBook-Pro:~ Chris$ python - Python 3.6.2 (v3.6.2:5fd33b5926, Jul 16 2017, 20:11:06) + Python 3.7.4 (v3.6.2:5fd33b5926, Jul 16 2017, 20:11:06) [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> -Your result may be slightly different, but it should say Python 3.6. *something* there at the top, and give you the command prompt (``>>>``) at the end. +Your result may be slightly different, but it should say Python 3.7. *something* there at the top, and give you the command prompt (``>>>``) at the end. -You can get out of it by typing ctrl+D (on OS_X and Linux) or ctrl+Z (On Windows), or typing ``exit()`` and hitting . +You can get out of it by typing ctrl+D (on OS-X and Linux) or ctrl+Z (On Windows), or typing ``exit()`` and then . Your first "program" -------------------- @@ -120,7 +121,7 @@ In this case, python ran the one line of code you put in that file, which told i The print function ------------------ -you can display jsut about anything in Python with the ``print()`` function. Simply type:: +you can display just about anything in Python with the ``print()`` function. Simply type:: print(what you want to print) @@ -129,20 +130,20 @@ examples: print(45) print("this is a bit of text") -you can print more than one thing by separating them with parentheses:: +you can print more than one thing by separating them with commas, inside the parenthesis:: - print("the value of pi is:", 3.1459, "to two decimal places") + print("the value of pi is:", 3.1459, "to four decimal places") Text in Python -------------- -Text in python is supported by the "str" datatype, which is short for "string". The text datatype is often referred to called "strings" in computer science because it is strings of characters. +Text in python is supported by the "str" datatype, which is short for "string". The text datatype is often referred to as "strings" in computer science because it is a series, or string of characters. -In Python3, strings can be any length, and contain any character (even in virtually any language). This is because they support "Unicode" which is a system for representing all the characters of virtually all the languages used on earth. +In Python3, strings can be any length, and contain any character (in virtually any language). This is because they support "Unicode" which is a system for representing all the characters of virtually all the languages used on earth. There are many complications to full support of Unicode, but for the most part, in Python it "just works". Any text you can put in your text editor should work fine. -To create a str, you simply type what you want surrounded by either double or, single quotes (the apostrophe). +To create a str, you simply type what you want surrounded by either double or single quotes (the apostrophe). Type this in a new file, called ``strings.py``: @@ -161,13 +162,13 @@ run the file, and you should get something like this:: MacBook-Pro:tutorial Chris$ python strings.py This is a basic string This is exactly the same string - you want to use double quotes if there's an apostrophe, like this: ' in the string - and you can use single quotes if you want to "quote" a word + You want to use double quotes if there's an apostrophe, like this: ' in the string + You can use single quotes if you want to "quote" a word Numbers in Python ----------------- -Python support two types of numbers in Python: integers (int) -- or whole numbers: +Python supports two types of numbers: integers (int) -- or "whole numbers", with no fractional part: .. code-block:: python @@ -180,18 +181,18 @@ integers can be negative or positive and as large as you want: >>> print(12345678987654321234567890987654321234567898765) 12345678987654321234567890987654321234567898765 -"real numbers" are called "floating point" (float) numbers. They are internally stored as binary, but you wirte them as regular decimal numbers: +"real numbers" are called "floating point" (float) numbers. They are internally stored as binary, but you write them as regular decimal (base 10) numbers: .. code-block:: python 2.3, 3.0, 3.2459, -23.21 -For the most part, Python will convert from integer to floating point numbers for you. +Note tht while the integer`3` and the float `3.0` have the same value, they are different types of numbers. But for the most part, Python will convert from integer to floating point numbers for you, so this distiction is rarely important. Math ---- -Being a computer language, python, of course supports the regular math functions. type the following into a file named math.py and run it: +Being a computer language, Python, of course, supports the regular math functions. Type the following into a file named math.py and run it: .. code-block:: python @@ -204,12 +205,12 @@ Being a computer language, python, of course supports the regular math functions print("twelve divided by 5 is:") print(12 // 5) -What is the difference between ``12 / 5`` and ``12 // 5`` ? +What is the difference between ``12 / 5`` and ``12 // 5`` ? Run your this code and find out. -Order of operations +Order of Operations ------------------- -Python follows the standard rules of operator precedence -- which operations are performed first when there are a bunch in a row: +Python follows the standard rules of "operator precedence" from algebra -- which operations are performed first when there are a bunch in a row: https://en.wikipedia.org/wiki/Order_of_operations @@ -237,7 +238,7 @@ Variables Directly printing things is not all that useful -- though Python does make a good calculator! -Do do anything more complicated, you need to store values to be used later. We do this by "assigning" them to a variable. SAve the follwing in a variables.py file: +To do anything more complicated, you need to store values to be used later. We do this by "assigning" them to a "variable", essentially givng them a name. Save the follwing in a ``variables.py`` file: .. code-block:: python @@ -249,7 +250,7 @@ Do do anything more complicated, you need to store values to be used later. We d The equals sign: ``=`` is the "assignment operator". It assigns a value to a name, and then when you use the name in the future, Python will replace it with the value it is assigned to when it is used. -names can (and generally should) be long and descriptive, and can contain letters, numbers (but not at the beginning) and some symbols, like the underscore character: +Names can (and generally should) be long and descriptive, and can contain letters, numbers (but not at the beginning) and only a few symbols, like the underscore character: .. code-block:: python @@ -289,7 +290,14 @@ And this? print ("that") # I think we need this line too print("the other") -comments can come after running code on a line. +comments can come after running code on a line as well. USing the hash to "comment out" parts of code is used in two ways: + +1) To add a little extra description to some code, to explain what it doing. + +2) To temporarily disable some code + + + diff --git a/source/modules/Unicode.rst b/source/modules/Unicode.rst new file mode 100644 index 00000000..9f2ca514 --- /dev/null +++ b/source/modules/Unicode.rst @@ -0,0 +1,574 @@ +:orphan: + +.. _unicode: + +================= +Unicode in Python +================= + + +A quick run-down of Unicode, + +Its use in Python 2 and 3, + +and some of the gotchas that arise. + + +History +======= + +A bit about where all this mess came from... + + +What the heck is Unicode anyway? +--------------------------------- + +* First there was chaos... + + * Different machines used different encodings -- different ways of mapping + binary data that the computer stores to letters. + +* Then there was ASCII -- and all was good (7 bit), 127 characters + + * (for English speakers, anyway) + +* But each vendor used the top half of 8bit bytes (127-255) for different things. + + * MacRoman, Windows 1252, etc... + + * There is now "latin-1", a 1-byte encoding suitable for European languages -- but still a lot of old files around that use the old ones. + +* Non-Western European languages required totally incompatible 1-byte encodings + +* This means there was no way to mix languages with different alphabets in the same document (web page, etc.) + + +Enter Unicode +-------------- + +The Unicode idea is pretty simple: + * One "code point" for all characters in all languages + +But how do you express that in bytes? + * Early days: we can fit all the code points in a two byte integer (65536 characters) + + * Turns out that didn't work -- 65536 is not enough for all languages. So we now need 32 bit integer to hold all of Unicode "raw" (UTC-4). + * But it's a waste of space to use 4 full bytes for each character, when so many don't require that much space. + +Enter "encodings": + * An encoding is a way to map specific bytes to a code point. + + * Each code point can be represented by one or more bytes. + + * Each encoding is different -- if you don't know the encoding, you don't know how to interpret the bytes! (though maybe you can guess) + + +Unicode +------- + +A good start: + +The Absolute Minimum Every Software Developer Absolutely, +Positively Must Know About Unicode and Character Sets (No Excuses!) + +http://www.joelonsoftware.com/articles/Unicode.html + + +**Everything is Bytes** + +* If it's on disk or on a network, it's bytes + +* Python provides some abstractions to make it easier to deal with bytes + +**Unicode is a Biggie** + +Actually, dealing with numbers rather than bytes is big + +-- but we take that for granted + + +Mechanics +========= + +What are strings? +----------------- + +Py2 strings were simply sequences of bytes. When text was one per character that worked fine. + +Py3 strings (or Unicode strings in py2) are sequences of "platonic characters". + +It's almost one code point per character -- there are complications +with combined characters: accents, etc -- but we can mostly ignore those -- you will get far thiking of a code point as a character. + +Platonic characters cannot be written to disk or network! + +(ANSI: one character == one byte -- so easy!) + + +Strings vs Unicode +------------------ + +Python 2 had two types that let you work with text: + +* ``str`` + +* ``unicode`` + +And two ways to work with binary data: + +* ``str`` + +* ``bytes()`` (and ``bytearray``) + +**but:** + +.. code-block:: ipython + + In [86]: str is bytes + Out[86]: True + +``bytes`` is there in py2 for py3 compatibility -- but it's good for making your intentions clear, too. + +py3 is more clear: + + ``str`` for text + ``byte`` for binary data + +Unicode +-------- + +The py3 string (py2 ``Unicode``) object lets you work with characters, instead of bytes. + +It has all the same methods you'd expect a string object to have. + +Encoding / Decoding +------------------- + +If you need to deal with the actual bytes for some reason, you may need to convert between a string object and a particular set of bytes. + +**"encoding"** is converting from a string object to bytes + +**"decoding"** is converting from bytes to a string object + +(sometimes this feels backwards...) + +And can get even more confusing with py2 strings being *both* text and bytes! + +This is actually one of the biggest differences between Python 2 and Python 3. As an ordinary user (particularly one that used English...), you may not notice -- text is text, and things generally "just work", but under the hood it is very different, and folks writing libraries for things like Internet protocols struggle with the differences. + +Using Unicode in Py2 +--------------------- + +If you do need to write Python2 code, you really should use Unicode. + +Here are the basics: + +Built in functions +.................. + +.. code-block:: python + + ord() + chr() + unichr() + str() + unicode() + +The codecs module +................. + +.. code-block:: python + + import codecs + codecs.encode() + codecs.decode() + codecs.open() # better to use ``io.open`` + + +Encoding and Decoding +---------------------- + +(Python 2!) + +**Encoding:** text to bytes -- you get a bytes (str) object + +.. code-block:: ipython + + In [17]: u"this".encode('utf-8') + Out[17]: 'this' + + In [18]: u"this".encode('utf-16') + Out[18]: '\xff\xfet\x00h\x00i\x00s\x00' + +**Decoding** bytes to text -- you get a unicode object + +.. code-block:: ipython + + In [2]: text = '\xff\xfe."+"x\x00\xb2\x00'.decode('utf-16') + + In [3]: type(text) + Out[3]: unicode + + In [4]: print text + ∮∫x² + + +Unicode Literals +------------------ + +1) Use Unicode in your source files: + +.. code-block:: python + + # -*- coding: utf-8 -*- + +(This is only required on Py2 -- the UTF-8 encoding is default for Python 3) + +2) Escape the Unicode characters: + +.. code-block:: python + + print u"The integral sign: \u222B" + print u"The integral sign: \N{integral}" + +Lots of tables of code points are available online: + +One example: http://inamidst.com/stuff/unidata/ + +:download:`hello_unicode.py <../examples/unicode/hello_unicode.py>`. + + +Using Unicode +-------------- + +Use ``unicode`` objects in all your code + +**Decode on input** + +**Encode on output** + +Many packages do this for you: *XML processing, databases, ...* + +**Gotcha:** + +Python has a default encoding (usually ascii) + +.. code-block:: ipython + + In [2]: sys.getdefaultencoding() + Out[2]: 'ascii' + +The default encoding will get used in unexpected places! + +Using Unicode Everywhere +------------------------- + +Python 2.6 and above have a nice feature to make it easier to use Unicode everywhere + +.. code-block:: python + + from __future__ import unicode_literals + +After running that line, the ``u''`` is assumed + +.. code-block:: ipython + + In [1]: s = "this is a regular py2 string" + In [2]: print type(s) + + + In [3]: from __future__ import unicode_literals + In [4]: s = "this is now a unicode string" + In [5]: type(s) + Out[5]: unicode + +NOTE: You can still get py2 strings from other sources! So you still need to think about ``str`` vs ``unicdode`` + +This is a really good idea if you want to write code compatible with Python2 and 3. + +Encodings +---------- + +What encoding should I use??? + +There are a lot: + +http://en.wikipedia.org/wiki/Comparison_of_Unicode_encodings + +But only a couple you are likely to need: + +* utf-8 (``*nix``) +* utf-16 (Windows) + +and of course, still the one-bytes ones. + +* ASCII +* Latin-1 + +UTF-8 +----- + +Probably the one you'll use most -- most common in Internet protocols (xml, JSON, etc.) + +Nice properties: + +* ASCII compatible: First 127 characters are the same as ASCII + +* Any ascii string is a utf-8 string + +* Compact for mostly-English text. + +Gotchas: + +* "higher" code points may use more than one byte: up to 4 for one character + +* ASCII compatible means in may work with default encoding in tests -- but then blow up with real data... + +UTF-16 +------ + +Kind of like UTF-8, except it uses at least 16bits (2 bytes) for each character: NOT ASCII compatible. + +But is still needs more than two bytes for some code points, so you still can't process it as two bytes per character. + +In C/C++ held in a "wide char" or "wide string". + +MS Windows uses UTF-16, as does (I think) Java. + +UTF-16 criticism +----------------- + +There is a lot of criticism on the net about UTF-16 -- it's kind of the worst of both worlds: + +* You can't assume every character is the same number of bytes +* It takes up more memory than UTF-8 + +`UTF-16 Considered Harmful `_ + +But to be fair: + +Early versions of Unicode: everything fit into two bytes (65536 code points). MS and Java were fairly early adopters, and it seemed simple enough to just use 2 bytes per character. + +When it turned out that 4 bytes were really needed, they were kind of stuck in the middle. + +Latin-1 +-------- + +**NOT Unicode**: + +A 1-byte per char encoding. + +* Superset of ASCII suitable for Western European languages. + +* The most common one-byte per char encoding for European text. + +* Nice property -- every byte value from 0 to 255 is a valid character ( at least in Python ) + +* You will never get an UnicodeDecodeError if you try to decode arbitrary bytes with latin-1. + +* And it can "round-trip" through a unicode object. + +* Useful if you don't know the encoding -- at least it won't raise an Exception + +* Useful if you need to work with combined text+binary data. + +:download:`latin1_test.py <../examples/unicode/latin1_test.py>`. + + +Unicode Docs +------------ + +Python Docs Unicode HowTo: + +http://docs.python.org/howto/unicode.html + +"Reading Unicode from a file is therefore simple" + +use io.open: + +.. code-block:: python + + from io import open + io.open('unicode.rst', encoding='utf-8') + for line in f: + print repr(line) + +(https://docs.python.org/2/library/io.html#module-interface) + +.. note: This is all for Python 2 -- the built in ``open`` in Py3 does utf-8 by default. + +Encodings Built-in to Python: + http://docs.python.org/2/library/codecs.html#standard-encodings + + +Gotchas in Python 2 +-------------------- + +file names, etc: + +If you pass in unicode, you get unicode + +.. code-block:: ipython + + In [9]: os.listdir('./') + Out[9]: ['hello_unicode.py', 'text.utf16', 'text.utf32'] + + In [10]: os.listdir(u'./') + Out[10]: [u'hello_unicode.py', u'text.utf16', u'text.utf32'] + +Python deals with the file system encoding for you... + +But: some more obscure calls don't support unicode filenames: + +``os.statvfs()`` (http://bugs.python.org/issue18695) + + +Exception messages: + + * Py2 Exceptions use str when they print messages. + + * But what if you pass in a unicode object? + + * It is encoded with the default encoding. + + * ``UnicodeDecodeError`` Inside an Exception???? + + NOPE: it swallows it instead. + +:download:`exception_test.py <../examples/unicode/exception_test.py>`. + +Unicode in Python 3 +---------------------- + +The "string" object **is** Unicode (always). + +Py3 has two distinct concepts: + +* "text" -- uses the str object (which is always Unicode!) +* "binary data" -- uses bytes or bytearray + +Everything that's about text is Unicode. + +Everything that requires binary data uses bytes. + +It's all much cleaner. + +(by the way, the recent implementations are very efficient...) + +So you can pretty much ignore encodings and all that for most basic text processing. +If you do find yourself needing to deal with binary data, you ay need to encode/decode stuff yourself. IN which case, Python provides an ``.encode()`` method on strings that encode the string to a bytes object with the encoding you select: + +.. code-block:: ipython + + In [3]: this_in_utf16 = "this".encode('utf-16') + + In [4]: this_in_utf16 + Out[4]: b'\xff\xfet\x00h\x00i\x00s\x00' + +And bytes objects have a ``.decode`` method that decodes the bytes and makes a string object: + + In [5]: this_in_utf16.decode('utf-16') + Out[5]: 'this' + +It's all quite simple an robust. + +.. note:: + During the long and painful transition from Python2 to Python3, the Unicode-always string type was a major source of complaints. There are many rants and `well thought out posts `_ about it still available on the internet. It was enough to think that Python had made a huge mistake. + + But there are a couple key points to remember: + + * The primary people struggling were those that wrote (or wored with) libraries that had to deal with protocols that used both binary and text data in the same data stream. + + * As of Python 3.4 or so, the python string object had grown the features it needed to support even those ugly binary+text use cases. + + For a typical user, the Python3 text model is MUCH easier to deal with and less error prone. + + +Exercises +========= + +Basic Unicode LAB +------------------- + +* Find some nifty non-ascii characters you might use. + + - Create a unicode object with them in two different ways. + - :download:`here <../examples/unicode/hello_unicode.py>` is one example + +* Read the contents into unicode objects: + + - :download:`ICanEatGlass.utf8.txt <../examples/unicode/ICanEatGlass.utf8.txt>` + - :download:`ICanEatGlass.utf16.txt <../examples/unicode/ICanEatGlass.utf16.txt>` + +and / or + + - :download:`text.utf8 <../examples/unicode/text.utf8>` + - :download:`text.utf16 <../examples/unicode/text.utf16>` + - :download:`text.utf32 <../examples/unicode/text.utf32>` + +* write some of the text from the first exercise to file -- read that file back in. + +Some Help +--------- + +reference: http://inamidst.com/stuff/unidata/ + +NOTE: if your terminal does not support unicode -- you'll get an error trying to print. +Try a different terminal or IDE, or google for a solution. + +Challenge Unicode LAB +---------------------- + +Here is an error in Python2: + +.. code-block:: ipython + + In [38]: u'to \N{INFINITY} and beyond!'.decode('utf-8') + --------------------------------------------------------------------------- + UnicodeEncodeError Traceback (most recent call last) + in () + ----> 1 u'to \N{INFINITY} and beyond!'.decode('utf-8') + + /Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/encodings/utf_8.pyc in decode(input, errors) + 14 + 15 def decode(input, errors='strict'): + ---> 16 return codecs.utf_8_decode(input, errors, True) + 17 + 18 class IncrementalEncoder(codecs.IncrementalEncoder): + + UnicodeEncodeError: 'ascii' codec can't encode character u'\u221e' in position 3: ordinal not in range(128) + + +But why would you **decode** a unicode object? + +And it should be a no-op -- why the exception? + +And why 'ascii'? I specified 'utf-8'! + +It's there for backward compatibility + +What's happening under the hood + +.. code-block:: python + + u'to \N{INFINITY} and beyond!'.encode().decode('utf-8') + +It encodes with the default encoding (ascii), then decodes + +In this case, it barfs on attempting to encode to 'ascii' + +So never call decode on a unicode object! + +But what if someone passes one into a function of yours that's expecting a py2 string? + +Type checking and converting -- yeach! + +Read: + +http://axialcorps.com/2014/03/20/unicode-str/ + +See if you can figure out the decorators: + +:download:`unicodify.py <../examples/unicode/unicodify.py>`. + +(This is advanced Python JuJu: Aren't you glad I didn't ask you to write that yourself?) diff --git a/source/modules/index.rst b/source/modules/index.rst index ac591c31..a37ee714 100644 --- a/source/modules/index.rst +++ b/source/modules/index.rst @@ -223,4 +223,5 @@ Assorted Additional Topics CodeReviews Packaging PersistanceAndSerialization + Unicode diff --git a/source/modules/lightning_talks.rst b/source/modules/lightning_talks.rst deleted file mode 100644 index 4c43d469..00000000 --- a/source/modules/lightning_talks.rst +++ /dev/null @@ -1,37 +0,0 @@ -.. _lightning_talks: - -######################## -Schedule Lightning talks -######################## - -Lightning Talks ----------------- - -"Lightning Talks" are a tradition in open-source technical conferences (and maybe others?). The idea is that people can do a nice, quick talk about a topic of their choice -- much lower pressure than a "real" talk -- but gives folks a chance to show off something they have worked on. - -For this class, it's a chance to us to learn a bit about each-other and maybe something new about Python. - -**Lightning Talks Requirments** - - * 5 minutes each (including setup) - no kidding! - * Every student will give one - * Purposes: introduce yourself, share interests, show Python applications - * Any topic you like that is related to Python -- according to you! - -Schedule the lightning talks: ------------------------------ - -* We need to schedule your lightning talks. - -* **Let's use Python for that !** - -There is a class list in the class repo here: - -``examples/session01/students.txt`` - -Let's write a script to generate a random talk schedule... - - - - - diff --git a/source/references/learning.rst b/source/references/learning.rst index 8d531b3c..f66ebbdd 100644 --- a/source/references/learning.rst +++ b/source/references/learning.rst @@ -48,9 +48,9 @@ Complete Books / Series ....................... * **Dive Into Python 3** - (http://www.diveintopython3.net/): The updated version + (https://www.apress.com/us/book/9781430224150): The updated version of a classic. This book offers an introduction to Python aimed at the student - who has experience programming in another language. + who has experience programming in another language. It used to be available free online -- you can still find PDF copies on the web. * **Python for You and Me** (http://pymbook.readthedocs.org/en/latest/): Simple @@ -72,7 +72,7 @@ Complete Books / Series print, with updated appendixes available for new language features. * **Python 101** - (http://www.blog.pythonlibrary.org/2014/06/03/python-101-book-published-today/) + (https://leanpub.com/python_101) Available as a reasonably priced ebook. This is a new one from a popular blogger about Python. Lots of practical examples. Also available as a Kindle book: http://www.amazon.com/Python-101-Michael-Driscoll-ebook/dp/B00KQTFHNK @@ -109,6 +109,8 @@ Advanced Books (http://chimera.labs.oreilly.com/books/1230000000393): The Python Cookbook provides practical solutions to various programming tasks, along with excellent explanations of how they work. Written by David Beazley -- Python author and instructor extraordinaire. Also available fully online: (http://chimera.labs.oreilly.com/books/1230000000393/index.html) +* **Fluent Python** + (http://shop.oreilly.com/product/0636920032519.do): Fluent Python is an excellent deeper dive into the inner workings of Python. Written for folks that already understand the basics of Python, it goes provides explainations of the more advanced topics in Python. If we were going to use a textbook for an advanced Python class, this would be it. Evaluating Your Options ----------------------------- diff --git a/source/scripts/Collections.rst b/source/scripts/Collections.rst index 4644c829..dcc4c8d0 100644 --- a/source/scripts/Collections.rst +++ b/source/scripts/Collections.rst @@ -2,7 +2,7 @@ .. _script_collections: -Collecitons Video +Collections Video ================= With Rick Riehle diff --git a/source/scripts/dict_as_switch.rst b/source/scripts/dict_as_switch.rst index 872bbf87..171f8d0d 100644 --- a/source/scripts/dict_as_switch.rst +++ b/source/scripts/dict_as_switch.rst @@ -105,7 +105,7 @@ Perhaps a little silly for only two options, but I hope you get the idea. And if you establish a protocol for what those functions return, -you can use the return value -- perhaps as simple as a True or FAlse to indicate success. +you can use the return value -- perhaps as simple as a True or False to indicate success. Or a sentinel value to indicate it's time to break out of a loop. diff --git a/source/solutions/Lesson01/codingbat/Logic-1/cigar_party.py b/source/solutions/Lesson01/codingbat/Logic-1/cigar_party.py deleted file mode 100755 index ad7df22c..00000000 --- a/source/solutions/Lesson01/codingbat/Logic-1/cigar_party.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python - - -def cigar_party(cigars, is_weekend): - """ - basic solution - """ - if is_weekend and cigars >= 40: - return True - elif 40 <= cigars <= 60: - return True - return False - - -def cigar_party2(cigars, is_weekend): - """ - some direct return of bool result - """ - if is_weekend: - return (cigars >= 40) - return (cigars >= 40 and cigars <= 60) - - -def cigar_party3(cigars, is_weekend): - """ - conditional expression - """ - return (cigars >= 40) if is_weekend else (cigars >= 40 and cigars <= 60) - -if __name__ == "__main__": - # some tests - - assert cigar_party(30, False) is False - assert cigar_party(50, False) is True - assert cigar_party(70, True) is True - assert cigar_party(30, True) is False - assert cigar_party(50, True) is True - assert cigar_party(60, False) is True - assert cigar_party(61, False) is False - assert cigar_party(40, False) is True - assert cigar_party(39, False) is False - assert cigar_party(40, True) is True - assert cigar_party(39, True) is False - - print("All tests passed") diff --git a/source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py b/source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py new file mode 100755 index 00000000..3bdae1d8 --- /dev/null +++ b/source/solutions/Lesson01/codingbat/Logic-1/walnut_party.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +""" +adapted from coding bat: https://codingbat.com/python +""" + + +def walnut_party(walnuts, is_weekend): + """ + basic solution + """ + if is_weekend and walnuts >= 40: + return True + elif 40 <= walnuts <= 60: + return True + return False + + +def walnut_party2(walnuts, is_weekend): + """ + Direct return of bool result + """ + if is_weekend: + return (walnuts >= 40) + return (walnuts >= 40 and walnuts <= 60) + + +def walnut_party3(walnuts, is_weekend): + """ + Conditional expression + """ + return (walnuts >= 40) if is_weekend else (walnuts >= 40 and walnuts <= 60) + +if __name__ == "__main__": + # some tests + + assert walnut_party(30, False) is False + assert walnut_party(50, False) is True + assert walnut_party(70, True) is True + assert walnut_party(30, True) is False + assert walnut_party(50, True) is True + assert walnut_party(60, False) is True + assert walnut_party(61, False) is False + assert walnut_party(40, False) is True + assert walnut_party(39, False) is False + assert walnut_party(40, True) is True + assert walnut_party(39, True) is False + + print("All tests passed") diff --git a/source/solutions/Lesson02/fizz_buzz.py b/source/solutions/Lesson02/fizz_buzz.py index 6204fc8c..45b9fd73 100755 --- a/source/solutions/Lesson02/fizz_buzz.py +++ b/source/solutions/Lesson02/fizz_buzz.py @@ -18,15 +18,31 @@ def fizzbuzz1(n): print(i) +def fizzbuzz1b(n): + """ + Save one computation -- if it's a multiple of 3 and 5, it's a + multiple of 15 + """ + for i in range(1, n + 1): + if i % 15 == 0: + print("FizzBuzz") + elif i % 3 == 0: + print("Fizz") + elif i % 5 == 0: + print("Buzz") + else: + print(i) + + def fizzbuzz2(n): """ Why evaluate i%3 and i%5 twice? """ for i in range(1, n + 1): msg = '' - if i % 3 == 0: + if not i % 3: msg += "Fizz" - if i % 5 == 0: + if not i % 5: msg += "Buzz" if msg: print(msg) diff --git a/source/solutions/Lesson02/fizz_buzz_one_liner.py b/source/solutions/Lesson02/fizz_buzz_one_liner.py index f77dde9f..494c5ecd 100644 --- a/source/solutions/Lesson02/fizz_buzz_one_liner.py +++ b/source/solutions/Lesson02/fizz_buzz_one_liner.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 # This One Liner solution to the Fizz Buzz problem -# was found by a student on the internet +# was found by a student on the Internet for i in range(1,101): print([i,'Fizz','Buzz','FizzBuzz'][(i%3==0)+2*(i%5==0)]) # this is a good example of why the most compact code is not always the # best -- readability counts! -# And this is pretty impenatrable. +# And this is pretty impenetrable. # but it's also pretty nifty logic, so below, -# It's unpacked to make it easeir to understand. +# It's unpacked to make it easier to understand. # first, add some white space to make it pep8 compatible, and more readable. diff --git a/source/solutions/Lesson02/print_grid.py b/source/solutions/Lesson02/print_grid.py index a270eaf7..651b3bf8 100755 --- a/source/solutions/Lesson02/print_grid.py +++ b/source/solutions/Lesson02/print_grid.py @@ -14,6 +14,9 @@ def print_grid_trivial(): """ Did anyone come up with the most trivial possible solution? + + Note that this may the the wy to go, oif this all you need to do: + print something specific once. Why write fancy code for that? """ print(""" + - - - - + - - - - + diff --git a/source/solutions/Lesson02/series.py b/source/solutions/Lesson02/series.py index 082e4c1e..3bf35665 100755 --- a/source/solutions/Lesson02/series.py +++ b/source/solutions/Lesson02/series.py @@ -3,14 +3,25 @@ """ series.py -solutions to the Fibonacci Series and Lucas numbers +solutions to the Fibonacci Series, Lucas numbers and +generalized sum_series + +These solutions use recursion +-- calling a funciton from within that function. + +These series are defined "recusively", so it's a +really natural way to express the solution. However, +recursion can be substantially less efficient.n + +See series_non_recursive.py for a more efficient way +to do it. """ def fibonacci(n): """ compute the nth Fibonacci number """ - if n < 0: + if n < 0: # check for negative number -- just in case. return None elif n == 0: return 0 @@ -35,15 +46,22 @@ def lucas(n): def sum_series(n, n0=0, n1=1): """ - compute the nth value of a summation series. + Compute the nth value of a summation series. :param n0=0: value of zeroth element in the series :param n1=1: value of first element in the series - if n0 == 0 and n1 == 1, the result is the Fibbonacci series + This function should generalize the fibonacci() and the lucas(), + so that this function works for any first two numbers for a sum series. + Once generalized that way, sum_series(n, 0, 1) should be equivalent to fibonacci(n). + And sum_series(n, 2, 1) should be equivalent to lucas(n). + + sum_series(n, 3, 2) should generate antoehr series with no specific name - if n0 == 2 and n1 == 1, the result is the Lucas series + The defaults are set to 0, 1, so if you don't pass in any values, you'll + get the fibonacci sercies """ + if n < 0: return None if n == 0: @@ -55,6 +73,7 @@ def sum_series(n, n0=0, n1=1): # Can you re-define fibonacci and lucas by using sum_series? + if __name__ == "__main__": # run some tests @@ -94,4 +113,17 @@ def sum_series(n, n0=0, n1=1): for n in range(0, 10): assert sum_series(n, 2, 1) == lucas(n) + # test if sum_series works for negative value + # (it should return None) + + assert sum_series(-1, 3, 2) is None + + # test if sum_series works for arbitrary initial values + assert sum_series(0, 3, 2) == 3 + assert sum_series(1, 3, 2) == 2 + assert sum_series(2, 3, 2) == 5 + assert sum_series(3, 3, 2) == 7 + assert sum_series(4, 3, 2) == 12 + assert sum_series(5, 3, 2) == 19 + print("tests passed") diff --git a/source/solutions/Lesson02/series_non_recusive.py b/source/solutions/Lesson02/series_non_recusive.py new file mode 100644 index 00000000..b6925b92 --- /dev/null +++ b/source/solutions/Lesson02/series_non_recusive.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 + +""" +a template for the series assignment +""" + +def fibonacci(n): + """ compute the nth Fibonacci number """ + a, b = 0, 1 + if n == 0: + return a + for _ in range(n - 1): + a, b = b, a + b + return b + + +def lucas(n): + """ compute the nth Lucas number """ + a, b = 2, 1 # notice that all I had to change from fib were these values? + if n == 0: + return a + for _ in range(n - 1): + a, b = b, a + b + return b + + +def sum_series(n, n0=0, n1=1): + """ + compute the nth value of a summation series. + + :param n0=0: value of zeroth element in the series + :param n1=1: value of first element in the series + + This function should generalize the fibonacci() and the lucas(), + so that this function works for any first two numbers for a sum series. + Once generalized that way, sum_series(n, 0, 1) should be equivalent to fibonacci(n). + And sum_series(n, 2, 1) should be equivalent to lucas(n). + + sum_series(n, 3, 2) should generate antoehr series with no specific name + + The defaults are set to 0, 1, so if you don't pass in any values, you'll + get the fibonacci sercies + """ + a, b = n0, n1 # notice that all I had to change from fib were these values? + if n == 0: + return a + for _ in range(n - 1): + a, b = b, a + b + return b + + +if __name__ == "__main__": + # run some tests + assert fibonacci(0) == 0 + assert fibonacci(1) == 1 + assert fibonacci(2) == 1 + assert fibonacci(3) == 2 + assert fibonacci(4) == 3 + assert fibonacci(5) == 5 + assert fibonacci(6) == 8 + assert fibonacci(7) == 13 + + assert lucas(0) == 2 + assert lucas(1) == 1 + + assert lucas(4) == 7 + + # test that sum_series matches fibonacci + assert sum_series(5) == fibonacci(5) + assert sum_series(7, 0, 1) == fibonacci(7) + + # test if sum_series matched lucas + assert sum_series(5, 2, 1) == lucas(5) + + # test if sum_series works for arbitrary initial values + assert sum_series(0, 3, 2) == 3 + assert sum_series(1, 3, 2) == 2 + assert sum_series(2, 3, 2) == 5 + assert sum_series(3, 3, 2) == 7 + assert sum_series(4, 3, 2) == 12 + assert sum_series(5, 3, 2) == 19 + + print("tests passed") diff --git a/source/solutions/Lesson04/mailroom2.py b/source/solutions/Lesson04/mailroom2.py index c4ca1790..1a118435 100755 --- a/source/solutions/Lesson04/mailroom2.py +++ b/source/solutions/Lesson04/mailroom2.py @@ -2,8 +2,11 @@ """ mailroom assignment -This version uses a dict for the main db, and exception handling to -check input +This version uses a dict for the main db, and a dict to"switch" on the user's +input choices. + +it also write the thank you letters to files. + """ import sys @@ -20,6 +23,7 @@ # This makes it easier to have a 'normalized' key. # you could get a bit fancier by having each "record" be a dict, with # "name" and "donations" as keys. + def get_donor_db(): return {'william gates iii': ("William Gates III", [653772.32, 12.17]), 'jeff bezos': ("Jeff Bezos", [877.33]), @@ -41,6 +45,14 @@ def list_donors(): return "\n".join(listing) +def print_donor_list(): + """ + Doesn't do much, but keeps the printing separate + """ + print(list_donors()) + print() + + def find_donor(name): """ Find a donor in the donor db @@ -64,23 +76,6 @@ def add_donor(name): donor_db[name.lower()] = donor return donor - -def main_menu_selection(): - """ - Print out the main application menu and then read the user input. - """ - action = input(dedent(''' - Choose an action: - - 1 - Send a Thank You - 2 - Create a Report - 3 - Send letters to everyone - 4 - Quit - - > ''')) - return action.strip() - - def gen_letter(donor): """ Generate a thank you letter for the donor @@ -101,35 +96,35 @@ def gen_letter(donor): '''.format(donor[0], donor[1][-1])) -def take_donation(name): +def take_donation(): """ Ask user for donation amount, and then add it to the DB """ # Now prompt the user for a donation amount to apply. Since this is # also an exit point to the main menu, we want to make sure this is # done before mutating the db. + print("in take_donation") + name = input("Enter a donor name (new or existing): \n >") while True: - amount_str = input("Enter a donation amount (or 'menu' to exit)> ").strip() - if amount_str == "menu": + amount_str = input("Enter a donation amount (or to exit)> ").strip() + if not amount_str: + # if they provide no input, go back to previous menu return # Make sure amount is a valid amount before leaving the input loop - try: - amount = float(amount_str) - # extra check here -- unlikely that someone will type "NaN", but - # it IS possible, and it is a valid floating point number: - # http://en.wikipedia.org/wiki/NaN - if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00: - raise ValueError - # in this case, the ValueError could be raised by the float() call, or by the NaN-check - except ValueError: + amount = float(amount_str) + # extra check here -- unlikely that someone will type "NaN", but + # it IS possible, and it is a valid floating point number: + # http://en.wikipedia.org/wiki/NaN + if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00: print("error: donation amount is invalid\n") + continue else: break - # If this is a new user, ensure that the database has the necessary - # data structure. donor = find_donor(name) + # If the donor is not found, it's a new donor if donor is None: + # add the new donor to the database donor = add_donor(name) # Record the donation @@ -138,23 +133,6 @@ def take_donation(name): print(gen_letter(donor)) -def send_thank_you(): - """ - Execute the logic to record a donation and generate a thank you message. - """ - # Read a valid donor to send a thank you from, handling special commands to - # let the user navigate as defined. - while True: - name = input("Enter a donor's name or 'list' to see all donors or " - "'menu' to exit to main menu > ").strip() - if name == "list": - print(list_donors()) - elif name == "menu": - return - else: - take_donation(name) - - def sort_key(item): # used to sort on name in donor_db return item[1] @@ -195,7 +173,7 @@ def save_letters_to_disk(): letter = gen_letter(donor) # I don't like spaces in filenames... filename = donor[0].replace(" ", "_") + ".txt" - print("writting letter to:", donor[0]) + print("writing letter to:", donor[0]) open(filename, 'w').write(letter) @@ -203,6 +181,10 @@ def print_donor_report(): print(generate_donor_report()) +def return_to_menu(): + ''' Return True to trigger exit out of sub-loop''' + return True + def quit(): """ quit the program @@ -212,19 +194,66 @@ def quit(): """ sys.exit(0) +def send_thank_you(): + """ + Execute the logic to record a donation and generate a thank you message. + """ + # Read a valid donor to send a thank you from, handling special commands to + # let the user navigate as defined. + prompt = ("To send a thank you, select one:\n\n" + "(1) Update donor and send thank-you\n" + "(2) List all existing DONORS\n" + "(3) Return to main menu\n > ") + selection_dict = {"1": take_donation, + "2": print_donor_list, + "3": return_to_menu, + } + run_menu(prompt, selection_dict) + +def main_menu(): + """ + Run the main menu for mailroom + """ + prompt = dedent(''' + Choose an action: -if __name__ == "__main__": + (1) - Send a Thank You + (2) - Create a Report + (3) - Send letters to everyone + (4) - Quit - donor_db = get_donor_db() + > ''') selection_dict = {"1": send_thank_you, "2": print_donor_report, "3": save_letters_to_disk, "4": quit} + run_menu(prompt, selection_dict) + + +def run_menu(prompt, selection_dict): + """ + run an interactive menu + + :param prompt: What you want to ask the user + + :param selection_dict: Dict of possible user impots mapped to + the actions to take. + """ while True: - selection = main_menu_selection() - try: - selection_dict[selection]() - except KeyError: + selection = input(prompt).strip().lower() + action = selection_dict.get(selection, None) + if action is None: print("error: menu selection is invalid!") + else: + if action(): + # break out of the loop if action returns True + break + + +if __name__ == "__main__": + + donor_db = get_donor_db() + + main_menu() diff --git a/source/solutions/Lesson05/.gitignore b/source/solutions/Lesson05/.gitignore new file mode 100644 index 00000000..88917bc7 --- /dev/null +++ b/source/solutions/Lesson05/.gitignore @@ -0,0 +1,4 @@ +Jeff_Bezos.txt +Mark_Zuckerberg.txt +Paul_Allen.txt +William_Gates_III.txt diff --git a/source/solutions/Lesson05/dict_set_with_comps_solution.py b/source/solutions/Lesson05/dict_set_with_comps_solution.py index a60375eb..6bb42b2e 100644 --- a/source/solutions/Lesson05/dict_set_with_comps_solution.py +++ b/source/solutions/Lesson05/dict_set_with_comps_solution.py @@ -22,7 +22,7 @@ # 2. Using a list comprehension, build a dictionary of numbers from zero # to fifteen and the hexadecimal equivalent (string is fine). -print(dict([(i, hex(i)) for i in range(16)])) +print(dict(((i, hex(i)) for i in range(16)))) # 3. Do the previous entirely with a dict comprehension -- should be a one-liner diff --git a/source/solutions/Lesson05/fizz_buzz_comprehension.py b/source/solutions/Lesson05/fizz_buzz_comprehension.py index 44588fde..a29fbf4d 100755 --- a/source/solutions/Lesson05/fizz_buzz_comprehension.py +++ b/source/solutions/Lesson05/fizz_buzz_comprehension.py @@ -1,6 +1,15 @@ #!/usr/bin/env python3 +""" +doing all of fizzbuzz in a comprehension -fb = [[str(i), 'Fizz', 'Buzz', 'FizzBuzz'][(i % 3 == 0) + 2 * (i % 5 == 0)] for i in range(1, 101)] +Is this a good idea? + -- probably not, but it's kind of cool + + Note that it uses a generator comprehension, so it won't actually get + computed until the join() call +""" + +fb = ([str(i), 'Fizz', 'Buzz', 'FizzBuzz'][(i % 3 == 0) + 2 * (i % 5 == 0)] for i in range(1, 101)) print('\n'.join(fb)) diff --git a/source/solutions/Lesson06/mailroom2.py b/source/solutions/Lesson05/mailroom3.py similarity index 68% rename from source/solutions/Lesson06/mailroom2.py rename to source/solutions/Lesson05/mailroom3.py index 49dfd81d..958cee05 100755 --- a/source/solutions/Lesson06/mailroom2.py +++ b/source/solutions/Lesson05/mailroom3.py @@ -2,12 +2,16 @@ """ mailroom assignment -This version uses a dict for the main db, and exception handling to -check input, and has been factored to be amenable to testing. +This version uses a dict for the main db, and a dict to"switch" on the user's +input choices. + +it also write the thank you letters to files. + """ import sys import math +from operator import itemgetter # handy utility to make pretty printing easier from textwrap import dedent @@ -20,6 +24,7 @@ # This makes it easier to have a 'normalized' key. # you could get a bit fancier by having each "record" be a dict, with # "name" and "donations" as keys. + def get_donor_db(): return {'william gates iii': ("William Gates III", [653772.32, 12.17]), 'jeff bezos': ("Jeff Bezos", [877.33]), @@ -30,7 +35,7 @@ def get_donor_db(): def list_donors(): """ - creates a list of the donors as a string, so they can be printed + Create a list of the donors as a string, so they can be printed Not calling print from here makes it more flexible and easier to test @@ -41,12 +46,19 @@ def list_donors(): return "\n".join(listing) +def print_donor_list(): + """ + Doesn't do much, but keeps the printing separate + """ + print(list_donors()) + print() + + def find_donor(name): """ - find a donor in the donor db + Find a donor in the donor db :param: the name of the donor - :returns: The donor data structure -- None if not in the donor_db """ key = name.strip().lower() @@ -58,7 +70,6 @@ def add_donor(name): Add a new donor to the donor db :param: the name of the donor - :returns: the new Donor data structure """ name = name.strip() @@ -67,28 +78,11 @@ def add_donor(name): return donor -def main_menu_selection(): - """ - Print out the main application menu and then read the user input. - """ - action = input(dedent(''' - Choose an action: - - 1 - Send a Thank You - 2 - Create a Report - 3 - Send letters to everyone - 4 - Quit - - > ''')) - return action.strip() - - def gen_letter(donor): """ Generate a thank you letter for the donor :param: donor tuple - :returns: string with letter note: This doesn't actually write to a file -- that's a separate @@ -104,27 +98,19 @@ def gen_letter(donor): '''.format(donor[0], donor[1][-1])) -def send_thank_you(): +def take_donation(): """ - Execute the logic to record a donation and generate a thank you message. + Ask user for donation amount, and then add it to the DB """ - # Read a valid donor to send a thank you from, handling special commands to - # let the user navigate as defined. - while True: - name = input("Enter a donor's name (or list to see all donors or 'menu' to exit)> ").strip() - if name == "list": - print(list_donors()) - elif name == "menu": - return - else: - break - # Now prompt the user for a donation amount to apply. Since this is # also an exit point to the main menu, we want to make sure this is # done before mutating the db. + print("in take_donation") + name = input("Enter a donor name (new or existing): \n >") while True: - amount_str = input("Enter a donation amount (or 'menu' to exit)> ").strip() - if amount_str == "menu": + amount_str = input("Enter a donation amount (or to exit)> ").strip() + if not amount_str: + # if they provide no input, go back to previous menu return # Make sure amount is a valid amount before leaving the input loop try: @@ -134,20 +120,21 @@ def send_thank_you(): # http://en.wikipedia.org/wiki/NaN if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00: raise ValueError - # in this case, the ValueError could be raised by the float() call, or by the NaN-check except ValueError: print("error: donation amount is invalid\n") + continue else: break - # If this is a new user, ensure that the database has the necessary - # data structure. donor = find_donor(name) + # If the donor is not found, it's a new donor if donor is None: + # add the new donor to the database donor = add_donor(name) # Record the donation donor[1].append(amount) + # print the thank you letter print(gen_letter(donor)) @@ -171,7 +158,7 @@ def generate_donor_report(): report_rows.append((name, total_gifts, num_gifts, avg_gift)) # sort the report data - report_rows.sort(key=sort_key) + report_rows.sort(key=itemgetter(1), reverse=True) report = [] report.append("{:25s} | {:11s} | {:9s} | {:12s}".format("Donor Name", "Total Given", @@ -191,6 +178,7 @@ def save_letters_to_disk(): letter = gen_letter(donor) # I don't like spaces in filenames... filename = donor[0].replace(" ", "_") + ".txt" + print("writing letter to:", donor[0]) open(filename, 'w').write(letter) @@ -198,23 +186,68 @@ def print_donor_report(): print(generate_donor_report()) -def quit(): - sys.exit(0) +def return_to_menu(): + ''' Return True to trigger exit out of sub-loop''' + return True -if __name__ == "__main__": - donor_db = get_donor_db() +def send_thank_you(): + """ + Execute the logic to record a donation and generate a thank you message. + """ + # Read a valid donor to send a thank you from, handling special commands to + # let the user navigate as defined. + prompt = ("To send a thank you, select one:\n\n" + "(1) Update donor and send thank-you\n" + "(2) List all existing DONORS\n" + "(3) Return to main menu\n > ") + selection_dict = {"1": take_donation, + "2": print_donor_list, + "3": return_to_menu, + } + run_menu(prompt, selection_dict) + +def main_menu(): + """ + Run the main menu for mailroom + """ + prompt = dedent(''' + Choose an action: - running = True + (1) - Send a Thank You + (2) - Create a Report + (3) - Send letters to everyone + (4) - Quit + + > ''') selection_dict = {"1": send_thank_you, "2": print_donor_report, "3": save_letters_to_disk, "4": quit} + run_menu(prompt, selection_dict) + + +def run_menu(prompt, selection_dict): + """ + run an interactive menu + + :param prompt: What you want to ask the user + + :param selection_dict: Dict of possible user impots mapped to + the actions to take. + """ while True: - selection = main_menu_selection() + selection = input(prompt).strip().lower() try: - selection_dict[selection]() + if selection_dict[selection](): + # break out of the loop if action returns True + break except KeyError: print("error: menu selection is invalid!") + + +if __name__ == "__main__": + donor_db = get_donor_db() + main_menu() diff --git a/source/solutions/Lesson06/.gitignore b/source/solutions/Lesson06/.gitignore new file mode 100644 index 00000000..8035ab50 --- /dev/null +++ b/source/solutions/Lesson06/.gitignore @@ -0,0 +1,3 @@ +*.txt + + diff --git a/source/solutions/Lesson06/cigar_party.py b/source/solutions/Lesson06/cigar_party.py deleted file mode 100644 index 992d99bd..00000000 --- a/source/solutions/Lesson06/cigar_party.py +++ /dev/null @@ -1,15 +0,0 @@ - -""" -When squirrels get together for a party, they like to have cigars. -A squirrel party is successful when the number of cigars is between -40 and 60, inclusive. Unless it is the weekend, in which case there -is no upper bound on the number of cigars. - -Return True if the party with the given values is successful, -or False otherwise. -""" - - -def cigar_party(num, weekend): - return num >= 40 and (num <= 60 or weekend) - diff --git a/source/solutions/mailroom/mailroom_basic/mailroom2.py b/source/solutions/Lesson06/mailroom3.py similarity index 68% rename from source/solutions/mailroom/mailroom_basic/mailroom2.py rename to source/solutions/Lesson06/mailroom3.py index 81d3a97e..958cee05 100755 --- a/source/solutions/mailroom/mailroom_basic/mailroom2.py +++ b/source/solutions/Lesson06/mailroom3.py @@ -2,12 +2,16 @@ """ mailroom assignment -This version uses a dict for the main db, and exception handling to -check input +This version uses a dict for the main db, and a dict to"switch" on the user's +input choices. + +it also write the thank you letters to files. + """ import sys import math +from operator import itemgetter # handy utility to make pretty printing easier from textwrap import dedent @@ -20,6 +24,7 @@ # This makes it easier to have a 'normalized' key. # you could get a bit fancier by having each "record" be a dict, with # "name" and "donations" as keys. + def get_donor_db(): return {'william gates iii': ("William Gates III", [653772.32, 12.17]), 'jeff bezos': ("Jeff Bezos", [877.33]), @@ -30,7 +35,7 @@ def get_donor_db(): def list_donors(): """ - creates a list of the donors as a string, so they can be printed + Create a list of the donors as a string, so they can be printed Not calling print from here makes it more flexible and easier to test @@ -41,12 +46,19 @@ def list_donors(): return "\n".join(listing) +def print_donor_list(): + """ + Doesn't do much, but keeps the printing separate + """ + print(list_donors()) + print() + + def find_donor(name): """ - find a donor in the donor db + Find a donor in the donor db :param: the name of the donor - :returns: The donor data structure -- None if not in the donor_db """ key = name.strip().lower() @@ -58,7 +70,6 @@ def add_donor(name): Add a new donor to the donor db :param: the name of the donor - :returns: the new Donor data structure """ name = name.strip() @@ -67,28 +78,11 @@ def add_donor(name): return donor -def main_menu_selection(): - """ - Print out the main application menu and then read the user input. - """ - action = input(dedent(''' - Choose an action: - - 1 - Send a Thank You - 2 - Create a Report - 3 - Send letters to everyone - 4 - Quit - - > ''')) - return action.strip() - - def gen_letter(donor): """ Generate a thank you letter for the donor :param: donor tuple - :returns: string with letter note: This doesn't actually write to a file -- that's a separate @@ -104,27 +98,19 @@ def gen_letter(donor): '''.format(donor[0], donor[1][-1])) -def send_thank_you(): +def take_donation(): """ - Execute the logic to record a donation and generate a thank you message. + Ask user for donation amount, and then add it to the DB """ - # Read a valid donor to send a thank you from, handling special commands to - # let the user navigate as defined. - while True: - name = input("Enter a donor's name (or list to see all donors or 'menu' to exit)> ").strip() - if name == "list": - print(list_donors()) - elif name == "menu": - return - else: - break - # Now prompt the user for a donation amount to apply. Since this is # also an exit point to the main menu, we want to make sure this is # done before mutating the db. + print("in take_donation") + name = input("Enter a donor name (new or existing): \n >") while True: - amount_str = input("Enter a donation amount (or 'menu' to exit)> ").strip() - if amount_str == "menu": + amount_str = input("Enter a donation amount (or to exit)> ").strip() + if not amount_str: + # if they provide no input, go back to previous menu return # Make sure amount is a valid amount before leaving the input loop try: @@ -134,20 +120,21 @@ def send_thank_you(): # http://en.wikipedia.org/wiki/NaN if math.isnan(amount) or math.isinf(amount) or round(amount, 2) == 0.00: raise ValueError - # in this case, the ValueError could be raised by the float() call, or by the NaN-check except ValueError: print("error: donation amount is invalid\n") + continue else: break - # If this is a new user, ensure that the database has the necessary - # data structure. donor = find_donor(name) + # If the donor is not found, it's a new donor if donor is None: + # add the new donor to the database donor = add_donor(name) # Record the donation donor[1].append(amount) + # print the thank you letter print(gen_letter(donor)) @@ -171,7 +158,7 @@ def generate_donor_report(): report_rows.append((name, total_gifts, num_gifts, avg_gift)) # sort the report data - report_rows.sort(key=sort_key) + report_rows.sort(key=itemgetter(1), reverse=True) report = [] report.append("{:25s} | {:11s} | {:9s} | {:12s}".format("Donor Name", "Total Given", @@ -191,6 +178,7 @@ def save_letters_to_disk(): letter = gen_letter(donor) # I don't like spaces in filenames... filename = donor[0].replace(" ", "_") + ".txt" + print("writing letter to:", donor[0]) open(filename, 'w').write(letter) @@ -198,23 +186,68 @@ def print_donor_report(): print(generate_donor_report()) -def quit(): - sys.exit(0) +def return_to_menu(): + ''' Return True to trigger exit out of sub-loop''' + return True -if __name__ == "__main__": - donor_db = get_donor_db() +def send_thank_you(): + """ + Execute the logic to record a donation and generate a thank you message. + """ + # Read a valid donor to send a thank you from, handling special commands to + # let the user navigate as defined. + prompt = ("To send a thank you, select one:\n\n" + "(1) Update donor and send thank-you\n" + "(2) List all existing DONORS\n" + "(3) Return to main menu\n > ") + selection_dict = {"1": take_donation, + "2": print_donor_list, + "3": return_to_menu, + } + run_menu(prompt, selection_dict) + +def main_menu(): + """ + Run the main menu for mailroom + """ + prompt = dedent(''' + Choose an action: - running = True + (1) - Send a Thank You + (2) - Create a Report + (3) - Send letters to everyone + (4) - Quit + + > ''') selection_dict = {"1": send_thank_you, "2": print_donor_report, "3": save_letters_to_disk, "4": quit} + run_menu(prompt, selection_dict) + + +def run_menu(prompt, selection_dict): + """ + run an interactive menu + + :param prompt: What you want to ask the user + + :param selection_dict: Dict of possible user impots mapped to + the actions to take. + """ while True: - selection = main_menu_selection() + selection = input(prompt).strip().lower() try: - selection_dict[selection]() + if selection_dict[selection](): + # break out of the loop if action returns True + break except KeyError: print("error: menu selection is invalid!") + + +if __name__ == "__main__": + donor_db = get_donor_db() + main_menu() diff --git a/source/solutions/Lesson06/test_cigar_party.py b/source/solutions/Lesson06/test_cigar_party.py deleted file mode 100644 index 80bc0920..00000000 --- a/source/solutions/Lesson06/test_cigar_party.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python - -""" -When squirrels get together for a party, they like to have cigars. -A squirrel party is successful when the number of cigars is between -40 and 60, inclusive. Unless it is the weekend, in which case there -is no upper bound on the number of cigars. - -Return True if the party with the given values is successful, -or False otherwise. -""" - - -# you can change this import to test different versions -from cigar_party import cigar_party -# from cigar_party import cigar_party2 as cigar_party -# from cigar_party import cigar_party3 as cigar_party - - -def test_1(): - assert cigar_party(30, False) is False - - -def test_2(): - - assert cigar_party(50, False) is True - - -def test_3(): - - assert cigar_party(70, True) is True - - -def test_4(): - assert cigar_party(30, True) is False - - -def test_5(): - assert cigar_party(50, True) is True - - -def test_6(): - assert cigar_party(60, False) is True - - -def test_7(): - assert cigar_party(61, False) is False - - -def test_8(): - assert cigar_party(40, False) is True - - -def test_9(): - assert cigar_party(39, False) is False - - -def test_10(): - assert cigar_party(40, True) is True - - -def test_11(): - assert cigar_party(39, True) is False - diff --git a/source/solutions/Lesson06/test_mailroom2.py b/source/solutions/Lesson06/test_mailroom3.py similarity index 94% rename from source/solutions/Lesson06/test_mailroom2.py rename to source/solutions/Lesson06/test_mailroom3.py index e633e575..49cfad52 100644 --- a/source/solutions/Lesson06/test_mailroom2.py +++ b/source/solutions/Lesson06/test_mailroom3.py @@ -5,7 +5,7 @@ """ import os -import mailroom2 as mailroom +import mailroom3 as mailroom # so that it's there for the tests mailroom.donor_db = mailroom.get_donor_db() @@ -50,6 +50,11 @@ def test_gen_letter(): def test_add_donor(): + """ + adds a new donor + + then tests that the donor is added, and that a donation is properly recorded. + """ name = "Fred Flintstone " donor = mailroom.add_donor(name) diff --git a/source/solutions/Lesson06/test_walnut_party.py b/source/solutions/Lesson06/test_walnut_party.py new file mode 100644 index 00000000..5bd7ae45 --- /dev/null +++ b/source/solutions/Lesson06/test_walnut_party.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +""" +When squirrels get together for a party, they like to have walnuts. +A squirrel party is successful when the number of walnuts is between +40 and 60, inclusive. Unless it is the weekend, in which case there +is no upper bound on the number of walnuts. + +Return True if the party with the given values is successful, +or False otherwise. +""" + + +# you can change this import to test different versions +from walnut_party import walnut_party +# from walnut_party import walnut_party2 as walnut_party +# from walnut_party import walnut_party3 as walnut_party + + +def test_1(): + assert walnut_party(30, False) is False + + +def test_2(): + + assert walnut_party(50, False) is True + + +def test_3(): + + assert walnut_party(70, True) is True + + +def test_4(): + assert walnut_party(30, True) is False + + +def test_5(): + assert walnut_party(50, True) is True + + +def test_6(): + assert walnut_party(60, False) is True + + +def test_7(): + assert walnut_party(61, False) is False + + +def test_8(): + assert walnut_party(40, False) is True + + +def test_9(): + assert walnut_party(39, False) is False + + +def test_10(): + assert walnut_party(40, True) is True + + +def test_11(): + assert walnut_party(39, True) is False diff --git a/source/solutions/Lesson06/walnut_party.py b/source/solutions/Lesson06/walnut_party.py new file mode 100644 index 00000000..781f71be --- /dev/null +++ b/source/solutions/Lesson06/walnut_party.py @@ -0,0 +1,14 @@ + +""" +When squirrels get together for a party, they like to have walnuts. +A squirrel party is successful when the number of walnuts is between +40 and 60, inclusive. Unless it is the weekend, in which case there +is no upper bound on the number of walnuts. + +Return True if the party with the given values is successful, +or False otherwise. +""" + + +def walnut_party(num, weekend): + return num >= 40 and (num <= 60 or weekend) diff --git a/source/solutions/Lesson07/.gitignore b/source/solutions/Lesson07/.gitignore new file mode 100644 index 00000000..dfec25c4 --- /dev/null +++ b/source/solutions/Lesson07/.gitignore @@ -0,0 +1,2 @@ +test_html_output?.html + diff --git a/source/solutions/html_render/class_html_render/sample_html.html b/source/solutions/Lesson07/sample_output.html similarity index 73% rename from source/solutions/html_render/class_html_render/sample_html.html rename to source/solutions/Lesson07/sample_output.html index f2687e95..9c2e675d 100644 --- a/source/solutions/html_render/class_html_render/sample_html.html +++ b/source/solutions/Lesson07/sample_output.html @@ -2,15 +2,15 @@ - PythonClass = Revision 1087: + Python Class Sample page -

PythonClass - Class 6 example

+

Python Class - Html rendering example

- Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text + Here is a paragraph of text -- there could be more of them, but this is enough to show that we can do some text


-