Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

【Techの道も一歩から】第43回「競技プログラミングやアルゴリズム練習サイト向けのPythonでの練習環境を整える」

f:id:kanjirz50:20190104142720j:plain

こんにちは。 DSOC R&D グループの高橋寛治です。

お昼休みに、社内制度Geek Seek Workshops*1を活用し懇親会代補助を受けて、ご飯を食べアルゴリズム勉強会を行っています。 そこでは、競技プログラミングサイトやプログラミングサイトを利用し問題を解いています。 これらのサイトでは、標準入出力で検証が行われます。 オンラインのエディタも用意されているのですが、手元の環境でコードを書き手軽に検証したいですので、環境を整えます。

コードは、GitHubで公開しています。

目標

  • 任意の環境でPythonコードが書けること
  • python -m unittest tests/tests_xxxx.py のように手軽にテストできること
  • 書いたPythonコードはそのまま提出できること

つくる

シンプルなパッケージ化

以下の構成でパッケージ化します。

.
├── README.md
├── algorithm_training
│   ├── __init__.py
│   └── sample.py
├── poetry.lock
├── pyproject.toml
└── tests
    ├── __init__.py
    ├── base.py
    └── tests_sample.py

標準入出力を検証可能にする

標準入出力を検証できるように、テストと実際のコードを書きます。

テストする際に標準入出力を奪う処理をベースクラスとしてbase.pyに書きます。 こちらの記事を参考にしました。

import contextlib
import unittest


class redirect_stdin(contextlib._RedirectStream):
    _stream = "stdin"


class BaseInputTests(unittest.TestCase):
    def _run(self, input_text, output_text, task):
        from io import StringIO
        buf_stdin = StringIO()
        buf_stdout = StringIO()

        buf_stdin.write(input_text)
        buf_stdin.seek(0)

        with redirect_stdin(buf_stdin), contextlib.redirect_stdout(buf_stdout):
            task.run()

        actual = buf_stdout.getvalue()
        expected = output_text
        self.assertEqual(actual, expected)

このBaseInputTestsを継承したクラスで、_runを実行することで標準入出力を評価します。 与えられるtaskrunというメソッドを持つことを前提にしています。

例題で検証する

次の例題を考えます。 空白区切りで2つの数値(N, M)が標準入力として入力されます。 結果は、2つの数値の和(N+M)が標準出力されることを求めます。

BaseInputTestsを継承したテストケースです。 入力と出力を用意し、_runメソッドに与えます。

from algorithm_training.sample import Task
from .base import BaseInputTests


class InputTests(BaseInputTests):
    def test_1(self):
        input_text = """4 2"""
        expected = "6\n"
        self._run(input_text, expected, Task())

    def test_2(self):
        input_text = """2 3"""
        expected = "5\n"
        self._run(input_text, expected, Task())

次に、アルゴリズムを実装するコード(sample.py)の準備です。 runメソッドを持ち、標準入出力を処理するように書きます。

class Task:
    def run(self):
        N, M = map(int, input().split())
        print(N + M)


if __name__ == "__main__":
    task = Task()
    task.run()

if __name__ == "__main__": 以下は、サイトに提出時に実行されるために必要です。 つまり、提出時にはコピペするだけで良いです。

これで準備は完了ですので、テストを実行します。

python -m unittest tests/tests_sample.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

無事テストが通りました。

自分でテストケースを追加したり、テストが通らない場合にコードを修正したりと試行錯誤ができるようになりました。

競技プログラミングやアルゴリズム練習サイトでの活用法

次の流れで利用します。

  • algorithm_training配下に、<サービス名>_<課題番号>.py のように課題ごとにファイルを作成する
  • 上記ファイルに対応するテストを作成する
  • テストに、テストケースを作成する
  • テストが通るまで、試行錯誤する
  • ファイルを提出する

課題ごとに、提出するためのコードが記述されたファイルとテストの2つが増えていきます。

試行錯誤しやすい環境を作る

MLOpsと言い続けていることもあり、試行錯誤をいかに繰り返しやすくするかということに興味が移っています。 競技プログラミングやアルゴリズムについては、MLとは異なりますが、試行錯誤できることが重要です。

目標に掲げていたことがどうなったかをまとめます。

  • 任意の環境でPythonコードが書けること→Pythonコードが動かせる環境なら何でも良い
  • python -m unittest tests/tests_xxxx.py のように手軽にテストできること→できる
  • 書いたPythonコードはそのまま提出できること→Taskを記述したコードをコピペすると提出できる

パッケージ化やテストといった方法と、標準入出力の乗っ取りにより、プログラミング・アルゴリズム練習サイト向けの試行錯誤しやすい環境を作成しました。

執筆者プロフィール

高橋寛治 Sansan株式会社 DSOC (Data Strategy & Operation Center) R&Dグループ研究員

阿南工業高等専門学校卒業後に、長岡技術科学大学に編入学。同大学大学院電気電子情報工学専攻修了。在学中は、自然言語処理の研究に取り組み、解析ツールの開発や機械翻訳に関連する研究を行う。大学院を卒業後、2017年にSansan株式会社に入社。キーワード抽出など自然言語処理を生かした研究に取り組む。

▼執筆者による連載記事はこちら

buildersbox.corp-sansan.com

*1:社内制度についてはこちらもご覧ください。

© Sansan, Inc.