4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python次の一歩

Last updated at Posted at 2024-02-26

はじめに

Pythonの入門書やチュートリアルを通じて、基本的な構文やプログラミングの概念―変数の宣言、基本的なデータ型、条件分岐(if文)、そしてループ処理など―を学び、なんとなく理解して書けるようになった後に学んだことを雑多にまとめました。
基本的には自走プログラマー独学プログラマーを参照しています。

Python次の一歩

1. コード実装

命名規則

可読性はコードの品質に直接影響を与えます。以下は効果的な命名規則に関するガイドライン例です

  • 関数名:動詞を前に置く(例:calculate_total)、または役割を明確にする名詞を使用します(例:total_amount)。getのような汎用的な動詞は、より具体的な動詞に置き換えることで意図を明確にします(例:fetch_user_data)。
  • プライベート変数/メソッド:アンダースコアを前に付ける(例:_internal_cache)ことで、外部からのアクセスを避ける意図を示します。
  • ブール変数/関数is_has_で始める(例:is_validhas_permission)ことで、返り値がブール型であることを示します。

https://codic.jphttps://github.com/search/ を活用すると良いです

関数の分け方

  • 副作用の管理:データを変更する関数と、データを参照のみする関数を明確に分けます。
  • 不変データの利用:可能であれば、不変データ(例:文字列、タプル)を使用し、副作用を減らします。
  • デフォルト引数:更新可能なデフォルト引数(リスト、辞書)の使用は避け、Noneを使って初期化します。
def append_to_element(element, target_list=None):
    if target_list is None:
        target_list = []
    target_list.append(element)
    return target_list

クラス作成

クラスは、特定の構造を持つデータが複数の関数にまたがって使用される場合に便利です。

  • dataclassを使用して、簡潔にデータ構造を定義できます。
from dataclasses import dataclass

@dataclass
class Product:
    name: str
    price: float
    in_stock: bool
  • propertyを使用して、
    • age_displayの前ageを計算する必要がある場合は、calc_ageなどを別途用意するのではなく、propertyを活用する
class User:
    def __init__(self, username, birthday):
        self.username = username
        self.birthday = birthday

    @property
    def age(self):
        today = date.today()
        age = (self.birthday - today).years
        if (self.birthday.month, self.birthday.day) < (today.month, today.day):
            age -= 1
        return age

    def age_display(self):
        return f"{self.age}"

オブジェクト指向の原則

  • 継承:共通の属性やメソッドを持つクラス間でコードを再利用します。
  • 多態性:同じインターフェースを持つが異なる動作をするオブジェクトを利用して、柔軟性を高めます。
  • カプセル化:オブジェクトの詳細を隠蔽し、外部からの直接的なアクセスを制限します。

コメントとドキュメンテーション

  • コメント:コードの「なぜ」を説明し、特定の実装の理由を記述します。
  • ドキュメンテーション:関数の仕様やクラスの説明はドキュメンテーション文字列に含めます。

レビュー

コードレビューでは、例えば下記のような複数の観点からコードを評価します。
- A:自動チェックできる項目
- B:イディオムレベルの項目
- C:セキュリティーのためのチェック内容
- D:仕様観点レビュー
- E:オブジェクト指向設計原則を元にしたチェック内容
- F:実装者向けより1段高いレイヤーの確認

2. モデル設計

モデル設計とは、アプリケーションが扱うデータの構造を定義するプロセスです。このプロセスには、データの種類、関連性、制約を明確にすることが含まれます。良いモデル設計はデータの整合性を保ち、アプリケーションの性能と拡張性を向上させます。

テーブル設計

  • マスターデータとトランザクションデータ
    • マスターデータとトランザクションデータは分けて設計します。マスターデータは変更されることが少ない情報(例:ユーザー情報)、トランザクションデータは頻繁に変更される情報(例:注文情報)です。
      トランザクションデータには、「そのときの行為」をデータとして正確に記録します。これにより、後からデータを分析しやすくなります。
  • 正規化と性能
    • 性能を改善するためには、あえて正規化を崩してデータを冗長に持たせることがあります。これにより、ジョインの回数を減らし、クエリの速度を向上させることができます。
  • NULLの扱い
    • NULLを許容すると、アプリケーション側で「NULLの場合」を扱う必要があります。可能な限りNULLを避け、デフォルト値を設定することで、コードの複雑さを減らすことができます。
  • ブール時ではなく日時にする
    • publishedカラムを用意する代わりに、published_atカラムを使用し、公開されたかどうかを判定できます。これにより、データの意味をより明確にし、余分なカラムを減らすことができます。
  • 論理削除の回避
    • 論理削除よりも物理削除を推奨します。論理削除を行うとデータが複雑になり、パフォーマンスに影響を与えることがあります。
      過去のデータは、バックアップを取って保管することを推奨します。これにより、本番環境のデータをスリムに保つことができます。
  • UUIDの活用
    • 外部とのやり取りには、予測不可能なUUIDを使用すると良いでしょう。これにより、セキュリティを向上させることができます。
      また、有意コードではなく、「記事コード」「記事ID」のようにメールでの問い合わせなどで必要ない値は、UUID4を使うと良いです。
import uuid

class Article(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    # その他のフィールド

マイグレーション

スキーママイグレーションとデータマイグレーションは、問題が発生した場合にロールバックを容易にするために、個別に実行できるようにします。

ORM

ORMを使ったクエリを書いたら、常に生成されるSQLを確認し、意図した通りのクエリが生成されているかを確認します。また、nplusoneライブラリなどを使用して、N+1問題を自動的に検出し、解決することが重要です。

3. エラー設計

try-except-else-finallyブロック

Pythonではtry-except-else-finallyブロックを使用して例外を処理します。

  • ただし、プログラムで想定外のことが起こったら、素直に例外を上げて終了した方が良いです。すべての不測の事態に備えてコードを書くことはできないからです。
  • try節のコードはできるだけ短く、1つの目的に絞って処理を実装します。
try:
    # 例外が発生する可能性のあるコード
    result = some_operation()
except SomeException as e:
    # 例外が発生したときの処理
    handle_exception(e)
else:
    # 例外が発生しなかったときの処理
    process_result(result)
finally:
    # 最後に必ず実行する処理
    cleanup()

カスタム例外の作成

特定のエラーケースに対処するため、またはコードの可読性を高めるために、カスタム例外を定義することが有効です。

class MyCustomException(Exception):
    pass

def my_function():
    if some_error_condition:
        raise MyCustomException("Error message")

ログ管理

例外情報を適切にログに記録することは、問題の診断と修正を容易にします。
loggerはモジュールパス__name__を使って取得します。

import logging

logger = logging.getLogger(__name__)

try:
    risky_operation()
except Exception as e:
    logger.exception("An error occurred")

ログ設定例(ファイルに出力する場合):

import logging

logging.basicConfig(filename='example.log', level=logging.DEBUG)

詳細に設定する場合はml-prefect-fastapi-examples 参照

トランザクションIDの使用

重要なバッチ処理を行う際には、トランザクションIDをログに含めることで、後のトラブルシューティングが容易になります。

import uuid

def generate_transaction_id():
    return uuid.uuid4()

transaction_id = generate_transaction_id()
logger.info(f"{transaction_id} - Starting operation")

最適なエラー設計のポイント

  • 例外処理を明確に: 予期せぬエラーにはtry-exceptブロックを使用し、想定内のエラーはカスタム例外で処理します。
  • ログを活用: エラー情報はログに記録し、分析やデバッグを容易にします。
  • コードの読みやすさを保つ: カスタム例外を使用して、何が問題かを明確にし、コードの意図を伝えます。

4. テスト設計

Pythonのテスト設計についての章を書く際、以下のポイントに注目して構成を組み立てます。この案は、テストの基本から高度なテクニックまでをカバーし、Pythonにおけるテストのベストプラクティスを紹介することを目的としています。

テストの基本

テストの種類

  • ユニットテスト: 個々の関数やメソッドの動作を検証します。
  • 統合テスト: 複数のコンポーネントが連携して期待通りに動作するかを検証します。
  • システムテスト: アプリケーション全体の動作を検証します。
  • 受け入れテスト: ユーザーの要件を満たしているかを検証します。

ユニットテストとpytest

pytestの基本

import pytest

def add(a, b):
    return a + b

@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (4, 5, 9),
    (-1, 1, 0),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

この例では、parametrizeデコレータを使用して複数のテストケースを簡単に追加しています。
@pytest.mark.parametrize を使って複数のパラメータでテストを実行しています。

※デコレータ: デコレータはそれ自身が関数やクラスです。
デコレータとして指定する関数を上記のように定義しておくことにより、任意の関数(この場合、wrapper(func)のfuncにexample関数が渡されます)に対して、何らかの前処理や後処理を実行することが可能になります。

@wrapper(arg1, arg2)
def example():
...

=

wrapper(example)()

テストの構造: Arrange (準備)、Act (実行)、Assert (検証) の3つのステップに分けることで、テストコードの可読性と保守性を向上させます。

テストの高度なテクニック

Mockの使用
テスト対象が外部システムに依存している場合、その依存部分を模倣(mock)することで、テストの実行速度を向上させ、外部システムの不安定さから独立させます。
Mockを使ってローカルでも動くようにします。

from unittest.mock import MagicMock

def external_api_call():
    # ここで外部APIを呼び出す
    pass

def test_external_api_call():
    with unittest.mock.patch('path.to.external_api_call', new=MagicMock(return_value="mocked response")):
        response = external_api_call()
        assert response == "mocked response"

validatorデコレータの使用
@validatorデコレータを使用して、ageフィールドが負の値でないことを確認するカスタムバリデーション

from pydantic import BaseModel, validator

class Person(BaseModel):
    name: str
    age: int

    @validator('age')
    def check_age(cls, value):
        if value < 0:
            raise ValueError('age must be a non-negative integer')
        return value

# 使用例
try:
    person = Person(name="John Doe", age=-5)
except ValueError as e:
    print(e)  # 出力: age must be a non-negative integer

テストユーティリティの活用

  • factory-boy: テストデータベースにテスト用のオブジェクトを簡単に追加できます。
    • データベースアクセスなどはfactory-boyで比較的簡単にデータが作れるので、不要なフィクスチャーの作成が不要になります。
  • responses: 外部APIの呼び出しをインターセプトして、テスト用のレスポンスを返すことができます。
    • requestsがバックエンドサーバーへアクセスするのを、responsesを使ってモックします。

ベストプラクティス

  • テストケースの設計
    • テストは他のテストと独立しているべきで、実行順序に依存しないようにします。
    • テストケース毎にテストデータを用意します。
    • 1つのテストメソッドでは、1つの項目のみ確認するようにします。
    • テストしにくい実装は設計が悪いので見直すようにします。
    • 全体を通すような処理は分岐を網羅するだけのテストメソッドで十分とします。
    • mockなどで無理に単体テストを通すよりも、「関数設計が良くならないか?」「データ、モデル設計が良くならないか?」と考えてみます。
  • テストの実装
    • テスト内で入出力を確認するときは、文字列や数値などの値をテスト内に直接書きます。

5. システム設計

コーディング規約

  • コーディング規約 目次サンプル
    • ・開発手順 ・要件定義 ・トピックブランチ ・機能設計 ・Pull Request ・実装チケット ・レビュー ・プログラミング ・コーディングスタイル ・docstring ・型ヒント ・関数設計 ・プロジェクト構成 ・環境変数 ・モジュール設計 ・ディレクトリ構造 ・システム設計 ・パラメーターのバリデーション ・データマイグレーション ・コードの分割 ・エラーハンドリング ・データベース ・テーブル定義 ・予備カラム ・NULLの扱い ・ロギング ・ログレベル ・ログファイル ・ログ出力内容 ・Sentry通知 ・ユニットテスト ・toxを使う ・コーディングスタイルチェック ・pytestを使う ・CI実行 ・UnitTestを書く ・構成管理 ・ChangeLog ・マージ戦略 ・依存パッケージ管理 ・デプロイ ・ブランチ戦略 ・トラブルシューティング ・本番運用 ・障害対応
  • Docstringと型ヒント: 関数やクラスの目的、引数、返り値を明確にするDocstringの記述と、型ヒントを用いた型の明示を推奨します。
    • 関数やクラスの目的、引数、返り値を説明するドキュメンテーションは、コードの可読性と保守性を高めます。
    • スタイルはいくつかありますが、例えばGoogleスタイルは下記です。
def add(a, b):
    """
    2つの数値を受け取り、その和を返す関数。

    Args:
        a (float): 数値1
        b (float): 数値2

    Returns:
        float: aとbの和
    """
    return a + b

パイプライン開発

※MLパイプラインを想定しています

  • ml-prefect-fastapi-examples 参考
  • Data Pipeline -> ML Pipelineで分けます。Data PipelineはDataformなど。ML Pipeline単体で動くようにもしておきます(test dataを与えるかで分岐する、debugオプション、など)
  • ML Pipelineの中はdataframeとかdastaclassでデータをやり取りします
  • IN/Outがすぐに分かるようにします
  • パラメータと定数は別ファイルで管理します
  • 引数はargparseかclickを使います

API開発

Fast API等を使用して、効率的なAPIを開発します。Nginxやgunicornなどのサーバー設定にも注意を払います。

FastAPI実装例

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

Webサーバーとアプリケーションサーバーの違い

Webサーバーは、HTTPリクエストを受け取り、HTMLドキュメントや画像などの静的コンテンツをクライアント(ブラウザ)に返す役割を持っています。
一方で、アプリケーションサーバーは、動的コンテンツを生成するためのアプリケーションの実行を管理します。これは主にプログラミング言語で書かれたコードを実行し、データベースの操作やロジックの処理を行い、動的に生成されたコンテンツをクライアントに返します。

  • Nginx
    • 役割: Webサーバー
    • 特徴: 高性能、高並列処理能力、リバースプロキシとしての機能を持ち、静的コンテンツの配信に優れています。また、バランスローダーとしても機能し、複数のアプリケーションサーバーへのリクエストの分散処理が可能です。
  • Gunicorn
    • 役割: アプリケーションサーバー(WSGIサーバー)
    • 特徴: Pythonで書かれたWebアプリケーションのためのWSGIサーバーです。FlaskやDjangoなどのフレームワークと組み合わせて使用されます。複数のワーカープロセスを使用してアプリケーションを実行し、並行リクエストを処理します。
  • Uvicorn
    • 役割: アプリケーションサーバー(ASGIサーバー)
    • 特徴: 非同期Webアプリケーションに特化したASGIサーバーです。FastAPIやStarletteなどの非同期フレームワークと組み合わせて使用されます。非同期IOを利用して高いパフォーマンスを実現します。
  • WSGI
    • 役割: Webサーバーとアプリケーションサーバー間のインターフェース
    • 特徴: PythonのWebアプリケーションとWebサーバー間の標準インターフェース。GunicornのようなWSGIサーバーは、このインターフェースを使用してアプリケーションと通信します。
  • 関連性
    • Nginxは、クライアントからのリクエストを最初に受け取り、静的コンテンツは直接配信し、動的コンテンツのリクエストをGunicornやUvicornのようなアプリケーションサーバーに転送(リバースプロキシ)します。
    • GunicornやUvicornは、それぞれWSGIやASGI規格に従い、PythonのWebアプリケーションを実行します。これにより動的コンテンツが生成され、クライアントに返送されます。
    • WSGIとASGIは、PythonのWebアプリケーションとアプリケーションサーバー間の通信規格であり、GunicornはWSGI、UvicornはASGIをサポートしています。

WSGIはWebアプリケーションの配信プロセスにおいて連携して機能します。Nginxが外部からのリクエストの入口として機能し、GunicornやUvicornが内部でアプリケーションの実行を担当し、WSGI/ASGIがそれらの橋渡しをする形です。
WSGIは同期的な通信に特化しており、ASGIは非同期通信に特化しています。従来の同期型のアプリケーションではWSGIが適していますが、リアルタイム性やスケーラビリティが求められる場合はASGIを選択するのが一般的です。

FastAPI詳細

  • Flaskとの違い
    • リクエストとレスポンスのスキーマ定義に合わせて自動的にSwagger UIのドキュメントが生成される
      • ドキュメントとコードが一致する
      • 実際にAPIの動作を検証することもできる。curlコマンドのサンプルも示してくれる
    • 上記のスキーマを明示的に定義することにより、型安全な開発が可能
      • スキーマを先に定義することによってフロントエンドとバックエンド間のインターフェイスを取り決め、それぞれの開発を同時にスタートする方式を、一般に「スキーマ駆動開発(Schema-Driven Development)」と呼びます
    • ASGIに対応しているので、非同期処理を行うことができ、高速
  • ディレクトリ構成例
    • schemas、routers、models、cruds。または、integration, io, logic
    • routersの分割は、リソース単位とすることをおすすめします
from fastapi import FastAPI
from api.routers import task, done

app = FastAPI()
app.include_router(task.router)
app.include_router(done.router)

オニオンアーキテクチャ

  • オニオンアーキテクチャとは

    • ドメイン駆動設計に基づくアーキテクチャパターンの一種で、ビジネスロジックを中心に据え、その周りにインフラストラクチャ層やUI層が配置される構成を取ります。これにより、システムの柔軟性とテストの容易さが向上します
  • ディレクトリ構成例

    • domain/: ビジネスロジックと規則を含む
      • 役割: ビジネスロジックとルールを表現します。アプリケーションの中核部分で、システムが解決しようとしている問題領域を扱います。具体的には、エンティティやドメインサービス、値オブジェクトなどが含まれます。
      • : ユーザーや製品といったエンティティ、ビジネスルールを実行するためのメソッドなど。
      • ドメイン層がない場合: ビジネスロジックが他の層(例: プレゼンテーションやインフラ)に分散し、ビジネスルールの一貫性が失われやすくなります。
    • application/: ユースケースとアプリケーションサービスを含む
      • 役割: ユースケースを実装し、ドメイン層のビジネスロジックを適切に実行するための調整を行います。外部からの要求に応じて、ドメインオブジェクトを操作しますが、ビジネスロジック自体は含みません。
      • : ユーザーが新しい注文を作成するユースケースを実行するサービス、ドメイン層のオブジェクトを利用してデータを取得・更新するメソッドなど。
      • アプリケーション層がない場合: ユースケースが明確に定義されず、ビジネスロジックの実行が複雑化する可能性があります。また、複数のプレゼンテーション層やインフラ層からドメインロジックに直接アクセスすることになり、結合度が高まります。
    • infrastructure/: 外部サービスやデータベースとの連携を担当
      • 役割: データベースや外部サービスとの連携を担当します。技術的な実装の詳細が含まれており、アプリケーション層がこれを利用してデータの保存や外部システムとの通信を行います。
      • : データベース接続ロジック、外部APIのクライアント、ロギング、ファイル操作など。
      • インフラストラクチャ層がない場合: 技術的な詳細が他の層に混在し、変更が困難になります。例えば、データベースの変更や外部サービスの置き換えが大規模な修正を必要とする場合があります。
    • presentation/: APIエンドポイントとコントローラーを含む
      • 役割: ユーザーとのインターフェースを提供します。Web APIやUIなど、アプリケーションのエントリーポイントとなる部分です。ユーザーの入力を受け取り、適切なユースケースを実行して結果を返します。
      • : FastAPIのエンドポイント、HTMLテンプレート、CLIインターフェースなど。
      • プレゼンテーション層がない場合: ユーザーとアプリケーションのインターフェースが不明確になり、ユーザーの要求を適切に処理することが難しくなります。
  • 各層の依存関係

  • 実装例: ml-prefect-fastapi-examples

  • 実装の説明

    1. プレゼンテーション層 (main.py) でリクエストを受け取る。
    2. アプリケーション層 (recommendation_service.py) でビジネスロジックを実行。
    3. ドメイン層 (user.py) でエンティティを使用。
    4. インフラストラクチャ層 (bigtable_repository.py) でデータを取得。
    5. プレゼンテーション層でレスポンスを返す。

ベストプラクティスとツール

  • 標準ディレクトリ構成: src, tests, docsなどの標準的なフォルダ構成に従います。
    • Cookiecutterなどを参考に検討します。
  • 変数管理
    • 定数: constants.pyに定数をまとめます。
    • 環境ごとの変数
      • 環境ごとの設定はsettingsディレクトリ内のbase.py, local.py, staging.pyに分割します。
      • 環境変数として.envファイルに記載します。python-dotenvのようなライブラリを使用して、アプリケーション起動時にこれらの環境変数を読み込むようにします。
  • パッケージ管理: poetryを使用して、依存関係の管理とパッケージの公開を行います。
    • PoetryはPythonプロジェクトの依存関係管理とパッケージングを簡単にするためのツールです。以下はPoetryができる主なことです
      • 依存関係の管理、パッケージの作成と公開、仮想環境の管理、依存関係の解決、バージョン管理、ビルドシステム
      • pytestとか開発環境だけ必要なパッケージは poetry install --no-dev とかで分ける
  • タスク管理
    • Invokeは、開発プロセス中に繰り返し発生する様々なタスクを自動化するために使われます。これにより、手作業によるエラーを減らし、効率を向上させることができます
from invoke import task

@task
def start(c):
    c.run("docker run --name myapp-instance -d myapp")
$ invoke start
4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?