Merge pull request #19641 from wxy/20200916-Teach-Python-with-Jupyter-Notebooks

TSL&PRF:20200916 Teach Python with Jupyter Notebooks
This commit is contained in:
Xingyu.Wang 2020-09-21 21:42:45 +08:00 committed by GitHub
commit 398be754a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 322 additions and 330 deletions

View File

@ -1,330 +0,0 @@
[#]: collector: (lujun9972)
[#]: translator: (wxy)
[#]: reviewer: ( )
[#]: publisher: ( )
[#]: url: ( )
[#]: subject: (Teach Python with Jupyter Notebooks)
[#]: via: (https://opensource.com/article/20/9/teach-python-jupyter)
[#]: author: (Moshe Zadka https://opensource.com/users/moshez)
Teach Python with Jupyter Notebooks
======
With Jupyter, PyHamcrest, and a little duct tape of a testing harness,
you can teach any Python topic that is amenable to unit testing.
![Person reading a book and digital copy][1]
Some things about the Ruby community have always impressed me. Two examples are the commitment to testing and the emphasis on making it easy to get started. The best example of both is [Ruby Koans][2], where you learn Ruby by fixing tests.
With the amazing tools we have for Python, we should be able to do something even better. We can. Using [Jupyter Notebook][3], [PyHamcrest][4], and just a little bit of duct tape-like code, we can make a tutorial that includes teaching, code that works, and code that needs fixing.
First, some duct tape. Usually, you do your tests using some nice command-line test runner, like [pytest][5] or [virtue][6]. Usually, you do not even run it directly. You use a tool like [tox][7] or [nox][8] to run it. However, for Jupyter, you need to write a little harness that can run the tests directly in the cells.
Luckily, the harness is short, if not simple: ` `
```
import unittest
def run_test(klass):
    suite = unittest.TestLoader().loadTestsFromTestCase(klass)
    unittest.TextTestRunner(verbosity=2).run(suite)
    return klass
```
Now that the harness is done, it's time for the first exercise.
In teaching, it is always a good idea to start small with an easy exercise to build confidence.
So why not fix a really simple test?
```
@run_test
class TestNumbers(unittest.TestCase):
   
    def test_equality(self):
        expected_value = 3 # Only change this line
        self.assertEqual(1+1, expected_value)
[/code] [code]
    test_equality (__main__.TestNumbers) ... FAIL
   
    ======================================================================
    FAIL: test_equality (__main__.TestNumbers)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-7-5ebe25bc00f3>", line 6, in test_equality
        self.assertEqual(1+1, expected_value)
    AssertionError: 2 != 3
   
    ----------------------------------------------------------------------
    Ran 1 test in 0.002s
   
    FAILED (failures=1)
```
`Only change this line` is a useful marker for students. It shows exactly what needs to be changed. Otherwise, students could fix the test by changing the first line to `return`.
In this case, the fix is easy: ` `
```
@run_test
class TestNumbers(unittest.TestCase):
   
    def test_equality(self):
        expected_value = 2 # Fixed this line
        self.assertEqual(1+1, expected_value)
[/code] [code]
    test_equality (__main__.TestNumbers) ... ok
   
    ----------------------------------------------------------------------
    Ran 1 test in 0.002s
   
    OK
```
Quickly, however, the `unittest` library's native assertions will prove lacking. In `pytest`, this is fixed with rewriting the bytecode in `assert` to have magical properties and all kinds of heuristics. This would not work easily in a Jupyter notebook. Time to dig out a good assertion library: PyHamcrest:
```
`from hamcrest import *`[/code] [code]
@run_test
class TestList(unittest.TestCase):
   
    def test_equality(self):
        things = [1,
                  5, # Only change this line
                  3]
        assert_that(things, has_items(1, 2, 3))
[/code] [code]
    test_equality (__main__.TestList) ... FAIL
   
    ======================================================================
    FAIL: test_equality (__main__.TestList)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-11-96c91225ee7d>", line 8, in test_equality
        assert_that(things, has_items(1, 2, 3))
    AssertionError:
    Expected: (a sequence containing <1> and a sequence containing <2> and a sequence containing <3>)
         but: a sequence containing <2> was <[1, 5, 3]>
   
   
    ----------------------------------------------------------------------
    Ran 1 test in 0.004s
   
    FAILED (failures=1)
```
PyHamcrest is not just good at flexible assertions; it is also good at clear error messages. Because of that, the problem is plain to see: `[1, 5, 3]` does not contain `2`, and it looks ugly besides:
```
@run_test
class TestList(unittest.TestCase):
   
    def test_equality(self):
        things = [1,
                  2, # Fixed this line
                  3]
        assert_that(things, has_items(1, 2, 3))
[/code] [code]
    test_equality (__main__.TestList) ... ok
   
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s
   
    OK
```
With Jupyter, PyHamcrest, and a little duct tape of a testing harness, you can teach any Python topic that is amenable to unit testing.
For example, the following can help show the differences between the different ways Python can strip whitespace from a string:
```
source_string = "  hello world  "
@run_test
class TestList(unittest.TestCase):
   
    # This one is a freebie: it already works!
    def test_complete_strip(self):
        result = source_string.strip()
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world")))
    def test_start_strip(self):
        result = source_string # Only change this line
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world  ")))
    def test_end_strip(self):
        result = source_string # Only change this line
        assert_that(result,
                   all_of(starts_with("  hello"), ends_with("world")))
[/code] [code]
    test_complete_strip (__main__.TestList) ... ok
    test_end_strip (__main__.TestList) ... FAIL
    test_start_strip (__main__.TestList) ... FAIL
   
    ======================================================================
    FAIL: test_end_strip (__main__.TestList)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-16-3db7465bd5bf>", line 19, in test_end_strip
        assert_that(result,
    AssertionError:
    Expected: (a string starting with '  hello' and a string ending with 'world')
         but: a string ending with 'world' was '  hello world  '
   
   
    ======================================================================
    FAIL: test_start_strip (__main__.TestList)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-16-3db7465bd5bf>", line 14, in test_start_strip
        assert_that(result,
    AssertionError:
    Expected: (a string starting with 'hello' and a string ending with 'world  ')
         but: a string starting with 'hello' was '  hello world  '
   
   
    ----------------------------------------------------------------------
    Ran 3 tests in 0.006s
   
    FAILED (failures=2)
```
Ideally, students would realize that the methods `.lstrip()` and `.rstrip()` will do what they need. But if they do not and instead try to use `.strip()` everywhere:
```
source_string = "  hello world  "
@run_test
class TestList(unittest.TestCase):
   
    # This one is a freebie: it already works!
    def test_complete_strip(self):
        result = source_string.strip()
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world")))
    def test_start_strip(self):
        result = source_string.strip() # Changed this line
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world  ")))
    def test_end_strip(self):
        result = source_string.strip() # Changed this line
        assert_that(result,
                   all_of(starts_with("  hello"), ends_with("world")))
[/code] [code]
    test_complete_strip (__main__.TestList) ... ok
    test_end_strip (__main__.TestList) ... FAIL
    test_start_strip (__main__.TestList) ... FAIL
   
    ======================================================================
    FAIL: test_end_strip (__main__.TestList)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-17-6f9cfa1a997f>", line 19, in test_end_strip
        assert_that(result,
    AssertionError:
    Expected: (a string starting with '  hello' and a string ending with 'world')
         but: a string starting with '  hello' was 'hello world'
   
   
    ======================================================================
    FAIL: test_start_strip (__main__.TestList)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<ipython-input-17-6f9cfa1a997f>", line 14, in test_start_strip
        assert_that(result,
    AssertionError:
    Expected: (a string starting with 'hello' and a string ending with 'world  ')
         but: a string ending with 'world  ' was 'hello world'
   
   
    ----------------------------------------------------------------------
    Ran 3 tests in 0.007s
   
    FAILED (failures=2)
```
They would get a different error message that shows too much space has been stripped:
```
source_string = "  hello world  "
@run_test
class TestList(unittest.TestCase):
   
    # This one is a freebie: it already works!
    def test_complete_strip(self):
        result = source_string.strip()
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world")))
    def test_start_strip(self):
        result = source_string.lstrip() # Fixed this line
        assert_that(result,
                   all_of(starts_with("hello"), ends_with("world  ")))
    def test_end_strip(self):
        result = source_string.rstrip() # Fixed this line
        assert_that(result,
                   all_of(starts_with("  hello"), ends_with("world")))
[/code] [code]
    test_complete_strip (__main__.TestList) ... ok
    test_end_strip (__main__.TestList) ... ok
    test_start_strip (__main__.TestList) ... ok
   
    ----------------------------------------------------------------------
    Ran 3 tests in 0.005s
   
    OK
```
In a more realistic tutorial, there would be more examples and more explanations. This technique using a notebook with some examples that work and some that need fixing can work for real-time teaching, a video-based class, or even, with a lot more prose, a tutorial the student can complete on their own.
Now go out there and share your knowledge!
--------------------------------------------------------------------------------
via: https://opensource.com/article/20/9/teach-python-jupyter
作者:[Moshe Zadka][a]
选题:[lujun9972][b]
译者:[译者ID](https://github.com/译者ID)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]: https://opensource.com/users/moshez
[b]: https://github.com/lujun9972
[1]: https://opensource.com/sites/default/files/styles/image-full-size/public/lead-images/read_book_guide_tutorial_teacher_student_apaper.png?itok=_GOufk6N (Person reading a book and digital copy)
[2]: https://github.com/edgecase/ruby_koans
[3]: https://jupyter.org/
[4]: https://github.com/hamcrest/PyHamcrest
[5]: https://docs.pytest.org/en/stable/
[6]: https://github.com/Julian/Virtue
[7]: https://tox.readthedocs.io/en/latest/
[8]: https://nox.thea.codes/en/stable/

View File

@ -0,0 +1,322 @@
[#]: collector: (lujun9972)
[#]: translator: (wxy)
[#]: reviewer: (wxy)
[#]: publisher: ( )
[#]: url: ( )
[#]: subject: (Teach Python with Jupyter Notebooks)
[#]: via: (https://opensource.com/article/20/9/teach-python-jupyter)
[#]: author: (Moshe Zadka https://opensource.com/users/moshez)
用 Jupyter Notebooks 教 Python
======
> 有了 Jupyter、PyHamcrest 和一点测试的代码把它们连在一起,你可以教任何可以进行单元测试的 Python 内容。
![Person reading a book and digital copy][1]
关于 Ruby 社区的一些事情一直让我印象深刻。其中两个例子是对测试的承诺和对易于上手的强调。这两方面最好的例子是 [Ruby Koans][2],在这里你可以通过修复测试来学习 Ruby。
要是我们能把这些神奇的工具也用于 Python我们应该可以做得更好。是的使用 [Jupyter Notebook][3]、[PyHamcrest][4],再加上一点类似于胶带的粘合代码,我们可以做出一个包括教学、可工作的代码和需要修复的代码的教程。
首先,需要一些“胶布”。通常,你会使用一些漂亮的命令行测试器来做测试,比如 [pytest][5] 或 [virtue][6]。通常,你甚至不会直接运行它。你使用像 [tox][7] 或 [nox][8] 这样的工具来运行它。然而,对于 Jupyter 来说,你需要写一小段粘合代码,可以直接在其中运行测试。
幸运的是,这个代码又短又简单:
```
import unittest
def run_test(klass):
    suite = unittest.TestLoader().loadTestsFromTestCase(klass)
    unittest.TextTestRunner(verbosity=2).run(suite)
    return klass
```
现在,装备已经就绪,可以进行第一次练习了。
在教学中,从一个简单的练习开始,建立信心总是一个好主意。
那么,让我们来修复一个非常简单的测试:
```
@run_test
class TestNumbers(unittest.TestCase):
def test_equality(self):
expected_value = 3 # 只改这一行
self.assertEqual(1+1, expected_value)
```
```
test_equality (__main__.TestNumbers) ... FAIL
======================================================================
FAIL: test_equality (__main__.TestNumbers)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<ipython-input-7-5ebe25bc00f3>", line 6, in test_equality
self.assertEqual(1+1, expected_value)
AssertionError: 2 != 3
----------------------------------------------------------------------
Ran 1 test in 0.002s
FAILED (failures=1)
```
“只改这一行” 对学生来说是一个有用的标记。它准确地表明了需要修改的内容。否则,学生可以通过将第一行改为 `return` 来修正测试。
在这种情况下,修复很容易:
```
@run_test
class TestNumbers(unittest.TestCase):
def test_equality(self):
expected_value = 2 # 修复后的代码行
self.assertEqual(1+1, expected_value)
```
```
test_equality (__main__.TestNumbers) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
```
然而,很快,`unittest` 库的原生断言将被证明是不够的。在 `pytest` 中,通过重写 `assert`中的字节码来解决这个问题,使其具有神奇的属性和各种启发式方法。但这在 Jupyter notebook 中就不容易实现了。是时候挖出一个好的断言库了PyHamcrest。
```
from hamcrest import *
@run_test
class TestList(unittest.TestCase):
def test_equality(self):
things = [1,
5, # 只改这一行
3]
assert_that(things, has_items(1, 2, 3))
```
```
test_equality (__main__.TestList) ... FAIL
======================================================================
FAIL: test_equality (__main__.TestList)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<ipython-input-11-96c91225ee7d>", line 8, in test_equality
assert_that(things, has_items(1, 2, 3))
AssertionError:
Expected: (a sequence containing <1> and a sequence containing <2> and a sequence containing <3>)
but: a sequence containing <2> was <[1, 5, 3]>
----------------------------------------------------------------------
Ran 1 test in 0.004s
FAILED (failures=1)
```
PyHamcrest 不仅擅长灵活的断言,它还擅长清晰的错误信息。正因为如此,问题就显而易见了。`[1, 5, 3]` 不包含 `2`,而且看起来很丑:
```
@run_test
class TestList(unittest.TestCase):
def test_equality(self):
things = [1,
2, # 改完的行
3]
assert_that(things, has_items(1, 2, 3))
```
```
test_equality (__main__.TestList) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
```
使用 Jupyter、PyHamcrest 和一点测试的粘合代码,你可以教授任何可以进行单元测试的 Python 主题。
例如,下面可以帮助展示 Python 从字符串中去掉空白的不同方法之间的差异。
```
source_string = " hello world "
@run_test
class TestList(unittest.TestCase):
# 这是个赠品:它可以工作!
def test_complete_strip(self):
result = source_string.strip()
assert_that(result,
all_of(starts_with("hello"), ends_with("world")))
def test_start_strip(self):
result = source_string # 只改这一行
assert_that(result,
all_of(starts_with("hello"), ends_with("world ")))
def test_end_strip(self):
result = source_string # 只改这一行
assert_that(result,
all_of(starts_with(" hello"), ends_with("world")))
```
```
test_complete_strip (__main__.TestList) ... ok
test_end_strip (__main__.TestList) ... FAIL
test_start_strip (__main__.TestList) ... FAIL
======================================================================
FAIL: test_end_strip (__main__.TestList)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<ipython-input-16-3db7465bd5bf>", line 19, in test_end_strip
assert_that(result,
AssertionError:
Expected: (a string starting with ' hello' and a string ending with 'world')
but: a string ending with 'world' was ' hello world '
======================================================================
FAIL: test_start_strip (__main__.TestList)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<ipython-input-16-3db7465bd5bf>", line 14, in test_start_strip
assert_that(result,
AssertionError:
Expected: (a string starting with 'hello' and a string ending with 'world ')
but: a string starting with 'hello' was ' hello world '
----------------------------------------------------------------------
Ran 3 tests in 0.006s
FAILED (failures=2)
```
理想情况下,学生们会意识到 `.lstrip()``.rstrip()` 这两个方法可以满足他们的需要。但如果他们不这样做,而是试图到处使用 `.strip()` 的话:
```
source_string = " hello world "
@run_test
class TestList(unittest.TestCase):
# 这是个赠品:它可以工作!
def test_complete_strip(self):
result = source_string.strip()
assert_that(result,
all_of(starts_with("hello"), ends_with("world")))
def test_start_strip(self):
result = source_string.strip() # 改完的行
assert_that(result,
all_of(starts_with("hello"), ends_with("world ")))
def test_end_strip(self):
result = source_string.strip() # 改完的行
assert_that(result,
all_of(starts_with(" hello"), ends_with("world")))
```
```
test_complete_strip (__main__.TestList) ... ok
test_end_strip (__main__.TestList) ... FAIL
test_start_strip (__main__.TestList) ... FAIL
======================================================================
FAIL: test_end_strip (__main__.TestList)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<ipython-input-17-6f9cfa1a997f>", line 19, in test_end_strip
assert_that(result,
AssertionError:
Expected: (a string starting with ' hello' and a string ending with 'world')
but: a string starting with ' hello' was 'hello world'
======================================================================
FAIL: test_start_strip (__main__.TestList)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<ipython-input-17-6f9cfa1a997f>", line 14, in test_start_strip
assert_that(result,
AssertionError:
Expected: (a string starting with 'hello' and a string ending with 'world ')
but: a string ending with 'world ' was 'hello world'
----------------------------------------------------------------------
Ran 3 tests in 0.007s
FAILED (failures=2)
```
他们会得到一个不同的错误信息,显示去除了太多的空白:
```
source_string = " hello world "
@run_test
class TestList(unittest.TestCase):
# This one is a freebie: it already works!
def test_complete_strip(self):
result = source_string.strip()
assert_that(result,
all_of(starts_with("hello"), ends_with("world")))
def test_start_strip(self):
result = source_string.lstrip() # Fixed this line
assert_that(result,
all_of(starts_with("hello"), ends_with("world ")))
def test_end_strip(self):
result = source_string.rstrip() # Fixed this line
assert_that(result,
all_of(starts_with(" hello"), ends_with("world")))
```
```
test_complete_strip (__main__.TestList) ... ok
test_end_strip (__main__.TestList) ... ok
test_start_strip (__main__.TestList) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.005s
OK
```
在一个比较真实的教程中,会有更多的例子和更多的解释。这种使用 Jupyter Notebooks 的技巧,有的例子可以用,有的例子需要修正,可以用于实时教学,可以用于视频课,甚至,可以用更多的其它零散用途,让学生自己完成一个教程。
现在就去分享你的知识吧!
--------------------------------------------------------------------------------
via: https://opensource.com/article/20/9/teach-python-jupyter
作者:[Moshe Zadka][a]
选题:[lujun9972][b]
译者:[wxy](https://github.com/wxy)
校对:[wxy](https://github.com/wxy)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]: https://opensource.com/users/moshez
[b]: https://github.com/lujun9972
[1]: https://opensource.com/sites/default/files/styles/image-full-size/public/lead-images/read_book_guide_tutorial_teacher_student_apaper.png?itok=_GOufk6N (Person reading a book and digital copy)
[2]: https://github.com/edgecase/ruby_koans
[3]: https://jupyter.org/
[4]: https://github.com/hamcrest/PyHamcrest
[5]: https://docs.pytest.org/en/stable/
[6]: https://github.com/Julian/Virtue
[7]: https://tox.readthedocs.io/en/latest/
[8]: https://nox.thea.codes/en/stable/