tachitomonn’s blog

IT技術関連の学習メモがメインでたまに趣味のこととか

Python Flask の基本

Python の軽量ウェブアプリケーションフレームワーク Flask の基本学習メモ。
公式ドキュメント
Welcome to Flask — Flask Documentation (1.1.x)
を元にお勉強。

使った環境はWin10およびPython3.7です。

インストール

venv でつくった仮想環境上で pip でインストールします。

>pip install Flask

Flask アプリケーションを動かす

アプリケーションを動かすには flask コマンドまたは python の -m スイッチを使う。
環境変数 FLASK_APP を設定して動かうアプリケーションを教えておく必要がある。

C:\selfstudy>set FLASK_APP=study_flask.py
(selfstudy) c:\selfstudy>flask run
 * Serving Flask app "study_flask.py"
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

もしくは

(selfstudy) c:\selfstudy>python -m flask run
 * Serving Flask app "study_flask.py"
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

http://127.0.0.1:5000/にブラウザからアクセスして確認することができる。
デバッグモードで起動するには環境変数 FLASK_ENV に development を指定してからサーバを起動する。
デバッグモードでは

  • デバッガーを使える
  • 自動リロードが使える

ので開発中は非常に便利です。

ルーティング

route()デコレーターを使って関数とURLの紐づけを行う。

可変URL

URL中にとしておくと関数側で任意の変数名で値を受け取ることができる。
で特定の型へコンバートして受け取ることもできる。

URL末尾のスラッシュの取り扱い

末尾のスラッシュを含めたURL指定をするとファイルシステム上のディレクトリへのアクセスのように扱われ、末尾スラッシュなしでアクセスしても末尾のスラッシュを付加してリダイレクトしてくれる。
一方、末尾のスラッシュなしでURL指定すると特定ファイルリソースへのアクセスのように扱われ、末尾スラッシュを付けてのアクセスは404エラーとなる。

URLのビルド

特定の関数からURLを逆引きするには url_for()関数を使う。
第1引数には関数名を受け付けて、オプションでキーワード引数を受け付ける(可変URL部分の変数名)。
未定義の変数を指定するとURLのクエリパラメータとして付加される。

HTTP メソッドのハンドリング

デフォルトではGETリクエストに対して応答するが、route()デコレーターのキーワード引数 methods で他のメソッドも扱うことが可能。

静的ファイル

static という名前のディレクトリを作ってアプリケーションのパッケージ内に置くか、モジュールと同ディレクリに置く。
アプリケーションからは /static で利用できる。
urlビルディングには特別な 'static' エンドポイント名を使う。

レンダリングテンプレート

Flask は Jinja2 テンプレートエンジンを使う。
render_template() メソッドにテンプレート名とテンプレートエンジンに渡したい変数をキーワード引数で渡す。
templates ディレクトリからテンプレートを探すのでアプリケーションのパッケージ内に置くか、モジュールと同ディレクリに置く。
テンプレート中では request, session, g といったオブジェクトにアクセス可能。
テンプレートは継承可能。

リクエストデータへのアクセス

request オブジェクトを通じてアクセスする。
request.method でリクエストメソッドに、 request.form でフォームデータへアクセスできる。
request.args から辞書ライクにURLパラメータの値へアクセスできる。
request.files から辞書ライクにアップロードファイルへアクセスできる。
アクセスしたファイルオブジェクトは save() メソッドで保存できる。
request.cookies から辞書ライクにクッキーへアクセスできる。

リダイレクトとエラー

リダイレクトには redirect() 関数を使う。
エラーコードとともにリクエストを中止させるには abort() 関数を使う。
デフォルトではエラーコードに応じた白黒のエラーページが表示される。
エラーページをカスタマイズするには errorhandler() デコレーターを使う。

JSONを提供するAPI

辞書を返すビューを作ればJSONレスポンスに変換してくれる。

ここまでで使ったスクリプトとテンプレート

#! /usr/bin/env python
# -*- coding: utf8 -*-

from flask import Flask, url_for, request, render_template, abort, redirect
from markupsafe import escape

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "Hello, World!"

@app.route("/bye")
def bye():
    return "Bye!"

@app.route("/user/<username>")
def show_user(username):
    return "User {username}".format(username=escape(username))

@app.route("/user_id/<int:user_id>")
def show_user_id(user_id):
    return "User ID {user_id:d}".format(user_id=user_id)

@app.route("/path/<path:subpath>")
def show_subpath(subpath):
    return "Subpath {subpath}".format(subpath=escape(subpath))

@app.route("/top/")
def top():
    return "The top page"

@app.route("/about")
def about():
    return "The about page"

@app.route("/url_building")
def show_url_for_result():
    result1 = url_for("hello_world")
    result2 = url_for("bye")
    result3 = url_for("show_user", username="test user")
    result4 = url_for("show_user", username="test user", user_id=1234)
    return "{}<br>{}<br>{}<br>{}<br>".format(
            result1, result2, result3, result4
            )

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        return "POST"
    else:
        return "GET"

@app.route("/css")
def show_css():
    return url_for("static", filename="style.css")

@app.route("/hello/")
@app.route("/hello/<name>")
def hello(name=None):
    return render_template("hello.html", name=name)

@app.route("/redirect")
def redirect_exsample():
    return redirect(url_for("redirect_to"))

@app.route("/redirect_result")
def redirect_to():
    abort(401)
    print("this is never executed")

@app.route("/redirect2")
def redirect_exsample2():
    return redirect(url_for("redirect_to2"))

@app.route("/redirect_result2")
def redirect_to2():
    abort(404)

@app.errorhandler(404)
def page_not_found(error):
    return "error page! 404!", 404

@app.route("/api_sample")
def sample_api():
    return {
            "key1":"value1",
            "key2":"value2",
            "key3":"value3",
            }
<!doctype html>
<title>Hello from Flask</title>
{% if name %}
  <h1>Hello {{ name }}!</h1>
{% else %}
  <h1>Hello, World!</h1>
{% endif %}

セッション

標準のセッション管理はクッキーを用いて実装されている。
このセッション管理をを使うには秘密鍵をセットする必要がある。
秘密鍵はできる限りランダムにするのが望ましい。
例えば os.urandom(16) などで作成するのも手。
標準のセッション管理はクライアント側で行うことになる。
サーバー側でセッション管理を行う場合はいくつかの Flask extension があるので使ってみるのも手。

セッションで使ったスクリプト

#! /usr/bin/env python
# -*- conding: utf8 -*-

from flask import Flask, session, redirect, url_for, request
from markupsafe import escape

app = Flask(__name__)

app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'

@app.route("/")
def index():
    if "username" in session:
        return "Logged in as {}".format(escape(session["username"]))
    return "You are not logged in"

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        session["username"] = request.form["username"]
        return redirect(url_for("index"))
    return """
    <form method="POST">
        <p><input type=text name=username>
        <p><input type=submit value=Login>
    </form>
    """

@app.route("/logout")
def logout():
    session.pop("username", None)
    return redirect(url_for("index"))

Python でGUI tkinter

PythonGUI の古典的定番の tkinter ライブラリを触ってみます。
まあ、ほとんど写経ですけど。

参考にさせていただいたのはこちら。
Pythonで簡単なGUIを作れる「Tkinter」を使おう - Qiita
tkinter --- Tcl/Tk の Python インタフェース — Python 3.8.2 ドキュメント

今回の環境では標準ライブラリに含まれているのでインストールは不要です。
コマンドラインから以下のコマンドを実行して Tk インターフェースを表示するウィンドウが開けば使用できる Tcl/Tk がどのバーションなのかがわかります。

>py -3 -m tkinter

今回はversion8.6でした。

とりあえずやってみましただけのスクリプトその1。
テキストとボタンがあってボタンをクリックするとボタンの表記が変わるだけのもの。

#! /usr/bin/env python
# -*- coding: utf8 -*-

import tkinter as tk

def submit(b):
    """ボタンが押されたら実行される
    """
    b["text"] = "submited"

root = tk.Tk() #トップレベルウィジェット(メインウィンドウ)
root.title("sample app") #メインウィンドウのタイトルを変更
root.geometry("640x480") #メインウィンドウサイズの変更
label = tk.Label(root, text="Hi!") #テキストラベルを生成
label.grid() #生成したテキストラベルを表示
button = tk.Button(root, text="submit", command=lambda: submit(button)) #ボタンを生成
button.grid() #生成したボタンを表示
root.mainloop() #メインウィンドウを表示して無限ループ

メインウィンドウを作ってそこに必要なウィジェットを定義、表示していく。最後におまじない。

とりあえずやってみましただけのスクリプトその2。
公式ドキュメントを参考にして同じことをクラスを使用して書いてみた。

#! /usr/bin/env python
# -*- coding: utf8 -*-

import tkinter as tk

class Application(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.master = master
        self.master.title("sample app")
        self.master.geometry("640x480")
        self.pack()
        self.create_widgets()

    def submit(self):
        self.button["text"] = "submited"
    
    def create_widgets(self):
        self.label = tk.Label(self, text="Hi!")
        self.label.pack()
        self.button = tk.Button(self, text="submit")
        self.button["command"] = self.submit
        self.button.pack()

if __name__=="__main__":
    root = tk.Tk()
    app = Application(master=root)
    app.mainloop()

何かちゃんとしたもの作るならこっちの書き方でしょうね。

Python の argparseモジュール

Python の標準ライブラリに含まれるコマンドライン引数の解析モジュール argparse を公式ドキュメントのチュートリアルでお勉強。
Argparse チュートリアル — Python 3.8.2rc1 ドキュメント

基本

#! /usr/bin/env python

import argparse

parser = argparse.ArgumentParser()
parser.parse_args()

ArgumentParser のインスタンスを作成、 parse_argsメソッドでコマンドライン引数をパースするという理解で良いのかな。
このスクリプトを実行してみる。

>py -3 study_argparse.py

>py -3 study_argparse.py --help
usage: study_argparse.py [-h]

optional arguments:
  -h, --help  show this help message and exit

>py -3 study_argparse.py -v
usage: study_argparse.py [-h]
study_argparse.py: error: unrecognized arguments: -v

コマンドライン引数を与えないと何も表示されない。
ヘルプオプションは特に何も設定しなくても利用できる。
設定していないコマンドライン引数を与えると怒られる。

位置引数

#! /usr/bin/env python

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("positional", help="positional argument sample")
args = parser.parse_args()
print(args.positional)

実行すると、

>py -3 study_argparse.py
usage: study_argparse.py [-h] positional
study_argparse.py: error: the following arguments are required: positional

>py -3 study_argparse.py -h
usage: study_argparse.py [-h] positional

positional arguments:
  positional  positional argument sample

optional arguments:
  -h, --help  show this help message and exit

>py -3 study_argparse.py Hello!
Hello!

add_argumentメソッドで受け付けるコマンドライン引数を追加できる。
キーワード引数 help で当該引数の説明も書ける。
属性名のように追加引数を参照できる。

コマンドライン引数の型の指定もできる。デフォルトでは与えられた引数は文字列として扱われる。

#! /usr/bin/env python

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("positional", help="positional argument sample")
parser.add_argument("int_positional", help="positional argument sample", type=int)
args = parser.parse_args()
print(args.positional)
print(args.int_positional*10)

実行すると、

>py -3 study_argparse.py Hello! 1
Hello!
10

>py -3 study_argparse.py Hello! Hi!
usage: study_argparse.py [-h] positional int_positional
study_argparse.py: error: argument int_positional: invalid int value: 'Hi!'

オプション引数

#! /usr/bin/env python

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--optional", help="optional argument sample")
parser.add_argument("--optional_flg", help="optional argument sample", action="store_true")
args = parser.parse_args()
if args.optional:
    print("optional arg={}".format(args.optional))
if args.optional_flg:
    print("Bye!")

実行すると、

>py -3 study_argparse.py

>py -3 study_argparse.py -h
usage: study_argparse.py [-h] [--optional OPTIONAL] [--optional_flg]

optional arguments:
  -h, --help           show this help message and exit
  --optional OPTIONAL  optional argument sample
  --optional_flg       optional argument sample

>py -3 study_argparse.py --optional Hello! --optional_flg
optional arg=Hello!
Bye!

>py -3 study_argparse.py --optional Hello! --optional_flg Bye!
usage: study_argparse.py [-h] [--optional OPTIONAL] [--optional_flg]
study_argparse.py: error: unrecognized arguments: Bye!

「--」 を頭に付けるとオプション引数になる
キーワード引数 action に store_true を指定すると、自動的に True がセットされ、オプションが指定されなければ False がセットされることにより、該当オプションをフラグ化できる
フラグ化したオプションに値を指定すると怒られる

短いオプションも設定できる

#! /usr/bin/env python

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--optional", help="optional argument sample")
parser.add_argument("--optional_flg", help="optional argument sample", action="store_true")
parser.add_argument("-s", "--short_optional", help="optional argument sample", action="store_true")
args = parser.parse_args()
if args.optional:
    print("optional arg={}".format(args.optional))
if args.optional_flg:
    print("Bye!")
if args.short_optional:
    print("Yeah!")

実行すると、

>py -3 study_argparse.py -h
usage: study_argparse.py [-h] [--optional OPTIONAL] [--optional_flg] [-s]

optional arguments:
  -h, --help            show this help message and exit
  --optional OPTIONAL   optional argument sample
  --optional_flg        optional argument sample
  -s, --short_optional  optional argument sample

>py -3 study_argparse.py -s
Yeah!

位置引数とオプション引数の併用

#! /usr/bin/env python

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("positional", help="positional argument sample", type=int, choices=[1,2])
parser.add_argument("-s", "--short_optional", help="optional argument sample", action="store_true")
parser.add_argument("-d", "--default_optional", help="optional argument sample", default="Bye")
args = parser.parse_args()
print(args.positional*10)
if args.short_optional:
    print("Yeah!")
if args.default_optional:
    print(args.default_optional)

実行すると、

>py -3 study_argparse.py -h
usage: study_argparse.py [-h] [-s] [-d DEFAULT_OPTIONAL] {1,2}

positional arguments:
  {1,2}                 positional argument sample

optional arguments:
  -h, --help            show this help message and exit
  -s, --short_optional  optional argument sample
  -d DEFAULT_OPTIONAL, --default_optional DEFAULT_OPTIONAL
                        optional argument sample

>py -3 study_argparse.py -s 1
10
Yeah!
Bye

>py -3 study_argparse.py -s 2
20
Yeah!
Bye

>py -3 study_argparse.py -s 3
usage: study_argparse.py [-h] [-s] [-d DEFAULT_OPTIONAL] {1,2}
study_argparse.py: error: argument positional: invalid choice: 3 (choose from 1, 2)

>py -3 study_argparse.py -s -d Hello 1
10
Yeah!
Hello

併用は当然できる
キーワード引数 choices で受け付ける値を制限できる
キーワード引数 default でデフォルト値を設定できる

競合するオプションの指定

#! /usr/bin/env python

import argparse

parser = argparse.ArgumentParser(description="sample")
group = parser.add_mutually_exclusive_group()
group.add_argument("-r", "--right", help="optional argument sample", action="store_true")
group.add_argument("-l", "--left", help="optional argument sample", action="store_true")
args = parser.parse_args()
if args.right:
    print("Right")
elif args.left:
    print("Left")
else:
    print("Right? Left?")

parser.add_mutually_exclusive_groupメソッドで競合オプション用のグループを作って、そこに引数を追加する
あと、 ArgumentParser のインスタンス作成時にキーワード引数 description を使ってプログラムの説明を書ける
実行すると、

>py -3 study_argparse.py -h
usage: study_argparse.py [-h] [-r | -l]

sample

optional arguments:
  -h, --help   show this help message and exit
  -r, --right  optional argument sample
  -l, --left   optional argument sample

>py -3 study_argparse.py -r
Right

>py -3 study_argparse.py -l
Left

>py -3 study_argparse.py
Right? Left?

>py -3 study_argparse.py -r -l
usage: study_argparse.py [-h] [-r | -l]
study_argparse.py: error: argument -l/--left: not allowed with argument -r/--right

Python の fileinputモジュール

複数のファイルにまたがる処理を行うfileinputモジュールについてメモ。
参考はこちら。

fileinput --- 複数の入力ストリームをまたいだ行の繰り返し処理をサポートする — Python 3.8.2rc1 ドキュメント

このモジュールは標準入力やファイルの並びにまたがるループを素早く書くためのヘルパークラスと関数を提供しています。

以下の2つのファイルに対して使ってみる。

sample1.txt

12345
67890

sample2.txt

abcde
fghij

>>> import fileinput
>>> with fileinput.input(files=("sample1.txt", "sample2.txt")) as f:
...     for line in f:
...         print(line)
...
12345

67890

abcde

fghij

単純に指定したファイルを順番に読み込んで処理を繰り返すってだけなのね。

Python の formatメソッド

長らく Python2 系をメインにしており、文字列のフォーマット化を行う formatメソッドに慣れていないのでメモしながら慣れていきます。
参考はこちら。

組み込み型 — Python 3.8.2rc1 ドキュメント
string --- 一般的な文字列操作 — Python 3.8.2rc1 ドキュメント

基本。メソッドを呼び出す文字列にフォーマット化して置換したい部分を {} で囲んで入れる。置換フィールドはメソッドの引数の順番に対応するようにインデックスを指定できる。

>>> "My name is {0}. I like {1}.".format("tachitomonn", "soccer")
'My name is tachitomonn. I like soccer.'

キーワード引数でも指定できる。

>>> "My name is {name}. I like {sport}.".format(name="tachitomonn", sport="soccer")
'My name is tachitomonn. I like soccer.'

その他、書式指定例。「:」の後に書式指定を付ける。

アラインメントと幅指定。埋める文字が省略された場合はスペース埋めになる。

>>> number = 12345
>>> "{number:<10d}".format(number=number)
'12345     '
>>> "{number:>10d}".format(number=number)
'     12345'
>>> "{number:0>10d}".format(number=number)
'0000012345'

千の位のセパレータのカンマ使用。

>>> "{number:,}".format(number=number)
'12,345'

Python でテスト pytest

nose に引き続き、やはりテストを容易に行う為のフレームワークである pytest の基本を押さえます。
公式ドキュメント
pytest: helps you write better programs — pytest documentation
を参考に基本だけやってみました。

テスト対象の自動探索ができることは nose と同様ですが pytest では検証はシンプルにassert文を使います。
インストールは pip で行います。

pip install pytest

今回はバージョン 5.3.4 が入りました。
インストールが完了すると pytest コマンドが使えるようになります。
前回の nose の勉強で使用したテストスクリプトを pytest 向けに書き直してテストを実行してみます。

test_sample2.py

#! /usr/bin/env python

u"""pytestの勉強用サンプル
"""

from pytest import raises

from study_doctest import heisei2seireki

def test_heisei2seireki():
    assert [heisei2seireki(n) for n in range(1, 32)] == [1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019]

def test_heisei2seireki_exception1():
    with raises(ValueError):
        heisei2seireki(-1)
    
def test_heisei2seireki_exception2():
    with raises(ValueError):
        heisei2seireki(0)

def test_heisei2seireki_exception3():
    with raises(ValueError):
        heisei2seireki(1.5)

def test_heisei2seireki_exception4():
    with raises(ValueError):
        heisei2seireki(32)

テストしてみます。

>pytest -v c:\selfstudy\test_sample2.py
============================= test session starts =============================
platform win32 -- Python 3.7.2, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- c:\venv\selfstudy\scripts\python.exe
cachedir: .pytest_cache
rootdir: c:\
collected 5 items

..\..\selfstudy\test_sample2.py::test_heisei2seireki PASSED              [ 20%]
..\..\selfstudy\test_sample2.py::test_heisei2seireki_exception1 PASSED   [ 40%]
..\..\selfstudy\test_sample2.py::test_heisei2seireki_exception2 PASSED   [ 60%]
..\..\selfstudy\test_sample2.py::test_heisei2seireki_exception3 PASSED   [ 80%]
..\..\selfstudy\test_sample2.py::test_heisei2seireki_exception4 PASSED   [100%]

============================== 5 passed in 0.05s ==============================

シンプルで使いやすいかも。
もう少しきちんと勉強する時は
pytest ヘビー🐍ユーザーへの第一歩 - エムスリーテックブログ
がわかりやすく書いてくださっているので参考にさせていただきます。

Python でテスト nose

前回で標準ライブラリの unittest の基本を押さえたところで今回はより簡単にユニットテストを実行できる nose というライブラリの基本を押さえてみます。

公式ドキュメント
Note to Users — nose 1.3.7 documentation
を参照しながらやってみました。

サードパーティーのライブラリなのでまずはインストール。 pip で入れます。

pip install nose

バージョン 1.3.7 が入りました。

インストールが完了するとテスト対象の自動探索とテスト実行を行ってくれる nosetests コマンドを使えるようになります。
テスト対象には unittest.TestCase のサブクラスも含まれます。試しに前回作成した study_unittest.py をテストしてみます。

>nosetests c:\selfstudy\study_unittest.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

テストが実行されました。 -v オプションで詳細なログを出力してくれます。

>nosetests -v c:\selfstudy\study_unittest.py
test_heisei2seireki (study_unittest.TestSample) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.004s

OK

このように unittest.TestCase のサブクラスで実装したテストケースのテストランナーとしても使えますが、 nose ではテストケースをより容易に関数として書くことができます。またテストをより簡単に書くための関数が用意されているのでテストスクリプトを nose 向けに書き直してみたのがこちら。

test_sample.py

#! /usr/bin/env python

u"""noseの勉強用サンプル
"""

from nose.tools import eq_, raises

from study_doctest import heisei2seireki

def test_heisei2seireki():
    eq_([heisei2seireki(n) for n in range(1, 32)],
            [1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019])

@raises(ValueError)
def test_heisei2seireki_exception1():
    heisei2seireki(-1)
    
@raises(ValueError)
def test_heisei2seireki_exception2():
    heisei2seireki(0)

@raises(ValueError)
def test_heisei2seireki_exception3():
    heisei2seireki(1.5)

@raises(ValueError)
def test_heisei2seireki_exception4():
    heisei2seireki(32)

このスクリプトを配置したディレクトリを対象にテストを実行してみます。 nose は test をファイル名に含むファイルを自動的にテスト対象にしてくれます。

>nosetests -v c:\selfstudy
test_sample.test_heisei2seireki ... ok
test_sample.test_heisei2seireki_exception1 ... ok
test_sample.test_heisei2seireki_exception2 ... ok
test_sample.test_heisei2seireki_exception3 ... ok
test_sample.test_heisei2seireki_exception4 ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.014s

OK

いちいちテストケースをクラス定義しなくても良いのは楽ですね。