narupo’s blog

プログラミングやWebサービスについての話題など

人気が出るWebサービス

 ブログを書くことにした。
 Webサービスを作っている。言語はPython3、フレームワークDjango. どんなサービスかと言うと、SNSである。
 今の時代、SNSなんて作っても流行らないという声を聞くが本当だろうか。確かに目ぼしいSNSは既にあらかた作られてしまっている。ここに切り込みを入れるのは至難の業だし、人も集まらないかもしれない。
 しかし、それでも別に構わないというモチベーションである。なぜかというと、技術的なノウハウの蓄積も課題としているからだ。つまり、たとえ作ったサービスが流行らなくてもノウハウが貯まればそれで良しとするのである。
 だが、正直な話、作ったサービスに人が集まらないというのはストレスである。作ったからには沢山の人に利用してもらいたいというのが親心と言えよう。
 人が集まるサービスというのはどういうサービスだろうか。需要があるということだが、どんなサービスに需要があるのだろうか。それを知っている人は少ない。「人々」というのは群衆だが、群衆に人気があるというのは大変判断がしづらい。判断がしづらいというのは、PVなどのアクセス解析があれば別だが、素のままの状態で、事前に、人気があるということを知るのは難しいという意味だ。何が人気があるのか?これを知っていればその人はモノ作りに置いて安泰と言えよう。
 普遍的なサービスというのは人気が安定していると思う。例えば人間の三大欲求だ。食欲、睡眠欲、性欲。これは人間である以上、普遍と言える。もちろん拒食症などの例外は除いて。ということはまずは「食欲」だが、これは安定しているはずである。代表的なサービスだと「Cookpad」などがそうだ。「など」と書いたが、これぐらいしか知らない。次に「睡眠欲」だが、これは厄介者だ。睡眠はビジネスに繋げづらい。私は思うのだが、技術革新などで睡眠を操作したり介入できるようになったら新しい市場が開けるだろう。睡眠ビジネスだ。睡眠はその名の通り眠る需要地帯、未来のフロンティアといえる。最後に「性欲」だが、これは言わずもがな、アダルトサイトが氾濫している現状である。
 三大欲求に限らず、人の「欲」に着目すると、人気の秘密がわかるかもしれない。代表的なのが「承認欲求」だ。これは他人から認められたい欲求である。Facebookの「いいね!」に代表されるサービス内のツールで、利用者の承認欲求を満たす。これの嬉しい所は、サービスにツールとして導入できる点だ。この場合、サービスは何でもいいのである。
 ということで、しばらくは人間の「欲」についての勉強が必要だろうと思われる。順次更新していくのでお楽しみに。

Pillowで遊ぶ

Pillow で遊ぶ

平行線

黒い背景に白い平行線を引くだけ。

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
from PIL import Image, ImageDraw

def main():
    wh = (640, 480)
    rgba = (0, 0, 0, 255)
    canvas = Image.new('RGBA', wh, rgba)
    draw = ImageDraw.Draw(canvas)

    y = wh[1]//2
    for x in range(0, wh[0]):
        draw.point((x, y), fill=(255, 255, 255))

    canvas.show()
    canvas.save('/tmp/tmp.png', 'PNG')

    sys.exit(0)

if __name__ == '__main__':
    main()

f:id:narupo:20171025025058p:plain

横たわる縞模様の円柱

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
from math import *
from PIL import Image, ImageDraw

def f(draw, frame, x, y):
    a = 64
    b = 4
    r = int(cos(y/a) * cos(frame/b) * 255)
    g = int(cos(y/(a-1)) * cos(frame/(b-1)) * 255)
    b = int(cos(y/(a-2)) * cos(frame/(b-2)) * 255)
    draw.point((x, y), fill=(r, g, b))

def main():
    wh = (640, 480)
    rgba = (0, 0, 0, 255)
    canvas = Image.new('RGBA', wh, rgba)
    draw = ImageDraw.Draw(canvas)

    frame = 0
    for y in range(0, wh[1]):
        for x in range(0, wh[0]):
            frame += 1
            f(draw, frame, x, y)

    canvas.show()
    canvas.save('/tmp/tmp.png', 'PNG')

    sys.exit(0)

if __name__ == '__main__':
    main()

f:id:narupo:20171025032327p:plain

グラデーション・タイル

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
from math import *
from PIL import Image, ImageDraw

def f(draw, frame, x, y):
    r = int(abs(1.0 - pow(abs(x), 0.5))) * 10
    g = int(abs(1.0 - pow(abs(y), 0.502))) * 10
    b = 0
    draw.point((x, y), fill=(r, g, b))

def main():
    wh = (640, 480)
    rgba = (0, 0, 0, 255)
    canvas = Image.new('RGBA', wh, rgba)
    draw = ImageDraw.Draw(canvas)

    frame = 0
    for y in range(0, wh[1]):
        for x in range(0, wh[0]):
            frame += 1
            f(draw, frame, x, y)

    canvas.show()
    canvas.save('/tmp/tmp.png', 'PNG')

    sys.exit(0)

if __name__ == '__main__':
    main()

f:id:narupo:20171025041409p:plain

例外とログ

例外とログ

例外処理とロギングの関係。 長期にわたって稼働するシステムでは、エラー内容などはログとして残すのが普通だと思う。 そこで、将来的に発生しうる未知の例外を、ログのフォーマットへと変換する必要性が生まれる。

Python3の例外機構

この記事で扱う言語はPython3だ。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def main():
    raise RuntimeError('runtime error')

if __name__ == '__main__':
    main()

Python3ではraiseで例外を送出する。他言語のthrowに相当。 このスクリプトを実行すると

$ python3 main.py 
Traceback (most recent call last):
  File "main.py", line 8, in <module>
    main()
  File "main.py", line 5, in main
    raise RuntimeError('runtime error')
RuntimeError: runtime error

シェルにスタックトレースが吐き出される。 例外の捕捉にはtry ~ exceptを使う。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def main():
    try:
        raise RuntimeError('runtime error')
    except Exception as e:
        print(e)

if __name__ == '__main__':
    main()

Python3の例外クラスはExceptionクラスを継承しており、Exceptionを捕捉すると派生クラスのRuntimeErrorの捕捉も行える。 このスクリプトを実行すると

$ python3 main.py 
runtime error

シェルに上記のような出力が得られる。

エラーハンドリングの範囲

エラーハンドリングは退屈な作業だ。できればやりたくない。しかし、やらないとプロジェクトはすぐに破綻するだろう。 エラーハンドリングの精度はプロジェクト全体の進捗に大きく関わっていると思う。 そこで問題なのが、エラーハンドリングをどのぐらいの範囲で行なうのかということだろう。

その1、行わない

へったくれもないのが、全く行わないという選択だ。つまり、ロギングを諦めてスタックトレースを吐き出させるがままという状態だ。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def main():
    raise RuntimeError('runtime error')

if __name__ == '__main__':
    main()

書き捨てのプログラムではよくある選択だと思う。

その2、一連の処理をまとめる

例えば関数を定義した場合、その関数内で例外を捕捉できるようにする。 関数内の処理をグループ化し、そのグループからの例外を補足する。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def f1():
    pass

def f2():
    raise RuntimeError('failed')

def main():
    try:
        """f1, f2はグループ化されている。
        """
        f1()
        f2()
    except Exception as e:
        print(e)

if __name__ == '__main__':
    main()

得られる出力はfailedだが、これだと何に失敗したのかよくわからない。一体お前は何にしくじったんだ?そもそもお前は一体誰だ?ふざけるな!所属を述べろこの醜いペテン師野郎!

それでは例外に自分の所属を書くことにしよう。そうすれば開発者は癇癪を起こさない。

def f2():
    # f2に失敗した
    raise RuntimeError('failed to f2')

しかしf2の処理はこれだけではない。内容的にはもっと量がある。つまり、それだけ発生しうる例外も増える。そうなると必要な所属も当然増えて……

def f2():
    # process 1
    if 0:
        # f2に失敗した。プロセス1に失敗した。
        raise RuntimeError('failed to f2. failed to process 1')

    # process 2
    if 1:
        # f2に失敗した。プロセス2に失敗した。
        raise RuntimeError('failed to f2. failed to process 2')

この野郎!一体何度failed to f2を書かせたら気が済むんだ!いい加減にしろ! これには開発者も怒りのキーボードクラッシュ。それはそうだろう。こんなに何度もfailed to f2を書きたくはない。それが心情である。

そこで我々の脳裏に浮かぶのが「共通化」という悪魔の3文字である。悪魔の助言に従って共通化を行なうと……

def f2():
    header = 'failed to f2' # エラー出力の共通化

    # process 1
    if 0:
        raise RuntimeError('{0}. failed to process 1'.format(header))

    # process 2
    if 1:
        raise RuntimeError('{0}. failed to process 2'.format(header))

なんだろうこれは。少なくとも私はエラーハンドリングでわざわざそれ専用の変数などは定義したくない。よってこの共通化の選択肢もない。実は過去に採用したケースがあるがそれはここだけの秘密である……。

その3、呼び出しごとに行なう

「その2」で所属の問題があったが、呼び出されたものが所属を述べるのではなく、呼び出したものが所属を述べる方法に変更すると、この問題はすんなりと解決できる。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def f1():
    pass

def f2():
    # process 1
    if 0:
        raise RuntimeError('failed to process 1')

    # process 2
    if 1:
        raise RuntimeError('failed to process 2')

def main():
    try:
        f1()
    except Exception as e:
        print('failed to f1.', e)
        return

    try:
        f2()
    except Exception as e:
        print('failed to f2.', e)
        return

if __name__ == '__main__':
    main()

呼び出したmainが所属を述べることでf2は自身の処理内容についての記述だけになってスッキリする。 開発者のストレスとしては、関数名が変更されると所属の記述も変更が必要になるということだが、それぐらいは開発者は我慢できるだろう(多分)。 この方法はもちろん再帰的に適用できる。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def f1():
    pass

def f2():
    # process 1
    if 0:
        raise RuntimeError('failed to process 1')

    # process 2
    if 1:
        raise RuntimeError('failed to process 2')

def f3():
    try:
        f2()
    except Exception as e:
        raise RuntimeError('failed to f2. ' + str(e))

def main():
    try:
        f1()
    except Exception as e:
        print('failed to f1.', e)
        return

    try:
        f3()
    except Exception as e:
        print('failed to f3.', e)
        return

if __name__ == '__main__':
    main()

f3が追加され、f3内でf2が呼び出されているが、mainの形には影響がない。出力は

$ python3 main.py 
failed to f3. failed to f2. failed to process 2

が得られるが、この出力からエラー内容は十分に把握できる。そのままログに残しても問題がなさそうだ。

その4、複合

しかし、実際にはそんなチマチマとハンドリングはできない。呼び出しごとにハンドリングするなら、例えばリストの添字参照などもハンドリングしなければならない。果てしなく面倒臭い。

    li = []
    try:
        li[100]
    except IndexError as e:
        print(e)

そこで、原則としては呼び出しごとにハンドリングを行うが、グループ化できる範囲はグループ化してハンドリングを行なう……という、妥協案が生まれる。「その2」と「その3」の複合である。

def f1():
    try:
        # 一連の処理をグループ化する
        # start a list work
        li = []
        li.append(1)
        li[0], li[1] = li[1], li[0]
    except Exception as e:
        # 所属はグループ名になる
        raise RuntimeError('failed to a list work. ' + str(e))

f1を上記に変更した場合、出力は

$ python3 main.py 
failed to f1. failed to a list work. list index out of range

になる。グループ化によって特定のエラーが抽象化されてしまう。「list index out of range」がグループ内で発生したことはわかるが、グループ内のどこで発生したのか?という疑問が生まれてしまう。このことから、グループ化の範囲は小さく留めたほうが良いということがわかる。 グループ化を行なう場合、その範囲を大きくしてしまったらきっと悲惨なことになるだろう。エラーハンドリングは行えたが、そのエラー内容の推理が必要になってしまうからだ。つらい。

グループ化を避けるためには、処理をメソッドや関数に分割し、メソッドや関数自体がグループになるように努めることだが、それも言わばメソッド名、関数名というグループに属していることになるので、同様の問題がついてくる。やはり呼び出しごとにハンドリングを行なうのがベターであり、グループ化はその過渡期と見るのが正しいように思える。かといって、際限なく細分化を行なうのも現実的ではないように思えるので、開発者のバランス感覚が必要になるのだと思う。