きり丸の技術日記

技術検証したり、資格等をここに残していきます。

Pythonで設定ファイルを読み込む処理をラップしてクラスとして扱うと便利

普段Javaで使っている感覚と同じように、Pythonでもプロパティファイルを扱いたかったため、調べた記事です。

環境

  • Python
    • 3.8.10

ゴール

  1. 設定ファイル(.ini)をコード内で読み込めるようにする
  2. 読み込んだ設定ファイルを他クラスからも簡単に扱えるようにする

対応

設定ファイル(.ini)を準備する

設定ファイルを準備します。config.iniという名前の設定ファイルにし、usernamepasswordの2つの変数を持たせます。

[DEFAULT]
username=default_user
password=default_pass

設定ファイル(.ini)をPythonで読み込む

標準ライブラリのconfigparserを使用します。事前に設定しておいたconfig.iniファイルから、usernamepasswordを読み込みます。

import configparser

# 設定ファイルを読み込む準備
config_ini = configparser.ConfigParser()
config_ini.read('config.ini', encoding='utf-8')

# セクションを指定して読み込む
config = config_ini['default']

# getメソッドで変数を読み込む
username = config.get('username')
password = config.get('password')

# ファイルが読み込めていることを標準出力に出力する
print(f"username='{props.username}', password='{props.password}'")
# 「username=default_user, password=default_pass」と出力される

読み込み処理をラップしたクラスを作成する

読み込む処理をラップしているTestPropertiesクラスを作成します。そして、各変数のgetterを作成します。また、変数であることをマークするために@propertyをgetterに付与します。

import configparser

class TestProperties:
    def __init__(self):
        config_ini = configparser.ConfigParser()
        config_ini.read('config.ini', encoding='utf-8')

        self.config = config_ini['default']

    @property
    def username(self):
        return self.config.get('username')

    @property
    def password(self):
        return self.config.get('password')

この状態にしておくことで、TestPropertiesのインスタンスを作成するだけで設定ファイル(.ini)から値を取得しつつ、簡単に変数にアクセスできます。

from TestProperties import TestProperties
props = TestProperties()

print(f"username='{props.username}', password='{props.password}'")

ソースコード

※ 記事にはまとめていませんが、pytestで各テストメソッドにDIできるようにpytest.fixtureを使用しています。

終わりに

思い付くことができれば簡単な話ではありますが、処理をラップしてクラスにすることを考えていなかったせいで、各クラスで毎回設定ファイルを読み込む処理を書いていました。処理自体は特殊なことをしていないのでコピー&ペーストで書けるとはいえ、毎回10行前後の処理が挿入されるので可読性が悪いと感じてはいました。ラップするだけで全部解決できるのは非常によいですね。

Pythonistからするとレベルの低いことをしているとは思いますが、こういう地道なところから改善していろんな言語でも可読性を上げられるようにしたいです。


この記事お役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考記事

JestでParameterizedTestを行う(異なるテストデータで同じ内容のテストを行う)

小ネタ。

Jestを使って、異なるテストデータで同じ内容のテストを行うParameterizedTestをやろうとしたときのメモ。なお、JestではParameterizedTestという名称では表現されていません。

なお、使い方自体は参考情報に乗せている公式を見てください。

環境

  • Jest
    • 27.2

ゴール

  • JestでParameterizedTestを行う

手順

describe.each(table)(name, fn, timeout)構文を使えば、ParameterizedTestを実装できます。たくさんオプションはありますが、公式の使い方で十分でしょう。

注意してほしいのは、Jestの次の書き方です。このObjectでテストデータと期待値を表現する方法は、Jestの27.0より実装されているので、Jestのバージョンが26.Xより前の場合は使えませんので気を付けてください。

// Jestの公式のソースコード。
describe.each([
  {a: 1, b: 1, expected: 2},
  {a: 1, b: 2, expected: 3},
  {a: 2, b: 1, expected: 3},
])('.add($a, $b)', ({a, b, expected}) => {
  test(`returns ${expected}`, () => {
    expect(a + b).toBe(expected);
  });

  test(`returned value not be greater than ${expected}`, () => {
    expect(a + b).not.toBeGreaterThan(expected);
  });

  test(`returned value not be less than ${expected}`, () => {
    expect(a + b).not.toBeLessThan(expected);
  });
});
# 出力されるログ
  .add(1, 1)
    ✓ returns 2
    ✓ returned value not be greater than 2 (1 ms)
    ✓ returned value not be less than 2
  .add(1, 2)
    ✓ returns 3
    ✓ returned value not be greater than 3
    ✓ returned value not be less than 3
  .add(2, 1)
    ✓ returns 3 (1 ms)
    ✓ returned value not be greater than 3
    ✓ returned value not be less than 3

テーブル形式のテストコード

仕様が複雑になると、仕様を一覧化して整理する必要があります。JestではParameterizedTestをテーブル形式で表現することもできます。私は、日付の前後関係や、権限関係をテストコードで整理する際、テーブル形式での表現をよく使用しています。

describe.each`
  a    | b    | expected
  ${1} | ${1} | ${2}
  ${1} | ${2} | ${3}
  ${2} | ${1} | ${3}
`('$a + $b', ({a, b, expected}) => {
  test(`returns ${expected}`, () => {
    expect(a + b).toBe(expected);
  });

  test(`returned value not be greater than ${expected}`, () => {
    expect(a + b).not.toBeGreaterThan(expected);
  });

  test(`returned value not be less than ${expected}`, () => {
    expect(a + b).not.toBeLessThan(expected);
  });
});
  1 + 1
    ✓ returns 2
    ✓ returned value not be greater than 2 (1 ms)
    ✓ returned value not be less than 2 (1 ms)
  1 + 2
    ✓ returns 3
    ✓ returned value not be greater than 3 (1 ms)
    ✓ returned value not be less than 3
  2 + 1
    ✓ returns 3
    ✓ returned value not be greater than 3 (1 ms)
    ✓ returned value not be less than 3

ソースコード

Jest公式をコピペしているだけです。

終わりに

Jestのバージョンが上がっていたことで、JestでのParameterizedTestが便利になりました。

テストの表示名をパラメータ名でマッピングできるようになっているので、失敗したときもエラーメッセージを確認すればInput/Outputを確認しやすいですね。

今後も、テストがしやすいようにいろんなライブラリの知識を勉強していきたいです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考情報

f:id:nainaistar:20210922173625p:plain

TypeScriptでPythonのキーワード引数みたいな挙動をさせる(ROROパターン)

TypeScriptを書いていて、Pythonでいうキーワード引数(名前付き引数)のような挙動をしたいとTwitterに呟いたところ、【公式】皮しばきさん、suinさんに反応していただけました。

その中でsuinさんに教えていただいたデザインパターンのROROパターン(Receive an object, return an object)が自分の期待した動きをしたので、ブログにまとめます。

twitter.com

なお、今回の記事はsuinさんに紹介していただいたmediumの記事をローカルで検証した結果ですので、より詳細なメリットは原文を読んだ方が理解できます。

環境

  • TypeScript
    • 不明
      • PlayWrightのv1.15.0に含まれるバージョン
    • v4.4.3
      • codesandboxで検証したバージョン

ゴール

  • TypeScriptでもPythonのキーワード引数(名前付き引数)に似た挙動をさせる

そもそも、キーワード引数とは

Pythonにはキーワード引数という機能があります。この機能はメソッド呼び出しでパラメータ前に識別できるパラメータ名を一緒に渡すことで、順番に依存せず、メソッドにパラメータを渡せる機能です。

# メソッドの定義
def send_mail(mail_from='example@co.jp', mail_to='example@co.jp', text='テストメール', name='運営'):

# メソッドの呼び出し
send_mail(mail_to='hogehoge@co.jp')
# mail_fromは「exmaple@co.jp」 
# mail_toは「hogehoge@co.jp」
# textは「テストメール」
# nameは「運営」
# のパラメータで渡される

メリットとしては、いくつかあります。

  • 必要な箇所にのみパラメータを渡すことができる
  • パラメータを増やしても呼び出し元に一切影響を与えない
  • Javaでいうオーバーロードせずに表現できる
    • シンプルに表現できる

なお、順番で識別されるパラメータのことをPythonでは位置引数と呼びます。

ROROパターン(Receive an object, return an object)とは

メソッド呼び出し時にobjectでパラメータを渡し、返却値もobjectで返却するデザインパターンです。なお、今回私が感銘を受けたのはメソッド呼び出し時の挙動ですので、返却値の話はしません。

// 既存の呼び出し
send_mail('example@co.jp', 'example@co.jp', 'テストメール', '運営');
// ROROパターンの呼び出し
send_mail({ mail_from='example@co.jp', mail_to='example@co.jp', text='テストメール', name='運営'});
// 既存の呼び出し元
function send_mail(mail_from, mail_to, text, name) {}
// ROROパターンの呼び出し元
function send_mail({ mail_from, mail_to, text, name }) {}

必須チェックと不要な変数チェック

現状のままでは、キーワード引数のメリットを享受できません。型を用意しましょう。今回はメール送信者の名前であるnameを非必須とします。

// メソッドの型
type EMailType = {
  mail_from: string; // メール送信元
  mail_to: string; // メール送信先
  text: string; // 本文
  name?: string; // メール送信者の名前
};
// 型を付与する
function send_mail({ mail_from, mail_to, text }: EMailType) {}

この状態で必須項目が足りない、または定義していない変数名を指定した場合、エラーを出力してくれるので間違いに気付きやすくなります。

f:id:nainaistar:20210924001351p:plain

初期値の設定

非必須項目に初期値を与えられます。

function send_mail({ 
  mail_from, 
  mail_to, 
  text, 
  name = '公式'
}) {}
// メソッドの呼び出し
send_mail({ mail_from: "a", mail_to: "b", text: "c" });
// nameには初期値の「公式」

send_mail({ mail_from: "a", mail_to: "b", text: "c", name: "d" });
// nameにはパラメータで渡した「d」

ソースコード

終わりに

objectでメソッドに渡して、メソッドでobjectを展開できるとは知りませんでした。

Mediumの記事では今回紹介しなかったROROパターンでのメリット・デメリットが紹介されています。今回、私の記事で紹介していないのは、ユースケースが分からなかったものと、再現ができなかったからです。

私の希望するPythonのキーワード引数みたいな挙動をTypeScriptでもできる、という最低条件は確認しましたので、ブログに残しました。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考情報

f:id:nainaistar:20210924001501p:plain