DEVGRU

プログラミングと競馬予想について書きます

エンジニア&エンジニアになりたい人におすすめの会社テックブログ

エンジニア&エンジニアになりたい人におすすめする会社テックブログ一覧です。

広告


株式会社テラスカイ のテックブログです。

Salesforce ネタが多めです。


株式会社スマートスタイル のテックブログです。

DB ネタが多めです。


クラスメソッド株式会社 のテックブログです。

クラウド、主にAWS系を中心に幅広い記事があります。


Retty株式会社 のテックブログです。

クラウド、データ分析、モバイルアプリの他、開発組織についての記事もあります。


株式会社メルカリ のテックブログです。

イベント開催報告が積極的に行われています。


株式会社リクルート のテックブログです。

複数あるリクルートのTECHBLOGの記事一覧です。


フューチャー株式会社 のテックブログです。

クラウドやIoTの他、登壇記事があります。


株式会社 エム・フィールド のテックブログです。

クラウド、モバイル他、組織やEC分野のトレンドなどの記事があります。


株式会社エウレカ のテックブログです。

AWS, Goの記事多めです。


KCCSモバイルエンジニアリング株式会社 のテックブログです。

Tableau の記事が多いです。


以上です。

neologdn が使えない (Python 3.8では)ので unicodedata.normalize() を使ったが、やっぱり使えた話

データの名寄せに必要な正規化で同僚から neologdn を進められて使おうとしたが、 Python 3.8 に対応していなくて、 unicodedata.normalize() で事足りたけど、3日前に対応していたお話です。

広告

ユーザの入力値とデータベースを照合して最もマッチする値を取得する実装が必要になり、同僚が neologdn ( PyPI) を教えてくれたので、さっそうと pip install neologdn したところ、見事に使えなかった。

$ pip install neologdn
...(なにか大量のコンパイルエラー)

検索して出てくる記事や、 PyPIの配布物 を見たところ、Python 3.7 までのビルド済みパッケージしかなく、 Python 3.8 でビルドが走るがエラーになる、といった具合のようだった。

ユーザの入力値はせいぜいカタカナひらがな数字の半角全角程度の変換だったので、unicodedata.normalize() で済ませた。

>>> unicodedata.normalize("NFKC", "あいうえお")
'あいうえお'
>>> unicodedata.normalize("NFKC", "アイウエオ")
'アイウエオ'
>>> unicodedata.normalize("NFKC", "12345.6")
'12345.6'
>>> unicodedata.normalize("NFKC", "㈱㈲")
'(株)(有)'

あとはレーベンシュタイン距離でデータベースに入っている値との類似度で並べ替えることで、無事タスクを完了することができた。

from unicodedata import normalize
from Levenshtein import distance

onigiri_db = ["ウメボシ", "タラコ", "オカカ"]


def lookup_onigiri(input_):
    with_similarities = [
        (
            onigiri,
            distance(onigiri, normalize("NFKC", input_))
            / max(len(onigiri), len(input_)),
        )
        for onigiri in onigiri_db
    ]

    for onigiri, similarity in sorted(with_similarities, key=lambda elem: elem[1]):
        print(f"{onigiri}\t{similarity:.4}")


if __name__ == "__main__":
    import sys

    if len(sys.argv) >= 2:
        lookup_onigiri(sys.argv[1])
    else:
        print(
            f"""
Usage:
    {sys.argv[0]} おにぎりの具
""".lstrip()
        )
$ python similarity.py ウメ
ウメボシ    0.5
タラコ   1.0
オカカ   1.0

そしてこの記事に顛末を書いている最中、 pip install neologdn のコンパイルエラーをもう一度拝もうとしたところ、インストールが無事終了してしまった。

pip install neologdn
Collecting neologdn
  Using cached https://files.pythonhosted.org/packages/12/46/0bb6c64ff8b9c549a3fbdff68240155fb5f938a2563ce5396278973919f0/neologdn-0.5.1.tar.gz
Installing collected packages: neologdn
  Running setup.py install for neologdn ... done
Successfully installed neologdn-0.5.1

はて…? と思って PyPI を見たところ、なんと3日前に更新されていたようだ。しかも2年と少しぶりに。

PyPI のスクリーンショット
https://pypi.org/project/neologdn/#history のスクリーンショット(2021/05/05)

GitHub のスクリーンショット (2021/05/05)
GitHub のスクリーンショット (2021/05/05)

タスクが 2021/05/02 の日中で、PyPIの更新日時が "2021-05-02 20:44:14" 1 なので、半日ほどのすれ違いだったようだ。なんと…。

自身のタスクには間に合わなかったが、同僚が別のタスクで使用する予定だったので非常にタイミングが良かった。

Pythonで動かして学ぶ 自然言語処理入門

Pythonで動かして学ぶ 自然言語処理入門

現場で使える!Python自然言語処理入門

現場で使える!Python自然言語処理入門


  1. スクリーンショットで示した PyPI の日付にマウスオーバーすると表示される

pytest と unittest.mock を使って標準出力のテストを書けなかった話

今回は、標準出力に文字列を出力する実装に対して pytest でテストを書く必要があり、 unittest.mock と Python のライブラリリファレンスにかかれていた方法を組み合わせたら見事にハマったお話です。

広告

シンプルにするとこんな感じに、標準出力への文字列が唯一の副作用となる関数があります。

def hello(name=None):
    if name:
        print(f"Hello, {name}")
    else:
        print("Hello")

if __name__ == "__main__":
    import sys
    
    if len(sys.argv) >= 2:
       name = sys.argv[1]
       hello(name)
    else:
       hello()

コマンドラインから実行するとこのように引数の有無で出力が異なる内容になります。

$ python hello.py
Hello

$ python hello.py John
Hello, John

テストのお作法としては、文字列と出力を分けることでテストしやすくするのが筋ですが、それができないこともたまにあります 1

def generate_message(name=None):
    if name:
        return f"Hello, {name}"
    else:
        return "Hello"

def hello(name):
      print(generate_message(name))

また、上記の例なら単純に doctest で良いのですが、もう少し複雑だったりするとそうも言っていられません。

Python のリファレンスマニュアルに標準出力のモック方法が書いてあったので、それにならって pytest でテストを書いてみました。

from hello import hello
import pytest
from unittest.mock import patch
from io import StringIO

@pytest.fixture()
def mock_stdout():
    with patch("sys.stdout", new_callable=StringIO) as m:
        yield m

def test_hello_without_name(mock_stdout):
    hello()
    assert mock_stdout.getvalue() == "Hello\n"

def test_hello_with_name(mock_stdout):
    hello("John")
    assert mock_stdout.getvalue()  == "Hello, John\n"

テストのデバッグをするためにいつもの -s をつけて実行してみると、きちんと通ります。

$ pytest -s test_hello.py
=========================================================================================================== test session starts ===========================================================================================================
platform darwin -- Python 3.8.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /Users/devgru/work/blog-sandbox/2021-05-05T21:20:52
collected 2 items

test_hello.py ..

============================================================================================================ 2 passed in 0.10s ============================================================================================================

しかし、 PR を作って CI でも動作することを確認したら、何故か落ちます。

$ pytest test_hello.py
=========================================================================================================== test session starts ===========================================================================================================
platform darwin -- Python 3.8.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /Users/devgru/work/blog-sandbox/2021-05-05T21:20:52
collected 2 items

test_hello.py FF                                                                                                                                                                                                                    [100%]

================================================================================================================ FAILURES =================================================================================================================
_________________________________________________________________________________________________________ test_hello_without_name _________________________________________________________________________________________________________

mock_stdout = <_io.StringIO object at 0x11104f9d0>

    def test_hello_without_name(mock_stdout):
        hello()
>       assert mock_stdout.getvalue() == "Hello\n"
E       AssertionError: assert '' == 'Hello\n'
E         - Hello

test_hello.py:13: AssertionError
---------------------------------------------------------------------------------------------------------- Captured stdout call -----------------------------------------------------------------------------------------------------------
Hello
__________________________________________________________________________________________________________ test_hello_with_name ___________________________________________________________________________________________________________

mock_stdout = <_io.StringIO object at 0x110b205e0>

    def test_hello_with_name(mock_stdout):
        hello("John")
>       assert mock_stdout.getvalue()  == "Hello, John\n"
E       AssertionError: assert '' == 'Hello, John\n'
E         - Hello, John

test_hello.py:17: AssertionError
---------------------------------------------------------------------------------------------------------- Captured stdout call -----------------------------------------------------------------------------------------------------------
Hello, John
========================================================================================================= short test summary info =========================================================================================================
FAILED test_hello.py::test_hello_without_name - AssertionError: assert '' == 'Hello\n'
FAILED test_hello.py::test_hello_with_name - AssertionError: assert '' == 'Hello, John\n'
============================================================================================================ 2 failed in 0.16s ============================================================================================================

どうやら、unittest.mock.patch() で差し替えた StringIO() に値が書き込まれていないようです。

-s オプションの有無で挙動が変わるということは、おそらく pytest 自体がモックをしているために、その後に patch しても届かないと予想されます。

散々悩んだ挙げ句、 python pytest mock stdout でググったところ、テックブログ界の雄こと Developers IO 2記事が見つかりました。

曰く、 capfd を用いるのが良いとのこと。

from hello import hello

def test_hello_without_name(capfd):
    hello()
    captured = capfd.readouterr()
    assert captured.out == "Hello\n"

def test_hello_with_name(capfd):
    hello("John")
    captured = capfd.readouterr()
    assert captured.out == "Hello, John\n"  
pytest test_hello2.py
=========================================================================================================== test session starts ===========================================================================================================
platform darwin -- Python 3.8.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /Users/devgru/work/blog-sandbox/2021-05-05T21:20:52
collected 2 items

test_hello2.py ..                                                                                                                                                                                                                   [100%]

============================================================================================================ 2 passed in 0.03s ============================================================================================================

今度は無事通りました。

テスト駆動Python

テスト駆動Python

  • 作者:BrianOkken
  • 発売日: 2018/08/29
  • メディア: Kindle版

テスト駆動開発

テスト駆動開発


  1. 納期とか、他人の書いたコードだったりとか。

  2. 最近は何を検索してもこのブログの記事が出てくるような気がしている。