このBlogは移転しました。今後は aish.dev を御覧ください。

Python 3.8 の概要 (その5) - デバッグ用 f文字列フォーマット

Python3.8の新機能で、これ一番好きかも。このためだけにPython3.8必須にしてもいい。

通常、 f文字列 に変数名や式を指定すると、その値が文字列に埋め込まれます。

>>> foo, bar = 10, 20
>>> print(f'value is {foo+bar}')
value is 30

便利な機能ですが、デバッグ用にデータを出力するときには、ちょっと面倒です。たとえば foobar の値を確認するときは、確認したい変数名のテキストと、表示したい式を別々に書く必要があります。

>>> print(f'foo={foo} bar={bar} foo+bar={foo+bar}')
foo=10 bar=20 foo+bar=30

そこで、f文字列に出力指定方法が追加され、出力したい式に続けて = を指定すると、その式と式の値の両方が文字列に埋め込まれるようになりました。

>>> print(f'{foo=} {bar=} {foo+bar=}')
foo=10 bar=20 foo+bar=30

出力フォーマットを指定する場合は、= に続けて記述します。

>>> print(f'{foo=:0.3f} {bar=:.1e} {foo+bar=:04d}')
foo=10.000 bar=2.0e+01 foo+bar=0030

Python 3.8 の概要 (その4) - multiprocessing.shared_memory モジュール

multiprocessing.shared_memory モジュールで、共有メモリを使ってプロセス間でデータを交換できるようになりました。似たような処理は mmap モジュールで実現できましたが、マルチプラットフォームで簡単に利用できるようになります。

Numpyの ndarray オブジェクトを複数のプロセスで共有する場合、まず最初のプロセスで次のように共有メモリを作成します。この例では、共有メモリの名前は "sharedmemory_test1" とします。

import math
from multiprocessing import  shared_memory
import numpy as np

SHAPE = (3,3)

# 共有メモリ "sharedmemory_test1" を作成
size = math.prod(SHAPE) * numpy.dtype("float").itemsize
shm = shared_memory.SharedMemory(create=True, size=size, name="sharedmemory_test1")

# 共有メモリを利用するndarray オブジェクトを作成する
arr = np.ndarray(shape=SHAPE, dtype=float, buffer=shm.buf)

# ndarray を初期化
arr[:] = 0

他のプロセスでは、"sharedmemory_test1 という名前を指定して共有メモリを参照できます。

from multiprocessing import  shared_memory
import numpy as np

SHAPE = (3,3)

# "sharedmemory_test1" を指定して共有メモリを作成
shm = shared_memory.SharedMemory(name="sharedmemory_test1")

#nbarrayオブジェクトを作成
arr = np.ndarray(shape=SHAPE, dtype=float, buffer=shm.buf)

# 共有メモリを参照
print(arr[0:0])

# 共有メモリを開放
shm.close()

共有メモリが不要になったプロセスでは、close() メソッドでリソースを開放します。

SharedMemoryManager

共有メモリが不要になったら、 unlink() を一度だけ呼び出して、共有メモリを削除します。

SharedMemoryManagerを使うと、共有メモリを管理する専用のプロセスを起動し、共有メモリの寿命を制御できます。

from multiprocessing import Process
from multiprocessing.managers import SharedMemoryManager

def func1(shm, arg):
    shm.buf[0] = arg
    shm.close()

if __name__ == '__main__':

    # 共有メモリ管理プロセスを起動
    with SharedMemoryManager() as smm:

        # 共有メモリを作成し、初期値を設定
        shm1 = smm.SharedMemory(10)  
        shm1.buf[0] = 0

        # func1を異なるプロセスで起動
        p1 = Process(target=func1, args=(shm1, 100))

        # func1を異なるプロセスで起動
        p2 = Process(target=func1, args=(shm1, 200))

        p1.start()
        p2.start()

        p1.join()
        p2.join()
        
        print(shm1.buf[0])

Python 3.8 の概要 (その3) - Pickle protocol 5 with out-of-band data

Pythonでは、複雑なデータの交換や保管する場合、よく Pickleモジュール が使われます。Pickleはデータを外部に出力可能な形式に変換してファイルに変換したり、サーバと通信して送信したりします。

Pythonconcurrent.futuresmultiprocessing を使って並列処理を行う場合も、プロセス間のデータ交換に Pickle が使われています。

PEP-574 Pickle protocol 5 with out-of-band data

Pickleは汎用的なデータフォーマットを定義していて、データを作成したハードウェアと異なるアーキテクチャのハード上で読み込んでも、ただしく元のデータを再現できるようになっています。

しかし、現在ではPickleの使い方は多様化しており、そういった汎用的なデータフォーマットだけでは効率的にデータの転送や保管を行えないことが多くなりました。特に、データサイエンス分野で利用する膨大なデータを高速に扱うには、用途に応じてデータを適切に処理する、専用のメカニズムが必要となります。

そこで、Pickleにあらたに5番目のプロトコルとして、Numpyのarrayオブジェクトなどのメモリ上のデータを直接取得して、効率的に出力する形式が開発されました。

Numpy配列のPickle

例として、1億個の乱数からなるNumpyの配列をPickleで保存してみましょう。

data = np.random.rand(100_000_000)

pickle.dump(data, open("data.pickle", "wb"))

この場合、手元のマシン(Macbook air 2018-1.6 GHz Intel Core i5) では、pickle.dumps() に 1.5秒かかります。

新しいPickleフォーマットを利用する場合は、次のようになります。

pickle.dump(data, open("data5.pickle", "wb"),
            protocol=5)

新しい形式を利用する場合は、protocol=5 と指定します。この場合、pickle.dump() の処理時間は0.7秒ほどで、通常のPickleの2倍以上高速になっています。

そのかわり、保存したPickeを読み込んで、オブジェクトを復元できるとは保証されていません。たとえば、このファイルを作成したPCと異なるアーキテクチャのハードウェアでロードしても、正しい値として復元できません。プロトコル5を利用する場合は、保存したPickleをロードできるかどうかは、アプリケーションの開発者が責任を持つ必要があります。

プロトコル5をサポートするオブジェクトをPickleする場合、バッファプロトコル を使ってデータを取得し、メモリ上のデータを値の変換せずに直接ファイルに出力できます。このとき、データのコピーが発生しないように考慮されており、大きなデータでも効率よく出力できます。

この例で使用している Numpyの ndarray オブジェクトは、メモリ上のデータを memoryviewで取得して出力しています。データに __reduce_ex__() メソッドを実装すると、この動作をカスタマイズできます。

Out-of-band data

プロトコル5では、実際のデータをPickle内に含めず、独自の形式で管理することもできます。データ本体はそれぞれのアプリケーションが用意する、最適な方式で処理します。

buffers = []

pickle.dump(data, open("data5.pickle", "wb"),
            protocol=5,
            buffer_callback=buffers.append)

with open("data-body.raw", "wb") as f:
    f.write(buffers[0])

この形式では、各オブジェクトのバイナリデータは buffer_callback に指定した関数に渡されます。この例では、buffers.append() を呼び出し、buffers リストに保管します。このデータはPickle本体には含まれていませんので、独自に別のファイルに保存しておきます。

buffers.append() には ndarrayのデータが渡されますが、このデータはオブジェクトから取得したバイナリデータそのもので、ここでもデータのコピーは発生していません。もとのndarrayがどれだけ大きくても、メモリの消費量を増やさずにpickleを出力できます。

保存したデータは、次のように復元します。

with open("data-body.raw", "rb") as f:
    buffers = [f.read()]

pickle5.load(open("data5.pickle", "rb"), buffers=buffers)

データの本体は事前にロードし、リストに格納しています。pickle.load() はこのリストから元データを取得し、オブジェクトを復元します。

サポート状況

Numpyでは、このプロトコルがサポートされる予定です。

Python3.6/3.7でもこの機能を利用するための pickle5 パッケージがリリースされており、Python3.8以外でも利用できるようになっています。

Python 3.8 の概要 (その2) - Positional-only parameters

Python 3.0 以降では、関数を定義するときに、キーワード専用引数 を指定できるようになりました。

def func(a, b, *, c=1, d=2):
    return a+b+c+d

こんなのですね。引数のリストに * がある関数を呼び出すとき、* の後ろにある引数の値は、かならずキーワード引数として指定しなければいけません。

↑の関数だと、引数 c はキーワード引数で指定すればちゃんと動きます。

>>> func(1, 2, c=10)
15

しかし、キーワードなしで呼び出すとエラーになります。

>>> func(1, 2, 10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: func() takes 2 positional arguments but 3 were given

「この関数呼び出すとき、この引数はかならず argname=value の形で指定してね。そうじゃないと読みにくくなっちゃうから」という、関数の開発者の気配りです。

で、キーワード専用引数はずいぶん前に実現してたんですが、その逆、キーワードなし専用引数 は存在しませんでした。不公平ですね。

しかし、キーワード専用引数に遅れること11年、Python 3.8でついに PEP 570 -- Python Positional-Only Parameters として実現することになりました。

PEP 570 -- Python Positional-Only Parameters

関数の引数のリストに / があると、/ より前にある引数はすべて 位置専用パラメータ (Positional-Only Parameters) となり、呼び出すときに値をキーワードでは指定できなくなります。

def func(a, b, /, c):
    return a+b+c

この例では、引数 c/ の後ろにありますから、キーワードで指定できます。

>>> func(1, 2, c=3)
6

しかし、ab/ の前にありますので、キーワードは使えません。

>>> func(1, b=2, c=3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: func() got some positional-only arguments passed as keyword arguments: 'b'

古くて新しい機能

じつは、位置専用パラメータは完全に新しい機能というわけではなく、Python 3.8以前にも存在していました。しかし、そういう関数はPython言語では作成できず、組み込みモジュールとしてC/C++言語などでしか作成できませんでした。C言語Pythonの関数を定義するときは、キーワードを無効化したほうが簡単でパフォーマンスも良かったりします。

昔からあるシンプルな組み込み関数などはだいたいそういうパターンで作られていて、例えば range()sum() などは、キーワードとして引数を指定できません。

>>> sum(itearble=[1, 2, 3])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sum() takes no keyword arguments

この sum() と同じインターフェースの関数は、これまでPythonの構文としてはサポートされていなかったのですが、位置専用パラメータを利用すれば

def sum(iterable, start=0, /):
    ...

と定義できるようになります。

ところで、まだ位置専用パラメータがサポートされていない、Python3.7で sum() 関数を調べてみましょう。

Python 3.7.2 (v3.7.2:9a3ffc0492, Dec 24 2018, 02:44:43)
>>> help(sum)

Help on built-in function sum in module builtins:

sum(iterable, start=0, /)
    Return the sum of a 'start' value (default: 0) plus an iterable of numbers
    ...

ここで表示される関数の定義をよく見てみると、helpドキュメントにはすでに

sum(iterable, start=0, /)

と、位置専用パラメータの構文で表示されてますね。実はこの構文、あたらしく作られたのではなく、Pythonの組み込み関数の開発に使われる ツール ですでに採用されていた書き方なのです。

これ、なにが嬉しいの?

例えば、こんな関数を考えてみましょう。

def set_attrs(obj, **kwargs):
    for k, v in kwargs.items():
        setattr(obj, k, v)

set_attrs() は、指定したオブジェクトに、キーワード引数に指定した属性を設定します。

# target.foo = 1, target.bar=2 とする
>>> set_attrs(target, foo=1, bar=2)
>>> target.foo
1
>>> target.bar
2

いい感じですが、この実装には欠点があります。obj という名前の属性を設定できないのです。

>>> set_attrs(target, obj=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: set_attrs() got multiple values for argument 'obj'

obj という名前の仮引数は、更新対象のオブジェクトを指定するために使われていますので、もう一度同じ objという名前を使うことはできません。

この問題、これまでは、仮引数 obj の名前を _target___obj などのあまり使われなさそうな名前にして、問題が発生しないことを祈る、などが主な対策でした。

しかし、位置専用パラメータを利用して

def set_attrs(obj, /, **kwargs):
    for k, v in kwargs.items():
        setattr(obj, k, v)

と定義すれば、位置パラメータしてしか指定できない obj という仮引数名は、なかった ものとして扱われ、キーワード引数として obj を指定できるようになります。

>>> set_attrs(target, obj=1)
>>> target.obj
1

他には?

PEP 570 -- Python Positional-Only Parameters には他にも想定される使い道が示されているのですが、あまり私のハートには刺さりませんでした… 😁

たとえば、

def div_obj(left, right):
    return left/right

という、left/right を計算する関数があったとき、ふつうは

>>> div_obj(10, 2)
5

という感じに使うと思います。しかし、キーワード引数を使って

>>> div_obj(right=2, left=10)
5

と書かれてしまうと、これはとても気持ち悪いコードになってしまいます。

そこで、PEP 570ではこの関数の定義を

def div_obj(left, right, /):
    return left/right

としてキーワード引数を使えなくしてしまえば、こういった気持ちの悪い使い方を禁止できますよ、と紹介しています。

しかし、そのためにわざわざキーワード引数を使えなくしてしまうのは、ちょっと余計なお世話じゃないかなっていう感じがして、たぶん自分ではこういう使い方はしないかなーと思います。

Python 3.8 の概要 (その1) - Assignment expressions

古来、Pythonでは「代入は文であるべき!」と一貫して主張してきました。

C言語などでは、代入は足し算や掛け算と同じ、値を計算する「式」で、たとえば

a = (b=100) / 2;

と書くと、b には 100 を代入し、a100/2=50 を代入します。1+12 という値になる ですが、b=100 も同様に値が 100 となる なのです。

Pythonでは、代入は式ではないので、こういう書き方はできません。

Pythonの代入は、足し算などの演算子の仲間ではなく、iffor のような制御文の仲間で、あまり自由な書き方は出来ないのです。

Python FAQ では、その理由として

Python の式中での代入を許さない理由は、この構造によって起こる、他の言語ではありがちで見つけづらいバグです:
if (x = 0) {
    // error handling
}
else {
    // code that only works for nonzero x
}

と説明しています。

このC言語のコードでは、if 文の中で x = 0 と書いて変数 x に値を代入しています。しかし、このコードは

if (x == 0) {
    // error handling
}
else {
    // code that only works for nonzero x
}

の書き間違いである場合が多いのです。本来、 if (x == 0) { と書くべきところを、if (x = 0) { と書いてしまうという間違いですね。

この間違いは比較的発生しやすく、一見正しい処理に見えてしまうため発見しにくいBugの原因として知られています。多くのプロジェクトで、このエラーを回避するために

== で比較するときには、x == 0 ではなく 0 == x のように書く

というコーディング規約を定めています。

このように、定数を左側に記述するようにすると、間違えて === と書いてしまっても、0 = x と定数に変数を代入する式となるので、コンパイルエラーが発生してミスを発見できるためです。このような書き方は、ヨーダ記法 とも呼ばれます。

PEP 572 -- Assignment Expressions

Pythonは1990年代初頭にリリースされて以来30年近く、代入が演算子であるプログラミング言語をこんな風にDisり続けてきたわけですが、ここへ来て突如方針を転換します。君子は豹変するのです。

あるとき、Pythonの開発者メーリングリストで「PythonC言語のような代入演算子を導入しよう」という提案がありました。過去にも似たような提案がされており、やれやれまたか、まあ採用はされないだろうけどできるだけいろいろ議論してドキュメントにまとめ、今後似たような提案があったときに参照できるようにしよう、ということになりました。

多くの開発者はこの提案が受け入れられるとは思ってもいなかったし、Pythonの仕様決定権をもつPythonの父 Guido van Rossum 氏もそのつもりはなかったようです。しかし、議論が進むにつれ、なんとGuidoが代入演算子を受け入れる意向を示し始めました。

多くのPython開発者はこの提案に反対で、大変な議論が巻き起こりましたが、Guidoを始めとする推進派は丁寧に議論を進め、最終的に PEP 572 -- Assignment Expressions としてPythonに導入されることとなりました。

この議論で疲れ果てた Guido は、Pythonの最高権力者であるBDFL(Benevolent Dictator For Life:慈悲深き終身独裁官) からの退任を表明することとなります。

セイウチ演算子

このような紆余曲折を経て、Python 3.8では 代入式 が導入され、次のように書けるようになりました。

>>> a = (b:=100) / 2
>>> a, b
(50.0, 100)

= は従来どおり代入文で使われます。新しく追加された代入式は、:= 演算子を使用します。

いつのまにか、:= 演算子は「セイウチ演算子」(Walrus operator) と呼ばれるようになりました。そう言われてしまうと、もうセイウチにしか見えなくなります。

f:id:atsuoishimoto:20190903002638p:plain

:= 演算子はふつうの式として、ほとんどの処理で利用できます。

たとえば、これまで

from random import random

a = random()
b = random()

print(a, '+', b, '=', a+b)

と書いていた処理は、

from random import random

print(a:=random() '+', b:=random(), '=', a+b)

のように書けます。

また、if 文や for などの制御文で

v = dist_data.get(key, -1)
if v != -1:
    do_something(v)

のように書いていた処理は、

if (v:= dist_data.get(key, -1)) != -1:
    do_something(v)

と一行にまとめて書けるようになります。

:= 演算子が特に便利なのは、リスト内包などの式でしょう。

これまで、データの編集と判定を同時に行うような処理は、リスト内包ではすっきりと書けませんでした。

for item in data:
    text = item.strip()
    if text:
        result.append(text)

この処理をリスト内包で書こうとすると、

result = [text.strip() for text in data if text.strip()]

のように、text.strip() を2回呼び出すか、

result = [s for s in (text.strip() for text in data) if s]

のように、一つ余計にイテレータを作成する必要がありました。

しかし、:=演算子のおかげで

result = [t for text in data if (t:=text.strip())]

とすっきり記述できるようになりました。

注意点

:=演算子は、あいまいな書き方ができないようにするために、 () でくくらないと SyntaxError とされる場合があります。

>>> a = b:=2
  File "<stdin>", line 1
    a = b:=2
         ^
SyntaxError: invalid syntax

>>> x=(1+y:=2)
  File "<stdin>", line 1
SyntaxError: cannot use named assignment with operator

>>> dict(a=a:=10, b=a*2)
  File "<stdin>", line 1
    dict(a=a:=10, b=a*2)
            ^
SyntaxError: invalid syntax

上記のようなエラーは、:=演算子() でくくって範囲を明確にすると解消します。

>>> a = (b:=2)
>>> x = 1+(y:=2)
>>> dict(a=(a:=10), b=a*2)
{'a': 10, 'b': 20}

また、:= 演算子は非常に優先順位が低いので、複雑な式では () を使うことが多くなるでしょう。たとえば

[a := 1 +  2]

という式は、

[(a := (1 +  2))]

と評価され、 a の値は 3 となります。

カッコをつかって

[(a := 1) +  2]

と書けば、a1 となります。

RecursionError は RecursionError とは限らない、という話

先日、 Pythonjp Discordサーバ の初心者部屋で、こんな質問 があった。

次のようなコードが RecursionError 例外を出して困っているという。

if a:
    ...
elif b:
    ...
elif c:
    ...
elif d:
    ...

のように、数千個の elif が並んでるコードだ。

多くの場合、RecursionError は、関数が自分自身を繰り返し呼び出す 再帰呼び出し で発生する。こんな感じだ。

>>> def f():
...     f()
... 
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
  File "<stdin>", line 2, in f
  File "<stdin>", line 2, in f
  [Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded
>>> 

しかし、問題のコードには、再帰呼出しはもちろん関数呼び出しも存在しない。なぜ RecursionError 例外が発生するのだろうか?

関数呼び出しのないRecursionError

問題のコードを実行すると、次のようなエラーが出る。

$ python3 foo.py
RecursionError: maximum recursion depth exceeded during compilation

実は、このエラーはPythonスクリプトを実行して発生しているのではない。ソースコードコンパイルするときに、Python言語のコンパイラで発生している。

Pythonは、ソースコードコンパイルするときに 抽象構文木(AST) というツリー状のデータ構造を作る。この AST を処理するときに再帰的に処理を行うため、ツリーのネストが深すぎると RecursionError 例外が発生する。

このケースでは if 〜 elif 〜 elif 〜 が大量に並んでおり、if 文の子ノードとして elif ノードが作られ、その elif ノードの子ノードとしてまた elif ノードが作られ、その elif ノードの子ノードとして…… という風に、elif の数だけツリーが深くなっている。

このため、ソースコードコンパイルする過程で行われる再帰呼び出しの回数が制限を超えてしまい、RecursionError 例外 が発生してしまうのだ。

対処方法

関数呼び出しがまったく存在しないソースコードであっても、Pythonコンパイラの制約により RecursionError 例外が発生してしまうことがある。この例では極端に長い if 〜 elif 〜 elif 〜 ブロックだったが、他にも ((((......(a)+1)+1)+1)......) のようなコードでも発生する場合がある。

このようなエラーが出てしまったら、ASTが深くなりすぎないようなコードに修正する必要がある。

if 〜 elif 〜 elif 〜 ブロックなどが長すぎる場合は、短く分割する。長すぎる式は複数行に分割する、などの変更ですむので、対処はそれほど難しくない場合が多いと思う。

Python.jp Discordサーバ雑感

Python.jp Discordサーバ の運用を本格的に開始してから半年以上経ったので、感想など。

ユーザ層

Pythonのユーザ層いうとビジネス層というか、おっちゃんが多いイメージがあるのだか、Discordというゲームで広く使われているシステムなためか、アクティブな参加者は若年層・学生が多いようだ。Discordの利用規約で禁止されている小学生の存在が発覚して、ご退去いただいたこともある。

Discord bot

また、プログラマプログラマ希望者だけではなく、単にDiscordをチャットとして利用している人たちも多い。こういう人たちはプログラミングの知識はあまりないが、ゲームを楽しく遊ぶためのツールとして、Discordにいろんなbotを入れて活用している。そのうち、出来合いのbotでは満足できなくなって、自分なりのbotを作りたくなってきた人たちだ。

私は、この「Discord botを作りたい素人」みたいなのが大好きだ。自分の子供のころを思い出す。プログラミングを仕事や就職の道具や教養として身に着けようというのではなく、なにか楽しみがあって、その楽しみをより満喫するするためにコンピュータやインターネットを活用したい。こういうのがHackというもので、一番楽しいプログラミングだと思う。

質問と回答

現在、Python.jpサーバでは、こういう人たちからのDiscord botに関する質問が多い。プログラミング未経験者がほとんどで、質問も的を得ていない。「この質問、往年のJavaHouseだったらどんだけ炎上するだろ」と怖くなるレベルの質問もあるが、今のところは常連のDiscord.py勢が丁寧に回答してくれていて、感謝に堪えない。

こういった初心者の質問への回答は、回答者の意欲と善意に依存しているのでいつまで続くかはわからない。しかし、現在のところ、とても円滑かつ円満にQ/Aが進行している。これは、

  • かんたんなDiscord botはコード量が比較的少なく、初心者でも取り組みやすい
  • プログラミング未経験者が多く、経験者にはできないような質問を躊躇なくしてくる
  • そういった質問でも回答がつくため、他の初心者もまた質問してくる
  • Discord bot経験者が多く、同じような問題を解決したことがあることが多い
  • まだそれほど経験が深くない参加者も多く、初心者に同情的である
  • 初心者が最初にPython.jpサーバでいろいろ教わったため、成長して自然と回答者になってしまった

あたりが要因なのではないかと思ってる。現在のところ、質問者と回答者の数とレベルが、ちょうどいい感じでバランスが取れているのだろう。

また、質問者がプログラミングの初心者であっても、「Discordでこういうことをしたい」という要望そのものは決して低レベルなものだけでなく、有識者の好奇心を刺激して「こうすればできるんじゃない?」という反応を引き出しやすいように思う。

Q/Aの余禄

こういった感じでDiscord関連でにぎわうようになってから半年以上たつが、あまり口出しせずに見ていると、かなりの速度で初心者を脱しつつある若者が目に付く。

よく言われるように、物事は教わるよりも教えるほうが身につくものだ。いろいろな質問の答えを考えるだけでも役に立つ。

また、アマチュアプログラマは、プログラミング言語やOSなどについてある程度覚えてしまうと、次に何をやったらいいのかわからなくなってしまうことが多い。特に、プログラミング適性の高い人はあっという間に基礎を覚えてしまうため、すぐに行き先を見失ってしまう。

Discord botは比較的シンプルで、実行環境を作って実験しやすいテーマだ。こういったテーマで、いろいろな質問が流れてくる、というのは、初心者が経験を積むには好都合だったのだと思う。いろんな経験を積めたのではないだろうか。

プログラミングという技術でも、経験というのが結構大事だ。一度やったことを、一回で覚えられる人は少ない。何度か繰り返して考え、調べ、実際に書いていると、体にパターンとしてしみこんでくる。実戦経験が重要なのだ。いろいろ能書きを垂れる前に、とりあえずプログラム100本ぐらいは書いてほしいものである。

f:id:atsuoishimoto:20190306014924j:plain:w400

こう考えると、未経験者や初心者が質問しやすい環境を整えるというのは、単に初心者をサポートするだけではない。経験を積んだ中堅を育成するために必要なのだと思う。初心者をサポートするために質問しやすい環境を作るのではなく、むしろ中堅予備軍が経験を積みやすいように初心者の質問が集まる場を作る、というのが重要なのかもしれない。

また、こういった場では、「答えを教えちゃうよりも自分で考えるようにしたほうが良いのでは?」とか余計なことを考えず、正解なり完成したコードなりをすんなり示してもらったほうが、良い結果につながると思う。我々は教育に関しては素人なのだから適切な問題設定などできないし、質問者が回答にたどりつけなかった時のフォローも難しい。なにより回答陣どうしでその回答について語り合うことができなくなってしまうので、問題の深掘りにつながらなくなってしまうのがもったいないと感じる。

これから

今のところうまく機能しているような気がしてるので、特にいじる予定はない。ただ、Discordの話題ばっかりというのも寂しいので、Webプログラミング勢や機械学習勢などの若者も、ぜひともご来駕賜りたいと思う次第である。