Open55

MLエンジニア1

かーなびかーなび

事業会社における研究開発の流れ

  1. 事業課題・ペインから解決方針・具体の解決策を考案
  2. ペインを解決し得る技術を調査するため、国内外の記事(Zenn, Qiita, Medium etc.)、ライブラリ・パッケージの公式ドキュメント、論文をチェック(ベース手法と参考手法のリストアップ、競合の確認)
  3. 調査した候補から手法を選定
  4. 使用できるかの確認として、ライセンス確認、githubクローン、動作確認
  5. staging:プロダクトへの採用を検討するタイミングで、自前データ学習、評価評価指標、定性的に学習データと結果の目視確認、処理速度計測などの検証を実施
  6. 精度・処理速度を加味して「事業貢献額 > ランニングコスト」が成り立ちペイするのかを判断
  7. production:本番環境で使えるように実装、基盤との繋ぎ込み
  8. PR、レビュー、2 aprove、マージ、デプロイ、本番環境評価・負荷試験、リリース

仮説検証

仮説を立てて素早くものを作って検証→課題抽出→課題をもとに新たな仮説を立てて検証→…のサイクルを回す

雑用で終わらない成果を出すモノづくり

  1. MRR達成を目標におく
  2. MRRを達成しうるモノをまず素早く作る
  3. 作ったモノをエフェクチュエーション的なアプローチで検証(課題がわかってるんだったらもうやっているので)
  4. 仮説をアプデするサイクルを回す

https://www.youtube.com/watch?v=gkEBUOj7jSw
https://www.youtube.com/watch?v=i8CwHRDCWIU

かーなびかーなび

MLエンジニア・データサイエンティストロードマップ

https://qiita.com/aokikenichi/items/644d03d403e3520e66ab
https://roadmap.sh/
https://roadmap.sh/ai-data-scientist

ソフトウェアエンジニアロードマップ

https://roadmap.sh/software-architect

フロントエンドエンジニアロードマップ

https://roadmap.sh/frontend

PMロードマップ

https://roadmap.sh/product-manager

Pythonロードマップ

https://roadmap.sh/python

かーなびかーなび

alphaXiv

arxiv→alphaxivにURLを変えるだけ
スプリットビューで論文を読みながら、チャットで論文を参照の上、質問できる(なぜか最先端のモデルが無料で使える)。先月 Deep Research が追加、こちらは論文サーベイに便利。
https://x.com/eycjur/status/1922636755620020513

かーなびかーなび

GitHub

Gitflow

https://danielkummer.github.io/git-flow-cheatsheet/index.ja_JP.html
https://qiita.com/KosukeSone/items/514dd24828b485c69a05
https://www.atlassian.com/ja/git/tutorials/comparing-workflows/gitflow-workflow

ブランチと役割

  • main (master): 本番環境にリリースしているコード。こちらに開発中のものを直接コミットせず一旦開発ブランチにコミットするようにする
  • release: リリース時にmainへマージするためのブランチ。開発したものを集約したdevからブランチを切り出して作成する
  • dev (develop): 開発中の機能を集約していくブランチ。基本的にこちらの 最新版をベースにブランチを切り出して開発を進める
  • feature: 機能や目的で区切って開発するためのブランチ。開発後はdevに集約する
  • hotfix: 本番環境に何かしらの問題が発生した場合に作成するブランチ。mainへ直接マージする

Pull Requestレビューとマージ

  • 複数人にapproveを受けた上でマージすることが推奨される
    • 理由
      • 複数人によるチェックを行うことで統合時の影響リスクを抑える
      • 属人的な開発体制を抑止しチームとして管理できるようにする
  • approveするメンバには在籍期間がある程度長いメンバ・リーダー格のメンバが1名以上いることが理想

本番環境(main)へのリリース

  • 本番環境リリース時には別の管理者の承認を経るようにすることでチームとして本番環境に対する開発状況の把握を行えるようにする
  • 把握しておくことで障害が起きた時の対応初動を早める
  • 週末などのリリースはどのメンバも障害対応できない可能性があるので避ける

git revert

  • 概要:特定のコミットで行われた変更を打ち消す新しいコミットを作成する」**という意味のコマンド。git reset --hardと違い履歴を書き換えないので便利。
  • 使い所:リモートリポジトリにおいて今の変更は保ったまま一つ前のコミットに戻したいとき
geminiによるコマンド解説

git revertで特定のコミットに戻す

リモートリポジトリにマージした変更を一時的に元に戻す場合、最も安全で推奨される方法は git revert コマンドを使用することです。これにより、履歴を書き換えることなく、マージコミットによって導入された変更を打ち消す新しいコミットが作成されます。

なぜ git revert が推奨されるのか?

  • 履歴の保持: git revert は新しいコミットを作成するため、元のマージコミットは履歴に残ります。これにより、変更の経緯が明確になり、他の開発者が影響を受けることなく共同作業を続けられます。
  • 共有リポジトリでの安全性: git reset --hard のように履歴を書き換えるコマンドは、すでにリモートにプッシュされている共有ブランチで使用すると、他の開発者のリポジトリとの不整合を引き起こし、深刻な問題につながる可能性があります。git revert はこのリスクを回避できます。

git revert を使用してマージコミットを元に戻す手順

  1. マージコミットのハッシュ(ID)を特定する:
    まず、元に戻したいマージコミットのハッシュを git log コマンドで確認します。マージコミットは通常、複数の親コミットを持ち、「Merge branch ...」のようなメッセージが含まれます。

    git log --oneline --graph
    

    このコマンドでコミット履歴がグラフィカルに表示され、マージコミットが分かりやすくなります。元に戻したいマージコミットのハッシュをメモしておきます。

    例:

    * abcdef1 (HEAD -> main) Merge branch 'feature/new-feature'  <-- これを元に戻したい
    |\
    | * 1234567 Add new feature B
    * | 890abcd Update existing functionality
    |/
    * fedcba9 Initial commit
    

    この例では、abcdef1 がマージコミットです。

  2. git revert コマンドを実行する:
    マージコミットをリバートする場合、-m オプションを使用して、どの親をメインライン(マージ先)として扱うかを指定する必要があります。通常、マージ先のブランチ(例: maindevelop)が1番目の親 (-m 1) になります。つまり、マージ元ブランチの変更を打ち消したい場合は -m 1 を指定します。

    git revert -m 1 <マージコミットのハッシュ>
    

    例:

    git revert -m 1 abcdef1
    
    • -m 1 を指定すると、マージコミットによってマージ元ブランチから取り込まれた変更が打ち消されます。結果として、マージ先のブランチ(例えば main)がマージ前の状態に戻ります。

    • もし、何らかの理由でマージ元ブランチの変更は残しつつ、マージ先ブランチでマージ時に加えられた変更(コンフリクト解決など)を打ち消したい場合は -m 2 を使用することもありますが、一般的ではありません。
       * -m は Git コマンドにおけるオプションで、--message の略です。主に以下のコマンドで使われます。

      • git commit -m "コミットメッセージ":
        コミットを行う際に、コミットメッセージを直接指定するためのオプションです。これをつけないと、エディタが開いてメッセージを入力することになります。

      • git revert -m <parent-number> <merge-commit-hash>:
        マージコミットをリバートする際に、どの親を「メインライン(マージ先)」とみなしてその逆の変更を打ち消すかを指定するためのオプションです。マージコミットには通常、複数の親があります。

        • -m 1: 1番目の親(通常、マージ先のブランチのHEAD)に対する変更を保持し、2番目以降の親(マージ元のブランチなど)から取り込まれた変更を打ち消します。
        • -m 2: 2番目の親に対する変更を保持し、1番目の親(マージ先のブランチ)から取り込まれた変更を打ち消します(稀なケース)。

      このように、-m"message"(メッセージ) または "mainline"(メインライン、マージの親の指定) の意味合いで使われますが、一般的な git commit の場合は「メッセージ」と覚えておけば間違いありません。git revert の場合はマージコミットの特性上、親の指定という特殊な意味合いを持つことになります。

  3. コミットメッセージの編集:
    git revert を実行すると、デフォルトのコミットメッセージがエディタで開きます。通常、マージコミットをリバートしたことを示すメッセージが自動で生成されます。必要に応じて編集し、保存して閉じます。

  4. コンフリクトの解決(必要な場合):
    マージコミットをリバートする際に、現在のブランチの状態とリバートされる変更の間にコンフリクトが発生する場合があります。その際は、Gitがコンフリクトを通知しますので、手動でファイルを編集してコンフリクトを解決し、git add <解決したファイル> でステージングし、git revert --continue でリバートを続行します。

  5. リモートリポジトリにプッシュする:
    リバートコミットが作成されたら、それをリモートリポジトリにプッシュします。

    git push origin <あなたのブランチ名>
    

これで、リモートリポジトリのマージコミットの変更が一時的に元に戻ります。マージコミット自体は履歴に残りますが、その変更は打ち消され、あたかもマージがなかったかのような状態になります。

注意点

  • コミュニケーション: 共有リポジトリで作業している場合、マージを元に戻す際は必ずチームメンバーにその旨を伝えてください。
  • 再マージの注意点: git revert でマージを元に戻した後、同じブランチを再度マージしようとすると、Gitはそのブランチからの変更が既に「取り消された」と認識するため、意図しない挙動になる可能性があります。この場合、新しくブランチを作成してそちらで作業を進めるか、より高度なGit操作が必要になる場合があります。

git revertを打った後のCLI画面からの抜け方→vimと同じく:wq

なるほど、状況が少し違うようですね。modified:, new file:といった表示が出ていて、そこから「抜けられない」とのこと、承知いたしました。

これはおそらく、以下のいずれかの状況にいる可能性が高いです。

  1. コミットメッセージの編集画面(テキストエディタ)が表示されている

    • git commit コマンドを実行した後や、git revert がコミットメッセージの編集を促す設定になっている場合、Vim(ヴィム)やnano(ナノ)といったテキストエディタが起動します。このエディタの操作方法が分からず、「抜けられない」と感じることがあります。

    • 画面の見た目のヒント:

      • 画面いっぱいにテキストが表示され、カーソルが点滅している。
      • 画面の最下行に -- INSERT -- やファイル名、行番号などが表示されている(Vimの場合)。
      • 画面下部に ^X Exit のような操作ガイドが表示されている(nanoの場合)。
    • 抜け方:

      • Vim (ヴィム) の場合:
        1. まず Esc キーを押します(これでノーマルモードという状態になります)。
        2. 次に、以下いずれかのコマンドを入力して Enter キーを押します。
          • :wq : (write and quit) コミットメッセージを保存して終了します。
          • :q! : (quit without saving) コミットメッセージを保存せずに強制終了します(コミットは中断されます)。
          • :q : (quit) 何も変更していなければ、これで終了できます。
      • nano (ナノ) の場合:
        1. Ctrl キー (コントロールキー) を押しながら X キーを押します (^X と表示されていることが多いです)。
        2. 「Save modified buffer? (変更を保存しますか?)」のような質問が表示されたら、
          • 保存する場合は Y キーを押し、その後 Enter キーを押します。
          • 保存しない場合は N キーを押します。
  2. Gitの出力がページャ(lessなど)で表示されている

    • git log, git diff, git status などのコマンドの出力結果が長い場合、自動的に less というページャ(閲覧ソフト)で表示されることがあります。この場合、画面の最後にいるとスクロールできなくなり、「抜けられない」と感じることがあります。

    • 画面の見た目のヒント:

      • 画面の最下行にコロン :(END) という文字が表示されている。
      • スペースキーで下にスクロールできるが、一番下まで行くとそれ以上進まない。
    • 抜け方:

      • q キーを押します。これでページャが終了し、通常のコマンド入力待機状態に戻ります。

どちらの状況か確認してみてください。

  • もし、画面の最後に :(END) が見えたら、それはおそらくページャです。q を押してみてください。
  • そうでなく、画面全体がテキスト入力欄のようになっている場合は、コミットメッセージのエディタの可能性が高いです。上記の手順(Vimまたはnano)を試してみてください。お使いの環境でどちらのエディタが起動するかは設定によりますが、Vimがデフォルトになっていることが多いです。

もし上記の方法で解決しない場合、あるいはどちらの状況か判断がつかない場合は、お手数ですが以下の情報を教えていただけますでしょうか?

  • 現在表示されている画面の具体的な内容(数行で構いません)。
  • 「抜けられない」状態になる直前に、どのようなコマンドを入力しましたか?

より詳しい情報があれば、的確な対処法をご案内できます。

git revertで元いた最新状態のコミットに戻す

git revert で一時的に元に戻した変更を、さらに最新のマージした状態に戻すには、**「リバートしたコミットをさらにリバートする」**という方法が最もシンプルで安全です。

つまり、変更を打ち消すために作成した「リバートコミット」自体を、もう一度リバートするのです。これにより、最初の変更が再び適用された新しいコミットが作成されます。

手順

  1. リバートコミットのハッシュ(ID)を特定する:
    まず、以前にマージした変更を元に戻すために作成した「リバートコミット」のハッシュを git log コマンドで確認します。リバートコミットは、通常、そのコミットメッセージに「Revert "元のコミットメッセージ"」のような内容が含まれています。

    git log --oneline
    

    例:

    abcdef1 Revert "Merge branch 'feature/new-feature'"  <-- これを元に戻したい(つまり、元のマージ変更を再適用したい)
    1234567 Merge branch 'feature/new-feature'
    890abcd Original commit
    ...
    

    この例では、abcdef1 がリバートコミットです。

  2. リバートコミットをリバートする:
    リバートコミットのハッシュが特定できたら、そのリバートコミットを対象に git revert コマンドを実行します。

    git revert <リバートコミットのハッシュ>
    

    例:

    git revert abcdef1
    
  3. コミットメッセージの編集:
    git revert を実行すると、デフォルトのコミットメッセージがエディタで開きます。通常、「Revert "Revert "元のコミットメッセージ"""」のようなメッセージが自動で生成されます。必要に応じて、より分かりやすいメッセージに編集し、保存して閉じます。

  4. コンフリクトの解決(必要な場合):
    リバートコミットをリバートする際に、現在のブランチの状態と、再適用される変更の間にコンフリクトが発生する場合があります。その際は、Gitがコンフリクトを通知しますので、手動でファイルを編集してコンフリクトを解決し、git add <解決したファイル> でステージングし、git revert --continue でリバートを続行します。

  5. リモートリポジトリにプッシュする:
    新しいリバートコミット(元の変更を再適用するコミット)が作成されたら、それをリモートリポジトリにプッシュします。

    git push origin <あなたのブランチ名>
    

なぜこの方法が安全なのか?

  • 履歴の整合性: この方法は、Gitの履歴を直線的に進め、既存のコミットを削除したり書き換えたりしないため、リモートリポジトリや他の共同作業者に影響を与えません。すべての変更と取り消しの履歴が明示的に残ります。
  • 追跡の容易さ: どの変更がいつ取り消され、いつ再適用されたのかがコミット履歴から明確に分かります。

注意点

  • コンフリクトの可能性: 最初のマージ後に他の変更が加えられていたり、リバート後もブランチが進んでいたりする場合、リバートコミットをリバートする際にコンフリクトが発生する可能性が高くなります。これは避けられない側面であり、手動で解決する必要があります。
  • 複雑性の増加: リバートを繰り返すことで履歴が長くなり、複雑に感じられるかもしれませんが、Gitの特性を理解していれば、どの時点の変更が適用されているかを正確に把握できます。

基本的には、**「変更を打ち消すコミットを作成する」**という git revert の原則に従い、さらにその打ち消しを打ち消す新しいコミットを作成することで、元の状態に戻す(再適用する)ことができます。

コンフリクトの解消

https://qiita.com/crarrry/items/c5964512e21e383b73da

かーなびかーなび

並列処理

with ThreadPoolExecutor(max_workers=8) as executor:
    results = []
    for args in samples:
        results.append(executor.submit(func, *args))
    images = [r.result() for r in results] # 並列実行した全てのjobを待っている(この行を書かないと実行されない)
ProcessPoolExecutorとの違い・使い分け

https://note.com/mega_gorilla/n/n3fe706a9a950
「プロセス」と「スレッド」の違い、およびそれらの違いが「ThreadPoolExecutor」と「ProcessPoolExecutor」の選択にどのように影響するかについて、文脈を踏まえて解説します。

プロセスとスレッドの違い

コンピュータプログラムの実行単位として、「プロセス」と「スレッド」という2つの概念があります。

  • プロセス (Process):

    • プログラムを実行するための独立した実行環境です。
    • それぞれが独自のメモリ空間、ファイルハンドル、リソースを持ちます。
    • プロセス間でのデータ共有は、プロセス間通信 (IPC: Inter-Process Communication) と呼ばれる特別なメカニズム(パイプ、ソケット、共有メモリなど)を介して行われ、このやり取りにはオーバーヘッドが発生します。
    • 1つのプロセスがクラッシュしても、通常は他のプロセスに直接的な影響はありません。
    • OSによって管理され、比較的起動や終了に時間がかかります。
  • スレッド (Thread):

    • プロセス内部で実行される軽量な実行単位です。
    • 同じプロセス内の他のスレッドとメモリ空間やリソース(ファイルハンドルなど)を共有します。これにより、スレッド間でのデータ共有は比較的容易ですが、データの整合性を保つための同期メカニズム(ロックなど)が必要になります。
    • 1つのスレッドがクラッシュすると、同じプロセス内の他のスレッドも影響を受け、プロセス全体がクラッシュする可能性があります。
    • プロセスに比べて起動や終了が高速です。

ThreadPoolExecutor と ProcessPoolExecutor の違い

上記のプロセスとスレッドの特性を踏まえて、Pythonの並列実行フレームワークであるThreadPoolExecutorProcessPoolExecutorの違いを理解できます。

  • ThreadPoolExecutor:

    • 実行単位: 複数のスレッドで関数を同時に実行します。
    • GILの制約: PythonにはGIL (Global Interpreter Lock) という仕組みがあります。これは、一度に1つのスレッドしかPythonバイトコードを実行できないようにするロックです。
      • このため、ThreadPoolExecutorは、計算集約型のタスク(CPU-boundタスク)では真の並列実行を実現できず、GILの制約を受けてしまいます。つまり、CPUのコアを最大限に活用できません。
      • 適しているタスク: 主にI/O処理(ファイルの読み書き、ネットワーク通信など)がボトルネックとなるI/O-boundタスクに適しています。I/O処理中はGILが解放されるため、別のスレッドがPythonバイトコードを実行できるようになり、見かけ上の並列性を向上させることができます。
    • データ共有: 同じプロセス内のスレッドなので、データ共有は比較的容易です。
  • ProcessPoolExecutor:

    • 実行単位: 複数のプロセスで関数を実行します。
    • GILの制約: 各プロセスが独立したPythonインタプリタを持つため、GILの制約を受けません
      • これにより、計算集約型のタスク(CPU-boundタスク)でも複数のCPUコアを効率的に利用し、真の並列実行を実現できます。
      • 適しているタスク: CPUの計算能力を最大限に活用したいCPU-boundタスク(数値計算、データ処理、画像処理など)に非常に適しています。
    • データ共有: プロセス間でのデータ共有は、プロセス間通信メカニズムを介して行われるため、ThreadPoolExecutorに比べてオーバーヘッドが発生します。特に大量のデータを頻繁にやり取りする場合には、このオーバーヘッドが性能に影響を与える可能性があります。

まとめ

特徴 ThreadPoolExecutor ProcessPoolExecutor
実行単位 スレッド プロセス
GILの制約 受ける (CPU-boundタスクには不向き) 受けない (CPU-boundタスクに効率的)
適したタスク I/O-boundタスク (I/O処理中にGILが解放されるため) CPU-boundタスク (真の並列実行が可能)
データ共有 容易 (同じメモリ空間を共有) オーバーヘッドあり (プロセス間通信が必要)
クラッシュの影響 1つのスレッドがクラッシュするとプロセス全体に影響 1つのプロセスがクラッシュしても他のプロセスは独立

したがって、実行したいタスクの性質(I/O-boundかCPU-boundか)と、データ共有の頻度や量によって、適切なExecutorを選択する必要があります。

スレッドとプロセスのわかりやすい例え

プロセスとスレッドの違いを、より分かりやすくするために、いくつか例えを交えながら図式的に解説します。


プロセス:それぞれの独立した工場

プロセスをイメージするなら、それぞれが独立した工場と考えると分かりやすいでしょう。

  • 工場 (プロセス) の特徴:
    • 独立した敷地と設備: 各工場は独自の敷地(メモリ空間)を持ち、そこで使う機械や工具(リソース)もそれぞれ持っています。他の工場と機械を直接共有することはありません。
    • 独自の生産計画: 各工場は独自の生産計画(プログラムの実行)に基づいて製品を作ります。
    • 製品のやり取りは大変: ある工場で作った部品を別の工場で使う場合、トラック(プロセス間通信)で運ぶ必要があり、手間と時間がかかります。
    • 閉鎖的: 一つの工場が火事になっても、隣の工場には直接的な影響は及びにくいです。

例えば、ウェブブラウザの各タブを別の工場と考えると分かりやすいかもしれません。Chromeなどでは、タブごとに独立したプロセスが起動していることが多く、一つのタブがクラッシュしても、他のタブは影響を受けずに動作を続けられます。


スレッド:工場の中の作業員

一方、スレッドは、その工場の中の作業員と考えると良いでしょう。

  • 作業員 (スレッド) の特徴:
    • 同じ工場で働く: 作業員は全員同じ工場(プロセス)の中で働きます。
    • 設備を共有: 同じ工場内の機械や工具(リソース)は、作業員全員で共有して使います。
    • 協力して作業: 複数の作業員が協力して一つの製品を作ったり、それぞれの作業員が別の工程を担当したりできます。
    • 連携がスムーズ: 同じ工場内なので、隣の作業員に部品を渡すのはとても簡単です。
    • 影響しあう: もし工場内の特定の機械(共有リソース)の使い方が悪い作業員がいると、他の作業員にも影響が出て、工場全体の生産が止まってしまう可能性があります。

例えば、工場で車を組み立てる工程を考えてみましょう。ある作業員はエンジンを取り付け、別の作業員はタイヤを取り付けます。これらは同時に進行できますが、同じ工場(プロセス)内の共有された設備(例えば、塗装ブース)を使う際には、順番を待ったり調整したりする必要があります。


ThreadPoolExecutor と ProcessPoolExecutor の使い分け

これらの例えを踏まえると、ThreadPoolExecutorProcessPoolExecutorの使い分けがよりクリアになります。

  • ThreadPoolExecutor:多くの作業員がいる一つの工場

    • これは、一つの大きな工場(プロセス)の中に、たくさんの作業員(スレッド)がいて、みんなで協力して作業を進めるイメージです。
    • 得意なこと: 作業員同士の連携(データ共有)がスムーズなので、例えば部品の受け渡しが多い作業(I/O-boundタスク:ネットワークからデータを読み込み、それを加工してファイルに書き出す、など)には非常に向いています。I/O待ちの時間に、別の作業員が別の作業を進められるため、効率が上がります。
    • 苦手なこと: Pythonでは、GIL(Global Interpreter Lock)という「一度に一人しか機械を使えない」というルールがあります。そのため、どれだけ作業員がいても、同時に多くの機械(CPUコア)を使うことができません。脳みそをたくさん使うような複雑な計算(CPU-boundタスク)を複数の作業員で同時に行っても、GILの制約で真の並列性は得られにくいです。
  • ProcessPoolExecutor:複数の独立した工場

    • これは、複数の独立した工場(プロセス)がそれぞれ独自の製品を生産するイメージです。
    • 得意なこと: 各工場は完全に独立しているので、それぞれの工場が全力で機械を稼働させられます(GILの制約を受けない)。つまり、CPUのコアを最大限に活用できるため、膨大な計算(CPU-boundタスク:画像処理、数値計算、データ分析など)を複数の工場に分担させることで、非常に高速に処理できます。
    • 苦手なこと: 工場間で部品(データ)をやり取りするには、トラックで運ぶ手間(オーバーヘッド)がかかります。そのため、頻繁にデータをやり取りするようなタスクには向いていません。

これらの例えが、プロセスとスレッド、そしてそれぞれのExecutorの概念を理解する手助けになれば幸いです。

concurrent.futures.Executor は、Pythonで非同期にタスクを実行するための高レベルなAPIを提供する抽象クラスです。これにより、スレッドやプロセスを直接管理することなく、並列処理を簡単に実装できます。

このモジュールには、主に2つの具体的な実装クラスがあります。

  • ThreadPoolExecutor: スレッドを使用して並行処理を実現します。I/Oバウンドなタスク(例:ファイルの読み書き、ネットワーク通信)に適しています。
  • ProcessPoolExecutor: プロセスを使用して並列処理を実現します。CPUバウンドなタスク(例:重い計算、データ処理)に適しています。

どちらのクラスも同じインターフェースを共有しているため、基本的な使い方は同じです。


Executor の基本的な使い方

Executor を使用する基本的な流れは以下の通りです。

  1. Executor のインスタンスを作成する: with 文を使って、ThreadPoolExecutor または ProcessPoolExecutor のインスタンスを生成します。with 文を使うことで、処理が終了した際に自動的にリソースが解放(shutdown()が呼ばれる)されます。
  2. タスクを投入(Submit)する: submit() メソッドまたは map() メソッドを使って、実行したい関数と引数を Executor に渡します。
  3. 結果を取得する: submit()Future オブジェクトを返します。このオブジェクトの result() メソッドを呼ぶことで、タスクの実行結果を取得できます。result() は処理が完了するまでブロックします。

submit() メソッド

submit(fn, *args, **kwargs) は、単一のタスクを投入し、即座に Future オブジェクトを返します。Future オブジェクトは、非同期処理の状態をカプセル化したものです。

Future オブジェクトの主なメソッド

  • result(timeout=None): タスクの結果を返します。タスクが完了していない場合は完了するまで待ちます。
  • done(): タスクが完了したかキャンセルされた場合に True を返します。
  • running(): タスクが現在実行中の場合に True を返します。
  • cancelled(): タスクが正常にキャンセルされた場合に True を返します。
  • add_done_callback(fn): タスク完了時に呼び出されるコールバック関数を登録します。

ThreadPoolExecutorsubmit() を使う例

import concurrent.futures
import time
import random

def task(name):
    """指定された時間だけスリープするダミーのタスク"""
    sleep_time = random.uniform(0.5, 2.0)
    print(f"タスク '{name}' 開始。{sleep_time:.2f}秒待機します。")
    time.sleep(sleep_time)
    return f"タスク '{name}' 完了"

# with文でExecutorを生成
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
    # 複数のタスクを投入
    future1 = executor.submit(task, "A")
    future2 = executor.submit(task, "B")
    future3 = executor.submit(task, "C")

    # Futureオブジェクトから結果を取得
    print(future1.result())
    print(future2.result())
    print(future3.result())

print("すべてのタスクが完了しました。")

このコードでは、最大3つのスレッドを持つプールを作成し、3つの task を非同期に実行しています。各タスクは submit された直後から実行が開始され、メインスレッドは result() が呼ばれた時点でタスクの完了を待ちます。


map() メソッド

map(func, *iterables, timeout=None, chunksize=1) は、組み込みの map() 関数と似ており、イテラブル(リストなど)の各要素に関数を適用します。submit() と異なり、結果はタスクが投入された順序でイテレータとして返されます。

map() は、同じ関数を異なる引数で何度も実行する場合に便利です。

ProcessPoolExecutormap() を使う例

import concurrent.futures
import time

def square(n):
    """数値を2乗して返すCPUバウンドな処理の例"""
    print(f"{n} の2乗を計算中...")
    time.sleep(1) # 重い計算をシミュレート
    return n * n

if __name__ == "__main__":
    numbers = [1, 2, 3, 4, 5, 6]

    # ProcessPoolExecutorを使用(CPUバウンドなタスクのため)
    with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
        # mapを使って各数値にsquare関数を適用
        results = executor.map(square, numbers)

        # 結果を順番に出力
        for result in results:
            print(f"結果: {result}")

    print("すべての計算が完了しました。")

注意: ProcessPoolExecutor を使用する際は、マルチプロセッシングの仕組み上、スクリプトが意図せず再実行されるのを防ぐために、メインの処理を if __name__ == "__main__": ブロック内に記述する必要があります。

map() は結果を順番通りに返すため、あるタスクが早く終わっても、それより前に投入されたタスクが終わるまで結果を取得できません。順不同でよい場合は、submit()as_completed() を組み合わせる方が効率的です。


as_completed() ヘルパー関数

concurrent.futures.as_completed(fs) は、Future オブジェクトのイテラブルを受け取り、いずれかのタスクが完了するたびにその Future を返すイテレータを返します。これにより、完了した順に結果を処理できます。

import concurrent.futures
import time
import random

def task(name):
    sleep_time = random.uniform(0.5, 3.0)
    time.sleep(sleep_time)
    return f"タスク '{name}' (所要時間: {sleep_time:.2f}秒)"

with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
    tasks = ["A", "B", "C", "D", "E"]
    futures = [executor.submit(task, t) for t in tasks]

    # 完了した順に結果を処理
    for future in concurrent.futures.as_completed(futures):
        print(future.result())

print("すべてのタスクが完了しました。")

この例では、実行時間が短いタスクの結果から順に表示されるため、全体の待ち時間を短縮できます。


ThreadPoolExecutor vs ProcessPoolExecutor

特徴 ThreadPoolExecutor ProcessPoolExecutor
並列化の単位 スレッド (Thread) プロセス (Process)
メモリ共有 同じメモリ空間を共有する 独立したメモリ空間を持つ
GILの影響 受ける(同時に複数のPythonコードを実行できない) 受けない(各プロセスが独自のPythonインタプリタを持つ)
オーバーヘッド 小さい(スレッド作成は軽量) 大きい(プロセス作成は重い)
適したタスク I/Oバウンド (ネットワーク、ファイルI/Oなど) CPUバウンド (数値計算、画像処理など)
データ交換 簡単(メモリを共有しているため) シリアライズが必要 (pickle)

簡単に言えば、待ち時間が多い処理(例:APIからのデータ取得)には ThreadPoolExecutor を、計算量が多い処理(例:大規模なデータ分析)には ProcessPoolExecutor を選ぶのが基本です。

concurrent.futures.Executor を使うことで、複雑な並行・並列プログラミングのロジックを隠蔽し、シンプルで読みやすいコードでタスクの非同期実行を実現できます。

I/Oバウンドなタスクに map を使うのは非常に効果的です。

例えば、「複数のURLからデータをダウンロードする」「たくさんのファイルを読み書きする」といったタスクは、まさに map の得意分野です。これらの処理はCPUをほとんど使わず、ネットワークやディスクの応答を待っている時間が大半を占めます。ThreadPoolExecutormap を組み合わせることで、その待ち時間を有効活用し、複数のI/O処理を効率的に並行実行できます。


I/Oバウンドと map の相性

ThreadPoolExecutormap は、イテラブル(リストなど)の各要素に対して、指定した関数を別々のスレッドで実行します。

import concurrent.futures
import requests

URLS = [
    'https://www.python.org/',
    'https://www.google.com/',
    'https://www.github.com/',
    'https://example.com/',
]

def download(url):
    """URLからデータを取得するI/Oバウンドなタスク"""
    try:
        response = requests.get(url)
        return f"{url} のサイズ: {len(response.content)} バイト"
    except requests.RequestException as e:
        return f"{url} でエラー: {e}"

# ThreadPoolExecutorで複数のダウンロードを並行処理
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
    # mapを使って、URLのリストに対してdownload関数を適用
    for result in executor.map(download, URLS):
        print(result)

この例では、requests.get() が実行されている間、スレッドはサーバーからの応答を待っているだけでCPUはほとんど使いません。その間に他のスレッドが別のURLへのリクエストを開始できるため、逐次的に実行するよりも圧倒的に早く処理が終わります。

submit()as_completed() を使う方法と比べて、map は**「全ての要素に同じ処理を行い、結果の順序が重要(または順序がどうでもよい)」**場合に、よりシンプルで直感的にコードを書けるというメリットがあります。


chunksize パラメータについて 🧠

chunksize は、主に ProcessPoolExecutormap を使う際のパフォーマンスを最適化するための重要なパラメータです。

ProcessPoolExecutor では、親プロセスと子プロセス間でデータをやり取りする必要があります。このデータ交換(引数や戻り値の受け渡し)には、オブジェクトをバイト列に変換するシリアライズという処理が伴い、これが意外とコストの高い処理になります。

chunksize の仕組み

map は、イテラブル(例:1000個の要素を持つリスト)を chunksize ごとの塊(チャンク)に分割します。そして、そのチャンク単位で子プロセスにタスクをまとめて渡します

  • chunksize=1 (デフォルト):

    • イテラブルの要素を1つずつ子プロセスに渡します。
    • 1000個の要素があれば、プロセス間のデータ交換が1000回発生します。
    • 個々のタスクの実行時間が長い場合に適しています。
  • chunksize=100:

    • イテラブルを100個ずつのチャンクに分割します。(1000個の要素なら10個のチャンク)
    • チャンクをまとめて子プロセスに渡すため、プロセス間のデータ交換は10回で済みます。
    • 個々のタスクの実行時間が非常に短く、データ交換のオーバーヘッドが無視できない場合に効果絶大です。

chunksize の効果

  • メリット: イテラブルのサイズが非常に大きい場合、chunksize を適切に設定することで、プロセス間通信のオーバーヘッドが劇的に減り、全体の処理速度が大幅に向上します。
  • デメリット: chunksize が大きすぎると、ある1つのチャンクの処理に時間がかかった場合、他の暇なプロセスにタスクを分散できなくなり、全体の効率が落ちる可能性があります。

ThreadPoolExecutorchunksize

ThreadPoolExecutor では、スレッドがメモリを共有しているため、プロセスのようなデータ交換のオーバーヘッドは基本的にありません。そのため、ThreadPoolExecutormap では chunksize パラメータは効果がありません(内部的には利用されていません)。

まとめ

Executor の種類 タスクの種類 map の利用 chunksize の設定
ThreadPoolExecutor I/Oバウンド 非常に有効 不要(効果なし)
ProcessPoolExecutor CPUバウンド 非常に有効 タスクの粒度に応じて調整すると効果大

結論として、I/Oバウンドな処理に map を使うのは全く問題なく、むしろ推奨される使い方です。その際、ThreadPoolExecutor を使うのであれば chunksize パラメータについて気にする必要はありません。

かーなびかーなび

すごい人の特徴

  • 早起き:4時位に起きて6時には仕事している人もいる
  • 運動をしている:ジム or ランニングを習慣的に行っている
  • もともと体力のいるスポーツをやっていた:野球、サッカー、水泳(全国大会出場などもザラ)etc.
  • 公式ドキュメントを読む
かーなびかーなび

負荷試験 Locust

設定値について:

  • Number of users:並列数
  • Ramp up(Spawn rate):1秒あたりにどれくらい並列数が増えていくのか。
    • 400usersの時Ramp up=1だと毎秒1userずつ増えていって400usersに到達するまで6分30秒とかかかるのでRamp up=2とかにすることもある
    • スパイクみたいに一瞬でユーザが増えるシミュレーションする場合、Ramp up=100などにするが、Cloud runがスケーリングに対応できず失敗してしまうので注意
  • Advanced options > Run time:負荷試験の継続時間
    • 10m(10分)で設定すると、4分くらいで400人には到達していて、そのあと5-6分ずっと負荷をかけ続ける試験になっている

負荷試験におけるLocustの見方

https://developers.play.jp/entry/2023/02/03/143619

  • Fails:負荷をかけて失敗した数
かーなびかーなび

PRの書き方

gemini

GitHubプルリクエストにおける理想的な最初のコメントの書き方

1. はじめに:効果的なコードレビューの準備

現代の協調的なソフトウェア開発ワークフローにおいて、プルリクエストは基本的なプラクティスとして広く採用されています。コードレビュープロセスは、コードの品質、知識の共有、チームの連携を確保するための重要なステップです。プルリクエストにおける最初のコメントは、作成者とレビュー担当者間の最初の接点であり、レビュープロセス全体のコンテキストとトーンを設定する上で非常に重要です。十分に練られた最初のコメントは、コードレビューの効率と効果に大きな影響を与え、レビューサイクルの回数を減らし、フィードバックの質を向上させる可能性があります。

2. 質の高い最初のコメントの重要性

プルリクエストの作成者にとって、最初のコメントは変更の背後にある理由を明確に説明する機会となります。また、レビュー担当者の注意を特定の懸念事項や興味のある領域に向けるのに役立ちます。潜在的な質問に事前に答えることで、よりスムーズなレビューにつながる可能性があります。さらに、導入される変更のドキュメントとしての役割も果たします 1。変更の背後にある「なぜ」を明確に説明することで、作成者はレビュー担当者間の理解を深め、実装の詳細だけでなく、アプローチ自体に関するより洞察に満ちたフィードバックにつながる可能性があります。レビュー担当者が解決しようとしている問題を理解し、選択されたソリューションの背後にある理由を理解すると、設計上の選択肢をより適切に評価し、代替案や改善されたアプローチを提案できるようになります。これは、単に構文エラーやコーディング規約の遵守を確認するだけではありません。

コードレビュー担当者にとって、最初のコメントは必要なコンテキストを事前に提供することで、認知的負荷を軽減します 2。変更の目的と範囲を明確にすることで、時間を節約できます。より集中的で効率的なレビューが可能になり、レビューの所要時間が短縮されます。作成者の意図を理解することで、フィードバックの質が向上します 3。レビュー担当者は、作成者と同じレベルのコンテキストを持たずにプルリクエストに臨むことがよくあります。優れた最初のコメントは、このギャップを埋め、変更内容をより効果的に、より少ない労力でレビューできるようにします 55 で強調されているように、レビュー担当者はコードやその背後にある理由を理解できないためにコメントを残すことがあります。詳細な最初のコメントは、必要な背景情報と説明を提供することで、これに積極的に対処します。

3. 理想的な最初のプルリクエストコメントの必須セクション

  • 3.1. 概要/目的

    プルリクエストの主な目的を明確かつ簡潔に述べます 2。変更が解決しようとしている問題や、導入する新しい機能を説明します 2。プロジェクト管理ツール(例:Jira)の関連する課題、ユーザーストーリー、またはタスクを参照します 3。プルリクエストを特定の課題やタスクにリンクすることで、重要なトレーサビリティとコンテキストが提供され、レビュー担当者はプロジェクトの目標内における変更のより広範な目的を理解できます。この関連付けは、コードの変更がプロジェクトの要件と一致していることを確認するのに役立ち、将来の議論や調査のための参照ポイントを提供します。

  • 3.2. 背景とコンテキスト

    レビュー担当者が変更内容を完全に理解するために必要な背景情報を提供します 3。特に、検討された代替ソリューションがあった場合は、選択されたアプローチの背後にある理由を説明します 3。変更の根拠となった関連する設計ドキュメント、仕様書、または以前の議論へのリンクを記載します 3。技術的な決定(例:ループの代わりに再帰メソッドを選択する 3)の背後にある「なぜ」を明示的に述べることで、不必要な議論を防ぎ、レビュー担当者が作成者の思考プロセスを理解するのに役立ちます。理由を説明することで、作成者は実装だけでなく、設計自体に関する、より情報に基づいたフィードバックを促します。これにより、より堅牢で十分に検討されたソリューションにつながる可能性があります。

  • 3.3. 変更点の概要

    コードに加えられた主要な変更の概要を説明します 3。変更の影響を受けた主な領域またはコンポーネントを強調します 6。明確さと読みやすさのために、箇条書きまたは番号付きリストを使用します 6。簡潔な概要により、レビュー担当者はコードをすぐに掘り下げることなく、変更の範囲を迅速に把握できます。これにより、レビューの取り組みに優先順位を付けることができます。レビュー担当者は、詳細なコード差分を調べる前に、この概要を使用して変更のメンタルモデルを形成できるため、レビュープロセスがより効率的になります。

  • 3.4. 影響範囲

    これらの変更がシステムまたはアプリケーションのさまざまな部分に与える可能性のある影響について説明します 1。レビュー担当者が注意すべき潜在的なリスクまたは副作用を特定します。変更にデータベースの移行、APIの変更、またはその他の重要なインフラストラクチャの更新が含まれるかどうかを指定します。影響範囲を明確にすることで、レビュー担当者はプルリクエストのマージの潜在的な結果を理解し、より慎重な検討が必要な領域に焦点を当てることができます。この情報は、変更が意図せずにリグレッションを引き起こしたり、システムの他の部分に悪影響を与えたりしないことを保証するために不可欠です。

  • 3.5. レビューの焦点/ガイダンス

    レビュー担当者に特に注意してほしいコードの特定の側面について指示します 4。疑問点がある場合や、より徹底的なフィードバックをいただけるとありがたい場合は示します 1。プルリクエストに複数のファイルや複雑な変更が含まれる場合は、変更をレビューする特定の順序を提案します 4。レビュー担当者に明確なガイダンスを提供することで、レビュープロセスの効率と焦点が大幅に向上し、作成者は最も重要な領域に関するフィードバックを確実に受け取ることができます。これにより、レビュー担当者が重要でない側面に時間を費やしたり、作成者が特に入力を必要とする領域を見落としたりすることを防ぐことができます。

  • 3.6. 検証手順

    レビュー担当者が変更をテストおよび検証する方法に関する明確かつ簡潔な手順を提供します 1。必要な場合は手動テストの手順を含め、確認する特定のシナリオを概説します 7。変更をカバーする関連する自動テストを指摘します 3。変更がテストされた環境について言及します 7。明確な検証手順を提供することで、レビュー担当者は変更の機能と正確性を簡単に確認でき、マージされたコードに対する信頼が高まります。これにより、メインコードベースにバグが導入されるリスクが軽減され、意図した機能が期待どおりに動作することが保証されます。

  • 3.7. 質問と議論のポイント

    コードまたはアプローチに関してレビュー担当者に質問したい特定の質問を明示的に提起します 1。潜在的な議論や代替の見解が予想される領域を強調します。レビュー担当者に意見や提案を共有するよう促します。潜在的な論点や不確実性について積極的に議論を開始することで、より協調的で生産的なレビュープロセスにつながり、共通理解とより良い意思決定が促進されます。これにより、オープンなコミュニケーションが奨励され、さまざまな視点の検討が可能になり、より堅牢で包括的なソリューションにつながる可能性があります。

4. 高業績チームからの学び

調査スニペットを分析して、最初のプルリクエストコメントに関して、成功したエンジニアリング組織が採用している一般的なパターンとベストプラクティスを特定します。コンテキスト、明確な要約、レビュー担当者へのガイダンスの重要性など、繰り返されるテーマを強調します 2。プルリクエストを小さく、焦点を絞ったものにすることの重要性について説明します 2。一般的に、小さなプルリクエストはレビューが容易で迅速であり、フィードバックサイクルが速くなり、レビュー担当者の認知的負荷が軽減されます 2。変更が小さく、より管理しやすい塊に分割されると、レビュー担当者はより効果的に注意を集中し、より徹底的なフィードバックを提供できます。これにより、大規模で複雑なプルリクエストで重要な問題を見落とすリスクも軽減されます。UIの変更については、スクリーンショットや録画を含める習慣に注目します 1。視覚的な補助は、UI関連の変更の理解を大幅に向上させ、レビュー担当者が視覚的な影響とユーザーエクスペリエンスを評価しやすくします。スクリーンショットや動画は変更の具体的な証拠を提供し、曖昧さを減らし、UIに関するより効果的なフィードバックを促進します。より良い組織化と追跡のために、ラベルの使用と関連する課題やプロジェクトへのリンクを強調します 3。レビューのために送信する前に、プルリクエストを自己レビューすることを推奨します 3。プルリクエストを送信する前にコードを自己レビューすることで、明らかなエラーを捕捉し、作成者が自身の変更を十分に理解していることを保証し、最初の問題が少なくなり、より生産的なレビュープロセスにつながります 4。このステップはオーナーシップと責任感を示し、作成者が可能な限り最高の形で自分の作業を提示し、レビュー担当者の注意をより実質的な側面に集中させることができます。

表 1:調査スニペットにおける最初のPRコメントの共通要素

スニペットID 要素 記載
2 目的 はい
2 変更点の概要 はい
2 スクリーンショット はい
3 目的 はい
3 変更点の概要 はい
3 背景情報 はい
3 関連する課題 はい
3 検証方法 はい
4 目的 はい
4 変更点の概要 はい
4 関連する課題 はい
4 レビュー担当者へのガイダンス はい
4 検証方法 はい
13 目的 はい
13 変更点の概要 はい
13 関連する課題 はい
13 レビュー担当者へのガイダンス はい
2 目的 はい
2 変更点の概要 はい
4 目的 はい
4 変更点の概要 はい
4 背景情報 はい
4 関連する課題 はい
4 レビュー担当者へのガイダンス はい
1 変更点の概要 はい
1 変更理由 はい
1 実施内容 はい
1 変更の詳細 はい
1 未実施内容 はい
1 影響範囲 はい
1 検証方法 はい
1 課題 はい
1 追加事項 はい

5. プルリクエストテンプレートの力

プルリクエストテンプレートを使用して、最初のコメントの構造を標準化することの利点について説明します 1。チームまたは組織内のすべてのプルリクエストで一貫性を確保します 18。作成者に必要なすべての情報を提供するよう促し、重要な詳細を見落とす可能性を減らします 18。定義済みの構造を提供することで、作成者とレビュー担当者の両方の時間を節約できます 18。さまざまなチームやプロジェクトの特定のニーズとワークフローに合わせてカスタマイズできます 18。調査資料から、効果的なテンプレートに見られる一般的なセクションの例を示します 1。これには、タイトル 10、目的/目標 7、関連する課題/リンク 7、変更点の概要 6、影響/範囲 1、レビューポイント/ガイダンス 6、検証手順/テスト方法 1、質問/懸念事項 1、除外されたコンテンツ 1、スクリーンショット/動画 1、チェックリスト(例:テスト、ドキュメント) 6 などが含まれます。GitHubリポジトリ内でプルリクエストテンプレートを実装および管理する方法について説明します(例:.github/PULL_REQUEST_TEMPLATE.md を使用) 6。プルリクエストテンプレートを実装することで、コードレビューへのより規律があり一貫したアプローチを促進し、不可欠な情報が常に提供されるようにし、より効率的で効果的なフィードバックにつながります 12。標準化された構造を提供することで、テンプレートは作成者の認知負荷を軽減し、含めるべき情報についてガイダンスを提供し、レビュー担当者が変更を理解するために必要な情報を簡単に見つけられるようにします。

6. 明確かつ簡潔な最初のコメントを書くためのヒント

明確かつ簡潔な言語を使用することの重要性を強調します 4。変更に慣れていない人の視点から書くよう作成者にアドバイスします 2。最初のコメントを作成する際に「読者第一」の考え方を採用することで、さまざまなレベルのコンテキストを持つレビュー担当者にとって情報が理解しやすくなります 5。これには、潜在的な質問を予測し、必要な情報を事前に提供することで、レビュー担当者が明確にする必要性を減らすことが含まれます。読みやすさを向上させるために、書式設定(例:見出し、箇条書き、コードブロック)を使用することをお勧めします。最初のコメントを焦点を絞ったものにし、不必要な詳細を避けることを提案します。プルリクエストを送信する前にコメントを校正するよう作成者にアドバイスします 4。変更の「内容」と「理由」を要約することの利点を強調します 2。何が変更されたのか、そしてなぜそれらの変更が行われたのかを明確に説明することは、効果的な最初のプルリクエストコメントの基本であり、レビュー担当者に不可欠なコンテキストを提供します 2。変更の「内容」は技術的な修正についてレビュー担当者に知らせ、「理由」は根拠とコンテキストを提供し、より包括的に変更を評価できるようにします。

7. 結論:効率的なコードレビュー文化の育成

効果的な最初のプルリクエストコメントの重要性と構造に関する主要なポイントを繰り返します。十分に練られた最初のコメントが、エンジニアリングチーム内のポジティブで生産的なコードレビュー文化にどのように貢献するかを強調します 20。明確なコミュニケーションと効率的なフィードバックループを重視する文化は、効果的なPRコメントによって促進され、コードの品質の向上、開発サイクルの高速化、チームコラボレーションの改善につながります 12。コードレビューが効率的で建設的である場合、エンジニアはプロセスに積極的に参加する可能性が高くなり、継続的な改善と知識共有のサイクルにつながります。チームが特定のニーズに合わせてプルリクエストテンプレートを採用およびカスタマイズすることを推奨します。コードレビュープロセス中の建設的な議論と情報に基づいた意思決定の準備における最初のコメントの役割を強調します 3。効果的なプルリクエストプラクティスへの投資の利点について、前向きで励みになるメッセージで締めくくります。


Pull Requestで最初に書く文章の章立てとして、以下の構成が理想的です。

1. タイトル

  • 簡潔かつ内容を特定しやすいタイトルをつけます。
  • 例:「〇〇機能の実装」「〇〇に関するバグ修正」「〇〇のリファクタリング」など、変更の目的がひと目でわかるように記述します。
  • 必要に応じて、関連するIssue番号をタイトルに含めることも有効です。

2. 概要

  • このプルリクエストで何を実現しようとしているのかを簡潔に説明します。
  • 背景や目的を明確にすることで、レビュー担当者が変更の意図を理解しやすくなります。
  • 例:「本プルリクエストでは、ユーザーが〇〇を行えるように△△機能を追加します。」

3. 詳細な変更内容

  • 具体的な変更点や修正箇所を箇条書きなどで示します。
  • 技術的な詳細や実装上の工夫などを記述することで、より深い理解を促せます。
  • 関連するファイルやディレクトリを明示することも有効です。

4. 関連Issue/背景情報

  • このプルリクエストが解決するIssueや、関連する背景情報へのリンクを記載します。
  • Issue管理システムとの連携により、変更の経緯や議論の流れを追跡しやすくなります。

5. レビュー観点/注意点

  • レビュー担当者に特に注目してほしい点や、確認してほしい事項を伝えます。
  • 例:「〇〇のパフォーマンスについて重点的にレビューをお願いします。」「△△に関するテストケースを追加しましたので、ご確認ください。」

6. テスト方法

  • 変更を検証するための具体的な手順を記載します。
  • 動作確認の手順、テストデータ、期待される結果などを明確にすることで、レビュー担当者が効率的にテストを実施できます。

7. その他

  • 上記以外に伝えるべき事項があれば、ここに記述します。
  • 例:「未解決の課題が残っています。」「今後の対応予定について」など。

これらの章立てで記述することで、プルリクエストの意図、内容、レビューポイントが明確になり、スムーズなコードレビューとマージにつながります。

かーなびかーなび

PRの書き方

chatgpt

了解です。GitHubのPull Request(PR)の理想的な書き方と、その理由を多角的かつ網羅的にまとめます。開発者やレビュアーの視点、チーム運用の観点、コラボレーション効率なども考慮しながら整理します。
少しお時間ください。調査が終わり次第ご報告します。

GitHubのPull Request: 理想的な書き方と推奨される理由

PRタイトルと本文の理想的な構成

シンプルで具体的なタイトル: Pull Requestのタイトルは一目で変更内容がわかるよう簡潔かつ情報豊富に書きます。 (Best practices for GitHub pull request descriptions) (Pull Requests are a reflection of your engineering culture — by Igor Šarčević)

例えば「Fix overflow bug in user profile modal(ユーザープロフィールモーダルでのオーバーフロー不具合の修正)」と書けば、単に「Bug fix」などと書くより何を修正するのか明確です (Best practices for GitHub pull request descriptions)。可能であれば何を・なぜを含め、ビジネス上の目的や課題も示唆すると尚良いとされています (Pull Requests are a reflection of your engineering culture — by Igor Šarčević)(例:「スケジューリング戦略を最適化してサーバー費用を削減」など)。タイトル自体が履歴に残る重要な情報源なので、後で見た人にも意味が伝わるようにしましょう。

本文の基本構成: PR本文では変更の背景や内容を体系立てて説明します。一般に以下のような項目を含めると効果的だとされています (Best practices for GitHub pull request descriptions) (Helping others review your changes - GitHub Docs):

かーなびかーなび
chatgpt

以上のようにテンプレート的な構成で書くことで、漏れなく情報提供できます。 (Best practices for GitHub pull request descriptions) (Best practices for GitHub pull request descriptions)実際、多くのプロジェクトが「Pull Requestテンプレート」を用意し、上記のような項目をあらかじめ記入欄として設けています (Best practices for GitHub pull request descriptions) (GitHubのPull Requestテンプレートに何を書いているか? | VISITS TechBlog)。テンプレートに沿って記入すれば、誰がPRを書いても基本的な情報が揃うためレビュワーは安心ですし、書き手も何を書けばよいか迷わずに済みます。 (Best practices for GitHub pull request descriptions) (Code Reviews Should Not Suck! - DEV Community)

また、1つのPRは1つの目的に絞るのが理想です。 (Good Manners of a Pull Request & Some Best Practices | by M. Kerem Keskin | Delivery Hero Tech Hub | Medium) (Helping others review your changes - GitHub Docs) 修正と機能追加を混ぜたり、複数の問題をまとめて解決しようとせず、単一の課題にフォーカスしましょう。これはいわゆる「シングル・レスポンシビリティ原則」をPR単位でも適用するイメージです (Good Manners of a Pull Request & Some Best Practices | by M. Kerem Keskin | Delivery Hero Tech Hub | Medium)。関連しない変更を含めると説明もしづらくなり、レビューや将来の履歴追跡が複雑になります。どうしても小さな修正(ついでのリファクタやタイポ修正など)を入れたい場合は、レビュワーに断った上で別PRに分けるか、このPRの範囲外であることを明示すると良いでしょう (Good Manners of a Pull Request & Some Best Practices | by M. Kerem Keskin | Delivery Hero Tech Hub | Medium) (プルリクエストを使う3つの理由について | 名古屋のシステム・ウェブ開発・株式会社ウェブネーション)。

(補足:Draft PRの活用)

作業途中であっても早めにレビュー意見が欲しい場合は、Draft状態でPRを作成して共有する手もあります (Pull Requestは書き方が9割 #GitHub - Qiita)。Draft Pull Requestにしておけば誤ってマージされる心配がなく、実装途中のコードでも「WIP: ~」としてチームに見せフィードバックを得られます (Pull Requestは書き方が9割 #GitHub - Qiita)。ただしDraftのまま放置しないよう、準備が整ったら本文を整備して正式なレビュー依頼に切り替えましょう。

なぜそのような書き方が望ましいのか

適切に構成されたPRは、コードレビューの効率と成果を大きく向上させます。以下、レビュー効率・バグ防止・ドキュメンテーションの観点からその理由を説明します。

レビュー効率の向上

十分な文脈と情報が与えられたPRは、レビュワーが変更内容と意図を素早く理解できるため、レビューにかかる時間と手戻りが大幅に削減されます。 (Helping others review your changes - GitHub Docs)

GitHub公式ドキュメントでも「明確な文脈を提供することで、レビュー工程はより迅速かつ円滑になり、無駄な往復が減る」と説明されています (Helping others review your changes - GitHub Docs)。実際、説明不足のPRではレビュワーが開発者に追加質問を投げ、その回答を待つ間に少なくとも数時間~1日が無駄になることもあります(タイムゾーンが異なればなおさらです) (Code Reviews Should Not Suck! - DEV Community)。反対に、最初から必要情報が揃っていればこうした不毛なタイムロスを防げます。

また、PRの粒度が適切(小さく焦点が合っている)であることもレビュー効率に直結します。小さく目的が明確なPRはレビューしやすくマージもしやすいですし、バグを持ち込む余地も少なく、変更履歴も明快です (Helping others review your changes - GitHub Docs)。ある研究では、コードレビューの効率はある程度までコード量に比例し、その後急激に低下することが示されています (Code Reviews Should Not Suck! - DEV Community)。90分以上かけて大量の変更を見続けると人間の脳は判断力が鈍り、400行を超えるような差分では指摘漏れが増えるため、PRは400行程度までに留めるのが望ましいという報告もあります (Code Reviews Should Not Suck! - DEV Community)。このように適度なサイズと明確な説明のPRは、レビュワー一人ひとりの負担を減らし、結果としてチーム全体のフロー効率を上げます。

情報が整理されたPRはレビュワーが本質的なコード品質や設計の議論に集中できるという効果もあります (Support the Reviewers with detailed Pull Request descriptions - DEV Community)。背景や意図の推測にエネルギーを割かれないため、レビューコメントも的確で建設的なものになりやすく、レビューの質自体も向上します。結果としてバグの見落としが減り、改善点の発見も増えるでしょう。これはチームにとって大きなメリットです。

バグ防止・コード品質向上

理想的なPR記述はバグの予防にもつながります。開発者がPR本文を書く過程で、自身のコードを振り返り整理するため、レビュー前に不備に気づくことが多いからです。 (Support the Reviewers with detailed Pull Request descriptions - DEV Community)

「なぜこの変更が必要か」「この実装で目的を達成できているか」を文章化することは、一種のセルフレビュー(自己検証)です。自分では理解しているつもりでも、他人に説明するとなると理解不足が露呈する――という経験はないでしょうか? 開発者にとって、説明できない箇所は理解できていない箇所です。説明を書く途中で矛盾や抜け漏れに気づけば、その時点でコードを修正できます (Support the Reviewers with detailed Pull Request descriptions - DEV Community)。丁寧なPRを書くことは、結果的に開発者自身のコード理解度を高め、バグの温床を事前に潰す機会を与えてくれます (Support the Reviewers with detailed Pull Request descriptions - DEV Community)。

さらに、PRにテスト結果や再現手順を書く習慣は、テスト漏れの防止につながります。説明を書く段階で「このケースはテストしただろうか?」と自然に思い至り、不十分なら追加の確認をするでしょう。多くのプロジェクトで「PR提出前にローカルテストやLintをすべて通す」ことをチェックリストにしているのも、レビュー前に明らかな不具合を除去するためです (Pull Requestは書き方が9割 #GitHub - Qiita) (Pull Requestは書き方が9割 #GitHub - Qiita)。実際、PR作成前にコンソールエラーやタイポが残っていないか確認することは基本中の基本であり (Pull Requestは書き方が9割 #GitHub - Qiita)、これを怠るとレビュワーから「テストが落ちています」「デバッグ用のログが残っています」と指摘され、往復のやり取りが発生します。きちんと自己チェックした上でPRを書けば、レビューはより本質的な議論に集中でき、表面的なバグ修正に時間を割かれなくなります。

また、明確なPRにはCI(継続的インテグレーション)の結果(テストのグリーンなど)やリンタのスコア等、客観的な品質シグナルが伴いやすいという指摘もあります (Pull Requests are a reflection of your engineering culture — by Igor Šarčević)。不明瞭なPRだと「テスト通っているのかな?品質は大丈夫か?」と不安になりますが、理想的なPRは説明とともにそうした品質指標も示され、レビュワーが安心してマージ判断できる傾向があります (Pull Requests are a reflection of your engineering culture — by Igor Šarčević)。

かーなびかーなび
chatgpt

ドキュメンテーション(記録)の観点

書き方の整ったPRは、そのまま貴重なドキュメントとして機能します。PRは「コードを出すついでに書くものではなく、仕様書や開発資料と同じくドキュメントの一種」である、という認識が重要です (Pull Requestは書き方が9割 #GitHub - Qiita)。なぜならPRは第三者(レビュワーや将来の開発者)が読むものであり、後から見返すことも多い資料だからです (Pull Requestは書き方が9割 #GitHub - Qiita)。実際、製品の寿命が長くなれば何年も開発が続きメンバーも入れ替わります。後任のエンジニアが過去の変更に疑問を持ったとき、当時の担当者は既にいなかったり詳細を忘れているかもしれません。 (GitHubのPull Requestテンプレートに何を書いているか? | VISITS TechBlog)しかし丁寧に書かれたPRが残っていれば、そこから当時の状況や判断理由を読み取ることができます (GitHubのPull Requestテンプレートに何を書いているか? | VISITS TechBlog)。これはプロジェクトの知的財産とも言える蓄積です。

また、PRの記述がしっかりしていれば、リリースノートやユーザ向けドキュメントの作成にも役立ちます。例えばオープンソースプロジェクトでは、PRのタイトルや本文から変更点をまとめてCHANGELOGを生成したり、リリース記事を書くことがあります。その際に、「Fix bug #123」ではなくユーザ目線で意義がわかるタイトルや、背景を含めた説明が書かれていると、そのまま外部への説明にも再利用できます。企業内でも、プロジェクトマネージャーがPR記述を参考に社内報告やユーザ説明を行うケースは多いです。要するに、PRは半永続的な情報源なので、後で自分や他人が読んだとき価値あるものになるよう書いておくことは、長期的な開発効率とプロダクト品質に寄与します。 (Pull Requestは書き方が9割 #GitHub - Qiita) (GitHubのPull Requestテンプレートに何を書いているか? | VISITS TechBlog)

最後に、丁寧なPRを書くこと自体がチームのコミュニケーション活性化にもつながります。十分な情報提供はレビューを円滑にし、結果としてレビュワーからのフィードバックも充実します。双方向の建設的な議論が生まれやすくなり、PRが設計や仕様の議論の場として機能することもあります。「コードを書いて終わり」ではなく、PRというドキュメントを通じてチームで知識共有し合うことで、開発プロセス全体の質と透明性が向上します。

開発者・レビュアー・プロジェクトマネージャー視点のメリット

理想的なPRの書き方は、関わるすべての立場の人にとってメリットをもたらします。それぞれの視点から主な利点を整理します。

開発者にとってのメリット

  • 自己レビューによる品質向上: 上述の通り、詳細なPRを書くプロセスは自分の実装を見直す機会となり、結果的にバグの発見や設計の改善につながります (Support the Reviewers with detailed Pull Request descriptions - DEV Community)。説明を書けない箇所があれば理解が不十分な証拠なので、それを埋めることで自分のスキルアップにもなります。
  • スムーズなレビューと早いマージ: 十分な情報を提供すればレビュワーからの質問が減り、修正依頼も的確になります。その結果、レビューサイクルの往復回数が減って早く承認が得られる傾向があります。開発者にとって自分の実装が素早く本番に反映されるのは嬉しいことですし、ストレスの軽減にもなります。
  • 建設的なフィードバックの獲得: 背景や狙いを共有することで、レビュワーから「何がしたいか分からない…」と言われることはなくなります。代わりに設計や最適化に関する有益なアドバイスをもらえる可能性が高まります。これは開発者自身の成長機会にもなります。
  • 大きすぎる変更への気付き: 説明を書いていて「あれもこれも書くことが多すぎる…」と感じたら、それはPRのスコープが広すぎるサインです (Support the Reviewers with detailed Pull Request descriptions - DEV Community)。開発者はこの段階で変更を分割する判断ができ、結果として小さなPRに分けることでレビューが通りやすくなります。PRを書きながら軌道修正できるのも開発者のメリットです。
  • チーム内での信頼向上: いつも丁寧で分かりやすいPRを書いていると、チームから「この人のPRなら安心してレビューできる」と評価されるようになります。コードそのものだけでなく、コミュニケーション面でも信頼を得ることは、将来的にリーダーシップを任されたり自身の提案が通りやすくなったりすることにもつながるでしょう。
  • 将来の自分を助ける記録: 人間は時間が経つと自分の書いたコードの意図すら忘れるものです。その点、しっかり書かれたPRは自分自身へのメモにもなります。数ヶ月後に「この変更、どういう理由だっけ?」となった時、書いた本人がPR説明を読んで思い出せることもあります。これはバグ修正やリファクタリングの際の助けになります。
かーなびかーなび
chatgpt

レビュワーにとってのメリット

  • 時間と労力の節約: 情報が揃ったPRは、レビュワーがいちいち動作確認や仕様の把握に時間を割く必要を減らします。場合によってはレビュワー1人あたり1時間以上の節約になるとも言われます (Support the Reviewers with detailed Pull Request descriptions - DEV Community)。開発者が説明に1時間かけても、3人のレビュワーが各1時間ずつ節約できればチーム全体ではプラスです。
  • レビューフォーカスの向上: コンテキストが明確なため、レビュワーはコードの中身に集中できます (Support the Reviewers with detailed Pull Request descriptions - DEV Community)。実装方針についての無駄な推測をせずに済み、ロジックの正しさやベストプラクティス遵守、影響範囲の検証といった本質的なレビューに注力できます。その結果、レビューの質が高まり、開発者への建設的な提案もしやすくなります。
  • 安心感と判断の容易さ: 十分な説明とテスト結果が示されたPRであれば、レビュワーは安心して承認できます。逆に情報が欠けていると「見落としがあるのでは」「この変更で本当に問題が解決するのか」と不安になり、追加の確認作業に時間を取られます。理想的なPRはそうした不安を取り除き、レビュワーが自信を持って「LGTM (Looks Good To Me)」と言える状態を作ります。
  • 一貫したレビュー基準の維持: プロジェクトによってはPRテンプレート内に「レビュー観点」やチェックリストを含めていることがあります (GitHubのPull Requestテンプレートに何を書いているか? | VISITS TechBlog)。レビュワーにとっても、それが常に提示されていることで見逃し防止や基準のブレ防止になります。例えば「セキュリティ観点での確認項目」が毎回書かれていれば、レビュワーは忘れずにその点をチェックできます。このように、良いPRはレビュワーのレビュー作業自体をガイドしてくれる面もあります。
  • モチベーションの維持: 常に読みやすいPRが来る環境では、レビュワーの心理的負担も軽減されます。対照的に、説明がなく大きすぎるPRばかりだとレビュワーは疲弊し、「レビューが面倒だ」と敬遠しがちです。理想的なPRを書く文化はレビュワーのモチベーションを保ち、コードレビュー文化の定着にも寄与します。

プロジェクトマネージャー(PM)・チーム全体へのメリット

  • 進捗管理とチームの透明性: PRに関連チケットや目的が明記されていることで、PMはそれを追跡するだけで「何が完了し何が未対応か」を把握できます (GitHubのPull Requestテンプレートに何を書いているか? | VISITS TechBlog)。例えばJIRAなどチケット駆動の開発では、PRからチケットURLに飛べたり、逆にチケットからPRがリンクされていることで、進捗状況が一目瞭然です。これはプロジェクト管理の効率を上げ、関係者間の情報共有コストを下げます。
  • 品質とリリースの予測可能性向上: PR毎にテスト状況やデプロイ手順、リスクが共有されていれば、PMはリリース計画を立てやすくなります。例えば「このPRはマイグレーションが必要だからリリースには○時間余分に見ておこう」「この変更はユーザー通知が必要だ」といった判断を事前に下せます。PRの情報充実は、リリース時のサプライズを減らすことにつながります。
  • プロジェクトナレッジの蓄積: しっかり書かれたPRが蓄積されていくことは、組織の知見のデータベース化でもあります。PMやリードエンジニアは過去のPRを参照することで、以前似た問題にどう対処したか、ある機能の設計意図は何だったかを学ぶことができます。それにより将来の計画や意思決定に活かせる情報が増えます。ドキュメントとしてのPRは組織の記憶として機能します。
  • QAや他部門との連携効率: チームによってはQA(品質保証)担当やデザイナー、カスタマーサポートなどもPRを参照します。特にスタートアップや小規模チームではPM自らが受け入れテストを行ったりもします。例えばWantedly社の事例ではQA専門職が不在のためPMが主にテストを行っており、開発者とレビュワーも協力して手元でテストする文化をとっています (GitHubのPull Requestテンプレートに何を書いているか? | VISITS TechBlog)。この場合、PRにテスト項目やスクリーンショットがあるとPMの確認作業が大いに助かり、他部門への周知もスムーズになります。つまり、PRを通じた部門間連携が進み、結果としてプロジェクト全体のスピードと品質が底上げされます。
  • チーム文化・エンジニアリング文化の醸成: PRはチームのコミュニケーションの核であり、その質はエンジニアリング文化の指標でもあります (Pull Requests are a reflection of your engineering culture — by Igor Šarčević)。常に明快なPRを書きレビューし合う文化は、相互信頼と責任感のある開発態勢を築きます。PMにとっても、そうした文化のもとでは安心して権限移譲ができ、各自が自律的に動いてくれるというメリットがあります。逆にPRが形骸化しているチームはコミュニケーション不足や属人化のリスクが高まり、プロジェクトマネジメント上の不安要素となります。理想的なPRを書くことは、健全なチーム運営にも寄与するのです。

よくある悪いPRの例とその弊害

反面教師として、悪いPRの典型とそれによる弊害も押さえておきましょう。以下に頻出する例を挙げます。

  • 内容が不明瞭なPR: タイトルや説明がほとんど書かれておらず、何をしたいのか分からないPRです。例えばタイトルが「update index.js」だけ、本文は空欄か「fix bug」とだけ書かれているようなケースです。こうしたPRは目的や理由が明示されていないため、レビュワーは「何のための変更か?」「この実装方針で良い理由は?」と頭を悩ませることになります。 (Pull Requests are a reflection of your engineering culture — by Igor Šarčević)説明不足のPRはレビュワーにとって悪夢であり、結局レビュワー側で元Issueや関連コードを探し回ったり、開発者に質問して回答待ちしたりと余計な手間が発生します。その結果、レビューが遅延し、下手をすると誤解に基づいた指摘や見落としが起きてバグを招く恐れもあります。
  • 一度に変更が大きすぎるPR: 差分が何百ファイル・数千行にも及ぶ巨大なPRや、複数の機能改修がまとめられたPRです。変更範囲が広範すぎるとレビュワーは全体を把握しきれず、重要な問題を見逃したりレビューを諦めてしまうことさえあります。実際、人間の集中力には限界があり、レビュー効率は400行を超えると急激に低下するとの報告があります (Code Reviews Should Not Suck! - DEV Community)。巨大PRではレビュワーが疲弊し、「とりあえず動くならOK」と細部の検証を怠ってしまうケースもあります。また、万一不具合が見つかっても部分的にリバート(巻き戻し)するのが難しく、プロジェクト全体のリスクが高まります。大きすぎるPRはレビュー遅延と品質低下の両方を招きかねません。
  • 複数の目的が混在したPR: 単一のPRで無関係な変更が混ざっている例です。例えば「新機能Aの実装」と「たまたま見つけた別件のバグ修正」が一つのPRに入っている場合などが該当します (Pull Requests are a reflection of your engineering culture — by Igor Šarčević)。これではレビューの焦点が定まらず、レビュワーは頭を切り替えながら両方の確認をしなければなりません。更に困るのは、一方の変更に問題があって修正が必要になった場合、もう一方までマージが保留になる点です (Good Manners of a Pull Request & Some Best Practices | by M. Kerem Keskin | Delivery Hero Tech Hub | Medium)。結果として開発サイクルの遅延を招いたり、最悪一部の変更をリリースしたいのに分離できず見送りになる、といった事態も起こります。前述の通り、PRはシングルタスクにするのが鉄則であり、混在させると百害あって一利なしです。
  • テンプレート無視・情報不足のPR: プロジェクトでPRテンプレートを用意しているにも関わらず、それを無視して必要事項が書かれていないPRです。例えばテンプレートの項目がすべて「N/A」(該当なし)や簡単な一文だけで埋められているケースがこれに当たります。「テスト: なし」「関連チケット: なし」のように極端に情報が不足したPRは、レビュワーが再度「この変更の背景は?テスト方法は?」と質問せざるを得なくなり、コミュニケーションコストが跳ね上がります。 (Code Reviews Should Not Suck! - DEV Community) (Code Reviews Should Not Suck! - DEV Community)特にリモート環境では質問→回答に丸1日かかることもあり、開発スピードの低下を招きます (Code Reviews Should Not Suck! - DEV Community)。情報不足PRはレビュワーに「ちゃんと書いてほしい」というフラストレーションを与え、レビュー自体が敬遠される原因にもなります。
  • タイトルが曖昧なPR: タイトルは一応書いてあるものの、非常に曖昧で内容を反映していない例です。例えば「Fix issue」とだけ書かれたタイトルでは、何の問題を修正したのか分かりません。同様に「変更いろいろ」なども論外です。タイトルが不適切だと、プロジェクトの履歴を見返す際に役立たず、後から「どのPRがあの変更だっけ?」と探すのが困難になります。またレビュワーも一覧画面でPRを把握できず見落とす可能性があります。理想のタイトルと逆の「悪い例」として、「Change src/scheduler.go」というタイトルはその典型で、何を意図した変更か全く伝わりません (Pull Requests are a reflection of your engineering culture — by Igor Šarčević)。曖昧なタイトルは履歴管理とレビュアビリティ(レビューされやすさ)の低下を招きます。
  • レビュー軽視・コミュニケーション不全のPR: PRの書き方そのものではないですが、レビュー文化に反したPR運用も悪い例と言えます。例えばレビューコメントを無視して勝手にマージしたり、指摘事項を放置したまま追加コミットを積み上げてしまうケースです (Code Reviews Should Not Suck! - DEV Community)。このような態度はレビュワーの労力を無駄にし、チームの信頼関係を損ないます。またCIでテストが落ちているのに「とりあえずコードはOKなのでマージします」と強行するのも悪い例です。せっかくの自動チェックを無視するのは本末転倒で、プロセスの形骸化を招きます。PRはあくまでチーム開発のコミュニケーションツールであり、一方通行になったりプロセスを飛ばすことは全員にマイナスです。

これら悪いPRの弊害は一言でまとめると、**「時間の無駄」と「品質リスク」**です。レビューに余計な時間がかかったり、最悪場合によっては誤った理解のままコードがマージされバグを埋め込んでしまう危険があります。チームメンバーの精神的な負担も増え、コードレビューが嫌がられる文化になってしまうと開発プロセス全体の健全性が損なわれます。逆に言えば、こうした悪い例を避け理想的なPRを書こうと心掛けるだけで、チーム開発の生産性と信頼性は大きく向上するのです。

オープンソースや企業におけるPR運用ベストプラクティス

多くのオープンソースコミュニティやソフトウェア企業では、Pull Requestの運用について蓄積されたベストプラクティスがあります。理想的なPRを書くための組織的な仕組みやルールの例を紹介します。

  • PRテンプレートの活用: ほとんどの成熟したプロジェクトは、.githubフォルダにPULL_REQUEST_TEMPLATE.mdを用意し、PR作成時に自動で雛形が表示されるようにしています (GitHubのPull Requestテンプレートに何を書いているか? | VISITS TechBlog)。テンプレートには「目的」「変更内容」「テスト方法」「関連Issue」などプロジェクトに応じた項目が含まれます。例えばFacebookが管理するReactリポジトリでは、テンプレート中で貢献者に対し「変更の動機と解決される問題の説明」「テスト方法の記述」を求めています (OSSプルリクエストテンプレート集)。このようなテンプレートは、貢献者がPR前に前提条件(ブランチ作成やローカルビルド)、テスト・リンティング、型チェック、CLA署名などを済ませていることを確認し、変更の動機と結果を明確に伝えることを促す設計になっています (OSSプルリクエストテンプレート集) (OSSプルリクエストテンプレート集) (OSSプルリクエストテンプレート集)。テンプレートに沿ったPRは必要情報が網羅されているため、プロジェクトメンテナーやレビュワーは効率的に評価できます (OSSプルリクエストテンプレート集)。

  • 小さなPRを推奨・強制: 前述したように、小規模でフォーカスされたPRはレビュー効率と品質双方で有利です。 (Helping others review your changes - GitHub Docs)そのため多くの企業で「機能ごとにPRを分ける」「500行を超える大きなPRは原則禁止」といったガイドラインがあります。 (Code Reviews Should Not Suck! - DEV Community) 実際、ある企業では500行を超えるPRは自動的に警告や拒否をする仕組みにしている例もあります (Code Reviews Should Not Suck! - DEV Community)。Googleもコードレビューに関する公開ガイドラインで「100行程度が望ましい、1000行はまず大きすぎる」という目安を示しています(Google Testing Blog In Praise of Small Pull Requests より)。このようにPRサイズの上限を設けることは、強制力はあれどレビュワー・開発者双方にメリットが大きいため、エンジニアリング文化として定着しつつあります。

  • レビュー必須とコードオーナー制度: オープンソースプロジェクトや多くの企業では、特定のブランチ(特にmain/masterブランチ)へのマージにはプルリクエストを経由し、一定人数の承認レビューが必要というルールを設定しています。GitHubのブランチ保護機能を使えば、レビューゼロではマージできないよう制約できます。さらにモノレポや大規模プロジェクトでは**コードオーナー(Code Owners)**制度を使い、ファイルごとに責任者を定めて自動的にレビューアサインする運用も一般的です。例えば特定モジュールの変更では、そのモジュールのエキスパートが必ずレビュワーになる、といった設定です。これにより、常に適切なレビュワーの目が通る体制を保証しています。

    また金融や医療系など厳格さが求められる現場では、2人以上の承認が必要・ペアプログラミング的に交替でレビューする、といったダブルチェック体制を敷く企業もあります。これはプロセスとしては時間がかかりますが、バグや不正の混入リスクを抑えるための業界標準とも言えます。

  • CIによる自動チェックと統合: CI/CDパイプラインとPRを統合した運用もベストプラクティスの一部です。PRが作成されると自動でテストや静的解析が走り、結果がPR画面上に表示されるよう設定します。テストがすべてグリーンにならないとマージできないようにしたり、カバレッジやリンタ違反も可視化されるようにすることで、品質の担保とフィードバックサイクルの短縮が図れます。例えばReactリポジトリのテンプレートでは貢献者にローカルでyarn testyarn lintを実行するよう求めていますが (OSSプルリクエストテンプレート集)、CIでも同様のジョブを実行し不備があれば即座に検出します。こうした人と自動チェックの二重体制で、PRの信頼性を高める運用は一般的です。

  • DangerやLintツールでPR内容を検証: 一歩進んだ運用では、Dangerなどの自動レビューbotを用いてPR本文の内容や変更内容に基づくチェックを行います。たとえばDangerを使うと、「PR本文に特定のキーワード(JIRAチケット番号など)が含まれていなければ警告コメントする」「変更行数が大きい場合に注意喚起する」「ドキュメントを変更したらラベルを付与する」といった自動ルールを実装可能です。これにより、PRテンプレートの項目漏れやプロジェクトルール違反を機械的に指摘し、レビュワーの手間を減らしています。企業でも、自社向けにカスタムルールを設定した社内ツールでPRチェックを回す例があります。例えば、「セキュリティに関わる変更なのにセキュリティレビュー項目が未記入なら警告する」「特定のモジュール変更時には関連ドキュメント更新が必要だと通知する」など、プロジェクト固有の約束事を自動化しています。

  • コミットメッセージやブランチ命名規則: PRとは少し離れますが、多くのプロジェクトでコミットメッセージの書式規約(例えばConventional Commits)やブランチの命名規約を設けています。これはPRにも影響するベストプラクティスです。例えばAngularやElectronなどではコミットメッセージにfeat:fix:などのプレフィックスと簡潔な概要を書くルールがあり、これがそのままPRタイトルやリリースノートに使われます。ブランチ名にも課題番号や目的が入っていると、PR画面で参照しやすくなります。こうした規約を守ることで、PRタイトル・本文と合わせて変更の意図が多層的に伝わる仕組みになっています。

  • ドキュメントの整備と教育: OSSではCONTRIBUTING.mdに詳細なPR作成ガイドを載せているケースも多いです。例えばRuby on RailsやDjangoといった有名OSSは「バグ報告とPRのガイドライン」を公式サイトで提示し、適切なPRを書くためのチェックリストや注意点を共有しています。企業内でも新人研修で「良いPRの書き方」を教えたり、定期的にチーム内で過去のPRを振り返ってベストプラクティスをアップデートする取り組みがあります。実際に運用しながら「もっとこの情報を書いてほしい」「この書き方は分かりづらい」といった声をテンプレートに反映していくのは有効です (Pull Requestは書き方が9割 #GitHub - Qiita) (Pull Requestは書き方が9割 #GitHub - Qiita)。常に現場のフィードバックを取り入れてテンプレートやルールを改善することで、ベストプラクティスは進化していきます。

  • Draft PRやWIPラベルの活用: 上述のDraft機能の活用はOSSでも推奨されています。また、正式なレビューではない作業中の共有には「WIP(Work in Progress)」ラベルを付ける文化もあります。ラベルによって一目で作業中かレビュー依頼中か判別できるようにし、未完成PRが誤ってマージされたり放置されたりしないようにしています。OSSでは「WIPのPRはレビューしません、完了したらWIPを外してください」といったルールを明示している場合もあります。これらはPRの状態管理におけるベストプラクティスです。

  • マージ戦略とリリースノート連携: プロジェクトによっては、Squashマージ(一つのコミットにまとめてマージ)かリベースマージかなどのマージ戦略も統一しています。Squashマージを採用する場合、PRタイトルがそのままコミットメッセージになるため、タイトルの質がより重要になります。さらに、自動でリリースノートを生成するようなCIフローを組んでいる企業では、PRタイトル・本文にリリースノート向けの記述フォーマットを要求することもあります。例えば「変更内容: ユーザが~~できるよう改善」「影響範囲: ~に影響あり」といった書式で書かせ、CIがそれをパースしてリリースノートに反映する、といった仕組みです。これは高度な運用ですが、うまく機能すれば開発とドキュメンテーションの連携が取れた理想形となります。

以上のように、組織・プロジェクトレベルでPR運用を整えることで、開発者一人ひとりが理想的なPRを書きやすい環境を作っています。「良いPR」を個人の努力やセンスに任せず、仕組みで支援することが重要です。その結果、プロジェクト全体の生産性と品質が向上するため、多くのコミュニティや企業がこれらベストプラクティスを採用しています。

かーなびかーなび
chatgpt

PRテンプレートや自動チェック(CI)との相性

最後に、PRの書き方とテンプレート/CIの相乗効果について触れます。前述したようにテンプレートとCIはベストプラクティスの中核ですが、特に理想的なPRを書く上でこれらは強力なサポート役となります。

  • テンプレートで漏れ防止: PRテンプレートは開発者への単なるお仕着せではなく、書き手自身が大事な情報を書き漏らすのを防ぐチェックリストです。 (Code Reviews Should Not Suck! - DEV Community) 例えば項目に沿って書くだけで「テストしたか?」「関連Issueはあるか?」といった点を自然に確認できます。テンプレートがなければ忘れがちな事項(デプロイ手順の共有など)も、テンプレート上で思い出させてくれます。結果として開発者⇔レビュワー間の往復を削減し、トータルの所要時間を短縮できます (Code Reviews Should Not Suck! - DEV Community)。小さな工夫ですが、テンプレート導入で「必要情報の聞き忘れ・書き忘れによるレビュー遅延」が劇的に減ったという報告もあります。
  • CIがテンプレート遵守をチェック: 自動化ツール(CIやBot)は、テンプレートと組み合わせることでより効果を発揮します。例えば先述のDangerを使えば、テンプレートの特定セクションが未記入の場合にPRに警告コメントを出すことができます。CI上でスクリプトを組めば、PR本文の長さや構造をチェックして形式的なレビュー指摘を自動化できます。「説明があまりに短いPRはラベルを付けて注意喚起」「‘Fixes #’が含まれないPRはチケット紐付け漏れの可能性ありと通知」といった具合です。これによりレビュワーは本来のコード内容に集中でき、人力での漏れチェックを減らせます。
  • チェックリストとCIの連動: テンプレート内にチェックリスト形式で項目を入れておくと、開発者はPR作成時にそれを確認・実施します(例:「- ローカルで全テストが通っている」)。CIは実際にテストを回して結果を表示しますが、チェックリストによって開発者自身が確認を済ませてからPRを出すため、CIでの指摘が減ります。もしCIでテスト失敗しても、開発者は「チェックリストにチェックしたのに落ちた」という状況から学び、以後は事前確認を徹底するでしょう。このように**テンプレート(事前予防)CI(事後検出)**の二段構えで品質と効率を向上させます。
  • Issueトラッカーとの自動連携: PRにIssue番号を書く文化は、CIやGitHubの機能と連携します。Closes #番号と書かれたPRがマージされると対応するIssueが自動でクローズされるのは有名です。また、企業内ではPRのタイトルや枝番にチケットIDを含めることで、JIRAなどの外部トラッカーとリンクさせる運用も一般的です。例えばブランチ名をfeature/JIRA-1234-new-loginとすると、JIRA側でPRが表示されたりステータスが更新される、といった仕組みです。テンプレートに「関連チケット: 」欄があれば、開発者は確実にチケットIDを記入するでしょう。 (GitHubのPull Requestテンプレートに何を書いているか? | VISITS TechBlog)それによりCIやWebhookがその情報を拾ってチケットを進行中→レビュー中→完了へ自動で遷移させることも可能です。これはPMやチーム全体の進捗把握に直結するため、PR記述とプロジェクト管理ツールがシームレスに繋がるメリットがあります。
  • リリースノート自動生成: 上述のように、PRの内容を利用してリリースノートを自動生成するCIワークフローもあります。その際、テンプレートでタイトルや本文の書式をある程度統一しておくとパースが容易になります。例えば、すべてのPRタイトルにカテゴリラベル(feat/fix/docなど)を付けるルールにし、それをCIが集計して次のリリースの変更点リストを作る、といったことも可能です。テンプレートとCIの協調により、ドキュメント作業の自動化まで視野に入れられます。
  • 高度な静的解析との連携: 一部のCIではPRの差分に対して自動的に静的解析(セキュリティスキャンやコード複雑度分析)を走らせ、結果をPRにコメントする仕組みもあります。これも広義には「PRの自動チェック」です。開発者がPR本文で「この部分はパフォーマンスに影響あり」と書けば、それに対応する形でCIが計測結果を載せる、といった使い方も工夫次第でできます。今後はGitHubのCopilotなどAIがPR本文から要約を生成したり、逆にPR本文の内容をチェックしてアドバイスする、といった高度な支援も増えていくでしょう (Helping others review your changes - GitHub Docs)。その前提として、テンプレートに沿った構造化されたPR本文があることが重要になります。

総じて、理想的なPRの書き方はテンプレートと自動チェックによって補強・促進されます。テンプレートが開発者の思考をガイドし、CIが客観的なフィードバックを与えることで、漏れやミスを最小化した高品質なPRが安定して生み出されるのです。 (OSSプルリクエストテンプレート集)ある記事では「このテンプレートは貢献者が必要事項を理解・遵守することを促し、レビュアーが効率的に評価できるよう設計されています」と述べられており (OSSプルリクエストテンプレート集)、まさにテンプレートとCIの組み合わせによる効果を表現しています。実際、テンプレート導入後に「PRの書き方次第でレビュワーの作業時間が何倍も変わる」ことを実感したという開発チームの声もあります (Pull Requestは書き方が9割 #GitHub - Qiita)。

以上、多角的に述べてきたように、GitHubにおけるPull Requestの理想的な書き方は、単なる形式美ではなく開発プロセスの効率と品質を左右する重要要素です。適切なタイトルや構成でPRを作成し、その文化をチームで徹底することで、レビューが円滑になりプロジェクトのドキュメントも充実します。ひいてはバグの早期発見・防止にもつながり、チームの生産性と信頼性が向上します。現代の分散開発や高速開発において、良いPRを書くことはエンジニアの基本スキルの一部とさえ言えるでしょう。ぜひ紹介したベストプラクティスを参考に、日々のPull Requestをより良いものに磨いていってください。

かーなびかーなび

PR書き方概要(gemini, chatgpt)

gemini
  • gemini
    • 3.1. 概要/目的

      プルリクエストの主な目的を明確かつ簡潔に述べます 2。変更が解決しようとしている問題や、導入する新しい機能を説明します 2。プロジェクト管理ツール(例:Jira)の関連する課題、ユーザーストーリー、またはタスクを参照します 3。プルリクエストを特定の課題やタスクにリンクすることで、重要なトレーサビリティとコンテキストが提供され、レビュー担当者はプロジェクトの目標内における変更のより広範な目的を理解できます。この関連付けは、コードの変更がプロジェクトの要件と一致していることを確認するのに役立ち、将来の議論や調査のための参照ポイントを提供します。

    • 3.2. 背景とコンテキスト

      レビュー担当者が変更内容を完全に理解するために必要な背景情報を提供します 3。特に、検討された代替ソリューションがあった場合は、選択されたアプローチの背後にある理由を説明します 3。変更の根拠となった関連する設計ドキュメント、仕様書、または以前の議論へのリンクを記載します 3。技術的な決定(例:ループの代わりに再帰メソッドを選択する 3)の背後にある「なぜ」を明示的に述べることで、不必要な議論を防ぎ、レビュー担当者が作成者の思考プロセスを理解するのに役立ちます。理由を説明することで、作成者は実装だけでなく、設計自体に関する、より情報に基づいたフィードバックを促します。これにより、より堅牢で十分に検討されたソリューションにつながる可能性があります。

    • 3.3. 変更点の概要

      コードに加えられた主要な変更の概要を説明します 3。変更の影響を受けた主な領域またはコンポーネントを強調します 6。明確さと読みやすさのために、箇条書きまたは番号付きリストを使用します 6。簡潔な概要により、レビュー担当者はコードをすぐに掘り下げることなく、変更の範囲を迅速に把握できます。これにより、レビューの取り組みに優先順位を付けることができます。レビュー担当者は、詳細なコード差分を調べる前に、この概要を使用して変更のメンタルモデルを形成できるため、レビュープロセスがより効率的になります。

    • 3.4. 影響範囲

      これらの変更がシステムまたはアプリケーションのさまざまな部分に与える可能性のある影響について説明します 1。レビュー担当者が注意すべき潜在的なリスクまたは副作用を特定します。変更にデータベースの移行、APIの変更、またはその他の重要なインフラストラクチャの更新が含まれるかどうかを指定します。影響範囲を明確にすることで、レビュー担当者はプルリクエストのマージの潜在的な結果を理解し、より慎重な検討が必要な領域に焦点を当てることができます。この情報は、変更が意図せずにリグレッションを引き起こしたり、システムの他の部分に悪影響を与えたりしないことを保証するために不可欠です。

    • 3.5. レビューの焦点/ガイダンス

      レビュー担当者に特に注意してほしいコードの特定の側面について指示します 4。疑問点がある場合や、より徹底的なフィードバックをいただけるとありがたい場合は示します 1。プルリクエストに複数のファイルや複雑な変更が含まれる場合は、変更をレビューする特定の順序を提案します 4。レビュー担当者に明確なガイダンスを提供することで、レビュープロセスの効率と焦点が大幅に向上し、作成者は最も重要な領域に関するフィードバックを確実に受け取ることができます。これにより、レビュー担当者が重要でない側面に時間を費やしたり、作成者が特に入力を必要とする領域を見落としたりすることを防ぐことができます。

    • 3.6. 検証手順

      レビュー担当者が変更をテストおよび検証する方法に関する明確かつ簡潔な手順を提供します 1。必要な場合は手動テストの手順を含め、確認する特定のシナリオを概説します 7。変更をカバーする関連する自動テストを指摘します 3。変更がテストされた環境について言及します 7。明確な検証手順を提供することで、レビュー担当者は変更の機能と正確性を簡単に確認でき、マージされたコードに対する信頼が高まります。これにより、メインコードベースにバグが導入されるリスクが軽減され、意図した機能が期待どおりに動作することが保証されます。

    • 3.7. 質問と議論のポイント

      コードまたはアプローチに関してレビュー担当者に質問したい特定の質問を明示的に提起します 1。潜在的な議論や代替の見解が予想される領域を強調します。レビュー担当者に意見や提案を共有するよう促します。潜在的な論点や不確実性について積極的に議論を開始することで、より協調的で生産的なレビュープロセスにつながり、共通理解とより良い意思決定が促進されます。これにより、オープンなコミュニケーションが奨励され、さまざまな視点の検討が可能になり、より堅牢で包括的なソリューションにつながる可能性があります。

chatgpt
かーなびかーなび

関数の引数にオブジェクトを渡すと同一オブジェクトを参照する

def f(candidate):
    candidate.score = replace # ここでオリジナルのCandidate()が上書きされるのか?
    return None

candidate = Candidate()
f(candidate)

の場合、先頭行のcandidateのオリジナルのポインタがさすものの値も変わるのか?

答え

はい、変わります。

Pythonでは、関数にオブジェクトを渡す際、そのオブジェクトへの参照(ポインタのようなもの)が渡されます。そのため、関数 f の中で candidate.score に新しい値が代入されると、それは関数外で定義されたオリジナルの candidate オブジェクトの score 属性に直接影響を与えます。

具体的には、

candidate = Candidate()

Candidate オブジェクトが作成され、candidate という変数名がそのオブジェクトを指します。

def f(candidate):
    candidate.score = replace
    return None

この関数 f が呼び出される際、引数として渡された candidate は、f のスコープ内でも全く同じ Candidate オブジェクトを指します。

candidate.score = replace の行では、この共有された Candidate オブジェクトの score 属性が replace の値で更新されます。

したがって、関数 f の実行後、candidate のオリジナルのポインタが指す Candidate オブジェクトの score 属性は、f の中で行われた変更が反映された状態になります。


ポインタの概念を使いながら、メモリ上の動きをもう少し具体的に説明

Pythonでのオブジェクトとポインタ

Pythonの変数名は、実際にはメモリ上のオブジェクトへのポインタ(参照)を保持しています。これは、変数名がオブジェクトそのものではなく、「オブジェクトがどこにあるか」という情報を持っていると考えると分かりやすいでしょう。

1. candidate = Candidate() の場合

まず、candidate = Candidate() が実行されると、次のようになります。

  1. メモリ上に新しい Candidate オブジェクトが作成されます。このオブジェクトには、その時点でのメモリ上の固有のアドレス(例えば、0x1234abcd のような番地)があります。
  2. 変数 candidate は、この新しく作成された Candidate オブジェクトのアドレス (0x1234abcd) を指すポインタとして機能します。
変数名           メモリ上のオブジェクト
----------     ----------------------
candidate ----> [Candidate Object at 0x1234abcd]
                   - score: (初期値, 例えば None)

2. f(candidate) が呼び出される場合

次に、f(candidate) が呼び出されます。

  1. 関数 f の仮引数も candidate という名前ですが、このfの中のcandidateは、関数が呼び出される際に渡された実引数のcandidateが指している同じアドレス (0x1234abcd) を指すポインタになります。つまり、関数内外で同じオブジェクトを指している状態です。
関数fの内部:
変数名           メモリ上のオブジェクト
----------     ----------------------
candidate ----> [Candidate Object at 0x1234abcd]
                   - score: (初期値)

関数fの外部:
変数名           メモリ上のオブジェクト
----------     ----------------------
candidate ----> [Candidate Object at 0x1234abcd]
                   - score: (初期値)

3. candidate.score = replace が実行される場合

関数 f の中で candidate.score = replace が実行されると、次のようになります。

  1. f の中の candidate が指しているアドレス (0x1234abcd) にある Candidate オブジェクトにアクセスします。
  2. そのオブジェクトの score 属性に、replace の値が新しく代入されます。
関数fの内部:
変数名           メモリ上のオブジェクト
----------     ----------------------
candidate ----> [Candidate Object at 0x1234abcd]
                   - score: replace (値が変更された!)

関数fの外部:
変数名           メモリ上のオブジェクト
----------     ----------------------
candidate ----> [Candidate Object at 0x1234abcd]
                   - score: replace (同じオブジェクトなので、値が変更されている!)

結論

このように、関数にオブジェクトが渡されると、そのオブジェクトへのポインタが渡されるため、関数内でオブジェクトの属性を変更すると、その変更は元のオブジェクトに直接反映されます。 変数candidate自体が別のオブジェクトを指すように変わるわけではなく、candidateが指しているオブジェクトの中身が変わるというイメージです。


逆に異なるオブジェクトとして切り離す方法

はい、関数内で異なるオブジェクトとして切り離す方法はいくつかあります。主な方法は以下の通りです。

  1. 新しいオブジェクトを作成して返す
  2. ディープコピーを作成する

それぞれの方法について詳しく説明します。


1. 新しいオブジェクトを作成して返す (Recreate and Return)

これは最も一般的で分かりやすい方法です。関数内で、引数として渡されたオブジェクトの情報を使って、全く新しいオブジェクトを作成し、それを関数の戻り値として返します。元のオブジェクトには一切変更を加えません。

メリット:

  • 明確で理解しやすい。
  • 元のオブジェクトに影響を与えないことが保証される。

デメリット:

  • 元のオブジェクトと同じ情報を保持する新しいオブジェクトを作成するためのコードが必要。

例:

class Candidate:
    def __init__(self, score=None, name=None):
        self.score = score
        self.name = name

def f_create_new(original_candidate, new_score):
    # 新しいCandidateオブジェクトを作成し、元の情報をコピーしつつscoreを変更
    new_candidate = Candidate(score=new_score, name=original_candidate.name)
    return new_candidate

# 元のオブジェクト
original_candidate = Candidate(score=100, name="Alice")
print(f"オリジナル(変更前): score={original_candidate.score}, name={original_candidate.name}") # score=100, name=Alice

# 関数を呼び出し、新しいオブジェクトを受け取る
modified_candidate = f_create_new(original_candidate, 150)

print(f"新しいオブジェクト: score={modified_candidate.score}, name={modified_candidate.name}") # score=150, name=Alice
print(f"オリジナル(変更後): score={original_candidate.score}, name={original_candidate.name}") # score=100, name=Alice (変更されていない)

# オブジェクトのIDを確認して、別々のオブジェクトであることを確認
print(f"オリジナルオブジェクトID: {id(original_candidate)}")
print(f"新しいオブジェクトID: {id(modified_candidate)}")
# 出力されるIDが異なることを確認できるはずです。

2. ディープコピーを作成する (Deep Copy)

copy モジュール(特に copy.deepcopy())を使用すると、オブジェクトとその中に含まれるすべてのオブジェクト(入れ子になったリストや辞書、カスタムオブジェクトなど)を再帰的にコピーし、完全に独立した新しいオブジェクトを作成できます。

メリット:

  • 複雑な構造を持つオブジェクトでも、簡単に独立したコピーを作成できる。
  • 手動で属性をコピーする手間が省ける。

デメリット:

  • シャローコピー (copy.copy()) と混同しやすい(シャローコピーでは、ネストされたオブジェクトは参照が共有されるため注意が必要)。
  • 循環参照がある場合など、一部のケースで問題が発生する可能性がある(通常は稀)。

例:

import copy

class Candidate:
    def __init__(self, score=None, name=None, preferences=None):
        self.score = score
        self.name = name
        self.preferences = preferences # リストなど、変更可能なネストされたオブジェクトを想定

def f_deep_copy(original_candidate, new_score):
    # original_candidateのディープコピーを作成
    copied_candidate = copy.deepcopy(original_candidate)
    # コピーされたオブジェクトのscoreを変更
    copied_candidate.score = new_score
    # ネストされたオブジェクトもコピーされているので、それも変更してみる
    if copied_candidate.preferences:
        copied_candidate.preferences.append("new_pref")
    return copied_candidate

# 元のオブジェクト
original_candidate = Candidate(score=100, name="Bob", preferences=["A", "B"])
print(f"オリジナル(変更前): score={original_candidate.score}, name={original_candidate.name}, preferences={original_candidate.preferences}")

# 関数を呼び出し、ディープコピーされたオブジェクトを受け取る
modified_candidate = f_deep_copy(original_candidate, 180)

print(f"コピーされたオブジェクト: score={modified_candidate.score}, name={modified_candidate.name}, preferences={modified_candidate.preferences}")
print(f"オリジナル(変更後): score={original_candidate.score}, name={original_candidate.name}, preferences={original_candidate.preferences}")

# オブジェクトのIDを確認
print(f"オリジナルオブジェクトID: {id(original_candidate)}")
print(f"コピーされたオブジェクトID: {id(modified_candidate)}")
# ネストされたリストのIDも確認 (ディープコピーなので異なるはず)
if original_candidate.preferences:
    print(f"オリジナル preferences ID: {id(original_candidate.preferences)}")
if modified_candidate.preferences:
    print(f"コピーされた preferences ID: {id(modified_candidate.preferences)}")

シャローコピー (copy.copy()) についての注意

copy.copy() はオブジェクトのシャローコピーを作成します。これは、オブジェクト自体は新しく作成されますが、そのオブジェクトが参照しているネストされたオブジェクト(リスト、辞書、カスタムオブジェクトなど)は、元のオブジェクトと参照が共有されるという特徴があります。

import copy

class Candidate:
    def __init__(self, score=None, preferences=None):
        self.score = score
        self.preferences = preferences

original = Candidate(score=100, preferences=["A", "B"])
shallow_copy = copy.copy(original)

shallow_copy.score = 200 # scoreは新しいオブジェクトに代入されるので、originalには影響しない

# preferencesリストは共有されているので、shallow_copyで変更するとoriginalも変更される
shallow_copy.preferences.append("C")

print(f"オリジナル: score={original.score}, preferences={original.preferences}") # score=100, preferences=['A', 'B', 'C']
print(f"シャローコピー: score={shallow_copy.score}, preferences={shallow_copy.preferences}") # score=200, preferences=['A', 'B', 'C']

# IDを確認すると、preferencesリストが同じオブジェクトを指していることがわかる
print(f"オリジナル preferences ID: {id(original.preferences)}")
print(f"シャローコピー preferences ID: {id(shallow_copy.preferences)}")

シャローコピーは、ネストされたオブジェクトがすべてイミュータブル(整数、文字列、タプルなど)である場合には問題ありませんが、ミュータブルなオブジェクト(リスト、辞書、カスタムオブジェクトなど)が含まれる場合は、意図しない副作用を引き起こす可能性があるため注意が必要です。


まとめると、オブジェクトを切り離して変更したい場合は、新しいオブジェクトを作成して返すか、copy.deepcopy() を使って完全に独立したコピーを作成するのが適切な方法です。

かーなびかーなび

github actionsからcloud runデプロイするときにCIテストを待たないといけないのか?

gemini

github actionsでcloud runへデプロイされるようになっています。

現在リモートへプッシュ(コミットA)してgithubのCIによるテストが実行中です。このテスト実行中にgithub actionsでデプロイを行った場合、コミットAはデプロイしたリビジョンへ反映されていますか?


GitHub ActionsでCloud Runへデプロイされるようになっているとのこと、承知いたしました。

ご質問についてですが、GitHub Actionsのワークフローの実行タイミングと、Cloud Runのリビジョンの反映には時間差が生じる可能性があります。

具体的には、以下のようになります。

  1. コミットAのプッシュ: コミットAをリモートリポジトリにプッシュします。
  2. CIワークフローの開始: プッシュをトリガーとして、GitHub ActionsのCIワークフロー(テスト)が開始されます。
  3. デプロイワークフローの開始: CIワークフローの実行中に、もし別のGitHub Actionsワークフローがデプロイをトリガーされた場合、そのデプロイワークフローが開始されます。

ここでのポイントは、デプロイワークフローがどの時点のコード(コミット)を参照しているかです。

  • 通常: デプロイワークフローは、そのワークフローがトリガーされた時点での最新のコミットを参照してデプロイを行います。
  • CIワークフローと同じコミットを参照する場合: もしデプロイワークフローが、CIワークフローをトリガーしたのと同じコミット(この場合はコミットA)を参照してデプロイするように設定されていれば、デプロイされたリビジョンにはコミットAの内容が反映されます。
  • それより新しいコミットを参照する場合: もしCIワークフローの実行中に、さらに新しいコミット(コミットBなど)がプッシュされ、デプロイワークフローがその新しいコミットをトリガーとして開始された場合、デプロイされるリビジョンにはコミットBの内容が反映され、コミットAは直接反映されません。
  • 明示的に特定のコミットを指定する場合: デプロイワークフロー内で、git checkoutなどで明示的にコミットAを指定してデプロイするように設定することも可能ですが、これは一般的ではありません。

結論として、

「デプロイしたリビジョンへコミットAは反映されていますか?」というご質問に対しては、デプロイをトリガーしたGitHub Actionsワークフローが、どのコミットをベースにデプロイを実行したかによります。

一般的には、CIが走っている最中に手動または別のトリガーでデプロイワークフローを起動した場合、そのデプロイワークフローが参照するのは、そのデプロイワークフローが開始された時点での最新のコミットです。

確認すべき点:

  • デプロイワークフローのトリガー: 何をきっかけにデプロイワークフローが実行されるように設定されていますか? (例: プッシュ、特定のブランチへのマージ、手動トリガーなど)
  • デプロイワークフロー内のコード参照: デプロイワークフローが、どのコミットをチェックアウトしてデプロイを行っているか、ワークフローのYAMLファイルを確認してください。通常はactions/checkout@vXなどでデフォルトの最新コミットをチェックアウトします。

もし、CIが成功してからデプロイするという一般的なフローであれば、CIが完了し、その後にデプロイが開始されるため、CIと同じコミットがデプロイされることになります。しかし、「CIテスト実行中にデプロイを行った場合」とのことなので、このシナリオでは時間差と参照コミットのずれが発生する可能性があります。


デプロイワークフローはどこを参照すればいい?


デプロイワークフローは、通常、GitHubリポジトリ内の.github/workflowsディレクトリに保存されているYAMLファイルとして定義されています。

以下の手順で確認できます。

  1. GitHubリポジトリにアクセスする: デプロイを行っているプロジェクトのGitHubリポジトリを開きます。
  2. .githubディレクトリを探す: リポジトリのルートディレクトリにある.githubという名前のフォルダを探します。
  3. workflowsディレクトリを開く: .githubフォルダの中にworkflowsという名前のフォルダがありますので、それを開きます。
  4. デプロイ関連のYAMLファイルを探す: workflowsディレクトリの中に、デプロイワークフローを定義しているYAMLファイル(例: deploy.yml, ci-cd.ymlなど、名前はプロジェクトによって異なります)があるはずです。

これらのファイルを開くと、デプロイワークフローの定義、つまり以下の情報が記述されています。

  • トリガー: いつ(どのイベントで)このワークフローが実行されるか(例: on: push, on: pull_request, on: workflow_dispatchなど)。
  • ジョブ: ワークフロー内で実行される一連のステップ。
  • ステップ: 各ジョブ内で実行される具体的なコマンドやアクション。特にactions/checkoutアクションがどのコミットをチェックアウトしているか、デプロイコマンドがどのソースを参照しているかを確認してください。

このYAMLファイルの内容を確認することで、デプロイワークフローがCIテスト中に実行された場合、どのコミットがデプロイ対象となるのかを詳しく把握できます。


「CIが回った後でないとActionsのデプロイに反映されないのか」というご質問についてですね。

結論から言うと、設定によります。

GitHub Actionsのデプロイワークフローは、必ずしもCIワークフローの完了を待ってから実行されるとは限りません。GitHub Actionsでは、複数のワークフローを独立して定義し、それぞれ異なるトリガーで実行させることが可能です。

考えられるケースは以下の通りです。

  1. CIワークフローの成功をトリガーとするデプロイ (一般的な構成)
    • 説明: 最も一般的で推奨される方法です。テストが成功したコードのみをデプロイするために、デプロイワークフローがCIワークフローの成功をトリガーとして実行されるように設定されます。

    • 設定例:

      YAML

      # .github/workflows/deploy.yml の一部 on: workflow_run: workflows: ["CI Workflow Name"] # CIワークフローの名前を指定 types: - completed # 完了を待つ branches: - main # 特定のブランチでのみ jobs: deploy: if: ${{ github.event.workflow_run.conclusion == 'success' }} # CIが成功した場合のみ runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.workflow_run.head_sha }} # CIワークフローが実行されたコミットをチェックアウト # ... デプロイステップ ...

    • 結果: この設定の場合、CIが成功しない限り、デプロイは実行されません。 CIが失敗したコミットはデプロイされません。

  2. 特定のブランチへのプッシュをトリガーとするデプロイ (CIと並行または独立)
    • 説明: mainブランチやproductionブランチへのプッシュを直接デプロイワークフローのトリガーとします。CIワークフローとは独立して、あるいは並行して実行されます。

    • 設定例:

      YAML

      # .github/workflows/deploy.yml の一部 on: push: branches: - main # mainブランチへのプッシュでデプロイ jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # デプロイワークフローがトリガーされた時点のコミットをチェックアウト # ... デプロイステップ ...

    • 結果: この設定の場合、mainブランチにプッシュされれば、CIのテスト結果を待たずにデプロイが開始される可能性があります。 もしCIとデプロイが同時に走り、CIが失敗したとしても、デプロイは進んでしまう可能性があります。これが、前回の質問で「CI実行中にデプロイを行った場合、コミットAは反映されていますか?」という状況で懸念される点です。

  3. 手動トリガーによるデプロイ (任意のタイミング)
    • 説明: workflow_dispatchを使って、GitHub ActionsのUIから手動でデプロイワークフローを実行するように設定します。

    • 設定例:

      YAML

      # .github/workflows/deploy.yml の一部 on: workflow_dispatch: # 手動でトリガー jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # 実行時に指定された、またはデフォルトのコミットをチェックアウト # ... デプロイステップ ...

    • 結果: この場合、CIの実行状況とは関係なく、ユーザーが「デプロイ」ボタンを押した時点でデプロイが開始されます。どのコミットをデプロイするかは、actions/checkoutの設定や手動トリガーのオプションによって異なります。

まとめると:

  • 「CIが回った後でないとActionsのデプロイに反映されない」という状態にしたいのであれば、デプロイワークフローのトリガーをworkflow_runなどにして、CIワークフローの成功に依存するように設定する必要があります。
  • もし、デプロイワークフローがpushなどの別のイベントでトリガーされている場合は、CIの完了を待たずにデプロイが実行され、その時点の最新のコミットがデプロイされる可能性があります。

現在のデプロイワークフローのYAMLファイルを確認し、on:セクションとjobs:セクションの内容を詳しく見てみてください。それによって、どちらのパターンに該当するかがわかります。


次のような場合は?

name: hoge

on:

workflow_dispatch:

inputs:

engine:

type: choice

description: "Engine"

required: true

options:

  • hgoe

environment:

type: choice

description: "GCP environment type."

required: true

options:

  • staging
  • eval
  • production

use_kaniko_cache:

1,1 先頭


いただいたGitHub Actionsのワークフロー定義を見ると、on: workflow_dispatch:が設定されていますね。


workflow_dispatchとは?

workflow_dispatchは、GitHub ActionsのUIから手動でワークフローを実行できるトリガーです。つまり、このワークフローはリモートへのプッシュやプルリクエストのマージなどの自動イベントでは実行されず、GitHubのWebインターフェースから「Run workflow」ボタンを押すことで開始されます。

このワークフローの場合の挙動

ご提示のワークフロー定義がデプロイワークフローであると仮定すると、以下のようになります。

  1. CIの実行状況とは独立: このワークフローは、CI(継続的インテグレーション)のテストが実行中であるか、成功したか、失敗したかに関わらず、ユーザーが手動でトリガーしたときに実行されます。
  2. デプロイされるコード: デプロイされるコードは、ワークフローが手動でトリガーされた時点でのデフォルトブランチ(例: mainmaster)の最新のコミット、またはactions/checkoutステップで明示的に指定されたコミットになります。
    • もし、デプロイワークフロー内でactions/checkoutが特定のコミットSHAを指定していない限り、通常はワークフローが実行されたブランチの最新のコードが使われます。
    • つまり、CIがコミットAで走っている最中に、この手動デプロイワークフローをトリガーした場合、デプロイされるのはCIが走っているコミットAか、それ以降にプッシュされた最新のコミットになる可能性があります。

まとめ

このworkflow_dispatchを使ったワークフローは、CIのテストが完了するのを待つことなく、任意のタイミングで手動でデプロイを実行できます。これにより、緊急のデプロイや特定のブランチのテストなどを、CI/CDパイプライン全体を介さずに柔軟に行うことが可能です。

ただし、CIの成功に依存せずにデプロイされるため、テストが失敗したコードが誤ってデプロイされるリスクも考慮する必要があります。もし、CIが成功したことを確認してからデプロイしたい場合は、workflow_runトリガーを使うか、手動デプロイを実行する前にCIのステータスを確認する運用ルールを設けることが重要です。:::

かーなびかーなび

ローカルでメモリエラーチェック

python: ローカルでメモリエラーチェック

Pythonで関数を抜けた時にメモリが解放されているかを確認するには、いくつかの方法があります。Pythonのメモリ管理は主に参照カウントガベージコレクションによって自動的に行われるため、通常はプログラマが明示的にメモリを解放する必要はありません。しかし、意図しないメモリリークがないかなどを調査したい場合に、以下の方法が役立ちます。


確認方法

1. gc モジュールを使用する

Pythonの gc (ガベージコレクタ) モジュールを使うと、ガベージコレクションの動作を制御したり、追跡対象オブジェクトの情報を取得したりできます。

特定の型のオブジェクトが関数呼び出し前後で増減するかを確認することで、メモリ解放の様子を間接的に見ることができます。

import gc

def my_function():
    # 関数内でオブジェクトを作成
    a = [1] * 10**6  # 大きなリストを作成
    b = "some_string" * 10**5
    # 関数を抜けると a と b はスコープ外になり、参照カウントが0になるはず
    # (他の参照がなければ)

# 関数呼び出し前のオブジェクト数を取得(型を指定することも可能)
gc.collect() # 事前に不要なオブジェクトを掃除
initial_objects = len(gc.get_objects())
initial_list_objects = len([obj for obj in gc.get_objects() if isinstance(obj, list)])


print(f"関数呼び出し前の総オブジェクト数: {initial_objects}")
print(f"関数呼び出し前のリストオブジェクト数: {initial_list_objects}")

my_function()

# 関数呼び出し後のオブジェクト数を取得
gc.collect() # 関数内で作られたオブジェクトの解放を促す
final_objects = len(gc.get_objects())
final_list_objects = len([obj for obj in gc.get_objects() if isinstance(obj, list)])


print(f"関数呼び出し後の総オブジェクト数: {final_objects}")
print(f"関数呼び出し後のリストオブジェクト数: {final_list_objects}")

# 理想的には、関数内で作成された一時的なオブジェクトは解放されているはず
# ただし、完全に同じ数に戻るとは限らない(Pythonの内部的な動作による変動があるため)

ポイント:

  • gc.collect(): 強制的にガベージコレクションを実行します。
  • gc.get_objects(): Pythonインタープリタが追跡している全てのオブジェクトのリストを返します。
  • 関数内で大きなオブジェクトを生成し、それが適切に解放されれば、gc.get_objects() で得られるオブジェクト数が元に戻るか、少なくとも大きく増加したままにはならないことが期待できます。ただし、Pythonの内部キャッシュなどにより、完全に元通りにならないこともあります。

2. tracemalloc モジュールを使用する (Python 3.4+)

tracemalloc モジュールは、メモリブロックの割り当てをトレースし、どこでメモリが割り当てられたかの統計情報を提供します。これを使うと、特定のコードブロックでのメモリ使用量の変化をより正確に把握できます。

import tracemalloc

def my_function():
    # 関数内でオブジェクトを作成
    a = [1] * 10**6
    b = "some_string" * 10**5
    # 何か処理
    return

tracemalloc.start() # メモリ割り当てのトレースを開始

# --- 1回目のスナップショット ---
snapshot1 = tracemalloc.take_snapshot()

my_function() # メモリを割り当てる可能性のある関数を実行

# --- 2回目のスナップショット ---
snapshot2 = tracemalloc.take_snapshot()

tracemalloc.stop() # トレースを停止

# スナップショットを比較して差分を表示
top_stats = snapshot2.compare_to(snapshot1, 'lineno')

print("[ Top 10 differences ]")
for stat in top_stats[:10]:
    print(stat)

# 関数 my_function が適切にメモリを解放していれば、
# snapshot1 と snapshot2 の間で my_function 内で割り当てられたメモリは
# 解放されている(あるいは差分が小さい)はずです。
# ただし、完全にゼロになるとは限りません。
# my_function の実行後に snapshot2 を取るタイミングで、
# まだガベージコレクションが走りきっていない可能性もあります。

# より正確に見るには、関数呼び出し前後だけでなく、
# 関数実行後の適切なタイミング(例:gc.collect()後)でスナップショットを取ることも考慮します。

print("\n--- 関数実行後のメモリ状況 ---")
tracemalloc.start()
my_function()
gc.collect() # 明示的にGCを実行
snapshot_after_func_and_gc = tracemalloc.take_snapshot()
tracemalloc.stop()

# 初期状態(トレース開始直後)との比較
initial_snapshot = tracemalloc.Snapshot([], 0) # 空のスナップショット
top_stats_after_func = snapshot_after_func_and_gc.compare_to(initial_snapshot, 'lineno')

print("[ Top 10 after function and GC ]")
for stat in top_stats_after_func[:10]:
    print(stat)

ポイント:

  • tracemalloc.start(): メモリ割り当てのトレースを開始します。
  • tracemalloc.take_snapshot(): 現在のメモリ割り当て状況のスナップショットを取得します。
  • スナップショットを比較することで、どのコードがどれだけメモリを割り当てたか、解放されたかを確認できます。
  • 関数のスコープを抜けた直後に期待通りメモリが解放されているかを確認するのに役立ちます。

3. psutil ライブラリを使用する (外部ライブラリ)

psutil はクロスプラットフォームのライブラリで、実行中のプロセスやシステム全体のCPU、メモリ、ディスク、ネットワークなどの情報を取得できます。プロセス全体のメモリ使用量を見ることで、関数実行による影響を大まかに把握できます。

まず、psutil をインストールする必要があります。

pip install psutil
import os
import psutil

def my_function():
    # 関数内でオブジェクトを作成
    a = [1] * 10**7 # かなり大きなリスト
    # 何か処理
    del a # 明示的に削除(参照をなくす)

# 現在のプロセスを取得
process = psutil.Process(os.getpid())

# 関数呼び出し前のメモリ使用量
mem_before = process.memory_info().rss  # Resident Set Size
print(f"関数呼び出し前のメモリ使用量: {mem_before / 1024**2:.2f} MB")

my_function()
# gc.collect() # 必要であればGCを明示的に呼び出す

# 関数呼び出し後のメモリ使用量
mem_after = process.memory_info().rss
print(f"関数呼び出し後のメモリ使用量: {mem_after / 1024**2:.2f} MB")

if mem_after < mem_before + (10 * 1024**2): # 多少の誤差やPython内部の増加は許容
    print("関数によって確保された大きなメモリは解放された可能性が高いです。")
else:
    print("関数によって確保されたメモリが解放されていない可能性があります。")

ポイント:

  • psutil.Process(os.getpid()).memory_info().rss: 現在のプロセスの物理メモリ使用量 (RSS) を取得します。
  • 関数呼び出し前後でこの値を比較することで、メモリ使用量の変化を確認できます。
  • ただし、これはプロセス全体のメモリ使用量なので、他の要因(Pythonインタープリタ自体の動作など)の影響も受ける可能性があります。非常に大きなメモリ確保・解放でないと変化が分かりにくい場合があります。

注意点

  • Pythonのメモリ管理の挙動: Pythonは使われなくなったオブジェクトのメモリを即座にOSに返すとは限りません。Pythonのメモリマネージャが、後で再利用するためにメモリを保持しておくことがあります (メモリプール)。そのため、gc.collect() を実行しても、OSから見たプロセス全体のメモリ使用量がすぐには減少しないことがあります。
  • 小さなオブジェクト: 小さなオブジェクトの場合、解放されてもその変化を捉えるのは難しいことがあります。
  • 測定タイミング: ガベージコレクションは非同期的に実行されることがあるため、測定のタイミングによって結果が変動することがあります。gc.collect() を挟むことで、ある程度タイミングを揃えられます。
  • 外部ライブラリやC拡張: C言語で書かれた拡張モジュールなどがメモリを確保・解放する場合、Pythonのガベージコレクタだけでは追跡できないことがあります。

まとめ

関数を抜けた後のメモリ解放を確認するには、以下の使い分けが考えられます。

  • 手軽に確認したい、特定の型のオブジェクトの増減を見たい場合: gc モジュール
  • より詳細に、どこでどれだけメモリが割り当てられたかを知りたい場合: tracemalloc モジュール
  • プロセス全体のメモリ使用量の大まかな変化を見たい場合: psutil ライブラリ

これらのツールを使って、メモリが期待通りに管理されているかを確認してみてください。多くの場合、Pythonの自動メモリ管理は非常に効率的ですが、大規模なデータを扱う場合や長時間実行するアプリケーションでは、メモリリークのチェックが重要になることがあります。

python: ローカルでメモリエラーチェック

Pythonコードのローカル環境でのメモリエラーの確認には、いくつかの効果的な方法があります。状況や目的に応じて使い分けるのが良いでしょう。


Pythonコードのローカル環境でのメモリエラー確認方法 🕵️‍♀️

ローカル環境でPythonコードのメモリエラー(特にメモリリークや予期せぬメモリ大量消費)が発生していないかを確認するための最適な方法は、以下のツールの組み合わせや段階的なアプローチです。

1. 標準ライブラリ・組み込み機能の活用

  • resource モジュール (Unix系):

    • プログラムが使用しているリソース(メモリを含む)の情報を取得したり、制限を設定したりできます。
    • 特定の時点でのメモリ使用量を確認するのに役立ちます。
    import resource
    print(f"現在のメモリ使用量 (RSS): {resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024:.2f} MB") # macOS/Linux
    
    • 注意点: Windowsでは利用できません。表示される値はResident Set Size (RSS) であり、Pythonオブジェクトが直接消費しているメモリとは異なる場合があります。
  • gc モジュール (ガベージコレクタ):

    • ガベージコレクションの統計情報を確認したり、手動でGCを実行したりできます。
    • 循環参照によって回収されないオブジェクトが存在しないか調査する手がかりになります。
    import gc
    gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_LEAK) # デバッグ情報を有効化
    # ... コード実行 ...
    gc.collect() # 明示的にGCを実行
    print(gc.garbage) # 回収されなかったオブジェクトのリスト (循環参照の可能性)
    
  • sys.getsizeof():

    • 個々のオブジェクトが消費するメモリサイズを確認できます。
    • 大きなオブジェクトを特定するのに役立ちますが、コンテナオブジェクト(リストや辞書など)の場合、その要素自身のサイズは含まないことに注意が必要です。

2. プロファイリングツールの利用 (推奨)

より詳細なメモリ使用状況の分析には、専門のプロファイリングツールが非常に有効です。

  • memory_profiler:

    • 最も推奨されるツールの一つです。
    • 関数ごと、あるいは行ごとのメモリ使用量を詳細に追跡し、時系列で表示できます。
    • メモリリークの箇所を特定するのに非常に強力です。
    • インストール: pip install memory_profiler psutil (psutilはオプションですが、より正確な情報取得に推奨されます)
    • 使い方:
      from memory_profiler import profile
      
      @profile # メモリ使用量を計測したい関数にデコレータを付与
      def my_func():
          a = [1] * (10 ** 6) # 100万要素のリストを作成
          b = [2] * (2 * 10 ** 7) # 2000万要素のリストを作成
          del b
          return a
      
      if __name__ == '__main__':
          my_func()
      
      実行はコマンドラインから行います: python -m memory_profiler your_script.py
      これにより、行ごとのメモリ増減が出力されます。
  • objgraph:

    • オブジェクト間の参照関係をグラフとして可視化できます。
    • 特定のオブジェクトがなぜメモリ上に残り続けているのか(どこから参照されているのか)を突き止めるのに役立ちます。メモリリークの原因究明に強力です。
    • インストール: pip install objgraph
    • 使い方 (例: 特定の型のオブジェクトで最も多いものを表示):
      import objgraph
      
      # ... コード実行 ...
      
      objgraph.show_most_common_types(limit=10)
      # objgraph.show_backrefs([objgraph.by_type('MyProblematicClass')[0]], max_depth=5, filename='backrefs.png') # 特定オブジェクトの参照元を画像出力
      
  • Pympler:

    • Pythonオブジェクトのメモリ使用状況をより深く調査するための開発ツールです。
    • 特定のクラスのインスタンス数や、それらが消費するメモリサイズなどを追跡できます。
    • インストール: pip install Pympler
    • 使い方 (例: メモリ上の全オブジェクトの概要を表示):
      from pympler import summary, muppy
      
      # ... コード実行 ...
      
      all_objects = muppy.get_objects()
      sum1 = summary.summarize(all_objects)
      summary.print_(sum1)
      

3. OSのモニタリングツール 💻

Pythonスクリプト実行中に、OSが提供するモニタリングツールでプロセス全体のメモリ使用量を確認するのも基本的ながら有効です。

  • Windows: タスクマネージャー (詳細タブでメモリ使用量を確認)
  • macOS: アクティビティモニタ (メモリタブで確認)
  • Linux: top コマンドや htop コマンド (RES や SHR, VIRT などの項目を確認)

これらのツールは、プログラム全体のメモリフットプリントの大まかな傾向を把握するのに役立ちます。急激なメモリ増加や、終了後もメモリが解放されないといった状況を発見できます。


4. コーディング上の注意点とデバッグ手法 ✍️

  • 大きなデータ構造の扱い:

    • 巨大なリストや辞書を一度にメモリに読み込むのではなく、ジェネレータやイテレータを活用して、データを逐次処理することを検討しましょう。
    • pandas などのライブラリを使用している場合、dtype の適切な選択や不要な列の削除、chunksize を使った逐次処理などが有効です。
  • オブジェクト参照の管理:

    • 不要になったオブジェクトへの参照は明示的に del で削除するか、スコープアウトさせてガベージコレクタが回収できるようにしましょう。
    • 特に、グローバル変数やクラス変数に大きなオブジェクトを保持し続けると、メモリリークの原因になりやすいです。
  • 循環参照の回避:

    • __del__ メソッドを持つオブジェクト同士が互いに参照し合っている場合など、循環参照が発生するとガベージコレクタがオブジェクトを回収できなくなることがあります。weakref モジュールを使って弱参照にすることを検討しましょう。
  • 段階的なテスト:

    • コードの特定の部分(特にループ処理やデータ処理部分)を実行する前と後でメモリ使用量を比較し、どの部分でメモリが大きく増加しているかを特定します。

まとめと推奨手順

  1. まずは OSのモニタリングツール で大まかなメモリ使用状況を確認します。
  2. 特定の関数や処理ブロックが怪しい場合は、memory_profiler を使って詳細なメモリ使用量の変化を追跡します。これが最も効果的な場合が多いです。
  3. 特定のオブジェクトがリークしている疑いがある場合は、objgraphPymplergc.garbage などを使って、オブジェクトの参照関係や残存状況を調査します。
  4. 並行して、コーディング上の注意点 を見直し、メモリ効率の良い書き方になっていないか確認します。

これらの方法を組み合わせることで、Pythonコードのメモリエラーを効率的に特定し、修正することができるでしょう。頑張ってください! 👍

かーなびかーなび

デフォルト引数を書くとき

デフォルト引数を書くメリット

呼び出し側がシンプルになる
毎回 is_parallel=True を渡さなくて良いので、ほとんどのケースで並列実行したい場合は記述が減り、読みやすくなります。

関数の意図が明確になる
デフォルト動作(ここでは並列実行)が明示され、関数の標準的な使い方が分かりやすくなります。

後方互換性
既存の呼び出し箇所に影響を与えずに、引数を追加できます。

デフォルト引数を書かない方が良い場合

挙動が明示的に必要な場合
並列・非並列どちらも頻繁に使われ、呼び出しごとに明示したい場合はデフォルト値をつけない方が間違いが起きにくくなります。

予期しないパフォーマンス問題を防ぎたい場合
並列実行が重い処理の場合、デフォルトで有効にしておくと、無意識にリソースを消費することがあります。
デフォルト引数を書くメリット
呼び出し側がシンプルになる
毎回 is_parallel=True を渡さなくて良いので、ほとんどのケースで並列実行したい場合は記述が減り、読みやすくなります。

関数の意図が明確になる
デフォルト動作(ここでは並列実行)が明示され、関数の標準的な使い方が分かりやすくなります。

後方互換性
既存の呼び出し箇所に影響を与えずに、引数を追加できます。

デフォルト引数を書かない方が良い場合
挙動が明示的に必要な場合
並列・非並列どちらも頻繁に使われ、呼び出しごとに明示したい場合はデフォルト値をつけない方が間違いが起きにくくなります。

予期しないパフォーマンス問題を防ぎたい場合
並列実行が重い処理の場合、デフォルトで有効にしておくと、無意識にリソースを消費することがあります。

かーなびかーなび
  • poetry環境でjupyternotebookを動かす方法
    • poetry env info --pathのパスの末尾がpoetry環境名なので控えておく

    • カーネルへ登録

      poetry run python -m ipykernel install --user --name=$(poetry env info --name)
      
かーなびかーなび

ローカルでテキストの差分を見る

VSCode(Visual Studio Code)には、標準機能としても強力な差分比較機能が備わっていますし、さらに便利な拡張機能も多数存在します。

目的に応じて使い分けるのがおすすめです。

  1. VSCodeの標準機能(インストール不要)
    まず、拡張機能を追加しなくても、VSCodeにはファイル同士を比較する機能が標準で搭載されています。

使い方
エクスプローラー(ファイル一覧)で、比較したいファイルを1つクリックします。
Ctrlキー(MacではCmdキー)を押しながら、もう1つの比較したいファイルをクリックして、2つのファイルを選択状態にします。
選択したファイルを右クリックし、メニューから [選択項目の比較 (Compare Selected)] を選びます。
これだけで、左右にファイルが並んだ差分表示画面が開きます。追加・削除・変更された行が色付きでハイライトされ、直感的に違いがわかります。

かーなびかーなび

GitHub上でブランチのマージ履歴を可視化する

GitHub上でブランチのマージ履歴を可視化するには、主にGitHubの標準機能Gitコマンド、そしてGUIクライアントツールの3つの方法があります。それぞれの特徴に合わせて使い分けるのがおすすめです。


1. GitHubのネットワークグラフ機能を利用する

GitHubのウェブサイトには、ブランチの歴史を視覚的に表示する「ネットワークグラフ」機能が組み込まれています。

手順

  1. 可視化したいリポジトリのページにアクセスします。
  2. タブの中から「Insights」をクリックします。
  3. 左側のサイドバーから「Network」を選択します。

このグラフでは、どのブランチがどのブランチから派生し、どこでマージされたのかが一目でわかります。各ドットがコミットを表しており、クリックするとそのコミットの詳細を確認できます。


2. git log コマンドを利用する(ターミナル)

ターミナル上でGitを操作している場合、git logコマンドにオプションを追加することで、マージ履歴をグラフ形式で表示できます。

基本的なコマンド

git log --graph

このコマンドで、コミット履歴がアスキーアートの線で結ばれたグラフとして表示されます。

より見やすく便利なコマンド例
さらにオプションを追加すると、よりコンパクトで分かりやすい表示になります。

git log --graph --oneline --all --decorate
  • --graph: コミットの親子関係をグラフで表示します。
  • --oneline: 各コミットを1行で簡潔に表示します。
  • --all: 現在のブランチだけでなく、すべてのブランチの履歴を表示します。
  • --decorate: ブランチ名やタグ名などの参照情報を表示します。

💡エイリアスの設定
毎回長いコマンドを打つのは大変なので、エイリアス(別名)を設定しておくと便利です。

git config --global alias.lol "log --graph --oneline --all --decorate"

上記のように設定すると、次からは git lol という短いコマンドで同じ表示を得られます。


3. GUIクライアントツールを利用する

より直感的で高機能な可視化を求める場合は、専用のGUIクライアントツールの利用がおすすめです。

  • SourceTree: Atlassianが提供する無料のGitクライアントです。ブランチの履歴が非常に見やすく、初心者にも扱いやすいのが特徴です。
  • GitKraken: 美しいUIと高度な機能を兼ね備えた人気のクライアントです。ブランチの操作やマージの状況を直感的に把握できます。
  • VSCode拡張機能 (Git Graph): Visual Studio Codeをお使いの場合、この拡張機能をインストールするだけで、エディタ内で美しく色分けされたブランチ履歴グラフを確認できます。

これらのツールは、コマンドラインに不慣れな方や、複雑なブランチ構造を頻繁に扱う開発者にとって強力な助けとなります。

かーなびかーなび

ブランチとタグの違い, タグからどうブランチを切るか

ブランチとタグの違い、そしてタグからのブランチ作成方法

Gitにおける「ブランチ」と「タグ」は、どちらも特定のコミットを指すポインタですが、その目的と性質が根本的に異なります。一言で言うと、ブランチは「開発の流れ」、**タグは「特定の時点の目印(印)」**です。


ブランチとタグの違い

項目 ブランチ (Branch) タグ (Tag)
目的 新機能開発やバグ修正など、履歴を分岐させて作業するため バージョンリリースなど、特定のコミットを永続的に記録するため
ポインタの動き 新しいコミットが追加されると、ポインタが前に進む(動く) 一度作成すると、基本的にそのコミットを指し続け、動かない
比喩 付箋(作業が進むにつれて貼り替えていく) 本のしおり(特定のページに挟んでおく)
主な用途 ・新しい機能(feature)の開発<br>・バグ修正(bugfix, hotfix)<br>・並行して複数の開発を進める ・バージョン番号の記録(v1.0, v2.1.3など)<br>・重要なマイルストーンの記録

ブランチとは?

ブランチは、maindevelopといった本流から分岐した、独立した開発のラインです。新しい機能を追加する際、他の開発者の作業に影響を与えないように新しいブランチを作成し、そこで作業を進めます。機能が完成したら、本流のブランチにマージ(統合)します。このように、ブランチは開発の進行と共に成長し、ポインタがどんどん先に進んでいきます。

タグとは?

タグは、「バージョン1.0のリリース時点」といった、歴史の中の特定の1点に付ける固定のラベルです。タグを付けておけば、後から「あのリリース時点のソースコードはどれだっけ?」となった時に、すぐにそのコミットを特定できます。ブランチと違い、タグのポインタは新しいコミットが作られても移動しません。


タグからfeatureブランチを作成する方法

過去の特定のリリースバージョン(タグ)を元に、バグ修正や新しい派生機能の開発を行いたい場合があります。その際は、タグを起点として新しいブランチを作成します。

手順

git checkoutコマンドに-bオプションを付けて実行するのが最も簡単です。このコマンドは、ブランチの作成と、そのブランチへの切り替え(チェックアウト)を同時に行います。

コマンドの基本形

git checkout -b <新しいブランチ名> <元にするタグ名>

具体的なコマンド例
例えば、v1.2というタグが付いたリリースバージョンを元に、新しい機能を追加するためのfeature/new-idea-from-v1.2というブランチを作成したい場合は、以下のようにコマンドを実行します。

git checkout -b feature/new-idea-from-v1.2 v1.2

実行後の流れ

  1. Gitはv1.2タグが指しているコミットを探します。
  2. そのコミットを起点として、feature/new-idea-from-v1.2という名前の新しいブランチを作成します。
  3. 現在の作業ブランチが、今作成したfeature/new-idea-from-v1.2に切り替わります。

これで、あなたはv1.2リリースの時点のソースコードを元に、安全に新しい作業を開始できます。

まとめ

  • ブランチは、日々の開発で作業を分離・並行させるための「動くポインタ」。
  • タグは、リリースなど重要な時点を記録するための「動かない目印」。
  • タグからブランチを作成することで、過去の特定のバージョンを元にした開発を簡単かつ安全に始めることができます。

この2つの機能を正しく理解し使い分けることで、Gitによるバージョン管理がより効率的になります。

かーなびかーなび

AI活用

  • gemini
    • 変数名、関数名、ブランチ名など命名を考えるときに使用
  • devin
    • 開発に使用
  • notion AIミーティングノート
    • 朝会などの議事録に使用
かーなびかーなび

vscodeでテキストの差分を確認する

VSCode(Visual Studio Code)には、標準機能としても強力な差分比較機能が備わっています。
目的に応じて使い分けるのがおすすめです。

  1. VSCodeの標準機能(インストール不要)
    まず、拡張機能を追加しなくても、VSCodeにはファイル同士を比較する機能が標準で搭載されています。
    使い方
    エクスプローラー(ファイル一覧)で、比較したいファイルを1つクリックします。
  2. Ctrlキー(MacではCmdキー)を押しながら、もう1つの比較したいファイルをクリックして、2つのファイルを選択状態にします。
  3. 選択したファイルを右クリックし、メニューから [選択項目の比較 (Compare Selected)] を選びます。
    これだけで、左右にファイルが並んだ差分表示画面が開きます。追加・削除・変更された行が色付きでハイライトされ、直感的に違いがわかります。
かーなびかーなび

Pathlib

はい、pathlibモジュールを使ってパスを定義し、ファイルの内容を読み込むPythonコードですね。


pathlibを使ったファイルの読み込み

以下のコードでは、まずPathオブジェクトを使ってファイルパスを定義し、そのパスが存在しない場合はサンプルファイルを作成します。その後、read_text()メソッドを使ってそのファイルの内容を読み込み、表示します。

from pathlib import Path

# --- 1. ファイルパスを定義 ---
# 現在のスクリプトがあるディレクトリに 'sample_file.txt' という名前のファイルを指定します。
# 別の場所にファイルを指定したい場合は、Path("C:/Users/YourName/Documents/my_file.txt") のようにフルパスで記述します。
file_path = Path("sample_file.txt")

# --- 2. ファイルが存在しない場合は作成(オプション) ---
if not file_path.exists():
    print(f"'{file_path}' が見つかりません。新しいファイルを作成します。")
    try:
        # write_text() も read_text() と同様に、ファイルを開いて書き込み、閉じる処理をまとめて行います。
        file_path.write_text("Hello, pathlib!\nこれはサンプルファイルです。\n日本語も読めます。", encoding="utf-8")
        print(f"'{file_path}' を作成し、内容を書き込みました。")
    except Exception as e:
        print(f"ファイルの作成中にエラーが発生しました: {e}")
else:
    print(f"'{file_path}' は既に存在します。")

# --- 3. ファイルの内容を読み込み ---
try:
    # read_text() を使ってファイルの内容を読み込みます。
    # encoding='utf-8' は、日本語などを含むテキストファイルを正しく読み込むために重要です。
    content = file_path.read_text(encoding="utf-8")
    print(f"\n--- '{file_path}' の内容 ---")
    print(content)
    print("-------------------------")

except FileNotFoundError:
    print(f"エラー: '{file_path}' が見つかりません。")
except UnicodeDecodeError:
    print(f"エラー: '{file_path}' のエンコーディングがUTF-8ではありません。encodingパラメータを確認してください。")
except Exception as e:
    print(f"ファイルの読み込み中に予期せぬエラーが発生しました: {e}")

# --- 4. (おまけ) ファイルの削除(必要なければコメントアウトしてください) ---
# この行を有効にすると、スクリプト実行後に作成されたファイルが削除されます。
# if file_path.exists():
#     file_path.unlink() # ファイルを削除
#     print(f"'{file_path}' を削除しました。")

コードの解説

  1. from pathlib import Path: pathlibモジュールからPathクラスをインポートします。
  2. file_path = Path("sample_file.txt"): Pathオブジェクトを作成し、操作したいファイルのパスを定義します。相対パス(この例のようにファイル名のみ)でも、絶対パス(例: Path("/home/user/document/my_file.txt"))でも指定できます。
  3. if not file_path.exists():: exists()メソッドは、指定されたパスにファイルまたはディレクトリが存在するかどうかを確認します。この例では、ファイルが存在しない場合に新しいファイルを作成しています。
  4. file_path.write_text(...): read_text()の対となるメソッドで、指定した文字列をファイルに書き込みます。ここでもencoding="utf-8"を指定して、日本語が正しく書き込まれるようにしています。
  5. content = file_path.read_text(encoding="utf-8"): ここが今回のメインです。read_text()メソッドを呼び出すことで、ファイルの内容が文字列としてcontent変数に格納されます。encoding="utf-8"は、ファイルがUTF-8エンコーディングで保存されていることを明示し、文字化けを防ぎます。
  6. エラーハンドリング: try...exceptブロックを使って、FileNotFoundError(ファイルが見つからない場合)やUnicodeDecodeError(エンコーディングが合わない場合)などのエラーを適切に処理しています。

このコードを実行すると、まずsample_file.txtが作成され(もしなければ)、その内容がコンソールに出力されます。これでpathlibを使ったファイルパスの定義とテキストファイルの読み込みの流れがご理解いただけたかと思います。

かーなびかーなび

ModelFactory

polyfactory.factories.pydantic_factory.ModelFactory は、Pydantic モデルのインスタンスを生成するためのツールです。簡単に言えば、Pydantic モデルのテストデータやダミーデータを簡単に作成できるように設計されています。

具体的に何ができるかというと、以下の点が挙げられます。

  • Pydantic モデルのインスタンス生成: Pydantic モデルを定義するだけで、そのモデルの型ヒントに基づいて、適切な値を持つインスタンスを自動的に生成します。
  • デフォルト値の自動入力: モデルにデフォルト値が設定されている場合、それを考慮してインスタンスを生成します。
  • ネストされたモデルのサポート: モデルが他のPydanticモデルをフィールドとして持つ場合(ネストされたモデル)、それらも適切に生成します。
  • 様々な型のサポート: str, int, float, bool, list, dict, datetime, UUID など、Pydanticがサポートする多くのPythonの組み込み型や標準ライブラリの型に対応しています。
  • カスタムジェネレータの定義: 特定のフィールドに対して、独自のデータ生成ロジック(例えば、特定の形式の文字列や範囲内の数値など)を定義することができます。これにより、より現実的で意味のあるテストデータを生成できます。
  • フィールドのオーバーライド: 生成時に特定のフィールドの値を明示的に指定して、デフォルトの生成ロジックを上書きすることができます。
  • 一貫性のあるデータ生成: シード値を設定することで、繰り返し同じデータが生成されるように制御でき、テストの再現性を高めます。
  • 複雑なデータ構造のテスト: Pydanticの機能(例: Optional, Union, Literal など)をフル活用したモデルに対しても、適切なデータを生成できます。

なぜこれが便利なのか?

  • テストの効率化: 手動でPydanticモデルのインスタンスを作成する手間を省き、テストコードの記述量を削減できます。特に、モデルが複雑でフィールドが多い場合に威力を発揮します。
  • テストデータの多様性: 自動的に多様なデータを生成してくれるため、エッジケースや予期せぬ入力に対するテストがしやすくなります。
  • 開発の迅速化: APIのモックデータやフロントエンドのダミーデータなど、開発の様々なフェーズで利用できます。

簡単な例:

from pydantic import BaseModel
from polyfactory.factories.pydantic_factory import ModelFactory
import datetime

class User(BaseModel):
    id: int
    name: str
    email: str
    is_active: bool = True
    created_at: datetime.datetime

# Userモデルのファクトリを作成
class UserFactory(ModelFactory[User]):
    __model__ = User

# インスタンスを生成
user_instance = UserFactory.build()
print(user_instance)

# 特定のフィールドをオーバーライドして生成
custom_user = UserFactory.build(name="John Doe", email="john.doe@example.com")
print(custom_user)

このように、polyfactory.factories.pydantic_factory.ModelFactory は、Pydanticモデルを利用したアプリケーションの開発やテストにおいて、非常に強力で便利なツールとなります。

かーなびかーなび

@propertyのメリットと使いどころ

Pythonにおける@propertyは、デコレータの一種で、クラスのメソッドを属性(アトリビュート)のように振る舞わせるために使われます。これにより、データのカプセル化を強化し、コードの可読性と保守性を向上させることができます。

より具体的に言うと、@propertyは以下のことを可能にします。

  1. ゲッター(Getter)の作成:
    @propertyをメソッドの上に付けると、そのメソッドがクラスの属性のようにアクセスできるようになります。つまり、メソッドを呼び出すための括弧()が不要になります。このメソッドは、通常、内部のプライベートなデータ(慣習的にアンダースコア_で始まる名前が付けられることが多い)を取得するために使われます。

    例:

    class Circle:
        def __init__(self, radius):
            self._radius = radius # プライベートな属性として扱う
    
        @property
        def radius(self):
            """円の半径を返すゲッター"""
            print("Getting radius...")
            return self._radius
    
    c = Circle(5)
    print(c.radius) # メソッドなのに、属性のようにアクセスできる
    # 出力:
    # Getting radius...
    # 5
    
  2. セッター(Setter)の作成:
    @<property_name>.setterという形で、プロパティに対応するセッターメソッドを定義できます。これは、属性に値を割り当てる際に実行されるロジックを記述するために使われます。値の検証や、関連する他の属性の更新などを行うことができます。

    例:

    class Circle:
        def __init__(self, radius):
            self._radius = radius
    
        @property
        def radius(self):
            """円の半径を返すゲッター"""
            print("Getting radius...")
            return self._radius
    
        @radius.setter
        def radius(self, value):
            """円の半径を設定するセッター(値の検証付き)"""
            print(f"Setting radius to {value}...")
            if value < 0:
                raise ValueError("Radius cannot be negative")
            self._radius = value
    
    c = Circle(5)
    c.radius = 10 # セッターが呼び出される
    print(c.radius)
    
    try:
        c.radius = -2 # エラーが発生
    except ValueError as e:
        print(e)
    # 出力:
    # Setting radius to 10...
    # Getting radius...
    # 10
    # Radius cannot be negative
    
  3. デリーター(Deleter)の作成:
    @<property_name>.deleterという形で、プロパティに対応するデリーターメソッドを定義できます。これは、delキーワードを使って属性を削除しようとしたときに実行されるロジックを記述します。

    例:

    class Circle:
        def __init__(self, radius):
            self._radius = radius
    
        @property
        def radius(self):
            return self._radius
    
        @radius.setter
        def radius(self, value):
            if value < 0:
                raise ValueError("Radius cannot be negative")
            self._radius = value
    
        @radius.deleter
        def radius(self):
            print("Deleting radius...")
            del self._radius # 実際の属性を削除
    
    c = Circle(5)
    del c.radius # デリーターが呼び出される
    try:
        print(c.radius) # 属性が削除されているためエラー
    except AttributeError as e:
        print(e)
    # 出力:
    # Deleting radius...
    # 'Circle' object has no attribute '_radius'
    

@propertyを使うメリット:

  • カプセル化の強化: 内部の実装(データの保存方法)を外部から隠蔽し、属性へのアクセスを制御できます。これにより、オブジェクトの整合性を保ちやすくなります。
  • インターフェースの維持: 初期段階ではシンプルな属性として実装しておき、後からゲッターやセッターに複雑なロジック(データ検証、計算、キャッシュなど)を追加する場合でも、外部からその属性にアクセスする方法(obj.attribute)を変える必要がありません。これにより、既存のコードを変更することなく、内部の実装を改善できます。
  • コードの可読性向上: ゲッターやセッターを明示的にメソッドとして呼び出すよりも、属性のようにアクセスできるため、コードがより自然で読みやすくなります。
  • "Pythonic"な記述: Pythonでは、Javaなどの言語でよく見られるgetSomething()setSomething()といった明示的なゲッター・セッターメソッドを多用するよりも、@propertyを使った「属性のように振る舞うメソッド」が推奨されます。これは「Don't ask for permission, ask for forgiveness(許可を求めるな、許しを請え)」というPythonのイディオムとも関連しています。

まとめると、@propertyはPythonにおけるオブジェクト指向プログラミングにおいて、属性のアクセス制御を柔軟かつPythonicに実現するための非常に強力な機能です。


@property を使うことで、なぜカプセル化が強化され、コードの可読性が向上するのかを具体的に掘り下げてみましょう。

1. カプセル化の強化

カプセル化とは、オブジェクトの内部状態や動作を外部から隠蔽し、外部とのやり取りを特定のインターフェース(メソッドやプロパティ)を通じてのみ行うようにするプログラミングの原則です。これにより、内部実装の変更が外部に影響を与えるのを防ぎ、オブジェクトの整合性を保ちやすくなります。

@property がカプセル化を強化する理由は以下の通りです。

  • 内部表現からの分離:

    • @property を使わない場合、クラスの属性に直接アクセスすることが一般的です(例: obj.data = new_value)。この場合、data という属性がどのような形で内部に保存されているかが外部に露出しています。
    • @property を使うと、外部からは引き続き obj.data = new_value のように属性としてアクセスしているように見えますが、実際には内部で定義されたゲッターやセッターメソッドが呼び出されます。これにより、data の実際の保存方法(例: _data というプライベート変数に格納する、計算して返す、データベースから取得する、など)を、外部のコードに影響を与えることなく変更できます。
    • 例: 最初は単に値を保存するだけだったpriceプロパティが、後から為替レートに応じて自動計算されるように変更されたとしても、外部コードはproduct.priceという書き方を変更する必要がありません。
  • 値の検証と制御:

    • セッター (@<property_name>.setter) を利用することで、属性に値が割り当てられる際に、その値が妥当であるかを検証したり、特定の処理(例: 関連する他の属性の更新、イベントの発火など)を実行したりするロジックを組み込むことができます。
    • 直接属性にアクセスさせる場合、このような検証や制御は、属性を割り当てる側(外部コード)に責任が委ねられてしまいます。これはエラーを引き起こしやすく、オブジェクトの整合性を損なう可能性があります。
    • 例: person.age = -5 のような無効な代入を防ぎ、エラーを発生させたり、デフォルト値に設定したりすることができます。
  • リードオンリー属性の実現:

    • @property デコレータを付けたゲッターメソッドのみを定義し、セッターを定義しないことで、そのプロパティを外部から読み取り専用(リードオンリー)にすることができます。これは、オブジェクトの初期化時に一度だけ設定され、その後は変更されてはならない属性(例: idcreation_date など)に対して特に有効です。
    • 例: user.id は読み取れるが、user.id = new_id とはできないようにすることで、誤って重要なIDを変更してしまうことを防ぎます。

2. 可読性の向上

@property がコードの可読性を向上させる理由は以下の通りです。

  • 直感的なアクセス:

    • プログラマーは、オブジェクトの属性にアクセスする際に、通常 obj.attribute_name の形式を期待します。@property を使用すると、メソッドであるにもかかわらず、この属性アクセスの形式を維持できます。
    • もし @property がなければ、ゲッターメソッドを obj.get_attribute_name() のように呼び出す必要があり、これは冗長で、データへのアクセスというよりも処理の実行のように見えてしまいます。
    • 例: person.age と書く方が person.get_age() と書くよりも、直感的に「Personオブジェクトの年齢というデータにアクセスしている」ことが伝わります。
  • 一貫したインターフェース:

    • 最初は単なるデータフィールドとして始まったものが、後から何らかの計算や検証を伴うデータに変わった場合でも、@property を使っていれば、外部のコードがそのデータにアクセスする方法(obj.attribute)を変える必要がありません。
    • これにより、API(外部から利用されるインターフェース)の一貫性が保たれ、利用者は実装の詳細を意識することなくコードを書くことができます。これは、大規模なプロジェクトやライブラリ開発において特に重要です。
  • Pythonicな表現:

    • Pythonの設計思想には、「Getters/Settersは必ずしも必要ない」という考え方があります。これは、Javaのような言語でよく見られる getSomething()setSomething() の多用を避け、より直接的な属性アクセスを推奨するものです。
    • @property は、このPythonicなスタイルに沿って、必要な場合にのみゲッター/セッターのロジックをカプセル化する手段を提供します。これにより、コードがよりPythonのイディオムに則った自然な形になり、Python開発者にとっては読みやすく理解しやすくなります。

まとめ

@property は、外部からオブジェクトのデータにアクセスする際、それが直接的なデータフィールドなのか、それとも裏で何らかのロジックが実行されているのかを意識させずに、一貫したシンプルな属性アクセスを提供します。これにより、内部実装の変更に対する柔軟性(カプセル化の強化)と、直感的で読みやすいコード(可読性の向上)の両方を実現できるのです。


「内部表現からの分離」は、@property を使うことで得られるカプセル化の重要な側面であり、クラスの外部にあるコードが、そのクラスがデータをどのように保持・管理しているかを知る必要がない、という状態を指します。

もっと詳しく見ていきましょう。

内部表現とは何か?

まず「内部表現」とは、クラスがその状態を保持するために使用する実際の変数やデータ構造のことです。これは通常、クラスのインスタンス変数(例: self._value, self._data_list, self._cached_result など)として定義されます。これらの変数は、クラスの内部でしか直接操作されるべきではない、という意図を込めて、慣習的にアンダースコア (_) で始まる名前が付けられることが多いです(Pythonでは厳密なプライベート変数はないため、これはあくまで慣習です)。

分離されていない状態(@property を使わない場合)

@property を使わずに、クラスの外部から直接属性にアクセスさせる場合を考えます。

class Product:
    def __init__(self, price_usd):
        self.price_usd = price_usd # 外部から直接アクセスされる属性

product = Product(100)
print(product.price_usd) # 外部から直接 price_usd にアクセス

# 後で円建ての価格も必要になったとする
# 外部コードは price_usd を参照しているため、
# 内部で price_jpy を持つように変更すると、外部コードも修正が必要になる

この例では、price_usd という属性がクラスの内部でどのように保存されているか(つまり、単純に self.price_usd という変数に格納されていること)が外部に露出しています。外部のコードは、product.price_usd と書くことで、内部の _usd という変数に直接アクセスしているという事実を知ってしまっています。

@[property] による内部表現からの分離

ここで @property を導入します。

class Product:
    def __init__(self, price_usd):
        self._price_usd = price_usd # 内部表現として_price_usdを使う

    @property
    def price(self):
        """外部からは price という属性として見えるが、内部は _price_usd"""
        print("Getting price...")
        return self._price_usd

    @price.setter
    def price(self, value):
        print(f"Setting price to {value}...")
        if value < 0:
            raise ValueError("Price cannot be negative")
        self._price_usd = value

product = Product(100)
print(product.price) # 外部からは obj.price とアクセス
product.price = 120 # 外部からは obj.price = value とアクセス

この例では、

  • 外部から見えるインターフェース: product.price
  • 内部でデータを保持する変数(内部表現): self._price_usd

となっています。

外部のコードは product.price という形でアクセスしますが、これは実際には Product クラスのゲッターメソッド (@property def price(self):) を呼び出しています。同様に、product.price = 120 と書くと、セッターメソッド (@price.setter def price(self, value):) が呼び出されます。

重要なのは、外部のコードが _price_usd という内部変数について何も知らないということです。外部コードは、price という「属性」があることだけを知っています。

分離のメリット(将来の変更への耐性)

この「内部表現からの分離」が、将来の変更に対してクラスをより堅牢にします。

シナリオ1: 内部データ形式の変更

  • 初期状態: 価格は常にUSDで、単純な数値として self._price_usd に保存。

    class Product:
        def __init__(self, price_usd):
            self._price_usd = price_usd
    
        @property
        def price(self):
            return self._price_usd # USD価格を直接返す
    
  • 変更後: アプリケーションがグローバルになり、価格を複数の通貨で管理する必要が出たとする。内部では辞書 self._prices_by_currency = {'USD': 100, 'JPY': 15000} のような形で保存したい。しかし、product.price でアクセスした場合は、常にUSDで返すか、ユーザーのデフォルト通貨で返すようにしたい。

    class Product:
        def __init__(self, price_usd):
            # 内部表現を変更: 単一の数値から辞書へ
            self._prices_by_currency = {'USD': price_usd}
    
        @property
        def price(self):
            # 内部表現から特定の値を計算して返す
            print("Fetching USD price from internal dictionary...")
            return self._prices_by_currency.get('USD') # または self._prices_by_currency.get(self._default_currency)
    

    この変更が行われたとしても、外部のコードは product.price と書き続けることができます。 外部コードは、price がどのように内部で管理されているか(単一の変数なのか、辞書のキーなのか、計算結果なのか)を知る必要がありません。この変更はクラスの内部にカプセル化されており、外部には影響しません。

シナリオ2: 値の計算や外部からの取得

  • 初期状態: total_items は、リストの長さを直接返す。

    class ShoppingCart:
        def __init__(self):
            self._items = []
    
        @property
        def total_items(self):
            return len(self._items) # リストの長さを直接返す
    
  • 変更後: カートの商品数が非常に多くなる可能性があり、常に len(self._items) を計算するのは非効率かもしれない。そこで、商品追加時に _total_items_count という変数に数をキャッシュし、そこから返すように変更したい。

    class ShoppingCart:
        def __init__(self):
            self._items = []
            self._total_items_count = 0 # 新しい内部表現
    
        def add_item(self, item):
            self._items.append(item)
            self._total_items_count += 1 # キャッシュを更新
    
        @property
        def total_items(self):
            # 内部表現(キャッシュされた値)を返す
            print("Returning cached total items...")
            return self._total_items_count
    

    ここでも、外部のコードは cart.total_items と書き続けることができます。 以前はリストの長さを計算していたのが、今ではキャッシュされた値を返していますが、この変更は外部コードには透過的です。

まとめ

「内部表現からの分離」とは、@property を使うことで、クラスの外部がそのクラスのデータが実際にどのように保存されているか(内部表現)を知る必要がないようにすることです。外部コードは、単に「price という概念的な属性がある」とだけ理解していればよく、その price が内部でどのような変数に、どのような形式で、どのように管理されているかは気にしません。

この分離のおかげで、クラスの内部実装(内部表現)を変更しても、そのクラスを利用している外部のコードに影響を与えることなく、安全かつ柔軟に改善や最適化を行うことができるのです。これが、カプセル化の強力なメリットの一つです。

かーなびかーなび

日本語の新字体と旧字体を変換

日本語の新字体と旧字体を変換するPython辞書を作成したいとのこと、素晴らしいアイデアですね!いくつかツールやライブラリがありますので、ご紹介します。

1. mojimoji ライブラリ

mojimoji は、Pythonで全角半角、ひらがなカタカナ、新旧漢字などの文字種変換を行うための非常に便利なライブラリです。特に新旧漢字の変換に対応しており、今回の用途にピッタリです。

特徴:

  • シンプルで使いやすいAPI
  • 新字体から旧字体、旧字体から新字体への変換が可能
  • 他にも様々な文字種変換に対応

インストール:

pip install mojimoji

使用例:

import mojimoji

# 新字体から旧字体へ
new_to_old_dict = {}
new_words = ["画", "為", "駅", "円", "会"] # 変換したい新字体漢字のリスト
for word in new_words:
    old_word = mojimoji.han_to_zen(word, kana=False, ascii=False, digit=False, old_chars=True)
    new_to_old_dict[word] = old_word
print("新字体から旧字体への辞書:", new_to_old_dict)
# 出力例: 新字体から旧字体への辞書: {'画': '畫', '為': '爲', '駅': '驛', '円': '圓', '会': '會'}


# 旧字体から新字体へ
old_to_new_dict = {}
old_words = ["畫", "爲", "驛", "圓", "會"] # 変換したい旧字体漢字のリスト
for word in old_words:
    new_word = mojimoji.zen_to_han(word, kana=False, ascii=False, digit=False, old_chars=False)
    old_to_new_dict[word] = new_word
print("旧字体から新字体への辞書:", old_to_new_dict)
# 出力例: 旧字体から新字体への辞書: {'畫': '画', '爲': '為', '驛': '駅', '圓': '円', '會': '会'}

# もし個別の文字での変換が必要な場合 (より細かく制御したい場合)
print(mojimoji.han_to_zen("画", old_chars=True)) # '畫'
print(mojimoji.zen_to_han("畫", old_chars=False)) # '画'

mojimojiは、内部的に新旧字体変換のためのマッピングを持っているので、これを活用するのが最も手軽で確実な方法です。上記の例のように、変換したい文字のリストを準備し、ループで変換して辞書に格納していくのが良いでしょう。

2. Unicodeの正規化と関連データ

新旧漢字の変換は、単なる1対1の対応だけでなく、Unicodeの正規化フォーム(NFKCなど)や、異体字セレクタ(IVS)などの概念が関わってくる場合があります。しかし、これらを直接操作して新旧漢字辞書を作成するのは非常に複雑です。

もし mojimoji で対応できないような特殊なケースや、より深いレベルでの文字情報にアクセスしたい場合は、以下の方向性も考えられますが、難易度は上がります。

  • Unicodedataモジュール: Python標準ライブラリのunicodedataモジュールは、Unicode文字データベースへのアクセスを提供します。しかし、直接的に新旧漢字の変換機能があるわけではありません。
  • より専門的な辞書データ: 新旧漢字の対応リストを公開しているウェブサイトやデータベース(例: https://www.unicode.org/charts/ などのUnicode Consortiumの関連文書、あるいは JIS X 0213 などの文字コード規格関連の情報源)からデータを取得し、自分でマッピング辞書を作成することも可能ですが、データの収集とパースに手間がかかります。

結論とおすすめ

最もおすすめするのは、mojimojiライブラリを使う方法です。

これは、新旧漢字変換に特化しており、手軽に目的の辞書を作成できます。上記のサンプルコードを参考に、変換したい漢字の範囲を広げて辞書を構築してみてください。

もし特定の漢字が変換できないなどの問題が発生した場合は、その漢字が mojimoji の変換ルールに含まれていない可能性があります。その場合は、手動で辞書に追加するか、より詳細な文字コード情報を参照する必要がありますが、まずは mojimoji で試すのが良いでしょう。

かーなびかーなび

poetry周りのファイル解説

Poetry関連ファイル徹底解説:pyproject.toml, poetry.lockなどを網羅的に

Pythonのパッケージ管理ツールであるPoetryは、プロジェクトの依存関係を宣言的かつ再現可能に管理するために、いくつかの重要なファイルを利用します。ここでは、pyproject.tomlpoetry.lockを中心に、Poetryに関連するファイルを網羅的に解説します。


1. pyproject.toml:プロジェクトの心臓部

pyproject.tomlは、Poetryプロジェクトにおいて最も重要なファイルです。PEP 518で標準化されたこのファイルは、プロジェクトのメタデータ、依存関係、ビルドシステムの設定などを一元管理します。

構造と主要なセクション

pyproject.tomlはTOML (Tom's Obvious, Minimal Language) 形式で記述され、主に以下の3つのセクションで構成されます。

a. [tool.poetry]

このセクションはPoetryがプロジェクトを管理するための中心的な場所です。プロジェクト名、バージョン、説明、作者などの基本的なメタ情報を記述します。

キー 説明
name パッケージ名。PyPIに公開する際に使用されます。 name = "my-awesome-app"
version プロジェクトの現在のバージョン。セマンティックバージョニングが推奨されます。 version = "0.1.0"
description プロジェクトの短い説明文。 description = "A short description of the project."
authors プロジェクトの作者リスト。 authors = ["Your Name <you@example.com>"]
license プロジェクトのライセンス。SPDX識別子が推奨されます。 license = "MIT"
readme READMEファイルへのパス。 readme = "README.md"
repository プロジェクトのリポジトリURL。 repository = "https://github.com/username/my-awesome-app"
homepage プロジェクトのホームページURL。 homepage = "https://my-awesome-app.com"
keywords PyPIで検索されやすくするためのキーワード。 keywords = ["web", "api", "async"]
b. [tool.poetry.dependencies]

このセクションでは、プロジェクトの実行に必要なライブラリ(依存パッケージ)を定義します。

  • Pythonのバージョン指定:
    python = "^3.8" のように、プロジェクトが対応するPythonのバージョンを指定します。^(キャレット)は互換性のある上位バージョンへの更新を許可します(この例では3.8以上4.0未満)。

  • パッケージの指定:
    パッケージ名 = "バージョン指定" の形式で記述します。バージョン指定には様々な方法があります。

    • * : 最新バージョン
    • ^2.1 : 2.1以上3.0未満
    • ~1.5.2 : 1.5.2以上1.6.0未満
    • >=1.2, <2.0 : 複雑な条件指定
    • {git = "..."} : Gitリポジトリからのインストール
    • {path = "..."} : ローカルのディレクトリからのインストール

例:

[tool.poetry.dependencies]
python = "^3.9"
fastapi = "^0.109.0"
uvicorn = {extras = ["standard"], version = "^0.27.0"}
c. [tool.poetry.group.<group-name>.dependencies] (Poetry 1.2以降)

以前の[tool.poetry.dev-dependencies]に代わり、より柔軟な依存関係のグルーピングが可能になりました。テスト、リンティング、ドキュメント生成など、特定の環境でのみ必要な依存関係を定義します。

  • --group <group-name> オプションでインストールを制御できます。
  • デフォルトでdevグループが存在し、開発時に必要なツール(pytest, black, ruffなど)をここに入れるのが一般的です。

例:

[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
ruff = "^0.2.0"

[tool.poetry.group.docs.dependencies]
mkdocs = "^1.5.0"
d. [build-system]

このセクションは、プロジェクトをビルドするためにどのツールを使用するかを定義します。Poetryプロジェクトでは、通常以下のように固定されます。

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

2. poetry.lock:依存関係の完全な再現性を保証する鍵

poetry.lockファイルは、poetry addpoetry installコマンドを実行した際に、Poetryが自動的に生成・更新するファイルです。このファイルを手動で編集するべきではありません。

役割と重要性

  • 依存関係の固定 (Locking): pyproject.tomlで指定されたバージョン制約(例: ^3.8)を満たす、実際にインストールされたパッケージの正確なバージョンを記録します。
  • 推移的依存関係の記録: プロジェクトが直接依存しているパッケージだけでなく、そのパッケージが依存している全ての「孫」や「ひ孫」のパッケージのバージョンも記録します。
  • ハッシュ値の記録: 各パッケージファイルのハッシュ値を記録し、改ざんがないことを保証します。
  • 再現性の保証: チーム内の他の開発者や、CI/CD環境、本番環境など、どこでpoetry installを実行しても、poetry.lockファイルに基づいて全く同じバージョンのパッケージ群がインストールされることを保証します。これにより、「自分の環境では動いたのに…」という問題を根本的に解決します。

poetry.lockファイルは、バージョン管理システム(Gitなど)に含めることが強く推奨されます。


3. poetry.toml:Poetry自体の挙動を設定する

poetry.tomlは、Poetryの挙動をプロジェクトごと、またはグローバルに設定するためのオプションファイルです。通常は自動生成されず、必要に応じて手動で作成します。

主な設定項目

  • 仮想環境の管理:

    • virtualenvs.in-project = true: 仮想環境をプロジェクトのルートディレクトリ配下(.venv)に作成するようにします。デフォルト(false)では、Poetryのキャッシュディレクトリ内に作成されます。プロジェクトごとに環境を分離したい場合に非常に便利です。
    • virtualenvs.path = "{cache-dir}/virtualenvs": 仮想環境が作成されるパスを指定します。
  • リポジトリの設定:

    • repositories.<name>: PyPI以外のプライベートリポジトリなどを追加できます。

設定の優先順位

Poetryは以下の順でpoetry.tomlを読み込み、設定を上書きしていきます。

  1. /etc/poetry/poetry.toml(システム全体)
  2. $POETRY_HOME/config.toml(ユーザーのグローバル設定)
  3. プロジェクトのルートディレクトリにあるpoetry.toml(プロジェクト固有設定)

4. .pypoetryディレクトリ(旧式)

Poetryの初期バージョンでは、プロジェクトのメタ情報を.pypoetryというディレクトリにキャッシュしていました。しかし、現在ではpyproject.tomlに全ての情報が集約されており、このディレクトリは通常見かけることはありません。


ファイル間の連携とワークフロー

これらのファイルは、Poetryのワークフローの中で密接に連携しています。

  1. プロジェクト初期化 (poetry init):

    • 対話形式で質問に答えることで、pyproject.toml[tool.poetry]セクションが生成されます。
  2. 依存関係の追加 (poetry add <package-name>):

    • pyproject.toml[tool.poetry.dependencies](またはgroup)にパッケージが追記されます。
    • Poetryが適切なバージョンを解決し、poetry.lockに正確なバージョン情報が記録(または更新)されます。
    • 実際のパッケージが仮想環境にインストールされます。
  3. 依存関係のインストール (poetry install):

    • poetry.lockファイルが存在する場合: lockファイルに記載された通りのバージョンのパッケージが、寸分違わずインストールされます。pyproject.tomlは参照されません。
    • poetry.lockファイルが存在しない場合: pyproject.tomlの依存関係定義を元に、最新の適切なパッケージバージョンを解決し、新しいpoetry.lockファイルを生成してからインストールを行います。
  4. 依存関係の更新 (poetry update):

    • pyproject.tomlのバージョン制約の範囲内で、利用可能な最新のパッケージに更新します。
    • poetry.lockファイルが新しいバージョン情報で更新されます。

まとめ

ファイル名 主な役割 手動編集 Git管理
pyproject.toml プロジェクトのメタデータと依存関係の定義 する 必須
poetry.lock 実際にインストールされた全パッケージの正確なバージョンの記録。再現性の保証。 しない 必須
poetry.toml Poetry自体の挙動を設定(仮想環境の場所など)。 必要に応じて プロジェクト設定なら推奨

これらのファイルを正しく理解し、適切に管理することで、Poetryの強力な依存関係管理機能と再現性のメリットを最大限に活用することができます。

かーなびかーなび

poetry→uv移行

移行のメリット

https://shinyorke.hatenablog.com/entry/poetry-to-uv

移行方法

https://zenn.dev/hrsma2i/scraps/95d361feee292a
この記事のように、
事前にpyproject.tomlのpoetry 特有の記法である ^3.12.0 などは公式の ~=3.12.0(厳密には>=3.12.0, <4.0.0) に書き換えた上で、

uvx migrate-to-uv

を実行することでpyproject.toml がuvように書き換わり、 poetry.lock → uv.lockとなる。
自動書き換えは万能ではなく変換後もバージョンエラーが起きることがあるのでそこは手作業で行わないといけないことがあるで注意。

Dockerfileもpoetryからuvを使ったコマンドに書き換える必要があり、

  • COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/の記載
  • COPY pyproject.toml uv.lock ./の記載
  • poetry installをuv syncへ書き換え
    などが必要となる。

詳しくは公式ドキュメントを見るのが良い。
https://docs.astral.sh/uv/guides/integration/docker/

uvで扱うhatch周りについて

Python uvhatch における [tool.hatch.build.targets.wheel] 設定を徹底解説

uv のようなモダンな Python パッケージングツールを扱う上で重要な設定ファイル pyproject.toml。その中でも、プロジェクトのビルド方法を定義する [tool.hatch.build.targets.wheel] は、配布するパッケージの品質と内容を決定づける重要なセクションです。

ここでは、この [tool.hatch.build.targets.wheel] が何であり、どのように設定するのか、そして uv とどう連携するのかを詳しく解説します。

[tool.hatch.build.targets.wheel] とは?

この設定は、Python プロジェクト管理ツール Hatch が、パッケージを Wheel 形式 (.whl) でビルドする際の挙動を定義するためのものです。pyproject.toml ファイル内に記述されます。

  • pyproject.toml: PEP 518 で導入された、Python プロジェクトの設定を統一的に管理するためのファイルです。ビルドシステム、プロジェクトのメタデータ、各種ツールの設定などを記述します。
  • Hatch: プロジェクトの作成、依存関係管理、ビルド、公開などを一貫して行える高機能なツールです。内部では Hatchling というビルドバックエンドが動作しています。
  • Wheel: Python パッケージの配布形式の一つ。コンパイル済みのバイナリを含むことができ、pipuv によるインストールを高速化します。

つまり、[tool.hatch.build.targets.wheel] は、「Hatchling を使って Wheel ファイルを作成する際に、どのファイルやディレクトリをパッケージに含めるか、あるいは除外するか」といったルールを細かく指定する場所なのです。

uv との関係

uv は、pip のようなパッケージインストーラーであり、プロジェクトをインストールする際には pyproject.toml[build-system] テーブルを読み取ります。

もし、プロジェクトがビルドバックエンドとして Hatchling を指定している場合 (build-backend = "hatchling.build"), uv pip install . のようなコマンドを実行すると、uv は内部で Hatchling を呼び出します。その際、Hatchling は [tool.hatch.build.targets.wheel] の設定に従って Wheel ファイルをビルドし、uv はその生成された Wheel を環境にインストールします。

したがって、uv ユーザーにとっても、このセクションを理解することは、意図通りのパッケージをビルド・インストールするために不可欠です。

主要な設定オプション

以下に、[tool.hatch.build.targets.wheel]内でよく使われる主要なオプションを解説します。

オプション 説明
packages Wheel に含めるトップレベルの Python パッケージのディレクトリを指定します。src レイアウトの場合によく使われます。
include パッケージに含めるファイルやディレクトリのパターンを指定します。デフォルトで多くのファイルが含まれるため、追加で含めたいものを指定します。
exclude パッケージから除外するファイルやディレクトリのパターンを指定します。テストコードやドキュメントなど、配布に不要なファイルを除外するのに便利です。
sources packages のより汎用的なバージョン。ソースディレクトリとパッケージ内でのパスのマッピングを定義できます。
only-include include と似ていますが、Hatchling のデフォルトの包含ルールをすべて無効にし、ここで指定されたファイル/ディレクトリのみをパッケージに含めます。
reproducible true (デフォルト) に設定すると、ビルド日時などの情報が固定され、同じソースコードからは常に同じ Wheel ファイルが生成されるようになります。再現性を高めるために重要です。
hooks ビルドプロセスの特定のタイミングで、カスタムスクリプト(フック)を実行できます。より高度なビルド処理が必要な場合に使用します。
dependencies Wheel をビルドする際にのみ必要となる追加の依存関係を指定します。例えば、ビルド時にコード生成を行うライブラリなどです。

設定例

実際の pyproject.toml での記述例をいくつか紹介します。

1. 基本的な src レイアウトのプロジェクト

プロジェクト構造:

my_project/
├── src/
│   └── my_package/
│       ├── __init__.py
│       └── main.py
├── tests/
└── pyproject.toml

pyproject.toml の設定:

[tool.hatch.build.targets.wheel]
# "src" ディレクトリの下にある "my_package" をパッケージとして含める
packages = ["src/my_package"]

この設定は、src ディレクトリをルートとして、その中の my_package ディレクトリを Wheel にパッケージングすることを意味します。インストール後、import my_package として利用できます。

2. 特定のファイルやディレクトリを除外する例

テストコードや設定ファイルなどを配布物から除外します。

pyproject.toml の設定:

[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]
exclude = [
  # testsディレクトリ全体を除外
  "/tests",
  # すべての .env ファイルを除外
  "**/.env",
  # ドキュメントディレクトリを除外
  "/docs",
]

exclude を使うことで、配布する Wheel ファイルのサイズを削減し、不要なファイルが含まれるのを防ぎます。

3. データファイルを含める例

Python コード以外のデータファイル(例: .csv, .json)をパッケージに含めたい場合、include を使います。

プロジェクト構造:

my_project/
├── src/
│   └── my_package/
│       ├── __init__.py
│       ├── data/
│       │   └── default_settings.json
│       └── main.py
└── pyproject.toml

pyproject.toml の設定:

[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]
# my_package/data ディレクトリ以下のすべてのファイルを再帰的に含める
include = [
  "/src/my_package/data",
]

まとめ

[tool.hatch.build.targets.wheel] は、Hatch をビルドバックエンドとして利用する現代的な Python プロジェクトにおいて、配布物の内容を正確にコントロールするための強力な機能です。

uv を使って開発やデプロイを行う際も、この設定がビルドの裏側でどのように機能しているかを理解することで、より堅牢でクリーンなパッケージ管理が可能になります。公式ドキュメントと合わせて、ぜひご自身のプロジェクトで活用してみてください。

かーなびかーなび

TensorRT、JIT、ONNXとは? AI推論を高速化する技術たち

AI、特にディープラーニングモデルを実際に活用する「推論」の段階では、その処理速度が極めて重要になります。TensorRT、JIT、ONNXは、この推論を高速化し、効率的に行うための重要な技術です。それぞれの役割と関係性について解説します。

TensorRT: NVIDIA GPUのための究極の推論オプティマイザ

**TensorRT(テンサーアールティー)**は、NVIDIAが提供するソフトウェア開発キット(SDK)です。その最大の目的は、学習済みのディープラーニングモデルをNVIDIA製のGPUで可能な限り高速に実行(推論)させることです。

TensorRTは、推論処理に特化した様々な最適化を自動的に行います。具体的には、以下のような処理を通じて、モデルの性能を最大限に引き出します。

  • 量子化: モデルの計算で使われる数値の精度を、性能への影響を最小限に抑えつつ低減(例:32ビット浮動小数点数を16ビットや8ビット整数へ)し、計算速度の向上とメモリ使用量の削減を図ります。
  • 層の融合(Layer Fusion): ニューラルネットワークの複数の層を一つの処理にまとめることで、GPUカーネルの起動回数を減らし、オーバーヘッドを削減します。
  • カーネルの自動チューニング: 使用するGPUのアーキテクチャに最適化された計算カーネルを自動的に選択・生成します。
  • 不要な計算の除去: 推論結果に影響しない計算処理をモデルから取り除きます。

これらの最適化により、TensorRTはPyTorchやTensorFlowといったフレームワークで実行するよりも、推論速度を数倍にまで高めることが可能です。

ONNX: AIモデルの「共通言語」

**ONNX(Open Neural Network Exchange、オニキス)**は、異なるディープラーニングフレームワーク間でモデルをやり取りするためのオープンな標準フォーマットです。

通常、PyTorchで学習したモデルはPyTorchの形式で、TensorFlowで学習したモデルはTensorFlowの形式で保存されます。これでは、例えばPyTorchで開発したモデルを、TensorFlowで構築された運用環境で使うことが困難です。

ONNXは、この問題を解決します。モデルをONNX形式でエクスポートすることで、いわば「共通言語」に翻訳するのです。これにより、開発時に使用したフレームワークに縛られることなく、様々なプラットフォームや推論エンジン(TensorRTなど)でモデルを利用できるようになります。

TensorRTとONNXの関係は非常に密接です。 一般的な開発フローとして、まずPyTorchやTensorFlowなどの使い慣れたフレームワークでモデルを学習させ、それをONNX形式でエクスポートします。そして、そのONNXファイルをTensorRTが読み込み、ターゲットとなるNVIDIA GPUに最適化された推論エンジンを生成する、という手順が広く用いられています。

JITコンパイラ: 実行時にコードを最適化する技術

JIT(Just-In-Time、ジット)コンパイラは、「実行時コンパイラ」とも呼ばれ、プログラムの実行直前にソースコードや中間コードを、実行環境に最適化された機械語(ネイティブコード)に変換する技術です。

従来のコンパイラ(事前コンパイラ)が開発時に全てのコードを機械語に変換するのに対し、JITコンパイラは実行時に必要な部分だけをコンパイルします。これにより、実行環境の特性(特定のCPUやGPUの能力など)を最大限に活用した、より効率的なコードを生成できるという利点があります。

ディープラーニングの文脈では、モデルの計算グラフを実行する際にJITコンパイルが利用されることがあります。例えば、PyTorchにはtorch.jitという機能があり、Pythonのコードを中間表現に変換し、それをC++の高速なランタイムで実行することでパフォーマンスを向上させます。

TensorRTも、内部でJITコンパイルの考え方を活用しています。 TensorRTは、ONNXなどのモデルを読み込んだ後、ターゲットとなるGPU上で最適な実行プラン(計算カーネルの選択や実行順序など)を構築します。このプロセスは、一種のJITコンパイルと見なすことができ、実行環境に特化した高度な最適化を実現しています。

まとめ:それぞれの役割と関係

これらの技術の関係をまとめると、以下のようになります。

  1. 開発: 開発者はPyTorchやTensorFlowなどのフレームワークを使い、モデルを設計・学習させます。
  2. エクスポート: 学習済みモデルを、フレームワーク間の互換性を確保するためにONNX形式に変換します。
  3. 最適化・実行:
    • TensorRTがONNXファイルを読み込み、ターゲットのNVIDIA GPUに特化した推論エンジンを生成します。この過程で、JITコンパイルのような実行時最適化の技術も活用し、層の融合や量子化など、様々な手法でモデルを徹底的に高速化します。
    • 最終的に、この最適化されたエンジンを用いて、高速な推論が実行されます。
用語 読み方 主な役割
TensorRT テンサーアールティー NVIDIA GPU上でのディープラーニング推論を高速化するSDK。
JIT ジット プログラム実行時にコードを最適化・コンパイルする技術。
ONNX オニキス ディープラーニングモデルの共通フォーマット。フレームワーク間の橋渡し役。

このように、TensorRT、ONNX、そしてJITコンパイルの技術は、AIモデルを研究開発の段階から、実際に社会で役立つアプリケーションへと展開する上で、それぞれが重要な役割を担い、相互に連携しています。

かーなびかーなび

uvx

「uvx」についてですね。おそらく、高速なPythonパッケージ管理ツール**「uv」に含まれるコマンド uvx** のことだと推察します。

uvx は、Python製のコマンドラインツールをインストールすることなく、一時的に実行するためのコマンドです。

これは、人気のツールである pipx と似た思想を持っていますが、uvx はツールを恒久的にインストールするのではなく、その場限りの実行に特化している点が特徴です。

uvxとは? ― その主な役割と利点

一言で言うと、uvx は「使い捨ての実行環境」を瞬時に作り、指定したツールを実行してくれるコマンドです。

主な利点:

  • 環境を汚さない: プロジェクトの仮想環境やグローバル環境に、たまにしか使わないツール(フォーマッター、リンターなど)をインストールする必要がありません。
  • 手間いらず: venv を作成して pip install して...といった手順を踏まずに、コマンド一つでツールを試せます。
  • 高速: uv の強力なキャッシュ機構により、2回目以降の実行は非常に高速です。

uvx の具体的な使用例

例えば、プロジェクトのソースコードに対してフォーマッターの black やリンターの ruff を実行したい場合を考えます。

従来のやり方(仮想環境を利用):

# 1. 仮想環境を作成して有効化
python -m venv .venv
source .venv/bin/activate

# 2. ツールをインストール
pip install black ruff

# 3. ツールを実行
black .
ruff check .

uvx を使ったやり方:

# これだけでOK!
uvx black .
uvx ruff check .

uvx を使えば、仮想環境の有効化やツールのインストールを意識することなく、直接コマンドを実行できます。

プラグインと一緒に使う

--with オプションを使うと、複数のパッケージを組み合わせた一時的な環境でコマンドを実行することも可能です。例えば、pytest をカバレッジ計測プラグイン pytest-cov と一緒に実行する場合は以下のようになります。

uvx --with pytest-cov pytest --cov

uvxuv の関係

uvx を理解するには、その親ツールである uv について知ることが重要です。

uv とは?

uv は、Pythonの静的解析ツール Ruff の開発チーム(Astral社)が開発した、Rust製の超高速なPythonパッケージ管理ツールです。

従来、Python開発で使われてきた pip (パッケージインストーラー)、venv (仮想環境)、pip-tools (依存関係解決)、pipx (ツール実行) といった複数のツールの役割を、たった一つのコマンド uv に統合し、圧倒的な速度で実行することを目指しています。

uv が持つ機能の一部:

  • uv venv: 仮想環境の作成
  • uv pip install: パッケージの高速インストール
  • uv pip sync: requirements.txt と仮想環境を完全に同期
  • uv pip compile: pyproject.toml から依存関係を解決し requirements.txt を生成
  • uv run: 仮想環境内でスクリプトを実行

そして、この uv の機能の一つが uvx なのです。

uvx はエイリアス

技術的には、uvxuv tool run というコマンドの**エイリアス(短い別名)**です。つまり、以下の2つのコマンドは全く同じ動作をします。

uvx black .
# 上記は以下と等価
uv tool run black .

uvxuv tool install の違い

uv には uvx と似た目的で uv tool install というコマンドもあります。両者の違いを理解することが重要です。

  • uvx (uv tool run)

    • 一時的な実行
    • ツールをインストールせず、その場限りで実行します。
    • 「ちょっとこのリンターを試したい」「このプロジェクトだけで一度だけ使いたい」といった場合に最適です。
  • uv tool install <package>

    • 恒久的なインストール
    • pipx と同じように、ツールを隔離された専用の環境にインストールします。
    • インストール後は、システムのどこからでも直接コマンド名(例: black, ruff)でツールを呼び出せるようになります。
    • 頻繁に使うツールはこちらが適しています。

まとめ

uvx は、モダンなPython開発ツール uv が提供する便利な機能の一つです。

  • Python製のCLIツールを、インストールせずに一時的に実行するコマンド。
  • 開発環境をクリーンに保ちながら、手軽にツールを試すことができる。
  • 親ツールである uv の高速性とキャッシュ機構の恩恵を受けることができる。

uv を導入することで、uvx のような便利な機能だけでなく、Python開発全体のワークフローを高速化・効率化することが可能です。

かーなびかーなび

CPythonとCythonとの違いについて

よく混同されがちなCPythonとCythonの違いについて解説します。


CPythonとCythonの違い

CPythonとCythonは名前が似ているため混同されがちですが、その役割と目的は全く異なります。

特徴 CPython (シーパイソン) Cython (サイソン)
分類 Pythonの処理系(実装) プログラミング言語 / コンパイラ
役割 Pythonコードを実行するプログラムそのもの PythonコードをC言語のコードに変換するツール
目的 Pythonという言語を解釈し、実行すること Pythonコードの実行速度を向上させること
使われ方 私たちが普段pythonコマンドで実行している、最も標準的なPython環境です。公式のPythonディストリビューションをインストールすると、CPythonが利用されます。 importして使うライブラリやモジュールの開発で利用されます。特に、数値計算やデータ分析など、処理速度が求められるライブラリ(例: NumPy, pandasの一部)の内部で、パフォーマンス向上のために使われています。

CPythonとは?

CPythonは、C言語で書かれたPythonの公式かつ最も広く使われている実装です。私たちがpip installでライブラリをインストールしたり、python myscript.pyでスクリプトを実行したりするとき、その背後ではCPythonが動いています。Pythonの言語仕様に最も忠実なリファレンス実装とされています。

Cythonとは?

Cythonは、PythonとC/C++の要素を組み合わせたプログラミング言語、およびそのコードをC/C++のコードに変換するコンパイラです。

主な目的は、Pythonのコードのパフォーマンスを向上させることです。Pythonは動的型付け言語であるため、実行速度が遅くなることがあります。Cythonを使うと、Pythonライクな構文で変数の型を静的に宣言でき、それを非常に高速なC言語のコードに変換してコンパイルできます。

これにより、Pythonの書きやすさを保ちながら、C言語に匹敵する実行速度を持つ拡張モジュール(.so.pydファイル)を作成することが可能になります。

まとめると以下のようになります。

  • CPython: Pythonコードを実行するための「エンジン」
  • Cython: Pythonコードをより速く動くように「改造(コンパイル)」するための「チューニングキット」

普段アプリケーションやスクリプトを書く際にはCPythonを意識することは少ないですが、ライブラリ開発などで極限のパフォーマンスが求められる場面でCythonが活躍します。

かーなびかーなび

uv addで特定グループにのみ追加する方法 / add・install・syncの違い

現在の uv には uv add コマンドが標準で搭載されており、poetry addnpm install のように、コマンド一つでパッケージの追加と pyproject.toml ファイルの更新を同時に行えます。

以前は pyproject.toml を手動で編集する必要がありましたが、プロジェクト管理機能が強化され、より便利になりました。


## 基本的な使い方

プロジェクトの依存関係([project.dependencies])にパッケージを追加するには、以下のコマンドを実行します。

# fastapiをプロジェクトに追加する
uv add fastapi

このコマンドを実行すると、uv は以下の処理を自動的に行います。

  1. pyproject.tomldependenciesfastapi を追記する。
  2. 依存関係を解決し、ロックファイル (uv.lock) を作成・更新する。
  3. パッケージを仮想環境にインストールする。

## 特定のグループに追加する方法

--dev--group オプションを使うことで、開発用の依存関係や任意のグループにパッケージを追加できます。

開発用の依存関係として追加 (--dev)

テストツールなど、開発時にのみ必要なパッケージは --dev オプションを付けて追加します。これは pyproject.toml[project.optional-dependencies] 内の dev グループに追加されます。

# pytestを開発用依存として追加する
uv add pytest --dev

任意のグループに追加 (--group)

ドキュメント生成用など、dev 以外のグループに分けたい場合は --group オプションを使います。

# sphinxをdocsグループに追加する
uv add sphinx --group docs

## uv adduv pip install の違い

この2つのコマンドは似ていますが、明確な役割分担があります。

  • uv add: プロジェクトの依存関係を管理するためのコマンド。pyproject.toml を更新し、プロジェクト全体で依存関係を管理したい場合に使います。
  • uv pip install: pip install の高速な代替。pyproject.toml を使わない環境で一時的にパッケージを入れたり、requirements.txt に基づいてインストールしたりする場合に使います。プロジェクトの定義ファイルは更新しません。

結論として、uv でPythonプロジェクトを管理している場合は、依存関係の追加には uv add を使うのが基本となります。


uv adduv pip sync の違い

uv adduv pip sync の違いは、**「パッケージを追加する」「環境を定義通りに再現する」**かです。

一言で言うと、こうです。

  • uv add: レシピに材料を書き足す 📝
  • uv pip sync: レシピ通りにキッチンを準備する 🧑‍🍳

## uv add

uv add は、プロジェクトに新しい依存パッケージを追加するためのコマンドです。

何をするか?

  1. pyproject.toml ファイルに、指定されたパッケージ名を追記・編集します。
  2. 依存関係を解決し、uv.lock ファイルを更新します。
  3. そのパッケージを現在の仮想環境にインストールします。

いつ使うか?

  • 「プロジェクトに fastapi を導入したいな」
  • 「テスト用に pytest が必要になったな」

# プロジェクトにfastapiを追加する
uv add fastapi

# 開発用にpytestを追加する
uv add pytest --dev

add は、プロジェクトの定義ファイル (pyproject.toml) を変更する「攻め」のコマンドです。


## uv pip sync

uv pip sync は、**現在の仮想環境を pyproject.tomluv.lock の状態と完全に一致させる(同期する)**ためのコマンドです。

何をするか?

  1. pyproject.tomluv.lock ファイルを読み込みます。
  2. ファイルに記載されていて、環境に無いパッケージをインストールします。
  3. ファイルに記載されていないのに、環境にある不要なパッケージをアンインストールします。

いつ使うか?

  • 他の人が追加したパッケージを、自分の環境に反映させたい時 (git pull の後など)。
  • 手動で pyproject.toml を編集した内容を、環境に反映させたい時。
  • 環境をクリーンな状態に戻したい時。

# pyproject.tomlの状態と仮想環境を完全に一致させる
uv pip sync

sync は、プロジェクトの定義ファイルを正として、環境をクリーンに保つ「守り」のコマンドです。


## まとめ

コマンド 目的 pyproject.toml への影響 主な利用シーン
uv add 依存関係の追加 編集する 新しいパッケージをプロジェクトに導入する時
uv pip sync 仮想環境の同期・再現 読み込むだけ git pull 後や環境をクリーンにする時
かーなびかーなび

uv runで特定グループの環境を指定して実行する方法

## 開発グループ(dev)のパッケージを使うPythonファイルを実行【自動】

dev グループのパッケージは、uv run を使うだけで自動的に読み込まれます

1. 準備

開発用のデバッグツール icecreamdev グループに追加します。

uv add icecream --dev

次に、icecream をインポートするPythonファイル (dev_script.py) を作成します。

# dev_script.py
import icecream

def calculate(a, b):
    result = a + b
    icecream.ic(a, b, result) # ic() で変数名を付けてデバッグ出力
    return result

calculate(10, 5)

2. 実行

特別な指定は不要です。uv run でPythonファイルを実行するだけで、dev グループの icecream が利用できます。

uv run python dev_script.py

出力結果:

ic| a: 10, b: 5, result: 15

このように、dev グループのパッケージは uv run で直接実行するスクリプトから問題なく利用できます。


## dev以外のグループのパッケージを使うPythonファイルを実行【手動】

dev 以外のグループ(例: analysis)は、事前にパッケージをインストールしておく必要があります

1. 準備

データ分析ライブラリ pandasanalysis グループに追加します。

uv add pandas --group analysis

pandas をインポートするPythonファイル (analysis_script.py) を作成します。

# analysis_script.py
import pandas as pd

# データを作成して表示
data = {'Name': ['Alice', 'Bob'], 'Age': [25, 30]}
df = pd.DataFrame(data)

print("--- Analysis Result ---")
print(df)

2. 実行

この状態で直接実行しようとすると、pandas が見つからずエラーになります。

# ❌ このままではエラーになる
uv run python analysis_script.py
# ModuleNotFoundError: No module named 'pandas'

解決策:
まず analysis グループを明示的にインストールします。

uv pip install -e ".[analysis]"

インストール後、再度 uv run で実行すれば成功します。

# ✅ インストール後に実行
uv run python analysis_script.py

出力結果:

--- Analysis Result ---
    Name  Age
0  Alice   25
1    Bob   30

## まとめ

  • dev グループのパッケージを使うスクリプト:
    uv run python <file.py> だけで自動で実行可能

  • dev 以外のグループのパッケージを使うスクリプト:
    uv pip install -e ".[group-name]"手動インストールしてから、
    uv run python <file.py> で実行する。

一時的な環境で実行したい場合

uvx --from google-cloud-aiplatform -- python main.py