标签 Jupyter 下的文章

Jupyter 笔记本将 IPython shell 提升到一个新的高度。

 title=

Jupyter 项目最初是以 IPython 和 IPython 笔记本的形式出现的。它最初是一个专门针对 Python 的交互式 shell 和笔记本环境,后来扩展为不分语言的环境,支持 Julia、Python 和 R 以及其他任何语言。

 title=

IPython 是一个 Python shell,类似于你在命令行输入 python 或者 python3 时看到的,但它更聪明、更有用。如果你曾经在 Python shell 中输入过多行命令,并且想重复它,你就会理解每次都要一行一行地滚动浏览历史记录的挫败感。有了 IPython,你可以一次滚动浏览整个块,同时还可以逐行浏览和编辑这些块的部分内容。

 title=

它具有自动补全,并提供上下文感知的建议:

 title=

它默认会整理输出:

 title=

它甚至允许你运行 shell 命令:

 title=

它还提供了一些有用的功能,比如将 ? 添加到对象中,作为运行 help() 的快捷方式,而不会破坏你的流程:

 title=

如果你使用的是虚拟环境(参见我关于 virtualenvwrapper 的帖子),可以在环境中用 pip 安装:

pip install ipython

要在全系统范围内安装,你可以在 Debian、Ubuntu 或树莓派上使用 apt

sudo apt install ipython3

或使用 pip

sudo pip3 install ipython

Jupyter 笔记本

Jupyter 笔记本将 IPython shell 提升到了一个新的高度。首先,它们是基于浏览器的,而不是基于终端的。要开始使用,请安装 jupyter

如果你使用的是虚拟环境,请在环境中使用 pip 进行安装:

pip install jupyter

要在全系统范围内安装,你可以在 Debian、Ubuntu 或树莓派上使用 apt

sudo apt install jupyter-notebook

或使用 pip

sudo pip3 install jupyter

启动笔记本:

jupyter notebook

这将在你的浏览器中打开:

 title=

你可以使用 “New” 下拉菜单创建一个新的 Python 3 笔记本:

 title=

现在你可以在 In[ ] 字段中编写和执行命令。使用 Enter 在代码块中换行,使用 Shift+Enter 来执行:

 title=

你可以编辑和重新运行代码块,你可以重新排序、删除,复制/粘贴,等等。你可以以任何顺序运行代码块,但是要注意的是,任何创建的变量的作用域都将根据执行的时间而不是它们在笔记本中出现的顺序。你可以在 “Kernel” 菜单中重启并清除输出或重启并运行所有的代码块。

使用 print 函数每次都会输出。但是如果你有一条没有分配的语句,或者最后一条语句没有分配,那么它总是会输出:

 title=

你甚至可以把 InOut 作为可索引对象:

 title=

所有的 IPython 功能都可以使用,而且通常也会表现得更漂亮一些:

 title=

你甚至可以使用 Matplotlib 进行内联绘图:

 title=

最后,你可以保存你的笔记本,并将其包含在 Git 仓库中,如果你将其推送到 GitHub,它们将作为已完成的笔记本被渲染:输出、图形和所有一切(如 本例):

 title=


本文原载于 Ben Nuttall 的 Tooling Tuesday 博客,经许可后重用。


via: https://opensource.com/article/21/3/ipython-shell-jupyter-notebooks

作者:Ben Nuttall 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

字典数据结构可以帮助你快速访问信息。

 title=

字典是 Python 编程语言使用的数据结构。一个 Python 字典由多个键值对组成;每个键值对将键映射到其关联的值上。

例如你是一名老师,想把学生姓名与成绩对应起来。你可以使用 Python 字典,将学生姓名映射到他们关联的成绩上。此时,键值对中键是姓名,值是对应的成绩。

如果你想知道某个学生的考试成绩,你可以从字典中访问。这种快捷查询方式可以为你节省解析整个列表找到学生成绩的时间。

本文介绍了如何通过键访问对应的字典值。学习前,请确保你已经安装了 Anaconda 包管理器Jupyter 笔记本

1、在 Jupyter 中打开一个新的笔记本

首先在 Web 浏览器中打开并运行 Jupyter。然后,

  1. 转到左上角的 “File”。
  2. 选择 “New Notebook”,点击 “Python 3”。

 title=

开始时,新建的笔记本是无标题的,你可以将其重命名为任何名称。我为我的笔记本取名为 “OpenSource.com Data Dictionary Tutorial”。

笔记本中标有行号的位置就是你写代码的区域,也是你输入的位置。

在 macOS 上,可以同时按 Shift + Return 键得到输出。在创建新的代码区域前,请确保完成上述动作;否则,你写的任何附加代码可能无法运行。

2、新建一个键值对

在字典中输入你希望访问的键与值。输入前,你需要在字典上下文中定义它们的含义:

empty_dictionary = {}
grades = {
    "Kelsey": 87,
    "Finley": 92
}

one_line = {a: 1, b: 2}

 title=

这段代码让字典将特定键与其各自的值关联起来。字典按名称存储数据,从而可以更快地查询。

3、通过键访问字典值

现在你想查询指定的字典值;在上述例子中,字典值指特定学生的成绩。首先,点击 “Insert” 后选择 “Insert Cell Below”。

 title=

在新单元格中,定义字典中的键与值。

然后,告诉字典打印该值的键,找到需要的值。例如,查询名为 Kelsey 的学生的成绩:

# 访问字典中的数据
grades = {
    "Kelsey": 87,
    "Finley": 92
}

print(grades["Kelsey"])
87

 title=

当你查询 Kelsey 的成绩(也就是你想要查询的值)时,如果你用的是 macOS,只需要同时按 Shift+Return 键。

你会在单元格下方看到 Kelsey 的成绩。

4、更新已有的键

当把一位学生的错误成绩添加到字典时,你会怎么办?可以通过更新字典、存储新值来修正这类错误。

首先,选择你想更新的那个键。在上述例子中,假设你错误地输入了 Finley 的成绩,那么 Finley 就是你需要更新的键。

为了更新 Finley 的成绩,你需要在下方插入新的单元格,然后创建一个新的键值对。同时按 Shift+Return 键打印字典全部信息:

grades["Finley"] = 90
print(grades)

{'Kelsey': 87; "Finley": 90}

 title=

单元格下方输出带有 Finley 更新成绩的字典。

5、添加新键

假设你得到一位新学生的考试成绩。你可以用新键值对将那名学生的姓名与成绩补充到字典中。

插入新的单元格,以键值对形式添加新学生的姓名与成绩。当你完成这些后,同时按 Shift+Return 键打印字典全部信息:

grades["Alex"] = 88
print(grades)

{'Kelsey': 87, 'Finley': 90, 'Alex': 88}

 title=

所有的键值对输出在单元格下方。

使用字典

请记住,键与值可以是任意数据类型,但它们很少是 扩展数据类型 non-primitive types 。此外,字典不能以指定的顺序存储、组织里面的数据。如果你想要数据有序,最好使用 Python 列表,而非字典。

如果你考虑使用字典,首先要确认你的数据结构是否是合适的,例如像电话簿的结构。如果不是,列表、元组、树或者其他数据结构可能是更好的选择。


via: https://opensource.com/article/21/3/dictionary-values-python

作者:Lauren Maffeo 选题:lujun9972 译者:DCOLIVERSUN 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在 Jupyter 里使用 Python 来分析日历,以了解你是如何使用时间的。

 title=

Python 在探索数据方面具有令人难以置信的可扩展性。利用 PandasDask,你可以将 Jupyter 扩展到大数据领域。但是小数据、个人资料、私人数据呢?

JupyterLab 和 Jupyter Notebook 为我提供了一个绝佳的环境,可以让我审视我的笔记本电脑生活。

我的探索是基于以下事实:我使用的几乎每个服务都有一个 Web API。我使用了诸多此类服务:待办事项列表、时间跟踪器、习惯跟踪器等。还有一个几乎每个人都会使用到:日历。相同的思路也可以应用于其他服务,但是日历具有一个很酷的功能:几乎所有 Web 日历都支持的开放标准 —— CalDAV。

在 Jupyter 中使用 Python 解析日历

大多数日历提供了导出为 CalDAV 格式的方法。你可能需要某种身份验证才能访问这些私有数据。按照你的服务说明进行操作即可。如何获得凭据取决于你的服务,但是最终,你应该能够将这些凭据存储在文件中。我将我的凭据存储在根目录下的一个名为 .caldav 的文件中:

import os
with open(os.path.expanduser("~/.caldav")) as fpin:
    username, password = fpin.read().split()

切勿将用户名和密码直接放在 Jupyter Notebook 的笔记本中!它们可能会很容易因 git push 的错误而导致泄漏。

下一步是使用方便的 PyPI caldav 库。我找到了我的电子邮件服务的 CalDAV 服务器(你可能有所不同):

import caldav
client = caldav.DAVClient(url="https://caldav.fastmail.com/dav/", username=username, password=password)

CalDAV 有一个称为 principal(主键)的概念。它是什么并不重要,只要知道它是你用来访问日历的东西就行了:

principal = client.principal()
calendars = principal.calendars()

从字面上讲,日历就是关于时间的。访问事件之前,你需要确定一个时间范围。默认一星期就好:

from dateutil import tz
import datetime
now = datetime.datetime.now(tz.tzutc())
since = now - datetime.timedelta(days=7)

大多数人使用的日历不止一个,并且希望所有事件都在一起出现。itertools.chain.from_iterable 方法使这一过程变得简单:

import itertools

raw_events = list(
    itertools.chain.from_iterable(
        calendar.date_search(start=since, end=now, expand=True)
        for calendar in calendars
    )
)

将所有事件读入内存很重要,以 API 原始的本地格式进行操作是重要的实践。这意味着在调整解析、分析和显示代码时,无需返回到 API 服务刷新数据。

但 “原始” 真的是原始,事件是以特定格式的字符串出现的:

print(raw_events[12].data)
    BEGIN:VCALENDAR
    VERSION:2.0
    PRODID:-//CyrusIMAP.org/Cyrus
     3.3.0-232-g4bdb081-fm-20200825.002-g4bdb081a//EN
    BEGIN:VEVENT
    DTEND:20200825T230000Z
    DTSTAMP:20200825T181915Z
    DTSTART:20200825T220000Z
    SUMMARY:Busy
    UID:
     1302728i-040000008200E00074C5B7101A82E00800000000D939773EA578D601000000000
     000000010000000CD71CC3393651B419E9458134FE840F5
    END:VEVENT
    END:VCALENDAR

幸运的是,PyPI 可以再次使用另一个辅助库 vobject 解围:

import io
import vobject

def parse_event(raw_event):
    data = raw_event.data
    parsed = vobject.readOne(io.StringIO(data))
    contents = parsed.vevent.contents
    return contents
parse_event(raw_events[12])
    {'dtend': [<DTEND{}2020-08-25 23:00:00+00:00>],
     'dtstamp': [<DTSTAMP{}2020-08-25 18:19:15+00:00>],
     'dtstart': [<DTSTART{}2020-08-25 22:00:00+00:00>],
     'summary': [<SUMMARY{}Busy>],
     'uid': [<UID{}1302728i-040000008200E00074C5B7101A82E00800000000D939773EA578D601000000000000000010000000CD71CC3393651B419E9458134FE840F5>]}

好吧,至少好一点了。

仍有一些工作要做,将其转换为合理的 Python 对象。第一步是 拥有 一个合理的 Python 对象。attrs 库提供了一个不错的开始:

import attr
from __future__ import annotations
@attr.s(auto_attribs=True, frozen=True)
class Event:
    start: datetime.datetime
    end: datetime.datetime
    timezone: Any
    summary: str

是时候编写转换代码了!

第一个抽象从解析后的字典中获取值,不需要所有的装饰:

def get_piece(contents, name):
    return contents[name][0].value
get_piece(_, "dtstart")
    datetime.datetime(2020, 8, 25, 22, 0, tzinfo=tzutc())

日历事件总有一个“开始”、有一个“结束”、有一个 “持续时间”。一些谨慎的解析逻辑可以将两者协调为同一个 Python 对象:

def from_calendar_event_and_timezone(event, timezone):
    contents = parse_event(event)
    start = get_piece(contents, "dtstart")
    summary = get_piece(contents, "summary")
    try:
        end = get_piece(contents, "dtend")
    except KeyError:
        end = start + get_piece(contents, "duration")
    return Event(start=start, end=end, summary=summary, timezone=timezone)

将事件放在 本地 时区而不是 UTC 中很有用,因此使用本地时区:

my_timezone = tz.gettz()
from_calendar_event_and_timezone(raw_events[12], my_timezone)
    Event(start=datetime.datetime(2020, 8, 25, 22, 0, tzinfo=tzutc()), end=datetime.datetime(2020, 8, 25, 23, 0, tzinfo=tzutc()), timezone=tzfile('/etc/localtime'), summary='Busy')

既然事件是真实的 Python 对象,那么它们实际上应该具有附加信息。幸运的是,可以将方法添加到类中。

但是要弄清楚哪个事件发生在哪一天不是很直接。你需要在 本地 时区中选择一天:

def day(self):
    offset = self.timezone.utcoffset(self.start)
    fixed = self.start + offset
    return fixed.date()
Event.day = property(day)
print(_.day)
    2020-08-25

事件在内部始终是以“开始”/“结束”的方式表示的,但是持续时间是有用的属性。持续时间也可以添加到现有类中:

def duration(self):
    return self.end - self.start
Event.duration = property(duration)
print(_.duration)
    1:00:00

现在到了将所有事件转换为有用的 Python 对象了:

all_events = [from_calendar_event_and_timezone(raw_event, my_timezone)
              for raw_event in raw_events]

全天事件是一种特例,可能对分析生活没有多大用处。现在,你可以忽略它们:

# ignore all-day events
all_events = [event for event in all_events if not type(event.start) == datetime.date]

事件具有自然顺序 —— 知道哪个事件最先发生可能有助于分析:

all_events.sort(key=lambda ev: ev.start)

现在,事件已排序,可以将它们加载到每天:

import collections
events_by_day = collections.defaultdict(list)
for event in all_events:
    events_by_day[event.day].append(event)

有了这些,你就有了作为 Python 对象的带有日期、持续时间和序列的日历事件。

用 Python 报到你的生活

现在是时候编写报告代码了!带有适当的标题、列表、重要内容以粗体显示等等,有醒目的格式是很意义。

这就是一些 HTML 和 HTML 模板。我喜欢使用 Chameleon

template_content = """
<html><body>
<div tal:repeat="item items">
<h2 tal:content="item[0]">Day</h2>
<ul>
    <li tal:repeat="event item[1]"><span tal:replace="event">Thing</span></li>
</ul>
</div>
</body></html>"""

Chameleon 的一个很酷的功能是使用它的 html 方法渲染对象。我将以两种方式使用它:

  • 摘要将以粗体显示
  • 对于大多数活动,我都会删除摘要(因为这是我的个人信息)
def __html__(self):
    offset = my_timezone.utcoffset(self.start)
    fixed = self.start + offset
    start_str = str(fixed).split("+")[0]
    summary = self.summary
    if summary != "Busy":
        summary = "&lt;REDACTED&gt;"
    return f"<b>{summary[:30]}</b> -- {start_str} ({self.duration})"
Event.__html__ = __html__

为了简洁起见,将该报告切成每天的:

import chameleon
from IPython.display import HTML
template = chameleon.PageTemplate(template_content)
html = template(items=itertools.islice(events_by_day.items(), 3, 4))
HTML(html)

渲染后,它将看起来像这样:

2020-08-25

  • -- 2020-08-25 08:30:00 (0:45:00)
  • -- 2020-08-25 10:00:00 (1:00:00)
  • -- 2020-08-25 11:30:00 (0:30:00)
  • -- 2020-08-25 13:00:00 (0:25:00)
  • Busy -- 2020-08-25 15:00:00 (1:00:00)
  • -- 2020-08-25 15:00:00 (1:00:00)
  • -- 2020-08-25 19:00:00 (1:00:00)
  • -- 2020-08-25 19:00:12 (1:00:00)

Python 和 Jupyter 的无穷选择

通过解析、分析和报告各种 Web 服务所拥有的数据,这只是你可以做的事情的表面。

为什么不对你最喜欢的服务试试呢?


via: https://opensource.com/article/20/9/calendar-jupyter

作者:Moshe Zadka 选题:lujun9972 译者:stevenzdg988 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

用 Jupyter 和 Python 在你的日常写作背后实现一些自动化。

有些人会遵循传统,制定一年的计划。不过,一年的时间很长,所以我以季节性的主题或轨迹来规划。每个季度,我都会坐下来,看看即将到来的三个月的季节,并决定在这段时间里我将努力做什么。

对于我最新的主题,我决定要每天写一篇日记。我喜欢有明确的承诺,所以我承诺每天写 5 分钟。我也喜欢有可观察的承诺,哪怕只是对我而言,所以我把我的记录放在 Git 里。

我决定在写日记的过程中实现一些自动化,于是我使用了我最喜欢的自动化工具:Jupyter。Jupyter 有一个有趣的功能 ipywidgets,这是一套用于 Jupyter Notebooks、JupyterLab 和 IPython 内核的交互式 HTML 组件。

如果你想跟着本文的代码走,请注意,让你的 JupyterLab 实例支持组件可能有点复杂,请按照这些说明来进行设置。

导入 ipywidgets 模块

首先,你需要导入一堆东西,比如 ipywidgets 和 Twisted。Twisted 模块可以用来创建一个异步时间计数器:

import twisted.internet.asyncioreactor
twisted.internet.asyncioreactor.install()
from twisted.internet import reactor, task
import ipywidgets, datetime, subprocess, functools, os

设置定时条目

用 Twisted 实现时间计数器是利用了 task.LoopingCall。然而,结束循环调用的唯一方法是用一个异常。倒计时时钟总会停止,所以你需要一个自定义的异常来指示“一切正常;计数器结束”:

class DoneError(Exception):
    pass

现在你已经写好了异常,你可以写定时器了。第一步是创建一个 ipywidgets.Label 的文本标签组件。循环使用 divmod 计算出分和秒,然后设置标签的文本值:

def time_out_counter(reactor):
    label = ipywidgets.Label("Time left: 5:00")
    current_seconds = datetime.timedelta(minutes=5).total_seconds()
    def decrement(count):
        nonlocal current_seconds
        current_seconds -= count
        time_left = datetime.timedelta(seconds=max(current_seconds, 0))
        minutes, left = divmod(time_left, minute)
        seconds = int(left.total_seconds())
        label.value = f"Time left: {minutes}:{seconds:02}"
        if current_seconds < 0:
            raise DoneError("finished")
    minute = datetime.timedelta(minutes=1)
    call = task.LoopingCall.withCount(decrement)
    call.reactor = reactor
    d = call.start(1)
    d.addErrback(lambda f: f.trap(DoneError))
    return d, label

从 Jupyter 组件中保存文本

下一步是写一些东西,将你输入的文字保存到一个文件中,并提交到 Git。另外,由于你要写 5 分钟的日记,你需要一个能给你提供写字区域的组件(滚动肯定是可以的,但一次能看到更多的文字就更好了)。

这就用到了组件 Textarea,这是一个你可以书写的文本字段,而 Output 则是用来给出反馈的。这一点很重要,因为 git push 可能会花点时间或失败,这取决于网络。如果备份失败,用反馈提醒用户很重要:

def editor(fname):
    textarea = ipywidgets.Textarea(continuous_update=False)
    textarea.rows = 20
    output = ipywidgets.Output()
    runner = functools.partial(subprocess.run, capture_output=True, text=True, check=True)
    def save(_ignored):
        with output:
            with open(fname, "w") as fpout:
                fpout.write(textarea.value)
            print("Sending...", end='')
            try:
                runner(["git", "add", fname])
                runner(["git", "commit", "-m", f"updated {fname}"])
                runner(["git", "push"])
            except subprocess.CalledProcessError as exc:
                print("Could not send")
                print(exc.stdout)
                print(exc.stderr)
            else:
                 print("Done")
    textarea.observe(save, names="value")
    return textarea, output, save

continuous_update=False 是为了避免每个字符都保存一遍并发送至 Git。相反,只要脱离输入焦点,它就会保存。这个函数也返回 save 函数,所以可以明确地调用它。

创建一个布局

最后,你可以使用 ipywidgets.VBox 把这些东西放在一起。这是一个包含一些组件并垂直显示的东西。还有一些其他的方法来排列组件,但这足够简单:

def journal():
    date = str(datetime.date.today())
    title = f"Log: Startdate {date}"
    filename = os.path.join(f"{date}.txt")
    d, clock = time_out_counter(reactor)
    textarea, output, save = editor(filename)
    box = ipywidgets.VBox([
        ipywidgets.Label(title),
        textarea,
        clock,
        output
    ])
    d.addCallback(save)
    return box

biu!你已经定义了一个写日记的函数了,所以是时候试试了。

journal()

 title=

你现在可以写 5 分钟了!


via: https://opensource.com/article/20/11/daily-journal-jupyter

作者:Moshe Zadka 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Jupyter 不仅仅是一个数据分析工具,让我们看看如何以最有创意的方式使用这个基于 Python 的软件。

Jupyter 项目提供了用 JupyterLab 和 Jupyter Notebook 等交互式编写软件的技术方式。这个软件通常用于数据分析,但你可能不知道(Jupyter 社区也没有想到),你可以用它做多少事情。

以下是我使用 Jupyter 的五大意想不到的创造性方法。

1、处理图像

图像编辑和处理方面,有很多很好的开源工具 —— 从那些可以与 Photoshop 媲美的工具到实验性的 Glimpse。但即使有这么多选择,有时我还是不想离开 Python 的世界。

幸运的是,Jupyter 是一个做轻量级图像处理的好选择。利用 Jupyter 直接将 Pillow 对象显示为图像的优势,让你可以尽情地对图片进行实验。我甚至还用它给孩子做了一个涂色画

2、做一个 SSH 跳板遥控器

由于 JupyterLab 可以让你上传和下载文件、编辑文件,甚至运行终端,所以它拥有制作 SSH 跳板环境所需的所有部件。

通过一些 SSH 转发魔法,你可以让 Jupyter 成为防火墙另一边的远程控制台

3、开发 Web 应用程序

我最喜欢的使用 Jupyter 的方式之一是用于一种意想不到的软件开发。我做了一次演讲,在演讲中,我使用 Jupyter Notebook 实时开发了一个 Web 应用。讲演的最后是一个简单的表单,它是 XSS 和 CSS 安全的,并包括一些轻量级的服务器端计算。

一个日常的 Jupyter 用户可能不会期望它是一个最棒的 Web 开发环境,但它是一个非常强大的环境。

4、从你喜欢的服务中提取报告

JupyterLab 中的数据分析是一种常见的用法,但 自我提升分析 self-improvement analysis 呢?

你可以使用 Jupyter 来分析你的日历。如果你最喜欢的服务允许 API 导出,甚至可以让你导出一个 CSV,你可以将这些与你的日历进行关联。如果你发现你在社交媒体上发帖的时候,你的日历上写着你应该和你的经理开会,那 Jupyter 也救不了你!

5、开发游戏

对于扩大对 Jupyter Notebook 的期望值,我最喜欢的方式是和孩子一起建立一个游戏。我之前写过这方面的文章,有一个使用 PursuedPyBear 和 Jupyter 编写游戏的分步教程。

在试图弄清游戏机制时,这种迭代式的游戏开发方法特别有用。能够在游戏中途改变规则(对不起,我必须得这样做)是一个改变游戏规则的方法。

你甚至可以使用 IPywidgets 来修改数字参数,就像这个视频所示。

下载电子书

JupyterLab 和 Jupyter Notebooks 提供了一个不可思议的实验环境。下载这本指南,其中包含了以令人吃惊的方式使用 Jupyter 的教程。

你是如何以创造性的方式使用它的?在下面的评论中分享你的最爱。


via: https://opensource.com/article/20/11/surprising-jupyter

作者:Moshe Zadka 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

有了 Jupyter、PyHamcrest,用一点测试的代码把它们连在一起,你就可以教任何适用于单元测试的 Python 内容。

关于 Ruby 社区的一些事情一直让我印象深刻,其中两个例子是对测试的承诺和对易于上手的强调。这两方面最好的例子是 Ruby Koans,在这里你可以通过修复测试来学习 Ruby。

要是我们能把这些神奇的工具也用于 Python,我们应该可以做得更好。是的,使用 Jupyter NotebookPyHamcrest,再加上一点类似于胶带的粘合代码,我们可以做出一个包括教学、可工作的代码和需要修复的代码的教程。

首先,需要一些“胶布”。通常,你会使用一些漂亮的命令行测试器来做测试,比如 pytestvirtue。通常,你甚至不会直接运行它。你使用像 toxnox 这样的工具来运行它。然而,对于 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):
   
    # 这是个赠品:它可以工作!
    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 Notebook 的技巧,有的例子可以用,有的例子需要修正,可以用于实时教学,可以用于视频课,甚至,可以用更多的其它零散用途,让学生自己完成一个教程。

现在就去分享你的知识吧!


via: https://opensource.com/article/20/9/teach-python-jupyter

作者:Moshe Zadka 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出