2014年10月30日

【翻訳】Pythonを教えるためのいくつかの提案

私は最近PyCon Australia 2014におけるクイーンズランド大学のチューターのプレゼンテーションを見るだけではなく、クイーンズランド大学のSoftware Carpentryブートキャンプに参加する機会がありました(ティーチングアシスタントとして)。

彼らが出くわした課題の多くはプログラミングを教えるときの固有の複雑さによるものでしたが、いくらかは避けられることのように思えました。

整数の除算から浮動小数点の値を得る

デフォルトのPython 2では、整数の除算は答えの小数点以下を切り捨てた商を表示します。

$ python -c "print(3/4)"
0

浮動小数点の表示は、コマンドラインのフラグやfuture importによる、型の強制を必要とします。

$ python -c "print(float(3)/4)"
0.75
$ python -Qnew -c "print(3/4)"
0.75
$ python -c "from __future__ import division; print(3/4)"
0.75

Python 3はデフォルトでまさにこのように動作します。そのためこの問題を完全に避ける一つの方法はPython 2の代わりにPython 3を教えることです。

$ python3 -c "print(3/4)"
0.75

(Python 2と3のいずれも、必要なら//floor division演算子で除算の切捨てを明確に指示します)

値の表示に関するPython 2と3の一般的な構文

私はPython 2と3を平行しながら今まで8年以上使ってきました(Python 3.0は2008年にリリースされましたが、そのプロジェクトはそれより数年早く本格的に始まりました。そのときPython 2.5はまだ開発中でした)。

それらをたびたび交互に切り替えるために学んだ必須の習慣の一つは、両方のバージョンで同様に動作する一般的なprint構文の使用へと私自身を制限することです。つまり丸括弧で囲まれた単一の引数を渡します。

$ python -c 'print("Hello world!")'
Hello world!
$ python3 -c 'print("Hello world!")'
Hello world!

もし複数の引数を渡す必要があれば、私は暗黙の連結機能よりも文字列の書式指定を使うでしょう。

$ python -c 'print("{} {}{}".format("Hello", "world", "!"))'
Hello world!
$ python3 -c 'print("{} {}{}".format("Hello", "world", "!"))'
Hello world!

このようにするのではなく、私が参加したブートキャンプで使われたSoftware Carpentryの教材はprint構文のみのレガシーなPython 2を広範囲で使いました。
そのためたまたまPython 3を実行していた学生にとって、うまく動いてきたはずなのに、いずれのバージョンでも失敗するといった例を引き起こしました。値の出力の共通構文を選ぶことで、Pythonのバージョンに依存しないようになります.講義をする分にはこれで十分でしょう。

戻り値と出力値の区別

ブートキャンプそしてPycon Australiaのプレゼンの両方で言及されていた問題が、出力値と戻り値の違いを学生に教えることの難しさでした。問題となるのは、Pythonの対話型インタプリタで提供されるRead-Eval-Print-Loopの”Print”の部分です。

>>> def print_arg(x):
...     print(x)
...
>>> def return_arg(x):
...     return x
...
>>> print_arg(10)
10
>>> return_arg(10)
10

対話型プロンプトの出力には、明らかな違いは見られません。特にstrreprの結果である数値を表す型は、どちらも同じです。これらの数値が違っていたとしても、学生にはこの違いが明確ではないでしょう。

>>> print_arg("Hello world")
Hello world
>>> return_arg("Hello world")
'Hello world'

私自身、この問題に対する明確な回答を持っているわけではありませんが、試してみる価値がありそうなのは、sys.displayhookの置換方法を生徒に教えてみることです。具体的には、次の変更を実際にやって見せ、ユーザに表示するための出力値とさらなる処理のための戻り値とがどう違うのかを説明してみせることです。

>>> def new_displayhook(obj):
...     if obj is not None:
...         print("-> {!r}".format(obj))
...
>>> import sys
>>> sys.displayhook = new_displayhook
>>> print_arg(10)
10
>>> return_arg(10)
-> 10

出力値と戻り値の違いを理解することは、関数を効果的に使うのに必要不可欠な学習です。この方法で結果を表示させることで、これらの違いを少しでも明確にすることができるでしょう。

追記:IPython(とIPython Notebook)

上に挙げた例は標準的なCPythonの実行環境に特化したもので、CPythonはデフォルトで対話型インタプリタを装備しています。これに対し、IPython(IPython Notebookも含む)の対話型インタプリタにはなかなか興味深いいくつかの相違点があります。

1つには、戻り値と出力値が違う形で表示されるという点です。結果の前に出力の参照番号が表示されます。

In [1]: print 10
10

In [2]: 10
Out[2]: 10

そして、オプションとして”autocall”機能が装備されています。これは関数を呼び出す時にユーザが括弧を除外していた場合、自動的にIPythonが括弧を付加してくれるよう設定しておける機能です。

$ ipython3 --autocall=1 -c "print 10"
-> print(10)
10

これはIPythonのセッションを、第一級関数を持たない言語と同じように動作させることができる汎用の機能です(なかでも注目に値するのは、 IPythonのautocall機能はMATLABの”コマンド構文”の関数呼び出しの表記法に酷似しているという点です)。

これによる弊害と言えば、”autocall”を有効にしているIPythonユーザがPython 2のあまり知られていないprint命令文(例えばstream redirectionや、suppressing the trailing newlineなど)を全く使っていない場合、Python 3でprintが標準のビルトイン関数になったことに気づきもしないという点でしょう。

I recently had the chance to attend a Software Carpentry bootcamp at the University of Queensland (as a teaching assistant), as well as seeing a presentation from one of UQ's tutors at PyCon Australia 2014.

While many of the issues they encountered were inherent in the complexity of teaching programming, a few seemed like things that could be avoided.

Getting floating point results from integer division

In Python 2, integer division copies C in truncating the answer by default:

Promoting to floating point requires type coercion, a command line flag or a future import:

Python 3 just does the right thing by default, so one way to avoid the problem entirely is to teach Python 3 instead of Python 2:

(In both Python 2 and 3, the // floor division operator explicitly requests truncating division when it is desired)

Common Python 2/3 syntax for printing values

I've been using Python 2 and 3 in parallel for more than 8 years now (while Python 3.0 was released in 2008, the project started in earnest a couple of years earlier than that, while Python 2.5 was still in development).

One essential trick I have learned in order to make regularly switching back and forth feasible is to limit myself to the common print syntax that works the same in both versions: passing a single argument surrounded by parentheses.

If I need to pass multiple arguments, I'll use string formatting, rather than the implicit concatenation feature.

Rather than doing this, the Software Carpentry material that was used at the bootcamp I attended used the legacy Python 2 only print syntax extensively, causing examples that otherwise would have worked fine in either version to fail for students that happened to be running Python 3. Adopting the shared syntax for printing values could be enough to make the course largely version independent.

Distinguishing between returning and printing values

One problem noted both at the bootcamp and by presenters at PyCon Australia was the challenge of teaching students the difference between printing and returning values. The problem is the "Print" part of the Read-Eval-Print-Loop provided by Python's interactive interpreter:

There's no obvious difference in output at the interactive prompt, especially for types like numbers where the results of str and repr are the same. Even when they're different, those differences may not be obvious to a student:

While I don't have a definitive answer for this one, an experiment that seems worth trying to me is to teach students how to replace sys.displayhook. In particular, I suggest demonstrating the following change, and seeing if it helps explain the difference between printing output for display to the user and returning values for further processing:

Understanding the difference between printing and returning is essential to learning to use functions effectively, and tweaking the display of results this way may help make the difference more obvious.

Addendum: IPython (including IPython Notebook)

The initial examples above focused on the standard CPython runtime, include the default interactive interpreter. The IPython interactive interpreter, including the IPython Notebook, has a couple of interesting differences in behaviour that are relevant to the above comments.

Firstly, it does display return values and printed values differently, prefacing results with an output reference number:

Secondly, it has an optional "autocall" feature that allows a user to tell IPython to automatically add the missing parentheses to a function call if the user leaves them out:

This is a general purpose feature that allows users to make their IPython sessions behave more like languages that don't have first class functions (most notably, IPython's autocall feature closely resembles MATLAB's "command syntax" notation for calling functions).

It also has the side effect that users that use IPython, have autocall enabled, and don't use any of the more esoteric quirks of the Python 2 print statement (like stream redirection or suppressing the trailing newline) may not even notice that print became an ordinary builtin in Python 3.