Python

目次

概要

まず、この章の中心構造を図で確認します。細部に入る前に、どの概念がどこへつながるかをつかむための地図です。

flowchart LR A["Pythonコード"] --> B["オブジェクト"] B --> C["動的型付け"] B --> D["参照カウント"] D --> E["GC"] C --> F["柔軟な記述"] F --> G["標準ライブラリ"] G --> H["Web・自動化・データ分析"] G --> I["機械学習"]
コード例の読み方

コード例は、そのまま写すためだけのものではありません。直前の本文で「何を確かめる例か」を押さえ、直後の説明で「どの性質が見えるか」を確認してください。実務では、ここに入力の境界、失敗時の挙動、依存する実行環境を足して読むと判断しやすくなります。

要点

Pythonは、読みやすさと表現力を重視し、スクリプトからデータ分析、Web、機械学習まで幅広く使われる言語です。

このページでは、オブジェクトモデル、型ヒント、実行環境、GIL、標準ライブラリ、パッケージ管理を、Pythonらしい設計思想とともに整理します。

1. Pythonとは何か

このセクションでは「Pythonはなぜ生まれたのか」「なぜ動的型付けなのに後から型ヒントが入ったのか」「どういう設計思想で動いているのか」を丁寧に説明します。最初にここで土台を作っておくと、後の章で出てくる「Pythonっぽさ(Pythonic)」の正体が見えやすくなります。

Pythonは、**「動的型付け・マルチパラダイム・インタプリタ型」**の汎用プログラミング言語です。1991年にGuido van Rossumによって公開され、現在は次のような幅広い領域で使われています。

  • データサイエンス・機械学習: NumPy・pandas・PyTorch・TensorFlowのデファクト言語
  • Webバックエンド: Django・Flask・FastAPI
  • スクリプト・自動化: 業務スクリプト、データ加工、CLIツール
  • 科学技術計算: SciPy、SymPy、天文学・物理学・バイオインフォマティクス
  • 教育: 大学のCS入門の標準言語
  • ゲーム・GUI・組み込み: Pygame、PyQt、MicroPython(マイコン用)

1-1. Pythonの歴史(誕生からPython 3.13まで)

Guido van Rossumとクリスマス休暇

1989年12月、オランダ国立情報科学・数学研究所(CWI)に勤めていたGuido van Rossumは、クリスマス休暇中の暇つぶしに新しいスクリプト言語を設計し始めます。当時Guidoは研究所で ABC という教育用言語のプロジェクトに関わっており、ABCの良さ(読みやすさ、インデント構文、対話実行)を引き継ぎつつ、ABCの欠点(拡張性の低さ、外部世界との接続の弱さ)を解消する言語を作ろうとしていました。

言語名 “Python” は彼が当時ハマっていたイギリスのコメディ集団 「モンティ・パイソン」 に由来します。蛇は無関係で、今でも公式ドキュメントの例は spam, eggs, bacon というモンティ・パイソンのコント由来の単語が使われ続けています

Python 0.9.0公開(1991)

1991年2月、GuidoはPython 0.9.0をUsenetのニュースグループ alt.sources に公開します。最初のバージョンの時点で、すでに以下が揃っていました。

  • 例外処理(try/except)
  • 関数・モジュール
  • 主要なデータ型(str, list, dict)
  • インデントベースの構文
  • C言語との連携機能

この「最初から最低限の道具立てが揃っていた」ことが、Pythonが他のスクリプト言語より早く実用的な地位を得た一因と言われます。

Python 1.0(1994)と “Computer Programming for Everybody”

1994年、Python 1.0がリリースされます。同年Guidoは CP4E(Computer Programming for Everybody) というビジョンを掲げ、「プログラミングを誰もが学べる教養にしたい」という目標を打ち出します。これが現在まで続く「読みやすさを最優先する」Pythonカルチャーの源流です。

Python 2系(2000〜2020)

2000年にPython 2.0がリリースされ、ガベージコレクション、Unicode対応、リスト内包表記などが導入されます。2.x系は2010年のPython 2.7まで続き、長らくWeb開発・スクリプト言語として標準的な地位を占めました。

2000  Python 2.0ガベージコレクション、リスト内包表記、Unicode
2004  Python 2.4ジェネレータ式、デコレータ
2008  Python 2.6   2と3の橋渡しバージョン
2010  Python 2.7   2系の最終バージョン(実質これがPython 2の代名詞)
2020  Python 2ついにEOL(サポート終了)

Python 3への大移行(2008〜)

2008年、後方互換性を破壊する Python 3.0 がリリースされます。文字列をデフォルトでUnicode(str = テキスト、bytes = バイト列)にする、print を関数化する、整数除算を /// で区別する、など「正しさのために互換性を捨てる」決断が下されました。

この移行は 「Python 2と3の分断時代」 と呼ばれ、約12年にわたって2系と3系が併存する苦しい時期が続きます。多くのライブラリは両対応のために six というブリッジライブラリを使い、from __future__ import で一部機能を先取りしました。

ようやく2020年1月1日、Python 2系の公式サポートが終了します。Pythonの歴史上もっとも教訓に富んだエピソードであり、「破壊的変更は早く・小さく入れる」ことの重要性を業界全体に教えました。

モダンPythonの確立(2015〜現在)

3.4 (2014)    asyncioが標準ライブラリに(高水準非同期I/O)
3.5 (2015)    async/awaitキーワード、PEP 484型ヒント導入
3.6 (2016)    f-string、変数アノテーション、dictが挿入順を保持(実装詳細)
3.7 (2018)    dictの順序保持が言語仕様化、dataclass、breakpoint()
3.8 (2019)    ウォルラス演算子 :=、位置専用引数 / 、TypedDict
3.9 (2020)    辞書のマージ演算子 |、組み込み型のジェネリクス(list[int] など)
3.10 (2021)   match文(構造的パターンマッチング)、Unionを | で書ける
3.11 (2022)   ExceptionGroup、TaskGroup、10〜60% 速度改善(Faster CPython)
3.12 (2023)   PEP 695新型構文、type文、より良いエラーメッセージ
3.13 (2024)   Free-Threadedビルド(実験的GIL無効化)、JIT(実験的)

特筆すべきは PEP 484(型ヒント/2014年)Faster CPythonプロジェクト(3.11〜) です。前者は「動的型付け言語に静的型情報を後付けする」というPythonの方向性を決定づけ、後者は「動的言語でも本気でチューニングすれば数倍速くなる」ことを実証しました。


1-2. なぜ動的型付けか/なぜ型ヒントが追加されたか

動的型付けとは

動的型付け(Dynamically Typed)とは、変数の型がコンパイル時ではなく実行時(ランタイム)に決まる仕組みです。Pythonでは「変数」は型を持たず、値(オブジェクト)が型を持ちます

# 動的型付け: 変数は型を持たず、値が型を持つ
x = 42           # xが指すオブジェクトはint
x = "hello"      # 同じ名前xにstrを指させても良い
x = [1, 2, 3]    # listでも良い
x = {"a": 1}     # dictでも良い

対照的に、JavaやC++ は**静的型付け(Statically Typed)**で、コンパイル時に型が固定されます。

// 静的型付け(Java): 型を明示しなければコンパイルエラー
int x = 42;
x = "hello";  // コンパイルエラー! intに文字列は代入できない

なぜPythonは動的型付けを選んだのか

GuidoがPythonを設計した1989年当時の文脈を理解することが重要です。

  1. 教育用途を強く意識: ABCの系譜にあり、「型宣言は学習者の障害になる」という発想があった。
  2. 当時のスクリプト言語の主流: Perl・Tcl・後に登場するRubyなど、当時の人気スクリプト言語はほぼすべて動的型付け。動的のほうが「軽量」と思われていた。
  3. 試行錯誤・対話的実行を最優先: REPLで試しながら書ける、という性質を捨てたくなかった。

動的型付けのメリットとデメリット

観点 動的型付け(Python) 静的型付け(Java/Go)
学習コスト 低い(型宣言不要) 高い(型システムの理解が必要)
開発速度(プロトタイプ) 速い(制約が少ない) 遅い(型を書く手間)
バグの発見タイミング 実行時(遅い) コンパイル時(早い)
IDE支援(補完・検査) 弱い 強い
大規模開発 困難(型情報がない) 向いている
実行速度 一般に遅い 一般に速い
柔軟性 非常に高い 制約がある

なぜ後から型ヒントが追加されたのか(PEP 484)

2014年、Guido自身が中心となって PEP 484: Type Hints が提案され、Python 3.5で導入されました。動機は次の3つでした。

  1. 大規模コードベースの破綻: Dropbox・Google・Instagramなど、数百万行規模のPythonコードベースで「動的型付けでは保守できない」という現実が訪れていた。
  2. IDE支援の強化: 型情報があれば補完・リファクタリングが劇的に楽になる。
  3. TypeScriptの成功: JavaScriptに静的型を後付けするTypeScriptが大成功し、「動的言語でも段階的型付け(Gradual Typing)が機能する」ことが実証された。
# 型ヒントの例(実行時にはチェックされない、ドキュメントとして機能する)
def greet(name: str, age: int = 0) -> str:
    return f"Hello, {name}! You are {age} years old."

# mypy / pyrightなどの型チェッカで静的解析する

ここでのポイントは、型ヒントは “オプション” であり、ランタイムでは型チェックされないということです。Pythonは依然として動的言語であり、型ヒントは外部ツール(mypypyright)が静的解析に使うための「メタデータ」にすぎません。これを Gradual Typing(段階的型付け) と呼びます。

「動的型付けの自由さ」と「静的型付けの安全さ」のいいとこ取りを狙った折衷案であり、業界全体での評価は概ね成功と見られています。詳細は第17章で扱います。


1-3. Pythonの設計哲学(The Zen of Python)

Pythonには、Tim Petersが書いた The Zen of Python(PEP 20) という哲学が埋め込まれています。Pythonインタプリタで import this と打つと表示されます。

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If it's hard hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

19の格言が並んでいますが、特に重要なものを4つピックアップします。

「Explicit is better than implicit.」(暗黙より明示)

Pythonは「裏で暗黙に何かが起こる」設計を嫌います。たとえばJavaScriptの '5' + 3 === '53' のような暗黙型変換はPythonでは禁止され、TypeError になります。

"5" + 3
# TypeError: can only concatenate str (not "int") to str

# 明示的に変換する
"5" + str(3)   # "53"
int("5") + 3   # 8

「Readability counts.」(読みやすさが大事)

Pythonがインデントで構文を表現するのは、この哲学の最も象徴的な現れです。「コードは書く時間より読まれる時間のほうが圧倒的に長い」という前提のもと、読みやすさを構文レベルで強制しています。

# Python: インデントが構文の一部
if x > 0:
    print("positive")
else:
    print("non-positive")
// C/Java: 中括弧。インデントは慣習にすぎない(剥がせる)
if (x > 0) { print("positive"); } else { print("non-positive"); }

「There should be one-- and preferably only one --obvious way to do it.」(一つの明らかなやり方)

Perlの “There’s More Than One Way To Do It”(TMTOWTDI)と対極の哲学です。**「同じことをする方法が複数あると、コードが分散して読みにくくなる」**という発想で、Pythonは機能の重複追加に保守的です。

ただし現実には、文字列フォーマットは %str.format、f-stringと3通りあり、完全には守られていません。それでも「迷ったら一番素直な書き方を選ぶ」という指針は今も生きています。

「Errors should never pass silently. Unless explicitly silenced.」(エラーを黙らせない)

エラーは無視せず、明示的に握りつぶしたいときだけ try/except で握りつぶすべき、という哲学です。これは第14章「例外処理」で深掘りします。


1-4. PEPとは何か

PEP(Python Enhancement Proposal) は、Pythonの言語仕様や標準ライブラリへの変更提案を文書化する仕組みです。Pythonの機能追加・廃止はすべてPEPとして提案され、議論され、採否が決まります。

代表的なPEPを覚えておくと、ドキュメントや議論についていきやすくなります。

PEP 内容 重要度
PEP 8 コーディング規約 ★★★
PEP 20 The Zen of Python ★★
PEP 257 docstringの書き方 ★★
PEP 328 絶対import / 相対import
PEP 484 型ヒント(Type Hints)導入 ★★★
PEP 492 async/awaitキーワード ★★★
PEP 498 f-string ★★★
PEP 525 非同期ジェネレータ ★★
PEP 544 Protocol(構造的部分型) ★★
PEP 557 dataclasses ★★★
PEP 572 ウォルラス演算子(:=) ★★
PEP 585 組み込み型のジェネリクス(list[int]) ★★
PEP 604 Unionを | で書く ★★
PEP 612 ParamSpec
PEP 634 match文 ★★★
PEP 654 Exception Groupsとexcept* ★★
PEP 673 Self型
PEP 695 新しい型パラメータ構文 ★★
PEP 703 GILを任意化(Free-Threaded) ★★★

注: ★は本ガイドにおける「最初に押さえるべき度」の主観評価です。PEP自体に格付けはありません。

PEP 8はPython全体の見た目を統一する事実上の標準で、blackruff といったフォーマッタがPEP 8(の拡張)に従ってコードを自動整形します。Pythonコミュニティでは「PEP 8に従っているか」が議論の最低ラインになることが多いです。


1-5. このセクションのまとめ

Pythonの出自:
  1989年クリスマス休暇にGuido van Rossumが設計
  ABC言語の正統後継 + Cとの連携 + 「読みやすさ第一」
  名前は「モンティ・パイソン」由来(蛇ではない)

歴史の節目:
  Python 2.7 (2010) → Python 2の代名詞、2020年EOL
  Python 3.0 (2008) → 後方互換性を捨てた大改革
  Python 3.5 (2015) → 型ヒント・async/await
  Python 3.11 (2022) → 高速化、ExceptionGroup
  Python 3.13 (2024) → Free-Threaded・JIT(実験的)

設計哲学(Zen of Python):
  - Explicit is better than implicit(暗黙より明示)
  - Readability counts(読みやすさ)
  - One obvious way(一つの明らかなやり方)

型システム:
  動的型付け + 後付けの型ヒント(PEP 484, 2014)
  ランタイムではチェックされない(mypy/pyrightで静的解析)

PEP(Python Enhancement Proposal):
  すべての言語拡張はPEPとして議論・記録される
  PEP 8(規約)とPEP 20(哲学)はまず読む

次のセクションでは、Pythonのコードが実際にどう動くのか――CPython・GIL・参照カウントといった実行環境の仕組みを見ていきます。


2. 実行環境と動作の仕組み

このセクションでは「Pythonのソースコードを python script.py と打ったとき、内部で何が起こっているのか」を解説します。CPythonのバイトコード化、GIL、参照カウント、ガベージコレクションを理解しておくと、後で出てくる「なぜPythonは遅いのか」「なぜスレッドCPUを使い切れないのか」の腑に落ちる説明ができるようになります。


2-1. CPython・PyPy・他の処理系

「Python」と一口に言っても、実は 言語仕様処理系(実装) は別物です。

Python(言語仕様)
  └── CPython公式実装。C言語で書かれている。世界の99%はこれ
  └── PyPy        Pythonで書かれた高速JIT処理系。CPythonより数倍速い
  └── Jython      JVM上で動くPython(Python 2.7まで、活発でない)
  └── IronPython  .NET上で動くPython(活発でない)
  └── MicroPython組み込み・マイコン向けの軽量実装
  └── Pyston / Cinder  Meta社・Dropbox社が独自に派生させた高速版
  └── GraalPy    GraalVM上のPython実装

CPython(公式実装)

普段我々が「Python」と呼んでいるのはほぼ CPython のことです。python.org からダウンロードしたバイナリも、Linuxのディストリビューションに入っているのもCPythonです。実装はC言語で書かれており、OS上で直接動きます。

CPythonは以下のステップでコードを実行します。

1. ソースコード(.py)を字句解析・構文解析
2. AST(抽象構文木)を構築
3. ASTをバイトコード(.pyc)にコンパイル
4. CPython仮想マシン(評価ループ)でバイトコードを1命令ずつ実行

注意したいのは、CPythonには伝統的にJITコンパイラがなかった点です。バイトコードは「Cで書かれた巨大なswitch文」(評価ループ)で逐次解釈されており、これがPythonが遅い主因の一つでした。Python 3.13で実験的JITが入りましたが、まだ標準ではありません。

PyPy(高速処理系)

PyPyは RPython(制限付きPython)で書かれた処理系で、JITコンパイラを持っています。実行中にホットなコードを機械語に変換するため、純粋なPythonコードではCPython3〜5倍速く動く ことが珍しくありません。

ただしPyPyには弱点があります。

  • C拡張モジュール(NumPyなど)との互換性に制約
  • 起動が遅い・メモリを多く使う
  • Pythonの最新仕様への追随がCPythonより遅れる

このため、データサイエンス(NumPyなどがCPython前提)ではPyPyは使えず、Webバックエンド・科学計算などのCPUバウンドな純Pythonコードで採用されます。

Pyston / Cinder / Faster CPython

Dropbox(Pyston)・Meta(Cinder)が独自にCPythonを高速化したプロジェクトを公開し、その成果がコミュニティにフィードバックされました。これが2021年にMicrosoft主導で始まった Faster CPython プロジェクトに合流し、Python 3.11〜3.13の継続的高速化につながっています。Guido van Rossum自身もMicrosoft入社後、このプロジェクトに参加しました。


2-2. バイトコードと .pycファイル

CPythonは実行前にPythonソースコードを バイトコード にコンパイルし、__pycache__/<モジュール名>.cpython-3xx.pycキャッシュします。次回実行時、ソースが変わっていなければバイトコードを直接ロードして起動を速くします。

foo.py  →  __pycache__/foo.cpython-312.pyc

バイトコードを覗いてみる

dis モジュールを使うと、バイトコードを人間が読める形で確認できます。

import dis

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

dis.dis(add)
# Python 3.12の出力例:
#   2           0 RESUME                   0
#   3           2 LOAD_FAST                0 (a)
#               4 LOAD_FAST                1 (b)
#               6 BINARY_OP                0 (+)
#              10 RETURN_VALUE

LOAD_FAST はローカル変数のロード、BINARY_OP 0 (+) は加算、RETURN_VALUE は返却。ここで重要なのは、Pythonの + は「整数の足し算」ではなく「__add__ を呼ぶ動的ディスパッチ」だということです。実行時に ab の型を見て、対応するメソッドを呼びます。これがPythonの遅さの根本原因の一つです。

.pycは何のためにあるのか

.pyc は単なる起動高速化のためのキャッシュです。Pythonのコードを難読化したり高速化したりするものではありません。配布バイナリに .pyc だけ含めても、簡単にデコンパイルできます(コードの秘匿には別の手段が必要)。


2-3. GIL(Globalグローバルインタープリターロック) の正体

Pythonの話で必ず出てくる GIL(Global Interpreter Lock) とは何かを解説します。

GILの定義

GILとは、CPythonインタプリタ全体に1つだけ存在する大きなロックで、同時にバイトコードを実行できるスレッドは1つだけ」 という仕組みです。

スレッドA ─┐
スレッドB ─┼─ GIL(1つだけ)─→  実行できるのは1スレッドだけ
スレッドC ─┘

これが意味するのは、CPUバウンドな純Pythonコードはマルチスレッドにしても並列に動かない、ということです。スレッドを4つ立てても、合計の処理速度は1スレッドとほぼ変わりません(むしろロック切替分わずかに遅くなる)。

# CPUバウンドな処理(GILで並列化されない)
import threading

def cpu_bound():
    total = 0
    for i in range(10_000_000):
        total += i
    return total

# 4スレッド並列にしても、CPUコア数を活かせない
threads = [threading.Thread(target=cpu_bound) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()

なぜGILがあるのか

GILの最大の理由は 参照カウントの安全性を保つため」 です。CPythonのメモリ管理は参照カウントを使っており、a = b といった単純な代入でも内部で参照カウントの増減が発生します。複数スレッドが同時にこのカウンタを書き換えると壊れてしまうため、すべてのバイトコード実行を1スレッドに直列化するのが最も簡単な解決策でした。

GILがある利点もあります。

  • C拡張モジュールが書きやすい(スレッドセーフ性をあまり気にしなくて良い)
  • シングルスレッド性能が高い(細粒度ロックを大量に持つよりオーバーヘッドが少ない)

しかしマルチコア時代になり、デメリットが目立つようになりました。

GILがあっても並列化できる場合

実は、すべての処理がGILに縛られるわけではありません。

GILで並列化されない:    CPUバウンドな純Pythonコード
GILを解放できる:       I/O待ち、time.sleep、C拡張中の処理

具体的には、

  • I/O待ち(ファイル読み書き、ネットワーク通信、time.sleep): GILを解放するので、threadingで並列化が効く
  • NumPyの行列演算など、C拡張内部の処理: NumPyはGILを解放してからCコードを呼ぶので、並列化が効く
  • multiprocessing: プロセスが別だからGILも別。CPUバウンド並列化はこちらを使うのが正解

これが「PythonではI/Oはthreading、CPUはmultiprocessing」という有名な使い分けの根拠です。詳細は第15章で深掘りします。


2-4. Python 3.13 Free-Threaded(実験的GIL無効化)

2024年10月リリースのPython 3.13で、長年「Pythonの宿痾」と呼ばれたGILを任意で外せる実験的ビルドが追加されました。これはPEP 703に基づくもので、Pythonの歴史上もっとも大きな実装変更の一つです。

Free-Threadedビルドとは

通常のCPython 3.13はGILあり(従来通り)です。一方、ビルドオプション --disable-gil を有効にしてビルドした Free-Threadedビルド ではGILがなくなります。

通常ビルド          : python  ←GILあり、従来互換
Free-Threadedビルド : python3.13t  ←GIL無し、CPUバウンドが並列化可能

何が変わるか

GILがなくなれば、CPUバウンドな純Pythonコードがスレッドで並列実行できるようになります。これはPythonにおける30年来の制約が初めて解かれることを意味します。

注意点

ただし2026年現在、Free-Threadedはまだ「実験的(experimental)」扱いです。

  • 既存のC拡張モジュール(NumPy、SciPy、Pillowなど)を再ビルド・対応する必要がある
  • シングルスレッド性能が10〜20%程度低下する(細粒度ロックや別の同期機構が必要なため)
  • すべての標準ライブラリが完全にスレッドセーフとは限らない

実用化には数バージョンかかると見られていますが、Pythonの並列処理の景色を一変させる変化として注視する価値があります。詳細は第15章で再度扱います。


2-5. 参照カウントとガベージコレクション

参照カウント(Reference Counting)

CPythonのメモリ管理の基本は 参照カウント です。すべてのオブジェクトは「自分が何箇所から参照されているか」を内部に記録しており、カウントが0になった瞬間にメモリが解放されます

import sys

a = [1, 2, 3]
sys.getrefcount(a)   # 2(変数a + getrefcount引数の一時参照)

b = a
sys.getrefcount(a)   # 3(a, b, 一時参照)

del b
sys.getrefcount(a)   # 2

del a   # ここでオブジェクト [1,2,3] のカウントが0になり解放される

参照カウント方式の長所は 「決定論的(deterministic)にメモリ解放のタイミングがわかる」 ことです。del した瞬間、最後の参照が消えた瞬間に解放されます。これはJavaなど世代別GCと大きく違う点で、__del__ メソッド(デストラクタ)が信頼できる挙動になる根拠です。

短所は、循環参照を解放できない ことです。

a = []
b = []
a.append(b)
b.append(a)
# a → b → a → b → ... と無限循環
# 参照カウントは互いに1ずつあるので、delしても解放されない

サイクルGC(世代別ガベージコレクション)

循環参照を回収するため、CPython参照カウントとは別に 世代別ガベージコレクション(generational GC) を持っています。gc モジュールで挙動を覗けます。

import gc

# GCは通常自動的に走る
# 手動で実行するなら
gc.collect()

# 世代ごとの統計
gc.get_count()  # (gen0数, gen1数, gen2数)

世代は3つあり、新しく生成されたオブジェクトは世代0に入り、生き残ったものが順に1, 2へ昇格していきます。古い世代ほどGCの頻度が低くなる、という標準的な世代別GCの考え方です。

参照カウント + GCのメリット

この 参照カウント + 補助としてのサイクルGC という組み合わせはPython独特で、以下の特性を生みます。

  • 多くの場合、決定論的に解放される(循環さえなければ)
  • with 文や try/finally でリソース解放が予測可能
  • 大きなGC停止(Stop-The-World)が起こりにくい

これに対しJavaScript(V8)やJavaは世代別GCのみで、解放タイミングは不定です。Pythonの __del__ がほぼ予想通り呼ばれるのは、参照カウントのおかげと言えます。


2-6. このセクションのまとめ

処理系:
  CPython(公式・C実装)が事実上の標準
  PyPyはJITで速いがNumPyなどC拡張と相性が悪い
  Faster CPython(3.11〜)で純CPython自体も継続的に高速化中

実行モデル:
  ソース → AST → バイトコード(.pyc) → 評価ループで逐次実行
  disモジュールでバイトコードを覗ける

GIL(Global Interpreter Lock):
  CPythonのスレッドはバイトコード実行を直列化される
  CPUバウンド:GILのためthreadingでは並列化されない
                → multiprocessingを使う
  IOバウンド:I/O待ち中はGILを解放するのでthreadingが効く
                → ただしasyncioが現代的選択肢

Python 3.13 Free-Threaded:
  GILを外せる実験的ビルド(--disable-gil)
  C拡張モジュール側の対応が必要、まだ移行期

メモリ管理:
  参照カウント(決定論的、カウント0で即解放)
  + サイクルGC(循環参照だけを回収する世代別GC)
  → Java/JSとは違う、予測可能なリソース解放挙動

次のセクションでは、変数・データ型・型ヒントの世界に入っていきます。「Pythonの変数は『箱』ではなく『ラベル』である」という最重要モデルから始めます。


3. 変数・データ型・型ヒント

このセクションはPythonを理解する上での最重要パートです。多くの初学者が「変数は箱だ」というモデルで他言語を学んできて、それをPythonに持ち込むと深刻なバグを生みます。Pythonの変数は 「箱」ではなく「ラベル(名札)」 であるという正しいメンタルモデルから始めます。


3-1. 名前束縛とオブジェクトモデル(変数は箱ではない)

C/Javaの「箱」モデル

CやJavaでは、変数は メモリ上の領域(箱) であり、代入はその箱に値を書き込む操作と見なせます。

int x = 42;   // メモリ4バイトの「箱」に42を書き込む
x = 43;       // 同じ箱の中身を上書きする

Pythonの「ラベル」モデル

Pythonでは、値(オブジェクト)は別の場所に存在し、変数はそのオブジェクトに付ける名札にすぎません。代入はオブジェクトに名札を貼り直す操作です。

x = 42        # intオブジェクト42を作り、xという名札を貼る
x = 43        # 別のintオブジェクト43を作り、名札xを貼り替える

このモデル差は、ミュータブルなオブジェクトを扱うときに鋭く現れます。

a = [1, 2, 3]
b = a            # bは同じlistを指す名札(コピーではない!)
b.append(4)
print(a)         # [1, 2, 3, 4] ← aも変わってしまった
print(a is b)    # True(同じオブジェクト)

このモデルを 「名前束縛(Name Binding)」 と呼びます。Pythonの代入はすべて「オブジェクトを生成or既存のものを参照 → 名前を束縛する」という二段階で行われています。

id() とisで確認する

id() はオブジェクトのメモリアドレス(CPython実装上)を返します。is 演算子は 同一オブジェクトかどうかを判定します。

a = [1, 2, 3]
b = a
c = [1, 2, 3]   # 内容は同じだが別のlist

print(id(a))    # 例: 140234551234176
print(id(b))    # aと同じ
print(id(c))    # aと違う

print(a is b)   # True(同一オブジェクト)
print(a is c)   # False(別オブジェクト)
print(a == c)   # True(内容は等しい)

ここで重要な区別があります。

演算 意味
== 値の等価性(__eq__ が呼ばれる)
is 同一オブジェクト(同じidか)

「内容は同じだが別オブジェクト」は == がTrueで is がFalse、というのが基本です。


3-2. ミュータブルvsイミュータブル

Pythonのオブジェクトは ミュータブル(変更可能)イミュータブル(変更不可) の二系統に分かれます。

イミュータブル(変更できない) ミュータブル(変更できる)
int, float, complex list
bool dict
str set
tuple(要素自体は別問題) bytearray
frozenset ユーザ定義クラス(基本ミュータブル)
bytes
None

イミュータブルの挙動

s = "hello"
s += " world"     # 新しいstrを作ってsに貼り直す(元の "hello" は不変)

n = 10
n += 1            # 新しいint 11を作ってnに貼り直す

文字列を += で連結するたびに新しい文字列が作られるため、ループ内で s += ... を繰り返すのはO(n²) になります。これが「Pythonでは大量の文字列連結は ''.join() を使うべき」と言われる理由です。

# アンチパターン: O(n²) になる
result = ""
for word in words:
    result += word

# 正しい書き方: O(n)
result = "".join(words)

ミュータブルの挙動

lst = [1, 2, 3]
lst.append(4)      # 同じlistが中身を変える(新しいlistは作らない)
lst[0] = 99        # 同じlistの0番目の要素を書き換える

ミュータブルなオブジェクトを関数引数に渡すと、関数側で変更すると呼び出し側にも影響します。

def add_one(lst):
    lst.append(1)

x = []
add_one(x)
print(x)   # [1] ← 関数内の変更が反映されている

3-3. 主要な組み込み型

Pythonの主な組み込み型を一覧化します。

リテラル例 ミュータブル 用途
int 42, -7, 0xff, 0b1010 × 整数(任意精度)
float 3.14, 1e10, inf, nan × IEEE 754倍精度浮動小数点
complex 3+4j × 複素数
bool True, False × 真偽値(intの派生)
str "hello", 'a', """...""" × Unicode文字列
bytes b"hello" × バイト列
bytearray bytearray(b"hello") 可変バイト列
list [1, 2, 3] 可変長配列
tuple (1, 2, 3), (1,) × 不変長配列
dict {"a": 1, "b": 2} キー値マッピング
set {1, 2, 3} 重複なし集合
frozenset frozenset({1, 2}) × 不変集合
NoneType None × 値の不在
range range(10) × 整数列の遅延ジェネレータ

boolはintの派生

意外に知られていない事実: boolint のサブクラスです。True == 1False == 0 であり、計算にも使えます。

True + True       # 2
True * 5          # 5
sum([True, False, True])  # 2

isinstance(True, int)   # True

これが時々予期しない挙動を生むので注意が必要です(例: 辞書のキーで 1True が同一視される)。

NoneとNoneType

None は「値がない」ことを表す唯一のシングルトンです。is None で比較するのが慣習で、== None ではなく is None を使う理由は速度と意図の明確さです。

x = None
if x is None:
    ...

3-4. intの任意精度・floatの落とし穴・Decimal/Fraction

intは任意精度

Pythonの int任意精度整数 です。CやJavaの int のように32bit / 64bitに制限されません。

x = 2 ** 1000
# 10715086071862673209... という超巨大な整数も普通に扱える
print(len(str(x)))   # 302(10進300桁ある)

これは数値計算で非常に楽な反面、巨大な整数の演算は遅い(多倍長演算になる)という代償があります。Python 3.11以降、長い整数を int(s) でパースする際にパフォーマンス警告が出る場合があります(DoS防止のための制限)。

floatはIEEE 754倍精度

float は他言語と同じ IEEE 754倍精度(64bit) で、有名な誤差問題を引き継ぎます。

0.1 + 0.2          # 0.30000000000000004
0.1 + 0.2 == 0.3   # False(!)

# 対策1: math.isclose
import math
math.isclose(0.1 + 0.2, 0.3)   # True

# 対策2: roundを使った丸め
round(0.1 + 0.2, 10) == round(0.3, 10)   # True

Decimal: 10進数の正確な計算

金融計算など、10進数で正確な計算が必要な場面では decimal.Decimal を使います。

from decimal import Decimal

Decimal("0.1") + Decimal("0.2")   # Decimal('0.3') ← 誤差なし

# 注意: Decimal(0.1) と書くと既にfloatの誤差が入ってしまう
Decimal(0.1)   # Decimal('0.1000000000000000055511151231257827021181583404541015625')
# 文字列で渡すこと
Decimal("0.1") # Decimal('0.1')

Fraction: 有理数

分数として正確に計算したいときは fractions.Fraction

from fractions import Fraction

Fraction(1, 3) + Fraction(1, 6)   # Fraction(1, 2)

特殊値(inf, nan)

import math

math.inf       # +∞
-math.inf      # -∞
math.nan       # NaN

math.nan == math.nan         # False(NaNは自分自身と等しくない)
math.isnan(math.nan)         # True

float("inf") + 1             # inf
float("inf") - float("inf")  # nan

3-5. 型強制(暗黙変換は最小限)

Pythonの哲学(Explicit is better than implicit)に従い、暗黙の型変換は最小限です。JavaScriptのような '5' + 3 === '53' は起こらず、TypeError で死にます。

"5" + 3
# TypeError: can only concatenate str (not "int") to str

数値型の暗黙昇格

数値型(int, float, complex)の間だけは、Pythonも暗黙昇格します。

1 + 2.0      # 3.0(int → floatに昇格)
1 + 2.0 + 3j # (3+3j)(→ complex)
True + 1     # 2(boolはintとして扱われる)

真偽値への暗黙変換

ifwhileandornot の文脈では、任意の値が真偽値として評価されます。これは「型強制」というより「真偽性(truthiness)」の話で、4-4で詳述します。

明示的な型変換

int("42")           # 42
int("42", 16)       # 66(16進数としてパース)
int(3.7)            # 3(小数点以下を切り捨て)
int("3.7")          # ValueError!("3.7" はintではない)

float("3.14")       # 3.14
float("inf")        # inf

str(42)             # "42"
str([1, 2, 3])      # "[1, 2, 3]"

bool(0)             # False
bool("")            # False
bool([])            # False
bool("False")       # True("False" は空でない文字列なのでTrue!)

list("abc")         # ["a", "b", "c"]
tuple([1, 2, 3])    # (1, 2, 3)
set([1, 1, 2])      # {1, 2}
dict([("a", 1), ("b", 2)])  # {"a": 1, "b": 2}

よくある落とし穴

結果 注意点
int("3.7") ValueError intは小数文字列をパースできない
int(3.7) 3 floatからintは切り捨て(四捨五入ではない)
bool("False") True 空でない文字列はすべてTrue
bool([0]) True 中身が0でも、空でなければTrue
True + 1 2 boolはintの派生なので算術可
1 == True True boolはintとして等価比較される
1 is True False しかしオブジェクトとしては別

3-6. 型ヒントの基礎

Python 3.5(PEP 484)から型ヒントが導入され、Python 3.12(PEP 695)でさらに簡潔な構文が加わりました。ここでは「最低限ここを押さえれば読み書きに困らない」レベルを示します。

関数の型ヒント

def greet(name: str, age: int = 0) -> str:
    return f"Hello, {name}! You are {age}."

変数の型ヒント

count: int = 0
items: list[str] = []
mapping: dict[str, int] = {}

Optional / Union(| 演算子)

Python 3.10+ では | で書けます。

# Python 3.10+ 推奨
def find(name: str) -> User | None: ...

# 古い書き方(typing.Optional, Union)
from typing import Optional, Union
def find(name: str) -> Optional[User]: ...
def parse(x: Union[int, str]) -> int: ...

list / dict / setの型パラメータ

Python 3.9+ では組み込み型に直接 [] で型パラメータを書けます(PEP 585)。

# 推奨(Python 3.9+)
nums: list[int] = []
mapping: dict[str, list[int]] = {}

# 古い書き方(Python 3.8以前)
from typing import List, Dict
nums: List[int] = []
mapping: Dict[str, List[int]] = {}

型エイリアス(Python 3.12+)

# Python 3.12+
type Vector = list[float]
type UserId = int

# 古い書き方(typing.TypeAlias)
from typing import TypeAlias
Vector: TypeAlias = list[float]

型ヒントは実行時にチェックされない

最重要ポイントを再掲します。型ヒントは実行時にチェックされません

def add(a: int, b: int) -> int:
    return a + b

add("hello", "world")   # "helloworld"(実行できてしまう)

mypypyrightのような 静的型チェッカを別途走らせて検証します。詳細は第17章で扱います。


3-7. このセクションのまとめ

変数 = ラベル(名札)モデル:
  Pythonの変数は箱ではなく、オブジェクトに貼る名札
  代入は名札の貼り替え
  isと == を区別する(is = 同一性、== = 値の等価性)

ミュータブルvsイミュータブル:
  イミュータブル: int, float, str, tuple, frozenset, bytes
  ミュータブル:   list, dict, set, bytearray, ユーザ定義クラス
  関数引数に渡すと、ミュータブルは関数内変更が呼び出し側に伝わる

数値の落とし穴:
  intは任意精度(巨大整数も扱える)
  floatはIEEE 754 → 0.1 + 0.2 != 0.3
  正確な10進演算はdecimal.Decimal、有理数はfractions.Fraction
  boolはintの派生(True + 1 == 2)

型強制:
  暗黙の型変換は最小限("5" + 3はエラー)
  例外: 数値型同士、真偽値文脈

型ヒント:
  関数: def f(x: int) -> str
  変数: x: int = 0
  Optional/Union: int | None(3.10+)
  list[int], dict[str, int](3.9+)
  実行時にはチェックされない(mypy/pyrightで検証)

次のセクションでは、演算子と式の世界を体系的に整理します。


4. 演算子と式

このセクションではPythonの演算子を整理します。is== の違い、論理演算子の短絡評価、ウォルラス演算子、チェイン比較といった、他言語と挙動が違う点を中心に解説します。


4-1. 算術演算子と整数除算

5 + 2     # 7
5 - 2     # 3
5 * 2     # 10
5 / 2     # 2.5(常にfloatになる!)
5 // 2    # 2(整数除算、商)
5 % 2     # 1(剰余)
5 ** 2    # 25(べき乗)

# 負の数の整数除算は数学的な床関数
-5 // 2   # -3(-2.5を切り捨てではなく床関数で -3になる)
-5 % 2    # 1(剰余の符号は除数に揃う)

Python 2と3の違い(歴史的に重要)

# Python 2では / が整数除算だった
5 / 2     # 2  (Python 2)
5 / 2     # 2.5(Python 3)

# Python 2から3への移行で最も多くのバグを生んだ変更の一つ
# from __future__ import divisionでPython 2でも3の挙動にできた

インクリメント / デクリメントは存在しない

Pythonには ++ -- がありません。a += 1 を使います。これは「++aa++ の混乱を避けるため」とされています。

i = 5
i += 1   # 6
i -= 1   # 5
# i++ は構文エラー

4-2. 比較演算子と同値性(isと ==)

1 == 1.0      # True(数値型は値で比較)
"a" == "a"    # True
[1, 2] == [1, 2]  # True(要素が同じlistは等しい)

# != は等しくない
1 != 2        # True

# isは同一オブジェクトか
a = [1, 2, 3]
b = a
c = [1, 2, 3]
a is b        # True
a is c        # False(内容は同じでも別オブジェクト)

# is not
a is not c    # True

Noneとの比較はisを使う

None はシングルトンなので is で比較するのが慣習です。

# Good
if x is None:
    ...

# Bad(動くが、__eq__ をオーバーライドしたクラスで誤動作するリスク)
if x == None:
    ...

小整数キャッシュと文字列インターン

CPythonの実装最適化として、小整数(-5 〜 256)と短い文字列は同じオブジェクトが使い回されます。これがisの挙動を惑わせる原因になります。

a = 100
b = 100
a is b       # True(小整数キャッシュ)

a = 1000
b = 1000
a is b       # Falseの場合がある(CPython実装依存)

a = "hello"
b = "hello"
a is b       # True(文字列インターン)

a = "hello world"
b = "hello world"
a is b       # 実装依存

教訓: 値の比較は常に == を使うこと。is は「同一オブジェクトか」を意図して問うときだけ使う。

チェイン比較

Pythonは数学に近い書き方ができます。

0 < x < 10        # 0 < xかつx < 10と等価

# 一般的な等価なコードは:
0 < x and x < 10

# 3つ以上もOK
1 <= x < y <= 100

# 落とし穴: 違う比較を混ぜると意外な意味になる
a == b == c       # a == bかつb == c
a == b < c        # a == bかつb < c

4-3. 論理演算子と短絡評価

Pythonの論理演算子は and, or, not です(&&, ||, ! ではない)。

短絡評価

and / or短絡評価(左辺で結果が決まれば右辺を評価しない)を行います。さらに結果は最後に評価された値そのものを返します(True/Falseに変換されない)。

# and: 左がfalsyなら左を返す、truthyなら右を返す
False and "world"   # False
"hello" and "world" # "world"
0 and 5             # 0(左がfalsyなので)

# or: 左がtruthyなら左を返す、falsyなら右を返す
None or "default"   # "default"
"value" or "default" # "value"
0 or 5              # 5

# 実用例: デフォルト値
name = user_input or "anonymous"

# 注意: 0や [] のようなfalsyをデフォルトと区別できない
count = config.get("count") or 10
# config["count"] が0のとき10になってしまう!
# このような場合は明示的に書く
count = config.get("count")
if count is None:
    count = 10

not

not True       # False
not 0          # True(0はfalsy)
not []         # True(空リストはfalsy)
not "hello"    # False

4-4. 真偽性(truthiness)

Pythonでは if などの真偽値文脈で、値そのものを評価できます。falsyな値は限られた集合で、それ以外はすべてtruthyです。

# falsyな値(覚えるリスト)
False
None
0       # int, float, complexの0
""      # 空文字列
[]      # 空リスト
()      # 空タプル
{}      # 空辞書
set()   # 空セット
range(0)
b""     # 空bytes
# 上記以外はすべてtruthy
bool(1)              # True
bool(-1)             # True(0以外の数値はTrue)
bool("False")        # True(空でない文字列)
bool([0])            # True(空でないリスト、中身が0でもOK)
bool({0: 0})         # True
bool(object())       # True(普通のオブジェクトはTrue)

boollen

クラスに __bool__ を定義すると真偽値変換をカスタマイズできます。__bool__ がなければ __len__ が呼ばれます。

class Box:
    def __init__(self, items):
        self.items = items
    def __bool__(self):
        return len(self.items) > 0

bool(Box([]))      # False
bool(Box([1, 2]))  # True

よくあるアンチパターン

# Bad
if len(items) > 0:
    ...
if items != []:
    ...
if items == []:
    ...

# Good(Pythonic)
if items:
    ...
if not items:
    ...

4-5. ウォルラス演算子(:=)

Python 3.8(PEP 572)で追加された ウォルラス演算子(walrus operator) は、式の中で変数に代入できる演算子です。:= がセイウチの目と牙に見えることから命名されました。

# 従来
data = read_data()
if data:
    process(data)

# ウォルラス
if (data := read_data()):
    process(data)

# 典型例: whileループでの読み込み
while (chunk := file.read(8192)):
    process(chunk)

# 内包表記での使い回し
results = [y for x in data if (y := compute(x)) > 0]

ウォルラスは便利だが乱用すると読みにくくなります。「同じ計算を2回書きたくないとき」「ループ条件と読み込みを一文にまとめたいとき」 に限って使うのがベターです。


4-6. 三項演算式・チェイン比較

三項演算式(条件付き式)

# 構文: (真の場合) if (条件) else (偽の場合)
x = 10
label = "positive" if x > 0 else "non-positive"

# ネストもできる(読みにくくなるので避けるのがベター)
sign = "+" if x > 0 else "-" if x < 0 else "0"

C/Javaの cond ? a : b とは順序が違うので注意。Pythonは 「結果が先、条件が後」 です。

スプレッド・アンパッキング

# シーケンスのアンパッキング
a, b, c = [1, 2, 3]   # a=1, b=2, c=3

# 残りをまとめる *
first, *rest = [1, 2, 3, 4]      # first=1, rest=[2,3,4]
*init, last = [1, 2, 3, 4]       # init=[1,2,3], last=4
a, *mid, b = [1, 2, 3, 4, 5]     # a=1, mid=[2,3,4], b=5

# dictのマージ(Python 3.9+)
d1 = {"a": 1}
d2 = {"b": 2}
merged = d1 | d2   # {"a": 1, "b": 2}

# スプレッド展開
[*a, *b]           # listを連結
{*s1, *s2}         # setを連結
{**d1, **d2}       # dictをマージ(Python 3.5+)

# 関数呼び出しでの展開
def f(a, b, c): ...
args = [1, 2, 3]
f(*args)           # f(1, 2, 3)
kwargs = {"a": 1, "b": 2, "c": 3}
f(**kwargs)        # f(a=1, b=2, c=3)

4-7. このセクションのまとめ

算術:
  / は常にfloat(5/2 == 2.5)
  // は整数除算(床関数)
  ** はべき乗
  ++ -- は無い → += 1 / -= 1

比較:
  == 値の等価性、is同一オブジェクト
  Noneとの比較はis None
  小整数(-5〜256)と短い文字列はキャッシュされる
  チェイン比較: 0 < x < 10が書ける

論理:
  and / or / not(短絡評価あり)
  最後に評価した値を返す(True/Falseではない)
  0や [] とNoneを区別したいときはorは使えない

真偽性:
  falsy: False, None, 0, "", [], (), {}, set(), b""
  それ以外は全部truthy
  Pythonic: if items: / if not items:

ウォルラス :=(3.8+):
  式の中で変数に代入できる
  ループや内包表記で「計算を1回にまとめたい」とき有効

その他:
  三項: a if cond else b
  アンパッキング: a, *rest, b = ...
  dictマージ: d1 | d2(3.9+)

次のセクションでは、制御フローを扱います。Pythonならではの for/elsematch、EAFPの流儀がポイントです。


5. 制御フロー

このセクションではPythonの制御フロー構文――ifforwhilematchtry/except――の中で、他言語と挙動が違う点を中心に解説します。


5-1. if / elif / else

if x > 0:
    print("positive")
elif x < 0:
    print("negative")
else:
    print("zero")

特殊性は少ないですが、else if ではなく elif であることに注意。switch は無い(その代わりがPython 3.10+ の match)。

三項演算式(再掲)

label = "positive" if x > 0 else "non-positive"

5-2. for / while / else節

# rangeは遅延列(メモリ効率が良い)
for i in range(10):
    print(i)

# enumerateでインデックス付き
for i, item in enumerate(items):
    print(i, item)

# zipで複数リストを同時イテレート
for a, b in zip(list_a, list_b):
    print(a, b)

# dictのイテレート
for key in d:                # キーだけ
    ...
for value in d.values():     # 値
    ...
for key, value in d.items(): # 両方
    ...

for / whileのelse節(Python独自)

forwhile には elseを付けられます。これは「ループが break せずに正常終了したときだけ実行される」という意味で、他言語にはほぼ無い機能です。

for x in items:
    if found_condition(x):
        break
else:
    # breakしなかった = 見つからなかった
    print("not found")

# whileでも同じ
while condition:
    ...
    if exit_condition:
        break
else:
    print("normal end")

これは「search-failedのロジックを別フラグなしで書ける」という独自機能。ただし else という単語が直感に反するため、Pythonコミュニティでも賛否があります。「break しなかったときだけ走る nobreak ブロック」と読み替えると分かりやすいです。


5-3. break / continue / pass

# break: ループを抜ける
for x in items:
    if x == "stop":
        break

# continue: 次のイテレーションへ
for x in items:
    if x is None:
        continue
    process(x)

# pass: 何もしない(構文上のplaceholder)
def todo():
    pass

class Empty:
    pass

pass の使い所: メソッドやifブロックを「あとで書く」と決めて空にしたいとき、Pythonは空ブロックを許さないので pass を入れます。


5-4. match文(構造的パターンマッチング)

Python 3.10(PEP 634)で導入された match は、単なる switch 文の置き換えではなく、構造的パターンマッチング という強力な機能です。

基本: 値マッチ

def http_message(status):
    match status:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case 500 | 502 | 503:   # 複数値マッチ
            return "Server Error"
        case _:                   # ワイルドカード(デフォルト)
            return "Unknown"

シーケンス分解

def describe_point(point):
    match point:
        case (0, 0):
            return "origin"
        case (x, 0):
            return f"on x-axis at x={x}"
        case (0, y):
            return f"on y-axis at y={y}"
        case (x, y):
            return f"point ({x}, {y})"
        case _:
            return "not a point"

dict分解

def handle(event):
    match event:
        case {"type": "click", "x": x, "y": y}:
            return f"click at ({x}, {y})"
        case {"type": "key", "key": k}:
            return f"key {k}"

クラス分解

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

def describe(p):
    match p:
        case Point(x=0, y=0):
            return "origin"
        case Point(x=0, y=y):
            return f"y-axis at {y}"
        case Point(x=x, y=y):
            return f"({x}, {y})"

ガード(if節)

match value:
    case x if x > 0:
        return "positive"
    case x if x < 0:
        return "negative"
    case 0:
        return "zero"

落とし穴: 名前と定数

case の中で すべての文字なし識別子は変数として束縛されてしまいます。定数とマッチさせたいときは クラス.属性 形式にする必要があります。

RED = 1
GREEN = 2

# Bad: REDは変数として束縛されてしまう
match color:
    case RED:    # これは「colorをREDに代入する」と同じ意味になる!
        ...

# Good: 定数を持つクラスやモジュール経由でアクセスする
class Color:
    RED = 1
    GREEN = 2

match color:
    case Color.RED:
        ...
    case Color.GREEN:
        ...

これが match 文の最大の落とし穴です。定数は必ずドット付きでアクセスすること。


5-5. EAFP vs LBYL(例外も制御フロー)

Pythonのコーディング哲学を語る上で重要な対比があります。

略語 意味
EAFP Easier to Ask Forgiveness than Permission(許可より許しを請う方が楽) まず実行し、エラーが出たら処理する
LBYL Look Before You Leap(飛ぶ前に見よ) 事前条件をチェックしてから実行
# LBYL: 事前にチェック(C/Javaで一般的)
if "key" in d:
    value = d["key"]
else:
    value = "default"

# EAFP: まず取得して、失敗したら例外(Pythonic)
try:
    value = d["key"]
except KeyError:
    value = "default"

PythonはEAFPを好む

Pythonでは一般にEAFPが推奨されます。理由は次の通りです。

  1. race conditionを回避できる: LBYLの事前チェックと実際のアクセスの間に状態が変わるかもしれない(特にファイルやマルチスレッド)
  2. 冗長なチェックを書かなくて済む: 結局チェック後にもアクセスでエラーが出る可能性は残る
  3. 例外処理コストは「例外が起きないとき」は0: 起きたときだけ重いが、起きない前提なら速い
# LBYL: race conditionがある
if os.path.exists(path):
    open(path)  # ここで他プロセスが消したらエラー

# EAFP: 安全
try:
    open(path)
except FileNotFoundError:
    ...

例外で制御するイディオム

# 数値変換のフォールバック
def parse_int(s, default=0):
    try:
        return int(s)
    except (ValueError, TypeError):
        return default

ただし「例外を制御フローに使うのは適度に」が肝心。本当に例外的でない状況で例外を多用するとコードが読みにくくなり、デバッグも難しくなります。


5-6. このセクションのまとめ

分岐:
  if / elif / else(else ifではなくelif)
  match文(3.10+): 構造的パターンマッチング
    - 値、シーケンス、dict、クラス、ガードを書ける
    - 落とし穴: 定数はClass.NAME形式で参照する

ループ:
  for x in iterable / while cond
  for/else: breakしなかったときだけelseが走る(Python独自)
  enumerate / zip / dict.items() を活用
  break / continue / passを使い分け

EAFP vs LBYL:
  LBYL: if 〜 in d: で事前チェック
  EAFP: try 〜 exceptで「やってみてダメなら」
  Pythonは一般にEAFP(race condition回避、Pythonic)

次のセクションでは、関数――Pythonの中核要素のひとつ――を体系的に解説します。


6. 関数

Pythonの関数は 第一級オブジェクト(変数に代入でき、引数として渡せ、戻り値にできる)です。このセクションでは関数定義の3形式、引数の種類、ミュータブル落とし穴、クロージャ、高階関数までを体系的に整理します。


6-1. defとlambda

def: 通常の関数定義

def add(a, b):
    """2つの数を足して返す。"""   # docstring
    return a + b

result = add(1, 2)   # 3

戻り値を書かないと暗黙的に None が返ります。

def greet(name):
    print(f"Hello, {name}")

x = greet("Alice")    # xはNone

lambda: 無名関数

lambda は単一の式から無名関数を作ります。1行で書ける単純な関数に限られます(文は書けない、return も書かない)。

add = lambda a, b: a + b
add(1, 2)   # 3

# よくある用例: ソートのキー、フィルタ
sorted(items, key=lambda x: x.age)
list(filter(lambda x: x > 0, numbers))

ただしPEP 8は lambda を変数に代入するな、def を使え」推奨しています。理由は lambda 関数の __name__"<lambda>" になりトレースバックが分かりにくくなるためです。

# Bad
add = lambda a, b: a + b

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

lambda は基本的に「他の関数の引数として一度だけ使う」ためのものと考えるのがPythonicです。

関数も第一級オブジェクト

def greet(name):
    return f"Hello, {name}"

# 変数に代入
f = greet
f("Alice")   # "Hello, Alice"

# リストに入れる
funcs = [greet, str.upper, str.lower]

# 引数に渡す
def call(fn, x):
    return fn(x)
call(greet, "Bob")   # "Hello, Bob"

# 関数を返す関数
def make_multiplier(n):
    def multiply(x):
        return x * n
    return multiply
double = make_multiplier(2)
double(5)   # 10

6-2. 引数の種類(位置・キーワード・*args・**kwargs)

Pythonの引数システムは非常に柔軟で、5種類の引数があります。

def f(pos_only, /, normal, *, kw_only, **kwargs):
    pass

位置引数とキーワード引数

def greet(name, greeting):
    return f"{greeting}, {name}"

greet("Alice", "Hello")            # 位置引数
greet(name="Alice", greeting="Hi") # キーワード引数(呼び出し時に名前指定)
greet("Alice", greeting="Hi")      # 混在もOK(位置が先)

デフォルト引数

def greet(name, greeting="Hello"):
    return f"{greeting}, {name}"

greet("Alice")             # "Hello, Alice"
greet("Alice", "Howdy")    # "Howdy, Alice"

重要なルール: デフォルト引数を持つ引数の後に、デフォルトのない引数は書けません。

# 構文エラー
def f(a=1, b):  # SyntaxError
    ...

*args: 可変長位置引数

def sum_all(*args):
    total = 0
    for x in args:
        total += x
    return total

sum_all(1, 2, 3, 4)   # 10
sum_all()             # 0

# 既存のリストを展開して渡す
nums = [1, 2, 3]
sum_all(*nums)        # 10

**kwargs: 可変長キーワード引数

def make_user(**kwargs):
    return kwargs

make_user(name="Alice", age=30)   # {"name": "Alice", "age": 30}

# 既存のdictを展開して渡す
data = {"name": "Bob", "age": 25}
make_user(**data)

位置専用引数とキーワード専用引数(Python 3.8+)

def f(pos_only, /, normal, *, kw_only):
    print(pos_only, normal, kw_only)

f(1, 2, kw_only=3)    # OK
f(1, normal=2, kw_only=3)  # OK(normalはキーワードでも渡せる)
f(pos_only=1, ...)     # TypeError!pos_onlyは位置でしか渡せない
f(1, 2, 3)             # TypeError!kw_onlyは名前必須
  • / の左: 位置専用(呼び出し時に名前で指定できない)
  • /* の間: 通常(位置でも名前でも可)
  • * の右: キーワード専用(呼び出し時に必ず名前指定)
# 標準ライブラリでも使われている
# range(start, stop, step) は位置専用
# print(*objects, sep=' ', end='\n', ...) のsep, endはキーワード専用に近い

6-3. デフォルト引数のミュータブル落とし穴

Python初心者が必ず一度ハマるバグを紹介します。

def append_one(items=[]):
    items.append(1)
    return items

print(append_one())   # [1]
print(append_one())   # [1, 1] ←!?
print(append_one())   # [1, 1, 1]

原因: デフォルト引数のオブジェクトは 関数定義時に1回だけ作られ、呼び出しごとに使い回されるitems=[] は最初の1回だけ評価され、その同じリストが毎回再利用される。

# 正しい書き方
def append_one(items=None):
    if items is None:
        items = []
    items.append(1)
    return items

これはPythonの 「関数定義時に評価される」というセマンティクス が招く非直観的な挙動で、知らずに書くと潜伏バグの温床になります。

dictやsetでも同じ問題

# Bad
def add(d={}):
    d["key"] = "value"
    return d

# Good
def add(d=None):
    if d is None:
        d = {}
    d["key"] = "value"
    return d

イミュータブルなデフォルト値はOK

def greet(name="Anonymous"):  # strはイミュータブル → 安全
    return f"Hello, {name}"

6-4. 関数アノテーションと型ヒント

def greet(name: str, age: int = 0) -> str:
    return f"Hello, {name}, age {age}"

# 型ヒントの取得
greet.__annotations__
# {'name': <class 'str'>, 'age': <class 'int'>, 'return': <class 'str'>}

複雑な型は typing モジュールを使います。

from typing import Callable

def repeat(fn: Callable[[int], str], n: int) -> list[str]:
    return [fn(i) for i in range(n)]

詳細は第17章「型ヒントの本気」で扱います。


6-5. クロージャとnonlocal

クロージャとは、関数の内部に定義した関数が、外側のスコープの変数を「捕捉(capture)」する仕組みです。

def make_counter():
    count = 0
    def increment():
        nonlocal count   # 外側のcountを書き換えると宣言
        count += 1
        return count
    return increment

counter = make_counter()
counter()   # 1
counter()   # 2
counter()   # 3

nonlocalが必要な理由

Pythonでは、関数内で代入された変数はその関数のローカル変数として扱われます。外側のスコープの変数を書き換えたい場合、明示的に nonlocal を宣言する必要があります(読み取りだけなら不要)。

def outer():
    x = 10
    def inner():
        x = 20    # これはinnerの新しいローカル変数を作るだけ
    inner()
    print(x)   # 10(変わってない)

def outer2():
    x = 10
    def inner():
        nonlocal x
        x = 20    # outer2のxを書き換える
    inner()
    print(x)   # 20

def outer3():
    x = 10
    def inner():
        return x   # 読み取りだけならnonlocal不要
    return inner()

global

モジュールレベルの変数を関数内で書き換えたいときは global を使います。基本的に避けるべきイディオム(テストしにくい・並行性が悪い)ですが、必要な場面はあります。

counter = 0

def increment():
    global counter
    counter += 1

6-6. 高階関数・部分適用

高階関数(Higher-Order Functions)

関数を引数に取ったり、戻り値として返す関数。

# map
list(map(str.upper, ["a", "b", "c"]))   # ["A", "B", "C"]

# filter
list(filter(lambda x: x > 0, [-1, 0, 1, 2]))   # [1, 2]

# reduce(functools)
from functools import reduce
reduce(lambda a, b: a + b, [1, 2, 3, 4])   # 10

ただし、Pythonでは map / filter よりも 内包表記を使うほうがPythonicです。

# Pythonic
[x.upper() for x in ["a", "b", "c"]]
[x for x in numbers if x > 0]
sum(numbers)   # reduce相当

functools.partial: 部分適用

from functools import partial

def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)

square(5)   # 25
cube(5)     # 125

functools.cache / lru_cache

メモ化(同じ引数の結果をキャッシュ)。

from functools import cache

@cache
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

fib(100)   # 一瞬で返る

cache は引数の数に上限なし、lru_cache(maxsize=N) はLRUで上限あり。


6-7. このセクションのまとめ

関数定義:
  def: 通常の関数(推奨)
  lambda: 1行限りの無名関数(引数として一度だけ使う場面)

引数の種類:
  位置 / キーワード / デフォルト / *args / **kwargs
  位置専用(/ の左)/ キーワード専用(* の右)

デフォルト引数の落とし穴:
  ミュータブル(list, dict)をデフォルトにしない
  → 呼び出しごとに同じオブジェクトが共有される
  正解: def f(x=None): if x is None: x = []

クロージャとnonlocal:
  内側関数は外側のスコープを捕捉できる
  書き換えるときはnonlocal(モジュール変数ならglobal)

高階関数:
  map / filterは内包表記でも書ける(Pythonicは内包表記)
  functools.partialで部分適用
  functools.cacheでメモ化(再帰アルゴリズムに有効)

次のセクションでは、Pythonのスコープと名前解決を深掘りします。


7. スコープと名前解決

Pythonの名前解決ルール LEGB を理解すると、「なぜこの変数はここで見える / 見えないのか」がすべて説明できます。


7-1. LEGB規則

Pythonは名前を 4階層のスコープ で順に探します。

略号 スコープ
L Local 関数内の変数
E Enclosing 外側の関数(ネスト関数の場合)
G Global モジュール(ファイル)レベルの変数
B Built-in print, len など組み込み
# B (Built-in)
# print, len, range, ... は最後の砦

x = "global x"   # G (Global)

def outer():
    x = "outer x"   # E (Enclosing)
    def inner():
        x = "inner x"   # L (Local)
        print(x)
    inner()

outer()   # "inner x"
def outer():
    x = "outer x"
    def inner():
        # x = なし → Lで見つからない → Eを探す → "outer x"
        print(x)
    inner()

outer()   # "outer x"
x = "global x"
def f():
    # x = なし → Lで見つからない → Eなし → G → "global x"
    print(x)
f()   # "global x"

7-2. global / nonlocal

読み取りはLEGBで自動探索されるが、書き込みは明示が必要」という非対称があります。

x = 10

def f():
    x = 20         # これは関数内のローカル変数を新規作成
    print(x)       # 20

f()
print(x)           # 10(モジュールのxは変わらない)

書き換えたいなら global / nonlocal を使う。

def f():
    global x
    x = 20         # モジュール変数を書き換え

def outer():
    x = 10
    def inner():
        nonlocal x
        x = 20     # outerのxを書き換え
    inner()

7-3. クラス本体スコープの罠

クラス本体は 特殊なスコープで、内部の関数定義からは見えません。

class A:
    name = "A"
    items = [1, 2, 3]

    # クラス内の通常の文ではアクセスできる
    print(name)

    # メソッドの中ではself. 経由でアクセスする
    def show(self):
        print(self.name)   # ここでnameは見えない

    # 内包表記の中もスコープが切れる
    doubled = [x * 2 for x in items]   # OK(itemsはクラス本体スコープ)
    # doubled = [x * len(items) for x in items]  # OK
    # ネストの仕方次第で見えなくなることがある(落とし穴)

これは「クラス本体は関数スコープではない」というPythonの構造上の制約です。class 文は単に名前空間を作るために使われており、Enclosingスコープにはならないのです。


7-4. ループ内クロージャの落とし穴

JavaScriptでも有名な「ループ内クロージャの遅延束縛」問題がPythonにもあります。

funcs = []
for i in range(3):
    funcs.append(lambda: i)

for f in funcs:
    print(f())
# 期待: 0 1 2
# 実際: 2 2 2

原因

lambda: i は「現在の i の値」をキャプチャするのではなく「変数 i への参照」をキャプチャします。ループが終わった時点で i は最後の値(2)になっており、すべてのlambdaがその値を参照します。

解決策

デフォルト引数で値を固定する(Pythonic)
funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)   # i=iでその時点の値を束縛

for f in funcs:
    print(f())
# 0 1 2
functools.partialを使う
from functools import partial

def identity(x):
    return x

funcs = [partial(identity, i) for i in range(3)]

7-5. このセクションのまとめ

LEGB規則:
  Local → Enclosing → Global → Built-inの順で探索
  読み取りは自動、書き込みはglobal / nonlocalが必要

クラス本体スコープ:
  メソッド内からはクラス変数を直接参照できない(self.NAME必須)
  クラス本体はEnclosingとして機能しない

ループ内クロージャ:
  lambdaは変数の参照をキャプチャ(値ではない)
  → ループ終了時の値ですべて固定されてしまう
  解決: lambda i=i: iでデフォルト引数として束縛

次のセクションでは、リスト・辞書・集合・タプルの内部表現計算量を扱います。


8. コレクション操作

このセクションではPythonの主要コレクション(list, tuple, dict, set)の内部実装計算量、内包表記、collections モジュールを整理します。


8-1. listの内部表現と計算量

内部表現

CPythonlist動的配列(Dynamic Array) で、内部的にはポインタ配列です。各要素は実際にはオブジェクトへの参照(ポインタ)で、要素自体は別の場所にあります。

list:  [ ptr0 | ptr1 | ptr2 | _ | _ | _ ]   ← 容量はやや余裕を持って確保される
         |      |      |
         v      v      v
        obj0   obj1   obj2

計算量

操作 平均計算量 補足
lst[i] O(1) インデックスアクセス
lst[i] = x O(1) 代入
lst.append(x) 償却O(1) 容量を超えたら拡張(時々O(n))
lst.pop() O(1) 末尾の削除
lst.pop(0) O(n) 先頭の削除(全要素を1つずつ前に詰める)
lst.insert(0, x) O(n) 先頭への挿入
x in lst O(n) 線形探索
len(lst) O(1) サイズはキャッシュされている

先頭への追加・削除がO(n) になるのが要注意。両端操作が必要なら collections.deque を使う。

from collections import deque
d = deque()
d.appendleft(1)   # O(1)
d.popleft()       # O(1)

スライスの挙動

lst = [1, 2, 3, 4, 5]
lst[1:4]      # [2, 3, 4]
lst[::-1]     # [5, 4, 3, 2, 1](逆順)
lst[::2]      # [1, 3, 5](偶数番目)
lst[1:4:2]    # [2, 4]

# スライスは新しいリストを返す(元は変更しない)
copy = lst[:]    # 浅いコピー

# スライス代入は元のリストを変える
lst[1:3] = [10, 20, 30]   # [1, 10, 20, 30, 4, 5]

浅いコピーと深いコピー

ミュータブルな要素を含むリストをコピーするときは要注意。

import copy

a = [[1, 2], [3, 4]]

# 浅いコピー(外側だけコピー、内側は共有)
b = a.copy()
# b = a[:]
# b = list(a)
# b = copy.copy(a)
b[0].append(999)
print(a)   # [[1, 2, 999], [3, 4]] ← 共有しているのでaも変わる

# 深いコピー(再帰的にコピー)
c = copy.deepcopy(a)
c[0].append(999)
print(a)   # 影響なし

8-2. tupleとアンパッキング

tupleイミュータブルなシーケンス。「変更されない複数値の集まり」を表すのに使います。

t = (1, 2, 3)
t[0]            # 1
len(t)          # 3
# t[0] = 999    # TypeError(変更不可)

# 1要素のタプルはカンマ必須
single = (1,)   # ← カンマ
not_tuple = (1) # これはint 1と同じ(括弧は単なる優先順位)

tupleの用途

  • 複数値を返す関数: return x, y は実はタプル
  • 辞書のキー: ミュータブルなlistはキーにできないが、tupleはできる
  • 構造的データ: 名前付きフィールドが必要なら namedtupledataclass
def divmod_(a, b):
    return a // b, a % b   # tupleを返している

q, r = divmod_(10, 3)   # アンパッキングで取り出す

アンパッキング

a, b = 1, 2
a, b = b, a   # スワップ(中間変数なし)

# 入れ子も分解できる
(a, b), c = (1, 2), 3

# 残りを集めるアスタリスク
first, *rest = [1, 2, 3, 4]   # first=1, rest=[2,3,4]
*init, last = [1, 2, 3, 4]    # init=[1,2,3], last=4

# 関数引数の展開
def f(x, y, z): ...
nums = (1, 2, 3)
f(*nums)

8-3. dictの歴史と順序保証

順序保証の歴史

dict の挙動はPythonの歴史で2回大きく変わっています。

〜Python 3.5: 順序は不定(実装依存)
Python 3.6:   順序保持がCPythonの実装詳細として導入(仕様ではない)
Python 3.7:   順序保持が言語仕様化(保証された動作になった)
d = {"a": 1, "b": 2, "c": 3}
list(d)        # ["a", "b", "c"](挿入順が保持される)

これにより、OrderedDict の出番は減りました(move_to_end などの機能が必要なときに残る)。

内部実装(オープンアドレス法)

dictハッシュテーブルで、CPython 3.6以降は コンパクト辞書(compact dict) という実装。キーのハッシュ値から内部配列のインデックスを計算し、衝突したらオープンアドレス法で次の空きを探します。

計算量

操作 平均 最悪
d[key] O(1) O(n)(衝突多発時)
d[key] = v O(1) O(n)
key in d O(1) O(n)
del d[key] O(1) O(n)
len(d) O(1) O(1)

よく使うdictメソッド

d = {"a": 1, "b": 2}

d.get("a")         # 1
d.get("c")         # None(KeyErrorにならない)
d.get("c", 0)      # 0(デフォルト値)

d.setdefault("c", 0)   # キーがなければ追加してデフォルトを返す

d.update({"x": 10, "y": 20})   # マージ(同じキーは上書き)

d.pop("a")         # 取得して削除
d.pop("z", None)   # なければデフォルトを返す(KeyError回避)

# Python 3.9+ の | オペレータ
d1 = {"a": 1}
d2 = {"b": 2}
merged = d1 | d2     # {"a": 1, "b": 2}(新しいdict)
d1 |= d2             # d1を更新

# キーやバリューを反復
for k in d: ...
for v in d.values(): ...
for k, v in d.items(): ...

dict内包表記

{x: x*2 for x in range(5)}   # {0: 0, 1: 2, 2: 4, 3: 6, 4: 8}

# キーバリューを入れ替える
inverted = {v: k for k, v in d.items()}

8-4. set / frozenset

set重複なしの順序なしコレクション。内部はハッシュテーブル(dictのキーだけ版に近い)。

s = {1, 2, 3}     # setリテラル
s = set([1, 2, 3, 1])   # 重複は除去される → {1, 2, 3}
empty = set()     # {} はdictなので注意

s.add(4)
s.remove(2)       # 存在しないとKeyError
s.discard(2)      # 存在しなくてもエラーにならない

# 集合演算
a = {1, 2, 3}
b = {2, 3, 4}
a | b      # 和集合 {1, 2, 3, 4}
a & b      # 積集合 {2, 3}
a - b      # 差集合 {1}
a ^ b      # 対称差 {1, 4}

frozenset

イミュータブルなset。dictのキーやsetの要素にできる。

fs = frozenset({1, 2, 3})
# fs.add(4)   # AttributeError
{fs}          # setの中に入れられる

8-5. 内包表記とジェネレータ式

Pythonの 内包表記(comprehension) は、for ループによるリスト生成を1行で書く構文。Pythonicコードの代名詞。

# リスト内包表記
squares = [x ** 2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]

# 入れ子(ネスト)
pairs = [(x, y) for x in range(3) for y in range(3) if x != y]

# 辞書内包表記
{x: x*2 for x in range(5)}

# 集合内包表記
{x % 5 for x in range(20)}

# ジェネレータ式(()で囲む)
gen = (x ** 2 for x in range(10))   # 即時にリストを作らず、必要に応じて1要素ずつ生成
sum(x ** 2 for x in range(10))      # 関数引数のときは括弧省略可

list内包表記vsジェネレータ式

sum([x for x in range(10**7)])     # リスト全体をメモリに作る
sum(x for x in range(10**7))       # 1要素ずつ消費(メモリ効率良い)

ループの結果を一度しか使わないなら、ジェネレータ式のほうがメモリ効率が良いです。第11章で詳述します。

ネストの読み方

ネスト内包表記は、対応するforループと同じ順序で書きます。

# これは
flat = [x for sub in matrix for x in sub]

# 同じ意味
flat = []
for sub in matrix:
    for x in sub:
        flat.append(x)

8-6. collectionsモジュール

Python標準ライブラリcollections モジュールには便利なデータ構造があります。

クラス 用途
Counter 要素の出現回数カウント
defaultdict キー欠損時に自動でデフォルト値を生成するdict
OrderedDict 順序操作が豊富なdict(順序保持自体は通常dictに移行)
deque 両端O(1) のキュー
namedtuple 名前付きフィールドのtuple
ChainMap 複数のdictを重ねて1つに見せる

Counter

from collections import Counter

c = Counter("mississippi")
c                # Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
c.most_common(2) # [('i', 4), ('s', 4)]
c["s"]           # 4
c["x"]           # 0(存在しないキーでもエラーにならない)

defaultdict

from collections import defaultdict

d = defaultdict(list)
d["a"].append(1)   # キーが無くても自動でlistが作られる
d["a"].append(2)
d["b"].append(3)
# defaultdict(list, {'a': [1, 2], 'b': [3]})

# int用は数え上げに便利
counts = defaultdict(int)
for word in words:
    counts[word] += 1

deque

from collections import deque

d = deque([1, 2, 3])
d.append(4)         # 右端追加O(1)
d.appendleft(0)     # 左端追加O(1)
d.pop()             # 右端削除O(1)
d.popleft()         # 左端削除O(1)

# maxlenを指定すると固定長サイズのリングバッファになる
recent = deque(maxlen=10)

namedtuple

from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])
p = Point(1, 2)
p.x       # 1
p[0]      # 1(タプルとしてもアクセス可)
p == (1, 2)   # True

# 現代的にはdataclassやtyping.NamedTupleのほうが多用される

8-7. このセクションのまとめ

list:
  動的配列。インデックスアクセスO(1)
  先頭操作(pop(0), insert(0))はO(n)、両端ならdeque
  浅いコピー / 深いコピー(copy.deepcopy)の使い分け

tuple:
  イミュータブル。アンパッキング・複数値返却・dictキーで活躍
  1要素タプルは (x,) のカンマが必須

dict:
  Python 3.7+ で挿入順保持が言語仕様
  d.get(k, default) / d.setdefault(k, default) / d | d2

set / frozenset:
  ハッシュベースの集合、和・積・差・対称差
  frozensetはdictキーやset要素になれる

内包表記 / ジェネレータ式:
  [x for x in ...] はPythonic
  (x for x in ...) はメモリ効率重視
  ネストは「対応するfor」の順序で書く

collections:
  Counter / defaultdict / deque / namedtuple
  実コードで頻出、覚えると効率がいい

次のセクションでは、Pythonのクラスとオブジェクト指向に踏み込みます。


9. オブジェクト指向

Pythonのクラスは「辞書として実装されたオブジェクト」「メソッドは属性アクセス + 呼び出し」と理解すると挙動が腑に落ちます。このセクションではクラス定義、self、継承とMROdataclass抽象基底クラスProtocol、ダックタイピングを段階的に解説します。


9-1. クラス定義の基本

class Person:
    """人を表すクラス。"""

    species = "Homo sapiens"   # クラス変数(インスタンス間で共有)

    def __init__(self, name, age):
        self.name = name        # インスタンス変数(インスタンスごと)
        self.age = age

    def greet(self):
        return f"Hi, I'm {self.name}"

p = Person("Alice", 30)
p.name        # "Alice"
p.greet()     # "Hi, I'm Alice"
Person.species  # "Homo sapiens"
p.species     # "Homo sapiens"(インスタンス → クラスの順で探す)

クラス変数とインスタンス変数の違い

class Bag:
    items = []   # 危険: クラス変数として共有される

a = Bag()
b = Bag()
a.items.append(1)
print(b.items)   # [1] ← bにも影響!

# 正しい書き方
class Bag2:
    def __init__(self):
        self.items = []   # 各インスタンスごとに新しいリスト

これは「ミュータブルなクラス変数」の落とし穴で、6-3のミュータブルなデフォルト引数の落とし穴と同じ性質のバグです。


9-2. selfと init / new

selfとは何か

メソッドの第1引数 self「呼び出されたインスタンス自身」。Javaの this に相当します。Pythonでは明示的に第1引数として渡すのが特徴です。

class A:
    def hello(self):
        print(self)

a = A()
a.hello()      # 暗黙的にaがselfとして渡される
A.hello(a)     # 同じこと(明示的に渡す形)

init : 初期化

__init__インスタンスが作られた直後に呼ばれる初期化メソッド。コンストラクタ「相当」ですが、厳密には「初期化メソッド」であり、オブジェクトの生成自体は __new__ が行います。

new : 生成(高度)

class A:
    def __new__(cls, *args, **kwargs):
        print("__new__ が呼ばれた")
        instance = super().__new__(cls)
        return instance

    def __init__(self, x):
        print("__init__ が呼ばれた")
        self.x = x

a = A(10)
# __new__ が呼ばれた
# __init__ が呼ばれた

通常のクラスでは __new__ をオーバーライドする必要はありません。__new__ を使う代表例は シングルトンイミュータブルな型のサブクラス(int など) です。

class Singleton:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

a = Singleton()
b = Singleton()
a is b   # True

9-3. 属性アクセスとMRO

継承

class Animal:
    def speak(self):
        return "some sound"

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Puppy(Dog):
    pass

p = Puppy()
p.speak()   # "Woof!"

super() の役割

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)   # 親クラスの __init__ を呼ぶ
        self.breed = breed

MRO(Method Resolution Order)

複数継承では、属性をどの順番で探すかが重要になります。Pythonは C3線形化アルゴリズムを使い、属性探索順を決定します。

class A:
    def hello(self): return "A"
class B(A):
    def hello(self): return "B"
class C(A):
    def hello(self): return "C"
class D(B, C):
    pass

D().hello()        # "B"
D.__mro__
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

C3線形化は「継承順を保持したまま、菱形継承で同じクラスが2回出現しないようにする」アルゴリズムです。Java(単一継承)と違って、Pythonの多重継承を使うときはこのMROを意識する必要があります。

object型と新スタイルクラス

Python 3では、すべてのクラスが暗黙的に object を継承します(古いPython 2のold-style classは廃止)。

class A: pass
A.__mro__   # (<class 'A'>, <class 'object'>)

9-4. dataclass / NamedTuple / TypedDict

データ保持クラスを書きやすくする現代Pythonの三羽烏。

dataclass(推奨)

Python 3.7(PEP 557)導入。@dataclass デコレータで __init____repr____eq__ を自動生成。

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

p = Point(1, 2)
print(p)          # Point(x=1, y=2)(__repr__ 自動生成)
p == Point(1, 2)  # True(__eq__ 自動生成)

dataclassのオプション

@dataclass(frozen=True)   # イミュータブル化(hashableになる)
class Point:
    x: float
    y: float

@dataclass(slots=True)    # __slots__ を生成(メモリ削減・属性追加禁止)
class Point2:
    x: float
    y: float

@dataclass(order=True)    # < <= > >= も生成
class Score:
    value: int

# fieldでデフォルト値を細かく制御
from dataclasses import field
@dataclass
class Bag:
    items: list[int] = field(default_factory=list)
    # default=[] とは書けない(ミュータブル落とし穴を防ぐため)

NamedTuple

typing.NamedTupleイミュータブルかつタプル互換のクラス を定義。

from typing import NamedTuple

class Point(NamedTuple):
    x: float
    y: float

p = Point(1, 2)
p.x       # 1
p[0]      # 1(タプルとしてもアクセス可)
# p.x = 5  # AttributeError(イミュータブル)

TypedDict

辞書として使いつつ、キーと値の型を明示したいとき。

from typing import TypedDict

class User(TypedDict):
    name: str
    age: int

u: User = {"name": "Alice", "age": 30}
u["name"]   # str(型チェッカが理解する)

TypedDict はランタイムでは普通の dict です。型チェッカ(mypy/pyright)が静的に検証します。

使い分け

場面 選択
ミュータブルなデータ持ち + メソッドあり @dataclass
イミュータブルが欲しい・hashable @dataclass(frozen=True) または NamedTuple
タプル互換が必要(既存APIに合わせる) NamedTuple
既存のdict構造に型を付けたい(JSON由来など) TypedDict
ランタイム検証(API入力など) pydantic.BaseModel(外部ライブラリ)

9-5. 抽象基底クラスとProtocol

抽象基底クラス(ABC)

abc モジュールで「実装必須」のメソッドを定義できます。

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        ...

class Dog(Animal):
    def speak(self):
        return "Woof!"

# Animal()  # TypeError: 抽象クラスはインスタンス化できない

Protocol(構造的部分型)

Python 3.8(PEP 544)導入。「明示的な継承不要、メソッド構造が一致すればOK」 という型システム。Goのinterfaceに似ています。

from typing import Protocol

class Speakable(Protocol):
    def speak(self) -> str: ...

def announce(x: Speakable) -> None:
    print(x.speak())

class Dog:
    def speak(self): return "Woof!"

class Cat:
    def speak(self): return "Meow"

announce(Dog())   # OK(speakを持っているのでProtocol一致)
announce(Cat())   # OK

Protocol はPythonの ダックタイピング に静的型情報を後付けする仕組みで、第17章で詳述します。


9-6. プロパティとディスクリプタ

@property

メソッド呼び出しを属性アクセスのように見せられます。

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("半径は0以上")
        self._radius = value

    @property
    def area(self):
        return 3.14159 * self._radius ** 2

c = Circle(5)
c.radius      # 5(メソッドとしてではなく属性として呼ぶ)
c.radius = 10 # setterが呼ばれる
c.area        # 314.159(読み取り専用、setter無し)

@property は「最初は単純な属性として公開した変数を、後から検証ロジック付きにしたい」というときに有効。Javaのgetter/setterのようなboilerplateを避けつつ、必要になったら追加できる柔軟性が魅力です。

ディスクリプタ(高度)

@property の一般化として、ディスクリプタプロトコルがあります。__get____set____delete__ を持つクラスを属性として置くと、属性アクセスをハイジャックできます。詳細は第21章「メタプログラミング」で扱います。


9-7. ダックタイピングと構造的部分型

Pythonの文化を象徴する言葉:

「アヒルのように歩き、アヒルのように鳴くなら、それはアヒルだ」

つまり、「型が何かは関係ない、必要なメソッドや属性を持っていれば使える」という考え方です。これを ダックタイピング(duck typing) と呼びます。

def make_sound(thing):
    return thing.speak()    # speakメソッドさえあれば何でもいい

# 異なるクラスでも同じ関数で扱える
make_sound(Dog())
make_sound(Cat())
make_sound(Robot())   # robot.speak() が定義されていれば動く

これはJavaの 公称的部分型(nominal typing) と対極で、構造的部分型(structural typing) とも呼ばれます。Protocol はこの文化に静的型を後付けする試みです。

EAFP(再掲)の根拠

ダックタイピングの哲学があるので、Pythonでは「まず使ってみて、ダメなら例外」というEAFPスタイルが自然になります。

# 「これはイテラブルか?」と事前に判定するより
try:
    for x in obj:
        ...
except TypeError:
    # イテラブルじゃなかった
    ...

9-8. このセクションのまとめ

クラスの基本:
  class Foo: で定義、selfが「呼び出されたインスタンス」
  __init__ は初期化、__new__ は生成(通常上書き不要)
  クラス変数とインスタンス変数の混同に注意

継承:
  super().__init__(...) で親クラスの初期化を呼ぶ
  多重継承の探索順はMRO(C3線形化)
  cls.__mro__ で確認できる

データ保持クラス:
  dataclass: ミュータブル + 自動生成 (__init__, __repr__, __eq__)
  NamedTuple: イミュータブル + tuple互換
  TypedDict: dictに静的型を付ける(ランタイムは普通のdict)
  pydantic: ランタイム検証込み(外部ライブラリ)

抽象化:
  abc.ABC + @abstractmethod: 公称的(明示的継承)
  typing.Protocol: 構造的(duck typingへの型)

property:
  メソッドを属性のように見せる
  @property / @<name>.setterでgetter/setter

ダックタイピング:
  「型が何かではなく、必要な操作ができるか」
  EAFPの流儀がこれと相性がいい

次のセクションでは、Pythonのもう一つの核――ダンダーメソッド――で言語の様々な機能をフックする方法を学びます。


10. ダンダーメソッドとデータモデル

__init____str__ のように 二重アンダースコアで囲まれたメソッドダンダー(dunder)メソッド または 特殊メソッド と呼びます。Pythonの演算子・組み込み関数・構文の多くは、内部でこれらのダンダーを呼び出すことで動いています。「Pythonは契約(プロトコル)でできている」という設計思想を理解する核がここです。


10-1. データモデルとは

Pythonの公式ドキュメント Data Model は、Pythonの挙動を 「オブジェクトがどんなダンダーメソッドを実装するか」 で定義しています。

len(x)        → x.__len__()
x + y         → x.__add__(y)(またはy.__radd__(x))
x[i]          → x.__getitem__(i)
x in y        → y.__contains__(x)
str(x)        → x.__str__()
repr(x)       → x.__repr__()
iter(x)       → x.__iter__()
next(it)      → it.__next__()
with x:       → x.__enter__() / x.__exit__()
hash(x)       → x.__hash__()
x == y        → x.__eq__(y)
x < y         → x.__lt__(y)

つまり、自作クラスにこれらのダンダーを実装すれば、組み込み構文が使えるようになるということです。これはPythonの最強の柔軟性であり、numpypandas の魔法のような構文(df["col"] > 0 など)の正体でもあります。


10-2. init / new / del

class Resource:
    def __new__(cls, *args, **kwargs):
        print("生成")
        return super().__new__(cls)

    def __init__(self, name):
        print("初期化")
        self.name = name

    def __del__(self):
        print(f"解放: {self.name}")

r = Resource("file")
del r        # __del__ が呼ばれる(参照カウント0で)

注意: __del__ の呼び出しタイミングは循環参照があると不定になります(サイクルGC経由になる)。リソース解放は __del__ ではなく with(コンテキストマネージャ)を使うのがPythonicです(第13章)。


10-3. str / repr

二つの「文字列化」ダンダーがあり、用途が違います。

メソッド いつ呼ばれる 用途
__str__ str(x)print(x) 人間向けの読みやすい表現
__repr__ repr(x)、REPLでの表示 開発者向け、可能ならeval可能な形
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point({self.x!r}, {self.y!r})"   # 開発者向け

    def __str__(self):
        return f"({self.x}, {self.y})"             # ユーザ向け

p = Point(1, 2)
str(p)    # "(1, 2)"
repr(p)   # "Point(1, 2)"
print(p)  # (1, 2)
[p]       # [Point(1, 2)](コンテナの中では __repr__ が使われる)

慣例: __repr__eval(repr(x)) で同じオブジェクトが再現できる形が望ましい(できないときは <...> で囲む)。

def __repr__(self):
    return f"<Point object at {self.x}, {self.y}>"   # eval不可な形

10-4. 演算子オーバーロード

Pythonの演算子は対応するダンダーを呼び出します。

class Vec:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __add__(self, other):
        return Vec(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar):
        return Vec(self.x * scalar, self.y * scalar)

    def __rmul__(self, scalar):    # 5 * vecのとき呼ばれる
        return self.__mul__(scalar)

    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

    def __repr__(self):
        return f"Vec({self.x}, {self.y})"

a = Vec(1, 2)
b = Vec(3, 4)
a + b      # Vec(4, 6)
a * 3      # Vec(3, 6)
3 * a      # Vec(3, 6)(__rmul__ が呼ばれる)
a == Vec(1, 2)   # True

主要な演算子ダンダー

演算子 ダンダー
+, -, *, /, //, %, ** __add__, __sub__, __mul__, __truediv__, __floordiv__, __mod__, __pow__
右辺版 __radd__, __rsub__, …
累算(+= など) __iadd__, __isub__, …
単項 -, +, ~ __neg__, __pos__, __invert__
==, !=, <, <=, >, >= __eq__, __ne__, __lt__, …
&, |, ^ __and__, __or__, __xor__
<<, >> __lshift__, __rshift__

functools.total_ordering

__eq____lt__ だけ書けば、残りの比較演算子を自動生成してくれます。

from functools import total_ordering

@total_ordering
class Score:
    def __init__(self, value):
        self.value = value
    def __eq__(self, other):
        return self.value == other.value
    def __lt__(self, other):
        return self.value < other.value

# <=, >, >= が自動的に使える

10-5. iter / next / contains

イテレートの仕組みは __iter____next__ の2つで定義されます。

class CountDown:
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.start <= 0:
            raise StopIteration
        self.start -= 1
        return self.start + 1

for x in CountDown(5):
    print(x)   # 5, 4, 3, 2, 1

第11章で詳述します。

x in container__contains__ を呼びます(無ければ __iter__ で順に比較)。


10-6. enter / exit

with 文に対応するダンダー。第13章で詳述。

class Connection:
    def __enter__(self):
        print("接続開始")
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("接続切断")
        return False    # Trueを返すと例外を握り潰す

with Connection() as c:
    print("中で何か")

10-7. hash / eq

set の要素や dict のキーになるためには hashable でなければなりません。これは __hash____eq__ の両方を実装することで実現します。

基本ルール

  1. __eq__ を再定義したら、必ず __hash__ も再定義する(または __hash__ = None で明示的に無効化)
  2. a == b がTrueなら hash(a) == hash(b) でなければならない
  3. ミュータブルなオブジェクトはhashableにすべきでない(中身が変わるとハッシュが変わって辞書が壊れる)
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

    def __hash__(self):
        return hash((self.x, self.y))   # tupleのhashを使うのが手堅い

@dataclass(frozen=True) を使うと __hash__ も自動生成されます(推奨)。


10-8. このセクションのまとめ

データモデル:
  Pythonの構文・組み込み関数はダンダーを呼び出して動く
  自作クラスにダンダーを実装すれば組み込み構文が効くようになる

代表的ダンダー:
  __init__ / __new__ / __del__: 生成・初期化・解放
  __str__ / __repr__: 文字列表現(用途が違う)
  __add__ など: 演算子オーバーロード
  __iter__ / __next__: イテレートプロトコル
  __enter__ / __exit__: コンテキストマネージャ
  __eq__ / __hash__: 等価性とハッシュ(両方セットで定義する)

おすすめ:
  普段は @dataclass(frozen=True) を使うとほぼ自動で揃う
  __del__ よりはwith文(コンテキストマネージャ)でリソース管理

次のセクションでは、Pythonの遅延評価とデータパイプラインの主役――イテレータとジェネレータ――を扱います。


11. イテレータとジェネレータ

Pythonでは「ループ可能なオブジェクト」と「1要素ずつ取り出すオブジェクト」を区別します。両者の関係を理解すると、メモリ効率の良いデータ処理が書けるようになります。


11-1. イテラブルvsイテレータ

イテラブル(Iterable)

for x in obj で繰り返せるオブジェクト。__iter__ を持つ。

[1, 2, 3]       # listはイテラブル
"hello"         # strもイテラブル
{1, 2, 3}       # setもイテラブル
{"a": 1}        # dictもイテラブル
range(10)       # rangeもイテラブル
open("f.txt")   # ファイルオブジェクトもイテラブル

イテレータ(Iterator)

next() で1要素ずつ取り出せるオブジェクト。__next____iter__ を持つ。

lst = [1, 2, 3]
it = iter(lst)        # listからイテレータを取り出す
next(it)              # 1
next(it)              # 2
next(it)              # 3
next(it)              # StopIteration例外

重要な区別

イテラブル: 繰り返せる「もと」(何度でも先頭から繰り返せる)
イテレータ: 「現在の位置」を持つカーソル(一度消費すると戻れない)
lst = [1, 2, 3]

for x in lst: print(x)   # 1 2 3
for x in lst: print(x)   # 1 2 3(再度先頭から)

it = iter(lst)
for x in it: print(x)    # 1 2 3
for x in it: print(x)    # 何も出ない(消費済み)

自作のイテラブル + イテレータ

class CountDown:
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        return CountDownIterator(self.start)

class CountDownIterator:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        self.current -= 1
        return self.current + 1

for x in CountDown(5):
    print(x)    # 5 4 3 2 1

イテラブルイテレータを別クラスにすることで、同じ CountDown(5) を複数回forで回せます。


11-2. ジェネレータ関数(yield)

イテレータを手書きするのは面倒なので、Pythonは ジェネレータ関数という糖衣構文を提供します。yield を使った関数は、自動的にイテレータを作る関数になります。

def count_down(start):
    while start > 0:
        yield start
        start -= 1

for x in count_down(5):
    print(x)   # 5 4 3 2 1

ジェネレータの仕組み

yield を含む関数は、呼び出された時点では実行されません。代わりにジェネレータオブジェクトが返ります。next() を呼ぶたびに yield まで実行が進み、値を返して一時停止します。次の next() でその続きから再開します。

def gen():
    print("a")
    yield 1
    print("b")
    yield 2
    print("c")
    yield 3

g = gen()       # 何も出力されない(関数は実行されない)
next(g)         # "a" → 1が返る
next(g)         # "b" → 2が返る
next(g)         # "c" → 3が返る
next(g)         # StopIteration

ジェネレータのメモリ効率

ジェネレータは値を1要素ずつ生成するので、巨大なデータを扱ってもメモリを食いません。

# 1億要素のリスト → 巨大なメモリ消費
nums = [x * 2 for x in range(10**8)]

# 1億要素のジェネレータ → メモリ消費は数百バイト
nums = (x * 2 for x in range(10**8))
total = sum(nums)

11-3. ジェネレータ式

リスト内包表記の []() に変えるとジェネレータ式になります。

gen = (x ** 2 for x in range(10))

# 関数の引数として使うときは括弧省略可
sum(x ** 2 for x in range(10))
max(len(line) for line in open("file.txt"))

list内包vsジェネレータ式

[x for x in ...]   即時にリスト全体を作る → メモリ食う、後で何度も使える
(x for x in ...)   遅延評価、1度しか使えない → メモリ効率良い

1回しか使わない、巨大なデータ」ならジェネレータ式。「複数回使う、小さいデータ」ならリスト内包。これが基本判断基準です。


11-4. yield fromと委譲

別のイテラブルから順にyieldしたいとき、yield from を使えます。

def chain(*iterables):
    for it in iterables:
        for x in it:
            yield x

# yield fromで簡潔に
def chain2(*iterables):
    for it in iterables:
        yield from it

list(chain2([1, 2], [3, 4]))   # [1, 2, 3, 4]

yield from は単なる委譲だけでなく、send()throw() も透過的に伝えるため、ジェネレータ間の協調動作にも使えます(コルーチンの基礎)。


11-5. itertoolsの活用

itertools 標準ライブラリには、イテレータを操作する関数が大量にあります。

import itertools

# 無限ジェネレータ
itertools.count(10)       # 10, 11, 12, ...
itertools.cycle([1, 2])   # 1, 2, 1, 2, ...
itertools.repeat("x", 3)  # "x", "x", "x"

# 連結
itertools.chain([1,2], [3,4])   # 1, 2, 3, 4

# 切り出し
itertools.islice(count(), 5)    # 0, 1, 2, 3, 4

# 組合せ
itertools.combinations([1,2,3], 2)   # (1,2), (1,3), (2,3)
itertools.permutations([1,2,3], 2)   # (1,2), (1,3), (2,1), ...
itertools.product([1,2], [3,4])      # (1,3), (1,4), (2,3), (2,4)

# グループ化(事前にソートが必要)
import itertools
data = [("a", 1), ("a", 2), ("b", 3)]
for key, group in itertools.groupby(data, key=lambda x: x[0]):
    print(key, list(group))
# a [(a,1), (a,2)]
# b [(b,3)]

# 累積
itertools.accumulate([1, 2, 3, 4])         # 1, 3, 6, 10
itertools.accumulate([1, 2, 3, 4], max)    # 1, 2, 3, 4

itertools を使いこなせるようになると、明示的なループを書かずに宣言的なデータ処理ができるようになります。


11-6. ジェネレータベースのデータパイプライン

ジェネレータを「Unixのパイプ」のように繋ぐと、巨大データのストリーム処理が書けます。

def lines(path):
    with open(path) as f:
        for line in f:
            yield line.rstrip()

def grep(pattern, lines):
    for line in lines:
        if pattern in line:
            yield line

def take(n, lines):
    for i, line in enumerate(lines):
        if i >= n:
            return
        yield line

# パイプライン
result = take(10, grep("ERROR", lines("server.log")))
for line in result:
    print(line)

各ステップは1要素ずつ処理されるため、ファイルが何GBあってもメモリは数百バイトしか使わない。これがジェネレータの真骨頂です。


11-7. このセクションのまとめ

イテラブルvsイテレータ:
  イテラブル: 何度でも回せる(list, str, range, ...)
  イテレータ: カーソル付き、1度消費したら戻れない(iter() で生成)

ジェネレータ関数:
  yieldを含む関数は自動的にイテレータを返す
  next() ごとにyieldまで実行 → 一時停止 → 再開
  メモリ効率が劇的に良くなる

ジェネレータ式:
  [] の代わりに () でジェネレータ
  関数引数のときは括弧省略可: sum(x for x in ...)

yield from:
  別のイテラブルへの委譲、send / throwの透過

itertools:
  count, cycle, chain, islice, combinations, groupby, accumulate
  宣言的データ処理の道具箱

データパイプライン:
  ジェネレータをパイプのように繋ぐ
  巨大ファイルでも一定メモリで処理可能

次のセクションでは、Pythonのメタプログラミング的に最も使われる仕組み――デコレータ――を扱います。


12. デコレータ

デコレータはPython上級者の必須技術ですが、「関数を引数にとり、関数を返す関数 という1行の説明で正体は終わります。あとは応用です。このセクションでは段階的に積み上げて、@property@cache@dataclass、Webフレームワークのルーティングまでを理解できるようにします。


12-1. デコレータの正体(関数を返す関数)

関数は第一級オブジェクトを再確認

def hello():
    return "hello"

f = hello       # 関数を変数に代入できる
f()             # "hello"

関数を引数にとる関数

def call_twice(fn):
    fn()
    fn()

call_twice(hello)

関数を返す関数

def make_greeter(greeting):
    def greet(name):
        return f"{greeting}, {name}"
    return greet

hi = make_greeter("Hi")
hi("Alice")    # "Hi, Alice"

デコレータの最小例

「関数を引数に取り、機能を追加した別の関数を返す」のがデコレータの正体です。

def loud(fn):
    def wrapper(*args, **kwargs):
        result = fn(*args, **kwargs)
        return result.upper()
    return wrapper

# 普通に使う
def greet(name):
    return f"hello, {name}"

greet = loud(greet)
greet("Alice")    # "HELLO, ALICE"

# @ シンタックス: 上と完全に同じ意味
@loud
def greet(name):
    return f"hello, {name}"

@loud という書き方は シンタックスシュガー(糖衣構文) で、greet = loud(greet) と同義です。

デコレータを書ける場所

# 関数
@deco
def f(): ...

# クラス(クラスデコレータ)
@deco
class A: ...

# メソッド
class A:
    @deco
    def method(self): ...

12-2. functools.wrapsとメタデータ保持

何も対策しないとデコレートされた関数は 元の名前・docstringを失います

def loud(fn):
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs).upper()
    return wrapper

@loud
def greet(name):
    """挨拶を返す。"""
    return f"hello, {name}"

greet.__name__     # "wrapper"  ← greetではない
greet.__doc__      # None       ← docstringが消えた

これを防ぐのが functools.wraps です。

from functools import wraps

def loud(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs).upper()
    return wrapper

@loud
def greet(name):
    """挨拶を返す。"""
    return f"hello, {name}"

greet.__name__     # "greet"
greet.__doc__      # "挨拶を返す。"

自作デコレータには必ず @wraps(fn) を付ける。これはPythonのお作法です。


12-3. 引数を取るデコレータ

デコレータ自身に引数を渡したい場合、「デコレータを返す関数」 を作る必要があります。3階層になります。

from functools import wraps

def repeat(n):                    # 第1階層: パラメータを受け取る
    def decorator(fn):            # 第2階層: 関数を受け取る
        @wraps(fn)
        def wrapper(*args, **kwargs):   # 第3階層: 実際の呼び出し
            for _ in range(n):
                result = fn(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print(f"hello, {name}")

greet("Alice")
# hello, Alice
# hello, Alice
# hello, Alice

混乱しがちなので「3階層になる」と覚えるのがコツです。


12-4. クラスデコレータ

クラスにデコレータを付けると、クラスを引数にとり、クラスを返す関数 になります。

def add_repr(cls):
    def __repr__(self):
        attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{cls.__name__}({attrs})"
    cls.__repr__ = __repr__
    return cls

@add_repr
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2)
print(p)    # Point(x=1, y=2)

@dataclass は本質的にこの仕組みのもっと高度な版です。


12-5. 標準ライブラリのデコレータ

@property(再掲)

メソッドを属性のように見せる(第9章9-6参照)。

@classmethod / @staticmethod

class Pizza:
    def __init__(self, ingredients):
        self.ingredients = ingredients

    @classmethod
    def margherita(cls):
        # cls = Pizza。継承時はサブクラスが渡る
        return cls(["mozzarella", "tomato"])

    @staticmethod
    def is_valid_topping(name):
        return name in {"mozzarella", "pepperoni", "tomato"}

p = Pizza.margherita()
Pizza.is_valid_topping("anchovy")    # False
デコレータ 第1引数 用途
通常メソッド self(インスタンス) インスタンスを操作
@classmethod cls(クラス) 代替コンストラクタ、クラス固有の操作
@staticmethod なし 名前空間付きの普通の関数

@cache / @lru_cache

from functools import cache, lru_cache

@cache
def fib(n):
    if n < 2: return n
    return fib(n - 1) + fib(n - 2)

@lru_cache(maxsize=128)
def expensive(x):
    ...

@dataclass

第9章9-4参照。

@contextmanager

第13章 参照。


12-6. よくある落とし穴

1. wrapsを忘れる

トレースバックやIDE補完で困るので必ず付ける。

2. ステートを持たせるとスレッド非安全

def counter(fn):
    count = 0
    @wraps(fn)
    def wrapper(*args, **kwargs):
        nonlocal count
        count += 1
        return fn(*args, **kwargs)
    return wrapper

これはマルチスレッドで count の更新が壊れる可能性があります(GILのおかげで += は壊れにくいが厳密には保証されない)。必要なら threading.Lock を使う。

3. クロージャでミュータブルを共有してしまう

ループ内で lambda や関数を作ってデコレータに渡す場合、第7章のクロージャ落とし穴と同じ問題が起こります。

4. デコレータの順序

複数のデコレータを重ねると、下から順に適用される

@a
@b
@c
def f(): ...

# 等価
f = a(b(c(f)))

@a が一番外側。実行時は a → b → c → f → c内処理 → b内処理 → a内処理 の順に流れます。


12-7. このセクションのまとめ

デコレータの正体:
  @deco
  def f(): ...

  ↓ 等価

  def f(): ...
  f = deco(f)

実装パターン:
  引数なしデコレータ: 2階層(fnを受けてwrapperを返す)
  引数ありデコレータ: 3階層(パラメータ → fn → wrapper)

必須テクニック:
  functools.wrapsで __name__ / __doc__ を保持

標準ライブラリ:
  @property, @classmethod, @staticmethod
  @cache, @lru_cache(メモ化)
  @dataclass(クラスデコレータの典型)
  @contextmanager(次章)

落とし穴:
  wraps忘れ、スレッド非安全な状態、デコレータの順序(下から適用)

次のセクションでは、リソース管理とパターンの宝庫――コンテキストマネージャ――を扱います。


13. コンテキストマネージャ

with 文はPythonの 「リソース取得・解放を構造化する仕組み」。ファイル、ロック、トランザクション、計測など、「始まりと終わりがあるもの」を綺麗に書くための核となる構文です。


13-1. with文の意味

# ファイルを安全に閉じたい
with open("file.txt") as f:
    data = f.read()
# ここでf.close() が自動的に呼ばれる(例外が起きても)

実は次のコードと等価です。

f = open("file.txt")
try:
    data = f.read()
finally:
    f.close()

withtry/finally を構造化して、取得と解放のペアを明示的にする仕組みです。


13-2. enter / exit

with 文に対応するダンダーメソッドを実装すると、自作クラスをコンテキストマネージャにできます。

class Timer:
    def __enter__(self):
        import time
        self.start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        self.elapsed = time.perf_counter() - self.start
        print(f"経過時間: {self.elapsed:.3f}秒")
        return False    # FalseまたはNoneを返すと例外を再送出する

with Timer() as t:
    sum(i for i in range(10**6))
# 経過時間: 0.045秒

exit の3つの引数

例外が発生した場合、3つの引数に値が入ります。

引数 意味
exc_type 例外クラス(ValueError など)または None
exc_val 例外インスタンス または None
exc_tb トレースバック または None

__exit__ の戻り値:

  • False または None: 例外があれば再送出
  • True: 例外を握りつぶす(例外が発生していた場合)
class Suppress:
    def __init__(self, *exc_types):
        self.exc_types = exc_types

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None and issubclass(exc_type, self.exc_types):
            return True  # 握りつぶす
        return False

with Suppress(ZeroDivisionError):
    1 / 0
print("継続")    # 例外は握り潰されたので継続される

13-3. contextlib.contextmanager

クラスを書くより、@contextmanager デコレータでジェネレータ関数からコンテキストマネージャを作るほうが手軽です。

from contextlib import contextmanager

@contextmanager
def timer():
    import time
    start = time.perf_counter()
    try:
        yield                   # ここまでが __enter__
    finally:
        elapsed = time.perf_counter() - start
        print(f"経過: {elapsed:.3f}秒")
        # ここからが __exit__

with timer():
    sum(i for i in range(10**6))

ジェネレータの yield の前 = __enter__、後 = __exit__ と思えば理解しやすい。yield で値を返すと as で受け取れます。

@contextmanager
def open_db(path):
    conn = connect(path)
    try:
        yield conn   # asで受け取れる
    finally:
        conn.close()

with open_db("data.db") as db:
    db.execute("SELECT * FROM users")

13-4. ExitStack(複数リソースの動的管理)

複数のリソースを同時に開きたい場合、ネストするか、ExitStack を使います。

# ネスト(数が固定なら)
with open("a") as f1, open("b") as f2:
    ...

# 動的に複数開く
from contextlib import ExitStack

paths = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
    files = [stack.enter_context(open(p)) for p in paths]
    # filesは同時に開かれており、with終了時に全部閉じる

ExitStack「動的なwith」 で、何個リソースが必要か実行時にしか分からないときに便利です。


13-5. async with(非同期コンテキストマネージャ)

非同期コードでは async with を使います。__aenter__ / __aexit__ を実装。

import aiohttp

async def fetch(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            return await resp.text()

@asynccontextmanager(contextlib)でジェネレータベースのasync版も書けます。第15章で詳述。


13-6. 主要なユースケース

用途
ファイル with open(...) as f:
ロック with lock:
DBトランザクション with conn.transaction():
一時ディレクトリ with tempfile.TemporaryDirectory() as d:
標準出力リダイレクト with redirect_stdout(io.StringIO()) as buf:
デバッガフック with breakpoint():(実装次第)
計測(時間・メモリ) 自作
デコレータの代替 @contextmanager
例外の抑制 with contextlib.suppress(ValueError):
# contextlibの便利関数
from contextlib import suppress, redirect_stdout
import io

with suppress(FileNotFoundError):
    os.remove("maybe-not-exist.txt")

buf = io.StringIO()
with redirect_stdout(buf):
    print("captured")
text = buf.getvalue()    # "captured\n"

13-7. このセクションのまとめ

withの意味:
  try/finallyの構造化、リソース取得と解放のペアを明示

実装方法:
  クラスベース: __enter__ / __exit__
  ジェネレータベース: @contextmanager + yield

__exit__ の戻り値:
  None/False: 例外を再送出(普通はこれ)
  True: 例外を握り潰す

複数リソース:
  ネスト: with open(a) as f1, open(b) as f2:
  動的: ExitStack

非同期:
  async with(__aenter__/__aexit__、@asynccontextmanager)

便利なcontextlib:
  suppress, redirect_stdout, redirect_stderr, ExitStack

次のセクションでは、Pythonの例外処理を体系的に解説します。


14. 例外処理

例外はPythonの制御フローの一級市民です。try/except/else/finally、例外階層、raise from、ExceptionGroupまでを整理します。


14-1. try / except / else / finally

try:
    risky_operation()
except ValueError as e:
    print(f"ValueError: {e}")
except (KeyError, IndexError):
    print("KeyかIndexエラー")
except Exception as e:
    print(f"その他: {e}")
else:
    print("例外が起きなかった場合だけ実行される")
finally:
    print("例外の有無にかかわらず必ず実行される")

各節の役割

実行されるタイミング
try まずこの中を実行
except 例外が起きたら、最初にマッチした節
else 例外が起きなかったときだけ
finally 例外の有無にかかわらず最後に必ず

elseの使い所

tryの中をできるだけ短くしたい」とき、例外が起きないコードを else に追い出すと意図が明確になります。

try:
    f = open("file.txt")
except FileNotFoundError:
    print("ファイル無し")
else:
    # 成功時のみ実行(例外が無いと分かっている場所)
    data = f.read()
    f.close()

14-2. 例外の階層

Pythonの組み込み例外は階層をなしています(抜粋)。

BaseException
 ├── SystemExit          sys.exit() で発生
 ├── KeyboardInterrupt   Ctrl+C
 ├── GeneratorExitジェネレータ閉鎖
 └── Exception           ← ユーザコードが捕まえるべき祖先
      ├── ArithmeticError
      │    ├── ZeroDivisionError
      │    └── OverflowError
      ├── LookupError
      │    ├── KeyError
      │    └── IndexError
      ├── ValueError
      ├── TypeError
      ├── AttributeError
      ├── NameError
      ├── OSError
      │    ├── FileNotFoundError
      │    ├── PermissionError
      │    └── ...
      ├── RuntimeError
      ├── StopIteration
      └── ...

重要な使い分け

  • except Exception: は許容できる(一般的な例外を捕まえる)
  • except BaseException: または except:避けるべき(KeyboardInterrupt まで握り潰す)
# Bad
try:
    long_running_loop()
except:
    pass     # Ctrl+Cも握り潰されてプロセスが止まらない!

# Good
try:
    long_running_loop()
except Exception:
    log_error()

自分が捕まえるべき例外を選ぶ

できるだけ具体的な例外を捕まえる」が原則。広すぎる except は、想定外のバグまで握り潰してデバッグを困難にします。

# Bad(KeyError以外まで握り潰す)
try:
    value = config[key]
except Exception:
    value = "default"

# Good
try:
    value = config[key]
except KeyError:
    value = "default"

14-3. raise fromと例外チェイン

例外を別の例外に変換するとき、元の例外情報を残すことが重要です。

def parse(s):
    try:
        return int(s)
    except ValueError as e:
        raise ParseError("不正な数値") from e

try:
    parse("abc")
except ParseError as e:
    print(e)
    print(e.__cause__)   # 元のValueError

raise NewError from original__cause__ に元の例外をぶら下げられます。トレースバックには両方が「The above exception was the direct cause of the following exception」として表示されます。

cause vs context

try:
    int("abc")
except ValueError:
    raise RuntimeError("変換失敗")    # __context__ に元のValueErrorが自動で入る
  • __context__: tryの中で別の例外が発生して新しい例外が出たとき自動でセット
  • __cause__: raise X from Y で明示的にセット

トレースバックでは __cause__ のほうが「直接の原因」として強調表示されます。

元の例外を抑制する

raise NewError from None    # __cause__ も __context__ も表示しない

14-4. ExceptionGroup(Python 3.11+)

Python 3.11(PEP 654)で ExceptionGroupexcept* 構文が導入されました。「複数の例外を一度にまとめて扱う」必要が出てきたためです(特に並行処理)。

# 複数の例外をまとめる
errors = [ValueError("a"), TypeError("b")]
raise ExceptionGroup("複数のエラー", errors)

except* で型ごとに捌く

try:
    raise ExceptionGroup("multi", [
        ValueError("v"),
        TypeError("t"),
        ValueError("v2"),
    ])
except* ValueError as eg:
    print(f"ValueErrorグループ: {eg.exceptions}")
except* TypeError as eg:
    print(f"TypeErrorグループ: {eg.exceptions}")

except*マッチした例外だけを取り出して別々のブロックで処理する新しい構文。同じtry内で2つ以上のexcept* が走ることもあります。

ExceptionGroupの主な用途は asyncio.TaskGroup(次章)。並行に走る複数のタスクで複数の例外が同時に起きたとき、それらをまとめて報告するのに使います。


14-5. EAFPの流儀

第5章でも触れた EAFP(Easier to Ask Forgiveness than Permission) スタイル。Pythonでは「まず実行、ダメなら例外」が主流です。

# LBYL
if hasattr(obj, "method"):
    obj.method()

# EAFP(Pythonic)
try:
    obj.method()
except AttributeError:
    pass

# dictの値取得
# LBYL
if key in d:
    return d[key]

# EAFP
try:
    return d[key]
except KeyError:
    return default

# 実用上はdict.get(key, default) が最も簡潔
return d.get(key, default)

EAFPには 「事前チェック後の状態変化(race condition)を回避できる」 という現実的なメリットもあります。


14-6. カスタム例外の設計

命名規則

慣例として、例外クラスは 〜Error という名前をつけます(Exception とはせず、〜Error)。

class ParseError(Exception):
    pass

class NetworkError(Exception):
    pass

class TimeoutError(NetworkError):
    pass

モジュール内の階層を作る

ライブラリでは、自前の基底例外を1つ作り、すべての自作例外をそこから派生させると、利用側が except mylib.Error で全部捕まえられて便利です。

# mylib/errors.py
class MyLibError(Exception):
    """このライブラリのすべての例外の基底。"""

class ConfigError(MyLibError):
    """設定ファイル関連エラー。"""

class NetworkError(MyLibError):
    """ネットワーク関連エラー。"""

例外には情報を持たせる

class HTTPError(Exception):
    def __init__(self, status, message):
        super().__init__(message)
        self.status = status
        self.message = message

try:
    raise HTTPError(404, "Not Found")
except HTTPError as e:
    print(e.status, e.message)

14-7. このセクションのまとめ

基本構文:
  try / except / else / finallyの役割を理解する
  exceptは具体的に書く(ExceptionよりもKeyError)
  except: やexcept BaseException: は避ける(Ctrl+C握り潰し)

例外チェイン:
  raise NewError from originalで __cause__ に元の例外を残す
  __context__ は自動で入る、抑制したいならfrom None

ExceptionGroup(3.11+):
  複数例外をひとまとめに扱う
  except* で型ごとに捌く
  asyncio.TaskGroupで活躍

EAFPの流儀:
  まずやってみて、ダメならexceptで捌く
  race conditionの回避にも

カスタム例外:
  〜Errorと命名
  ライブラリ内に共通の基底例外を1つ作る
  例外オブジェクトに情報を持たせる

次のセクションは長丁場――Python並行処理の世界。threadingmultiprocessingasyncio を一望します。


15. 並行・並列処理(threading / multiprocessing / asyncio)

Pythonの並行処理は 「3つの選択肢」 が存在します。それぞれ得意分野が違うので、最初に「どれをいつ使うか」を整理してから個別に深掘りします。

threading        IOバウンド向けGIL制約あり、CPUバウンドで並列化されない
multiprocessing  CPUバウンド向け  プロセスを分けてGILを回避、起動コスト高
asyncio          IO並行向け(推奨)コルーチン、シングルスレッドで超大量のI/O同時処理

15-1. 並行vs並列、IOバウンドvs CPUバウンド

並行(concurrency) vs並列(parallelism)

並行(concurrency): 「複数のタスクを進行中の状態にする」
                    タスクを切り替えながら進める。1コアでも実現可能
並列(parallelism): 「複数のタスクを同時に実行する」
                    マルチコアで物理的に同時に実行

並行は「論理」、並列は「物理」と覚えると分かりやすい。並行 ⊃ 並列 の関係です。

IOバウンドvs CPUバウンド

IOバウンド: ボトルネックがI/O待ち(ネットワーク、ディスク、DB)
            → スレッドを増やせば「待ちながら別の仕事ができる」
CPUバウンド: ボトルネックがCPU計算(数値計算、画像処理、暗号)
            → CPUが物理的に複数欲しい

早見表

性質 推奨 理由
IOバウンドが少数(〜10タスク) threading または asyncio スレッドでも十分
IOバウンドが大量(数百〜数万) asyncio スレッドはメモリ食う
CPUバウンド multiprocessing GIL回避、本物の並列
両方混在 concurrent.futures で組み合わせ 高レベルAPI
最先端: GILなし環境(3.13+ 実験) threading Free-Threadedビルドで真価

15-2. threadingとGILの制約

基本

import threading
import time

def worker(name):
    print(f"{name} 開始")
    time.sleep(2)
    print(f"{name} 終了")

threads = []
for name in ["A", "B", "C"]:
    t = threading.Thread(target=worker, args=(name,))
    t.start()
    threads.append(t)

for t in threads:
    t.join()

time.sleep はI/O待ちと同じくGILを解放するので、3スレッドが並行に動き、合計でも約2秒で終わります。これがthreadingの真価。

CPUバウンドでは並列化されない

def cpu_bound():
    total = 0
    for i in range(10**7):
        total += i
    return total

# シングルスレッド
import time
start = time.perf_counter()
for _ in range(4):
    cpu_bound()
print(time.perf_counter() - start)   # 例: 1.6秒

# マルチスレッド(同じ仕事量を4並列)
start = time.perf_counter()
threads = [threading.Thread(target=cpu_bound) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(time.perf_counter() - start)   # 例: 1.5秒(ほぼ変わらない!)

GILのため、4スレッドあっても 同時にバイトコードを実行できるのは1つだけ。CPU 8コアでも意味がない。

Lock / RLock / Event / Condition

import threading

# Lock: 1人だけクリティカルセクションに入れる
lock = threading.Lock()
counter = 0

def increment():
    global counter
    with lock:
        # クリティカルセクション
        counter += 1

# RLock: 同じスレッドが複数回acquireできる
rlock = threading.RLock()

# Event: スレッド間のシグナル
ev = threading.Event()
ev.set()
ev.is_set()
ev.wait()        # ev.set() されるまでブロック

# Condition: より複雑な同期
cond = threading.Condition()
with cond:
    cond.wait()       # 通知を待つ
    cond.notify()     # 通知を出す

スレッド安全なdict・キュー

queue.Queue複数スレッドから安全にpush/popできる FIFOキュー。プロデューサ・コンシューマパターンで活躍。

from queue import Queue
q = Queue()

def producer():
    for i in range(10):
        q.put(i)

def consumer():
    while True:
        item = q.get()
        if item is None: break
        process(item)

15-3. multiprocessingとpickle制約

GILの壁を越えるには、プロセスを分けます。プロセスはそれぞれ独立したPythonインタプリタを持ち、独自のGILを持つので並列に動きます。

from multiprocessing import Process

def worker(name):
    cpu_bound()
    print(f"{name} 完了")

if __name__ == "__main__":          # ← 重要: Windowsで必須
    procs = [Process(target=worker, args=(name,)) for name in "ABCD"]
    for p in procs: p.start()
    for p in procs: p.join()

if __name__ == "__main__": はWindowsで forkではなくspawnによってサブプロセスが立ち上がるため、モジュールの最上位コードが再実行されてしまうのを防ぐためのお作法です。

Poolで並列マップ

from multiprocessing import Pool

def square(x):
    return x * x

if __name__ == "__main__":
    with Pool(processes=4) as pool:
        results = pool.map(square, range(10))
    print(results)   # [0, 1, 4, 9, 16, ...]

pickle制約

multiprocessing はプロセス間でオブジェクトを pickle でシリアライズして渡します。pickleできないオブジェクトは渡せません。

# pickleできない例
- ローカル関数(lambda含む)
- 開いたファイルオブジェクト
- DBコネクション
- ロック

これが嵌りどころで、「クラスのメソッドだから渡せると思ったらlambdaが中にあって動かない」 というのがよくあります。

マルチコアの真の並列化

import multiprocessing as mp
print(mp.cpu_count())      # 物理CPUコア数

with Pool(mp.cpu_count()) as pool:
    results = pool.map(cpu_bound_func, items)

共有メモリ

multiprocessing.shared_memory(Python 3.8+)でプロセス間でメモリを共有することもできます。NumPy配列をpickleなしで渡せるので大規模計算に有効。


15-4. concurrent.futures(高レベルAPI)

threadingmultiprocessing共通インターフェース。最初はこれを覚えれば良い。

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

# IOバウンド: スレッドプール
with ThreadPoolExecutor(max_workers=10) as executor:
    results = list(executor.map(fetch_url, urls))

# CPUバウンド: プロセスプール
with ProcessPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(cpu_bound, items))

# 個別タスク(Future返却)
with ThreadPoolExecutor() as executor:
    futures = [executor.submit(task, x) for x in items]
    for fut in as_completed(futures):
        try:
            print(fut.result())
        except Exception as e:
            print(f"失敗: {e}")

15-5. asyncioの基本

asyncio はPython 3.4で標準化された コルーチンベースの非同期I/Oフレームワーク

重要なメンタルモデル

従来のスレッド: OSが「いつスレッドを切り替えるか」決める(プリエンプティブ)
コルーチン:   プログラム自身が「ここで切り替えていい」と宣言(協調的)

async def で定義したコルーチンは、await のところで処理を中断して他のタスクに譲ります。スレッドではなく、シングルスレッド上でイベントループが回す仕組みです。

Hello asyncio

import asyncio

async def hello(name, delay):
    await asyncio.sleep(delay)
    print(f"hello, {name}")

async def main():
    await asyncio.gather(
        hello("A", 1),
        hello("B", 2),
        hello("C", 3),
    )

asyncio.run(main())

asyncio.sleep(1) は実時間1秒待つが、その間 他のタスクが進行できる。すべてが並行に進み、合計時間は最大の3秒。

async / awaitの意味

  • async def f(): → コルーチン関数。呼んでもコルーチンオブジェクトが返るだけで、実行されない
  • await coro → コルーチンを実際に実行し、完了を待つ。完了するまで現在のタスクは中断され、他のタスクが進む
  • コルーチンは イベントループに登録しないと動かない(asyncio.runasyncio.create_task で登録)
async def f():
    return 1

result = f()              # コルーチンオブジェクト(実行されない)
# print(result)           # <coroutine object f at ...>
asyncio.run(f())          # 実行されて1を返す

15-6. async/awaitの内部動作

async def で定義したコルーチンは、内部的には ジェネレータの強化版 です。await に当たるたびに「この関数を中断、状態を保存、イベントループに制御を返す」を行っています。

[イベントループ]
  ↓ runコルーチンをstart
  コルーチン1実行 → await sleep → サスペンド
  ↓ コントロール戻る
  コルーチン2実行 → await fetch → サスペンド
  ↓ コントロール戻る
  ...タイムアウトしたコルーチンを再開...
  コルーチン1再開 → 終了

これが「シングルスレッドでも数千のI/Oを並行できる」根拠です。スレッドと違ってコンテキスト切替にOSを介さないので、メモリもCPUもはるかに少なく済みます。

イベントループ

asyncio.run(main()) の正体は、

loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(main())
loop.close()

を簡略化したものです。基本的に asyncio.run を使うのが現代的。


15-7. asyncio.gatherとTaskGroup

asyncio.gather

複数のコルーチンを並行に実行し、すべての結果を収集。

results = await asyncio.gather(
    fetch("a"),
    fetch("b"),
    fetch("c"),
)

# 例外があっても他は走り続ける(return_exceptions=True)
results = await asyncio.gather(
    fetch("a"),
    fetch("b"),
    return_exceptions=True,    # 例外もリスト要素として返る
)

asyncio.TaskGroup(Python 3.11+)

gather の問題点は 一つでも例外が起きると、他のタスクのキャンセルが面倒だったこと。TaskGroup はこれを構造化します。

async def main():
    async with asyncio.TaskGroup() as tg:
        tg.create_task(fetch("a"))
        tg.create_task(fetch("b"))
        tg.create_task(fetch("c"))
    # ここに来た時点で全タスク完了

# 例外が起きた場合、他のタスクは自動キャンセルされ、ExceptionGroupでまとめて報告

これは 構造化並行性(Structured Concurrency) という考え方で、Trioが先駆けたパターンが標準ライブラリに入りました。並行処理の例外を扱いやすくする現代的な書き方です。

create_taskとrun_in_executor

# タスクとして登録(バックグラウンドで動かす)
task = asyncio.create_task(fetch("a"))
result = await task

# 同期コードを別スレッドで動かす(ブロッキング処理を逃がす)
result = await asyncio.to_thread(blocking_func, arg)

# プロセスプールで動かす(CPUバウンド)
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(process_pool, cpu_bound, x)

主要なasyncライブラリ

ライブラリ 用途
aiohttp HTTPクライアント・サーバ
httpx requestsのasync版
aiofiles 非同期ファイルI/O
asyncpg PostgreSQL
redis.asyncio Redis
FastAPI Webフレームワーク
anyio asyncioとTrioの互換層
Trio より構造化されたasyncランタイム

15-8. 同期と非同期の混在

同期コードをasyncで動かす

# blocking_funcは同期コード(DB呼び出しやrequestsなど)
result = await asyncio.to_thread(blocking_func, arg)
# 内部で別スレッドに逃がしてくれる

asyncを同期コードから呼ぶ

result = asyncio.run(my_async_function())   # 一発限り

# 既にイベントループがある場合
loop = asyncio.get_event_loop()
result = loop.run_until_complete(my_async_function())

よくある罠: ブロッキングをそのまま呼ぶ

# Bad
async def fetch_user(id):
    return requests.get(url)    # 同期HTTP!イベントループが完全停止する

# Good
async def fetch_user(id):
    async with httpx.AsyncClient() as client:
        return await client.get(url)

asyncio は協調的なので、1つのasync関数がブロッキング呼び出しをすると、他のすべてのタスクが止まります。これは asyncio 利用時の最大の罠。


15-9. このセクションのまとめ

3つの選択肢:
  threading      IO少量、スレッド単位で書きたい
  multiprocessing CPUバウンド、本物の並列が必要
  asyncio        IO大量・大規模、シングルスレッドで超効率

GILの影響:
  CPUバウンドなthreadingは並列化されない(GILあり)
  Python 3.13 Free-Threadedで将来変わる可能性

concurrent.futures:
  ThreadPoolExecutor / ProcessPoolExecutor共通APIで便利
  最初に学ぶ高レベルAPIとして良い

asyncio:
  async def / awaitでコルーチン
  asyncio.run(main()) でイベントループ起動
  asyncio.gatherで並行収集
  asyncio.TaskGroup(3.11+)で構造化並行性
  to_thread / run_in_executorで同期コードを逃がす

落とし穴:
  asyncioで同期I/Oを直接呼ばない
  multiprocessingはpickleできないものを渡せない
  Windowsではif __name__ == "__main__": が必須

次のセクションでは、Pythonのコードを部品として組み立てる仕組み――モジュール・パッケージ・import――を扱います。


16. モジュール・パッケージ・importシステム

Pythonのコードを複数ファイルに分けるとき、必ず登場するのが モジュールパッケージ、そして import システム。最初は「ふんわり動く」状態でも、規模が大きくなると「なぜここで ImportError ?」「相対importって何?」と必ず躓きます。整理します。


16-1. モジュールとパッケージ

モジュール:    1つの .pyファイル
パッケージ:   __init__.pyを持つディレクトリ(複数のモジュールをまとめたもの)
名前空間パッケージ: __init__.pyが無くても動く特殊なパッケージ(PEP 420)

my_pkg/
├── __init__.py
├── core.py
├── utils.py
└── sub/
    ├── __init__.py
    └── helpers.py
import my_pkg.core
from my_pkg.utils import some_function
from my_pkg.sub.helpers import helper_fn

16-2. importの探索順序

import foo と書いたとき、Pythonは次の順で探します。

1. sys.modules (既にimport済みのキャッシュ)
2. 組み込みモジュール(sys, math, ...)
3. sys.pathに列挙されたディレクトリを順に
   - スクリプトのあるディレクトリ
   - PYTHONPATH環境変数
   - インストール済みのパッケージディレクトリ

sys.path は実行時に確認できます。

import sys
print(sys.path)

16-3. 絶対importと相対import

# 絶対import(Python 3のデフォルト・推奨)
from my_pkg.utils import f
import my_pkg.sub.helpers

# 相対import(パッケージ内部から)
from . import sibling          # 同じパッケージ
from .. import parent_pkg      # 親パッケージ
from .utils import f
from ..sub import helpers

相対importの制約

  • パッケージの中でしか使えない(スクリプトを直接 python file.py で起動すると壊れる)
  • スクリプトとして使うファイルでは絶対importを使う

16-4. init.pyの役割

# my_pkg/__init__.py
from .core import main_function
from .utils import some_function

__version__ = "1.0.0"
__all__ = ["main_function", "some_function"]
  • パッケージのインポート時に自動的に実行される初期化コード
  • from my_pkg import * のときに何がimportされるかを __all__ で制御
  • 公開APIの集約場所として使う(深いパスを覚えなくて済む)

16-5. 名前空間パッケージ

Python 3.3+ では __init__.py がなくてもディレクトリをパッケージとして扱える(PEP 420)。

foo/
└── bar.py

__init__.py がなくても import foo.bar できる。これは 複数の場所に分散したパッケージを統合する用途で使われます(Djangoのアプリケーションなど)。

ただし通常のプロジェクトでは __init__.py を置くのが推奨。意図せず名前空間パッケージになっていると、サブパッケージが認識されないなどの混乱が起きます。


16-6. importの落とし穴

スクリプトモードvsモジュールモード

python my_pkg/script.py    # スクリプトモード: 相対importが壊れる
python -m my_pkg.script    # モジュールモード: 相対importが動く

循環import

AがBをimportし、BがAをimportするとき、状態次第で ImportError

# Bad
# a.py
from b import B

# b.py
from a import A   # 循環!

対策:

  • importを関数内に遅延させる
  • 共通部分を別モジュールに分ける
  • from a import A ではなく import a にして遅延参照

pycache

importするとバイトコードが __pycache__/<name>.cpython-3xx.pyc にキャッシュされます。.gitignore に追加するのが慣習。


16-7. このセクションのまとめ

モジュール = .pyファイル
パッケージ = __init__.pyを持つディレクトリ
名前空間パッケージ = __init__.py無し(PEP 420)

絶対import vs相対import:
  絶対が推奨、相対はパッケージ内部のみ
  スクリプトとして起動するファイルでは絶対import

__init__.py:
  パッケージ初期化コード
  公開APIの集約場所
  __all__ でfrom pkg import * を制御

落とし穴:
  python -m pkg.moduleで起動すると相対importが動く
  循環importは構造を見直すか遅延参照に
  __pycache__ はgitignore

次のセクションでは、Python型ヒントの世界に深く入っていきます。


17. 型ヒントの本気

第3章で型ヒントの基礎を見ました。ここでは「実際に大規模コードベースで活かす」レベルまで踏み込みます。Generics、ProtocolTypeVar、TypeAlias、Self、Final、ClassVar、ParamSpec……Pythonの型システムは想像以上に豊富です。


17-1. PEP 484の歴史と目的

第1章で触れたように、PEP 484(2014年)はGuido自身が中心となって導入した型ヒントの仕様。動機:

  1. 大規模コードベースの保守困難
  2. IDE補完・リファクタリング支援
  3. TypeScriptの成功をPythonにも

導入時のキーフレーズが Gradual Typing(段階的型付け)。一度に全コードを型付けする必要はなく、必要なところから少しずつ進められる柔軟性が特徴です。


17-2. typingモジュールの主役たち

from typing import (
    Any, Optional, Union, Callable, Iterable, Iterator,
    TypeVar, Generic, Protocol, TypeAlias, Final, ClassVar,
    Literal, NewType, NoReturn, cast, TYPE_CHECKING,
)

Any

「型チェックしない」の宣言。最後の手段。

def parse(x: Any) -> Any:
    return x

Any を多用すると型ヒントの意味がなくなるので、避けたいときは object(読み取りのみ)を使うことも検討。

Optional / Union

# Optional[T] = Union[T, None] = T | None
def find(name: str) -> User | None:
    ...

Python 3.10+ では T | None推奨

Callable

from collections.abc import Callable

def repeat(fn: Callable[[int, str], bool], n: int) -> None:
    ...

Callable[[引数型のリスト], 返り値型] の構文。

Literal

特定の値だけを許す型。

from typing import Literal

def open_file(mode: Literal["r", "w", "a"]) -> None:
    ...

open_file("r")     # OK
open_file("rw")    # 型エラー("rw" はLiteralに含まれない)

FinalとClassVar

from typing import Final, ClassVar

MAX_SIZE: Final = 100         # 再代入禁止
class Config:
    debug: ClassVar[bool] = False   # クラス変数(インスタンス変数ではない)

NewType

「内容は同じだが意味が違う」型を作る。

from typing import NewType

UserId = NewType("UserId", int)
def fetch_user(id: UserId) -> User: ...

fetch_user(123)            # 型エラー(intをUserIdに渡せない)
fetch_user(UserId(123))    # OK

NoReturn

「決して返らない関数」の宣言。

from typing import NoReturn

def fail(msg: str) -> NoReturn:
    raise RuntimeError(msg)

17-3. Generics(PEP 484/PEP 695)

ジェネリクスは 「型をパラメータ化する」 仕組み。list[int]dict[str, int] がそれです。自作クラスでも使えます。

TypeVar(古い書き方)

from typing import TypeVar, Generic

T = TypeVar("T")

class Stack(Generic[T]):
    def __init__(self):
        self.items: list[T] = []
    def push(self, item: T) -> None:
        self.items.append(item)
    def pop(self) -> T:
        return self.items.pop()

s: Stack[int] = Stack()
s.push(1)
x: int = s.pop()

PEP 695(Python 3.12+)

3.12から、もっとシンプルな構文が入りました。

class Stack[T]:
    def __init__(self) -> None:
        self.items: list[T] = []
    def push(self, item: T) -> None:
        self.items.append(item)
    def pop(self) -> T:
        return self.items.pop()

# 関数のジェネリクス
def first[T](items: list[T]) -> T:
    return items[0]

# 型エイリアス
type Vector = list[float]
type Pair[T] = tuple[T, T]

これはTypeScriptの <T> 構文に近く、TypeVar を別途定義する手間がなくなりました。

制約付きTypeVar

from typing import TypeVar

# 数値型のみ受け付ける
NumT = TypeVar("NumT", int, float)

def add(a: NumT, b: NumT) -> NumT:
    return a + b

# bound(上界)を指定
T = TypeVar("T", bound="Comparable")

共変・反変・不変

T_co = TypeVar("T_co", covariant=True)        # 共変
T_contra = TypeVar("T_contra", contravariant=True)  # 反変

これは高度な話題で、最初は気にしなくてOK。深入りするときは公式ドキュメントへ。


17-4. Protocol(構造的部分型)

第9章で触れたProtocolを改めて整理。

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...

def render(obj: Drawable) -> None:
    obj.draw()

class Circle:
    def draw(self) -> None:
        print("○")

class Square:
    def draw(self) -> None:
        print("□")

render(Circle())   # OK(明示的にDrawableを継承していなくても、構造が一致すればOK)
render(Square())   # OK

runtime_checkable

通常、Protocolは静的型チェックのみ。isinstance で確認したいときは @runtime_checkable を付ける。

from typing import Protocol, runtime_checkable

@runtime_checkable
class Drawable(Protocol):
    def draw(self) -> None: ...

isinstance(Circle(), Drawable)  # True

ただしランタイムチェックは「メソッドの存在しか見ない」(シグネチャは無視)ので、過信は禁物。


17-5. TypedDict / NamedTuple / dataclassの使い分け

第9章9-4でも触れたが、より細かく整理。

種類 ミュータブル ランタイム検証 dict互換 tuple互換 主用途
dict[str, int] × × 単純な対応関係
TypedDict × × JSON由来のdictに型を付ける
dataclass × × × 独自データクラス
dataclass(frozen=True) × × × × イミュータブル + hashable
NamedTuple × × × 既存タプルAPI、座標など
pydantic.BaseModel × × API入出力(ランタイム検証)

17-6. mypy / pyrightの使い方

mypy(公式リファレンス実装)

pip install mypy
mypy my_module.py

# 設定ファイルmypy.ini
[mypy]
strict = True
python_version = 3.12

strict モードは「すべての関数に型付けを要求」する厳しいモード。新規プロジェクトでは推奨

pyright(Microsoft製、TypeScriptと同じチーム)

pip install pyright
pyright my_module.py

VSCodeのPython拡張(Pylance)の中身はpyright。型推論がmypyより強く、高速で、最近はこちらを採用するチームが増えています。

型チェックレベルの段階的導入

# ファイル先頭でレベル指定(mypy)
# mypy: strict

# 個別の無視
x = something()  # type: ignore
x = something()  # type: ignore[error-code]

17-7. ランタイム型チェック(pydantic, beartype)

型ヒントは静的にしかチェックされないので、外部入力(APIボディ、設定ファイル)の検証には別途ツールが必要。

pydantic

API入出力の検証で デファクトスタンダード。FastAPIの中核。

from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int
    email: str | None = None

# JSONやdictから検証付きで生成
u = User.model_validate({"name": "Alice", "age": 30})
# 型が違うとValidationError

beartype

関数呼び出しごとにランタイム型チェックする軽量ライブラリ。

from beartype import beartype

@beartype
def add(a: int, b: int) -> int:
    return a + b

add("a", "b")   # 即座にTypeError

オーバーヘッドが小さい(ほぼゼロ)ことを売りにしている。


17-8. このセクションのまとめ

typingの主要要素:
  Any / Union(|) / Optional / Callable / Literal / Final / ClassVar
  NewType / NoReturn / TypeVar / Protocol

PEP 695(Python 3.12+):
  class Stack[T]: ...
  def f[T](x: T) -> T: ...
  type Vector = list[float]

Protocol:
  構造的部分型(duck typingに静的型を付ける)
  @runtime_checkableでisinstance可能

データ表現の使い分け:
  TypedDict / dataclass / NamedTuple / pydanticを場面で

型チェッカ:
  mypy(公式・厳格)
  pyright(高速・型推論強い、VSCode標準)

ランタイム検証:
  pydantic(API入出力のデファクト)
  beartype(軽量)

次のセクションでは、Pythonプロジェクトを 配布可能な部品にする ためのパッケージング・依存管理を扱います。


18. パッケージング・依存管理

Pythonのパッケージングは 長らく業界の悩みのタネでした。pip だけでは足りない・足りすぎる、競合する規格が乱立する、ロックファイルがない……。2020年代に入り pyproject.toml 中心の世界に整理されつつあり、uv の登場でさらに状況が変わっています。このセクションでは現代的な構成を中心に解説します。


18-1. 仮想環境(venv)

なぜ仮想環境が必要か

Pythonはシステムグローバルにパッケージをインストールすると、プロジェクト同士で依存バージョンの衝突が必ず起きます。仮想環境は「プロジェクトごとに独立したPythonインタプリタ環境」を作る仕組み。

# venvで仮想環境を作る
python -m venv .venv

# 有効化
source .venv/bin/activate     # Linux/Mac
.venv\Scripts\activate        # Windows

# パッケージインストール
pip install requests

# 抜ける
deactivate

仮想環境のディレクトリ構造

.venv/
├── bin/        # python, pip, スクリプトの実行ファイル
├── lib/        # site-packages: インストールされたパッケージ
└── pyvenv.cfg  # 設定

.venv.gitignore に入れるのが慣習。代わりに pyproject.tomlrequirements.txt を共有して、各人が再構築する形にします。


18-2. pip / pip-tools

pipの基本

pip install requests             # インストール
pip install requests==2.28.1     # バージョン固定
pip install "requests>=2.28"     # 最低バージョン
pip uninstall requests           # アンインストール
pip list                         # インストール済み一覧
pip freeze > requirements.txt    # 現在のバージョンをロック
pip install -r requirements.txt  # ファイルから一括インストール

requirements.txtの限界

pip freeze「インストールされた全パッケージとそのバージョン」 を出します。これには次の問題があります。

  • 直接依存と間接依存が混ざる: どれが自分が指定したパッケージか分からない
  • プラットフォーム依存が含まれる: Linuxで生成したものがWindowsで使えないなど
  • 更新の意図が見えない: 「最低限これ以上」とか「このマイナーまで」が表現できない

pip-tools(pip-compile)

pip install pip-tools

# requirements.inに直接依存を書く
# requirements.in
requests
flask

# ロックファイルを生成
pip-compile requirements.in    # → requirements.txt(ハッシュ含む完全ロック)

# インストール
pip-sync requirements.txt

pip-toolsは 「ハッシュ付きの完全ロックファイル」 を作るため、再現性が高い。CIなどで重宝されます。


18-3. pyproject.tomlとPEP 517/518/621

pyproject.tomlとは

プロジェクトのメタデータと設定をすべて1つのファイルに集約する」という現代の標準setup.py / setup.cfgは徐々に時代遅れに。

# pyproject.tomlの例
[build-system]
requires = ["setuptools>=61", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "my_package"
version = "0.1.0"
description = "簡潔な説明"
authors = [{ name = "Alice", email = "alice@example.com" }]
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
    "requests>=2.28",
    "pydantic>=2.0",
]

[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]

[project.scripts]
my-cli = "my_package.cli:main"

PEP 517 / 518 / 621

PEP 内容
PEP 518 pyproject.toml[build-system] セクション標準
PEP 517 ビルドバックエンド(setuptools, hatch, flit)を切替可能に
PEP 621 [project] セクションの標準化(プロジェクトメタデータ)

これらの整備により、ビルドツールをパッケージ作成者が自由に選べるようになりました。


18-4. uv / poetry / hatch / Ryeの現状

パッケージマネージャ」のレベルで、複数の選択肢があります。2026年現在の状況を整理。

uv(Astral製、急速に普及中)

Rust製。極端に速いことで一気に普及。

# インストール
curl -LsSf https://astral.sh/uv/install.sh | sh

# プロジェクト作成
uv init my_project

# 仮想環境作成・依存インストール
uv add requests
uv sync

# 実行
uv run python script.py

# Python自体のインストールも管理してくれる
uv python install 3.12

uv の特長:

  • Rust製でpipより10〜100倍速い
  • 仮想環境・ロックファイル・Pythonバージョン管理を統合
  • pyproject.tomlを中心にすべて完結

poetry

長らくPythonパッケージング標準として人気。

poetry init
poetry add requests
poetry install
poetry run python script.py
poetry build
poetry publish

poetry.lock を生成してロック。最近は uv に押され気味。

hatch

PyPA(公式)寄りのツール。pyproject.toml 中心、テスト環境のマトリクス管理が得意。

Rye

Armin Ronacher(Flask作者)が始めた統合ツール。uv に統合される方向で発展解消の流れ。

選び方

状況 推奨
新規プロジェクト uv(速い・統合的)
既存poetryプロジェクト そのまま poetry、移行は急がず
ライブラリ公開重視 hatch
シンプルな個人スクリプト pip + venv で十分

18-5. wheelとsdist

Pythonのパッケージは 2種類の配布形式があります。

形式 拡張子 中身
wheel .whl ビルド済みバイナリ(インストールが速い)
sdist .tar.gz ソース配布(ビルドが必要)
# 両方を作る
python -m build

# 結果
dist/
├── my_package-0.1.0-py3-none-any.whl
└── my_package-0.1.0.tar.gz

wheelは プラットフォーム別が必要なケースもある(C拡張を含む場合)。NumPyのような大型プロジェクトはLinux/Mac/Windows×CPUアーキ別に多数のwheelをビルドしてPyPIにアップしています。


18-6. PyPIへの公開

# ビルド
python -m build

# PyPIへアップロード(APIトークンが必要)
pip install twine
twine upload dist/*

# テスト用PyPI(test.pypi.org)に先に上げて確認するのが推奨
twine upload --repository testpypi dist/*

PyPI公開時のチェックリスト:

  • pyproject.tomlname がユニーク(既に存在する名前は使えない)
  • README.mdreadme で指定し、PyPIページに表示される
  • LICENSE を含めて配布する
  • version を更新する(同じバージョンは再アップロード不可)

18-7. このセクションのまとめ

仮想環境:
  python -m venv .venvで作る
  .venvは .gitignoreに入れる
  代わりにpyproject.tomlやrequirements.txtを共有

pip / pip-tools:
  pip install / freezeは手軽
  ロックファイル目的ならpip-compile(pip-tools)

pyproject.toml(PEP 517/518/621):
  プロジェクトメタデータ・依存・ビルド設定を集約
  setup.py / setup.cfgは徐々に廃れる方向

パッケージマネージャの選択:
  uv(速い・統合的、推奨の新規)
  poetry(長く使われている)
  hatch(PyPA寄り)

配布形式:
  wheel(バイナリ・速い)とsdist(ソース)
  PyPIには両方アップする

PyPI公開:
  python -m build → twine upload dist/*

次のセクションでは、信頼できるPythonコードを書くために欠かせない テスト戦略 を扱います。


19. テスト戦略

動くコードはテストがあるから動き続ける」。Pythonのテスト文化は標準ライブラリunittest と外部の pytest の二段構えです。pytestがデファクト。


19-1. unittest / pytest

unittest(標準ライブラリ)

xUnit系の伝統的なAPI。

import unittest

class TestAdd(unittest.TestCase):
    def test_add(self):
        self.assertEqual(1 + 1, 2)

    def test_negative(self):
        self.assertLess(-1, 0)

if __name__ == "__main__":
    unittest.main()

pytest(推奨)

pip install pytest
# test_add.py
def test_add():
    assert 1 + 1 == 2

def test_negative():
    assert -1 < 0
pytest             # 自動的にtest_*.py / *_test.pyを探して実行
pytest -v          # 詳細表示
pytest -k "add"    # 名前にマッチするテストだけ
pytest --pdb       # 失敗時にデバッガ起動

pytestが圧倒的に普及している理由:

  • assert 文だけで書ける(assertEqual のような特殊メソッド不要)
  • 失敗時のメッセージが豊富(左右の値・差分を自動で表示)
  • fixtureシステムが強力
  • プラグインエコシステムが充実

19-2. fixtureとparametrize

fixture: テストの前準備

import pytest

@pytest.fixture
def sample_user():
    return {"name": "Alice", "age": 30}

def test_user_name(sample_user):
    assert sample_user["name"] == "Alice"

引数名とfixture名が一致すると自動的に注入される。依存性注入(DI)的なパターン

scopeで寿命を調整

@pytest.fixture(scope="session")     # セッション中1回だけ
def db_connection():
    conn = connect()
    yield conn
    conn.close()

scopefunction(デフォルト)/ class / module / session から選択。

parametrize: 同じテストを複数の入力で

import pytest

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

3つの入力の組合せで 3つのテストケースとして実行されます。


19-3. mock / patch

依存先(DB、外部API)を本物で動かしたくないときは unittest.mock でモック化。

from unittest.mock import patch, MagicMock

@patch("my_module.requests.get")
def test_fetch(mock_get):
    mock_get.return_value.json.return_value = {"key": "value"}
    result = my_module.fetch_data()
    assert result == {"key": "value"}
    mock_get.assert_called_once_with("https://api.example.com")
# 文脈で使う書き方
def test_fetch():
    with patch("my_module.requests.get") as mock_get:
        mock_get.return_value.json.return_value = {"key": "value"}
        result = my_module.fetch_data()
        assert result == {"key": "value"}

MagicMockの便利機能

m = MagicMock()
m.foo.bar.baz()         # 任意のチェインアクセスがエラーにならない
m.return_value = 42
m.side_effect = ValueError("test")  # 呼ばれた瞬間に例外

pytest-mock

pytest-mock プラグインを使うと mocker fixtureが便利。

def test_fetch(mocker):
    mocker.patch("my_module.requests.get", return_value=...)

19-4. coverage

テストでコードがどれだけ網羅されたかを測定。

pip install coverage
coverage run -m pytest
coverage report -m
coverage html       # HTMLレポート生成
# pytest-covプラグインで一発
pip install pytest-cov
pytest --cov=my_package --cov-report=term-missing

ただし 「カバレッジ100% = バグなし」ではない。むしろ「通っている = ロジック検証されている」とは限らないので、カバレッジは目安にして、テストの質も別途見ること。


19-5. property-based testing(hypothesis)

入力例を全列挙する」のではなく、「性質(プロパティ)を満たすかをランダム入力で大量に確認」するアプローチ。

pip install hypothesis
from hypothesis import given, strategies as st

@given(st.lists(st.integers()))
def test_sort_idempotent(lst):
    assert sorted(sorted(lst)) == sorted(lst)

@given(st.text())
def test_round_trip(s):
    assert decode(encode(s)) == s

Hypothesisは 失敗するケースを自動で見つけて、最小化(shrinking) して報告してくれます。エッジケースを発見する能力に優れる。


19-6. doctest

docstring内に書いた実行例をテスト化する仕組み。

def add(a, b):
    """2つの数を足す。

    >>> add(1, 2)
    3
    >>> add(-1, 1)
    0
    """
    return a + b

# 実行
# python -m doctest my_module.py -v

ドキュメントとテストを同居させられるPythonの伝統的な仕組み。pytestからも --doctest-modules で動かせます。


19-7. このセクションのまとめ

ツール選択:
  unittest(標準)かpytest(推奨)
  pytestはassert文・fixture・parametrizeが強い

fixture:
  関数引数で依存を注入
  scopeで寿命を制御(function / module / session)

parametrize:
  @pytest.mark.parametrizeで同じテストを複数入力で

モック:
  unittest.mock.patch / MagicMock
  pytest-mockのmocker fixtureが便利

カバレッジ:
  coverage / pytest-cov
  100% を目指すより「重要箇所をカバー」が大事

property-based:
  hypothesisでランダム入力テスト
  エッジケース発見に強い

doctest:
  docstringに実行例を書ける
  軽量な動作確認に

次のセクションでは、Pythonパフォーマンスチューニングの基本を扱います。


20. パフォーマンス

Pythonは遅い」と言われがちですが、実際は「書き方次第で100倍速くなる」言語でもあります。プロファイリング、ベンチマーク、データ構造の選び方、NumPy / Cythonなどの加速手段を整理。


20-1. プロファイリング

推測ではなく計測」。早すぎる最適化は害。まずどこが遅いか測ります。

cProfile(標準)

import cProfile
cProfile.run("expensive_function()")

# またはコマンドラインから
# python -m cProfile -o profile.out my_script.py

# 結果を読む
import pstats
p = pstats.Stats("profile.out")
p.sort_stats("cumulative").print_stats(20)

line_profiler

行単位の精密測定。

pip install line_profiler
@profile     # ↓ kernprofで実行
def slow_function():
    ...

# kernprof -l -v my_script.py

py-spy

実行中のプロセスを外から計測できるサンプリングプロファイラ。本番環境のデバッグに有効。

pip install py-spy
py-spy top --pid 12345
py-spy record -o profile.svg --pid 12345

20-2. ベンチマーク(timeit)

import timeit

# 1行コードのベンチマーク
timeit.timeit("'-'.join(str(n) for n in range(100))", number=10000)

# 関数のベンチマーク
def f():
    ...

timeit.timeit(f, number=1000)
# IPython / Jupyterなら %timeitマジック
%timeit f()
%%timeit
some_code

20-3. NumPy / Cython / Numba

NumPy: ベクトル化でC並みの速度

import numpy as np

# Pythonループ
def py_sum(n):
    return sum(i * i for i in range(n))

# NumPy
def np_sum(n):
    return (np.arange(n) ** 2).sum()

# 100倍以上速いことが多い

NumPyが速い理由:

  • データをCの配列としてまとめて持つ(Pythonオブジェクトのオーバーヘッド回避)
  • 演算がCで実装され、GILを解放できる

データサイエンスでは「ループはNumPyのベクトル化に置き換える」が鉄則。

Cython

Pythonの文法を拡張してCにコンパイルする。

# foo.pyx
def fast_sum(int n):
    cdef int i, total = 0
    for i in range(n):
        total += i * i
    return total

Numba

@jit デコレータを付けるだけでJITコンパイル。

from numba import jit

@jit
def fast_sum(n):
    total = 0
    for i in range(n):
        total += i * i
    return total

数値計算で純Pythonの数十〜数百倍速くなることがあります。NumPy配列との相性も良い。


20-4. 文字列結合・リスト結合の落とし穴

文字列の += は遅い

第3章でも触れたが再掲

# Bad: O(n²)
result = ""
for word in words:
    result += word

# Good: O(n)
result = "".join(words)

listの先頭操作

# Bad: 各pop(0) がO(n)
while lst:
    x = lst.pop(0)
    process(x)

# Good: dequeはO(1)
from collections import deque
q = deque(lst)
while q:
    x = q.popleft()
    process(x)

不要なコピー

# 全要素をスキャンしてフィルタしたいだけなら
# Bad: ジェネレータでよかった
sum([x for x in iter if x > 0])

# Good
sum(x for x in iter if x > 0)

20-5. データ構造の選び方

操作 list tuple dict set deque
末尾追加 O(1) × × × O(1)
先頭追加 O(n) × × × O(1)
要素検索 O(n) O(n) O(1) O(1) O(n)
順序保持 ○ (3.7+) ×
重複許容 キー× ×
イミュータブル × × × ×

何が頻繁に行われるか」で選びます。たとえば「数百万件の重複チェック」なら必ず set を使うこと(listの in で線形探索すると数千倍遅い)。


20-6. メモ化(functools.cache, lru_cache)

from functools import cache, lru_cache

@cache
def fib(n):
    if n < 2: return n
    return fib(n - 1) + fib(n - 2)

@lru_cache(maxsize=128)
def expensive(arg):
    ...

引数がイミュータブルでハッシュ可能であることが必要。list を引数にとる関数は直接キャッシュできないので tuple に変換する。


20-7. このセクションのまとめ

測定優先:
  cProfile(全体)/ line_profiler(行単位)/ py-spy(本番)
  timeitでマイクロベンチ

加速手段:
  NumPyベクトル化(数値計算で100倍)
  Cython(型を付けてCコンパイル)
  Numba(@jitでJIT)

落とし穴の改善:
  文字列 += → "".join
  list.pop(0) → deque.popleft()
  in検索が頻繁 → setを使う

メモ化:
  functools.cache / lru_cache(再帰アルゴリズムや純関数に)

次のセクションでは、Pythonの メタプログラミング――言語自体を操作する技法――を扱います。


21. メタプログラミング

メタプログラミングとは「プログラム自身を扱うプログラム」。Pythonは動的言語なので、クラスや関数を実行時に作ったり書き換えたりできる柔軟性があります。普段使う場面は限られますが、ライブラリ開発(特にORM・フレームワーク)では中核技術です。


21-1. type() による動的クラス生成

class 文は実は type() のシンタックスシュガーです。

# 通常の定義
class A:
    x = 10
    def hello(self): return "hi"

# 等価: type() で動的に作る
A = type("A", (object,), {"x": 10, "hello": lambda self: "hi"})

a = A()
a.x         # 10
a.hello()   # "hi"

type(name, bases, dict) の3引数版は「名前・親クラスのタプル・属性辞書からクラスを作る」関数です。class キーワードは結局これを呼ぶための糖衣構文。

typeは何者か

type(1)           # <class 'int'>
type(int)         # <class 'type'>  ← クラスのクラス!
type(type)        # <class 'type'>  ← 自分自身

type「クラスのクラス(メタクラス)」。すべての通常のクラスは type のインスタンスです。


21-2. メタクラス

クラスを作る方法をカスタマイズしたい」とき、独自のメタクラスを定義します。

class MyMeta(type):
    def __new__(mcs, name, bases, namespace):
        print(f"クラス {name} を作る")
        # 何か変更を加える
        namespace["created_by"] = "MyMeta"
        return super().__new__(mcs, name, bases, namespace)

class A(metaclass=MyMeta):
    pass

# クラスAを作る
A.created_by   # "MyMeta"

何のために使うか

メタクラスの典型用途:

  • ORM(Djangoモデル / SQLAlchemy): クラス定義からデータベーススキーマを自動生成
  • 登録パターン: クラス定義時にどこかに自動登録
  • 強制ルール: 「すべてのサブクラスに特定メソッドの実装を強制」
# 簡単なORM風メタクラスの例
class ModelMeta(type):
    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        if name != "Model":
            register_model(cls)
        return cls

class Model(metaclass=ModelMeta):
    pass

class User(Model):
    name = StringField()
    age = IntField()
# クラス定義しただけで自動登録される

注意

メタクラスは強力ですが、読みづらく、デバッグしにくいので最後の手段。多くの場合 __init_subclass__ で代替できるので、そちらを先に検討しましょう。


21-3. init_subclass

メタクラスの軽量代替。サブクラスが定義された瞬間に呼ばれるフック。

class Plugin:
    plugins: list = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.plugins.append(cls)

class JSONPlugin(Plugin):
    pass

class XMLPlugin(Plugin):
    pass

print(Plugin.plugins)  # [JSONPlugin, XMLPlugin]

メタクラスを使わずに「継承時の自動登録」が書けます。Python 3.6+。


21-4. ディスクリプタ

第9章9-6で軽く触れたディスクリプタを掘り下げます。属性アクセスを乗っ取る仕組みで、@property の一般化です。

class Validated:
    def __init__(self, validate):
        self.validate = validate

    def __set_name__(self, owner, name):     # クラス側の属性名を捕捉
        self.name = name
        self.private = f"_{name}"

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.private)

    def __set__(self, obj, value):
        if not self.validate(value):
            raise ValueError(f"無効な {self.name}: {value}")
        setattr(obj, self.private, value)

class Product:
    price = Validated(lambda v: isinstance(v, (int, float)) and v >= 0)
    name = Validated(lambda v: isinstance(v, str) and v)

p = Product()
p.price = 100      # OK
p.price = -1       # ValueError

ディスクリプタプロトコル:

メソッド いつ呼ばれる
__get__(self, obj, objtype) 属性を読むとき
__set__(self, obj, value) 属性に代入するとき
__delete__(self, obj) 属性を削除するとき
__set_name__(self, owner, name) クラス本体で名前を付けたとき

実は @property@classmethod@staticmethod はすべてディスクリプタとして実装されています。


21-5. exec / eval(と落とし穴)

exec("x = 10")        # 文を実行
print(x)              # 10

result = eval("1 + 2")  # 式を評価
print(result)         # 3

ほとんどの場合避けるべき機能です。

理由:

  • 任意コード実行のセキュリティリスク: eval(user_input) は最悪の脆弱性
  • デバッグが困難
  • 静的解析・補完が効かない
  • 大抵もっと良い解決策がある(getattr, dict, match, etc.)

動的にSQLを組み立てる」「設定ファイルから関数を取り出す」など、本当に必要な場面でだけ、信頼できる入力に対して限定的に使う。

信頼できない入力に対してはliteral_eval

import ast
ast.literal_eval("[1, 2, 3]")    # [1, 2, 3]
ast.literal_eval("os.system('rm -rf')")   # ValueError(組み込みリテラルしか許さない)

JSONライクなリテラルだけパースしたいときの安全な代替。


21-6. このセクションのまとめ

type() の3引数版:
  動的にクラスを作れる(class文の正体)

メタクラス:
  typeを継承してクラス生成をカスタマイズ
  ORM・登録パターン・継承ルール強制で活躍
  最後の手段、まず __init_subclass__ を検討

__init_subclass__:
  サブクラス定義時のフック
  メタクラスより軽量で推奨

ディスクリプタ:
  属性アクセスを乗っ取る
  @propertyの一般化
  __get__ / __set__ / __delete__ / __set_name__

exec / eval:
  ほとんど使わない
  セキュリティリスクが高い
  literal_evalは安全な代替

次のセクションでは、Pythonの歴史で最も大きな出来事――Python 2から3への移行――を扱います。


22. Python 2→3移行と歴史的経緯

第1章で軽く触れた「Python 2/3分断時代」は、業界全体への教訓を残した出来事でした。すでに移行は完了していますが、「なぜここまで非互換にする必要があったのか」「移行で何が大変だったのか」 は、現代Pythonを理解する上で意味があります。


22-1. なぜ大移行が必要だったか

文字列モデルの破綻

Python 2では strバイト列とテキストの両方の役割を担っていました。

# Python 2
s = "hello"           # str(バイト列でもテキストでもある)
u = u"こんにちは"     # unicode(Unicodeテキスト)

これは ASCIIの世界では問題なかったが、グローバルでは破綻した」 という典型例です。日本語・中国語・絵文字を扱うとき、strunicode の自動変換でバグが頻発しました。

整数除算の罠

# Python 2
5 / 2     # 2(整数のとき自動的に切り捨て)
5.0 / 2   # 2.5

入力がintかfloatかで動作が変わる」のは初心者を混乱させる罠。Python 3では / は常にfloat、// が整数除算と分けられました。

print文vs print関数

# Python 2
print "hello"      # 文(statement)

# Python 3
print("hello")     # 関数

関数化することで、他の関数と同じように扱える(mapに渡せる、デコレートできる、引数で出力先を切替できる)。

その他の主要変更

  • range() がジェネレータに(Python 2の xrange 相当が標準に)
  • dict.keys() などがビューに(リスト化はせず遅延評価)
  • iteritems などが廃止
  • unicode_literals がデフォルトに
  • 例外の構文: except X as e: に統一

22-2. 移行ツール

2to3

公式の自動変換ツール。

2to3 -w my_script.py     # ファイルを書き換え

ただし完全ではなく、特に文字列とバイト列の境界は手動修正が必要でした。

six(互換ライブラリ)

両対応のライブラリを書きたいときに使う互換ライブラリ。

import six
six.string_types       # Python 2: (str, unicode)、Python 3: (str,)

if six.PY2:
    ...

six は2系終焉とともに役目を終えました。

future インポート

Python 2でも3の挙動を先取りできる仕組み。

# Python 2で書く
from __future__ import (
    absolute_import, division, print_function, unicode_literals,
)
# print関数化、整数除算のint/float統一、相対importの禁止、文字列のデフォルトUnicode

Python 2のコードを書きながら3への移行準備をする」常套手段でした。


22-3. 移行の長期化が残した教訓

2008  Python 3.0リリース
2010  Python 2.7リリース(実質的な「最後の2系」)
2014  Guidoが「2.7のサポートを2020まで」と宣言(5年延長)
2020  Python 2サポート終了(EOL)

12年かかった

業界に残された教訓:

  1. 後方互換性のない変更は早く・小さく: 一度に大量に変えすぎた
  2. 移行ツールは1日目から提供すべき: 2to3__future__sixの整備は途中から
  3. コミュニティと協調: 主要ライブラリ(Django、Flask、NumPyなど)が3対応する前に2系のサポートを切ろうとしてバックラッシュがあった
  4. 2系で良いものは積極的に2系にバックポート: 結果として2.7が長寿命に

これらの教訓は、Ruby、PHP、Node.jsの主要バージョンアップなど、他言語にも影響を与えました。「破壊的変更には慎重に、ツールで支える」 が業界の標準的な姿勢になっています。


22-4. 現代のPythonとPython 2の名残

すでに2系はEOLですが、レガシーコードには名残が残っています。

# 古いコードで見かけるもの
print "hello"                  # → print("hello")
xrange(10)                     # → range(10)
unicode("a")                   # → str("a")
basestring                     # → str
dict.iteritems()               # → dict.items()
"a".encode("hex")              # → bytes.fromhex / binascii.hexlify
exec "code"                    # → exec("code")

これらが見つかったら 2 → 3に移行漏れがあるレガシーだと判断できます。


22-5. このセクションのまとめ

なぜ非互換が必要だったか:
  文字列モデル(strとunicodeの混乱)の根本修正
  整数除算の挙動統一
  printの関数化
  ジェネレータベースの遅延評価標準化

移行ツール:
  2to3(自動変換、不完全)
  six(両対応ライブラリ)
  from __future__ import ...(先取り)

12年の長期化が残した教訓:
  破壊的変更は早く・小さく
  移行ツールを1日目から
  主要ライブラリと協調

次のセクションは、最近のPythonの機能アップデートを総ざらい。


23. Python 3.10〜3.13の新機能

ここ数年のPythonの主要な追加機能をバージョン別に整理します。


23-1. Python 3.10(2021)

match文(PEP 634)

第5章で詳述。構造的パターンマッチング

Unionを | で書ける(PEP 604)

def f(x: int | str) -> bool: ...   # Union[int, str] と同じ

より良いエラーメッセージ

# 3.9
SyntaxError: unexpected EOF while parsing

# 3.10+
SyntaxError: '(' was never closed

withの括弧構文

with (
    open("a") as f1,
    open("b") as f2,
    open("c") as f3,
):
    ...

23-2. Python 3.11(2022)

Faster CPython(PEP 659)

ベンチマーク全般で 約25% 高速化。動的型システムを保ちながらの最適化。

ExceptionGroupとexcept*(PEP 654)

第14章で詳述。

TaskGroupとasyncio.timeout(PEP 654)

第15章で詳述。

tomllib標準ライブラリ

pyproject.toml を読むためのパーサが標準化。

import tomllib
with open("pyproject.toml", "rb") as f:
    data = tomllib.load(f)

Self型(PEP 673)

from typing import Self

class Node:
    def clone(self) -> Self:
        return type(self)()

クラスメソッドが「自分自身の型を返す」と表現できる。

より良いトレースバック

# 3.10
File "x.py", line 1, in <module>
    foo(bar())

# 3.11
File "x.py", line 1, in <module>
    foo(bar())
        ~~~^^   ← どこでエラーが起きたか強調表示

23-3. Python 3.12(2023)

PEP 695新型構文

第17章で詳述。

class Stack[T]: ...
def first[T](items: list[T]) -> T: ...
type Vector = list[float]

f-string内の制約緩和(PEP 701)

# 3.11以前: f-stringの中でクォートを再利用できなかった
# 3.12+: できる
s = f"{ "hello".upper() }"   # 3.12+ ではOK

より良いエラーメッセージ(さらに)

# 3.12
NameError: name 'srot' is not defined. Did you mean: 'sort'?

Per-Interpreter GIL(PEP 684)

サブインタプリタごとに独立したGILを持てるように。Free-Threadedへの布石。


23-4. Python 3.13(2024)

Free-Threadedビルド(PEP 703)

第15章で触れた、GILを任意で外せるビルド。python3.13t バイナリで動かす。実験的扱いだが、Pythonの並列処理を劇的に変える可能性。

JITコンパイラ(実験的、PEP 744)

CPythonコピー&パッチ方式のJIT が実験的に追加。マイクロベンチで5〜10% の改善。標準有効化は今後。

REPLの刷新

対話シェルが大幅に改良。

  • マルチライン編集
  • 履歴検索
  • カラー表示
  • exit / helpなどの簡略化

エラーメッセージのさらなる改善

# 3.13
TypeError: unsupported operand type(s) for +: 'int' and 'str'
   |
 1 | 1 + "2"
   |   ^^^^^

23-5. このセクションのまとめ

3.10 (2021):  match文、X | Y、より良いエラー
3.11 (2022):  25% 高速化、ExceptionGroup、TaskGroup、Self、tomllib
3.12 (2023):  PEP 695新型構文、type文、f-string制約緩和
3.13 (2024):  Free-Threaded、実験的JIT、REPL刷新

トレンド:
  - エラーメッセージの継続的改善(初学者に優しく)
  - 型システムの強化(PEP 695、Self、ParamSpec ...)
  - パフォーマンス(Faster CPython、JIT、Free-Threaded)
  - 標準ライブラリの整備(tomllib、asyncio.TaskGroup)

次のセクションでは、Pythonを学ぶ・使う中で遭遇する よくある落とし穴をFAQ形式 でまとめます。


24. よくある落とし穴FAQ

実務で頻発するPythonの落とし穴を一問一答形式でまとめます。


Q1. is== はどう違う?

is同一オブジェクトかどうか(idが同じか)。==値が等しいか(__eq__ が呼ばれる)。

a = [1, 2]
b = [1, 2]
a == b    # True(値が等しい)
a is b    # False(別オブジェクト)

x = 100
y = 100
x is y    # True(小整数キャッシュ。実装依存なので頼らない)

ルール: 値の比較は ==、シングルトン(特に None)の比較は is


Q2. ミュータブルなデフォルト引数で起きるバグは?

def f(items=[]):
    items.append(1)
    return items

f()    # [1]
f()    # [1, 1] ← 同じリストが使い回される

正解: items=None にして関数内で if items is None: items = []


Q3. lambda をループ内で作るとなぜ動作がおかしい?

funcs = [lambda: i for i in range(3)]
[f() for f in funcs]   # [2, 2, 2](最後のiがキャプチャされる)

解決: lambda i=i: iデフォルト引数として束縛。


Q4. 浮動小数点で 0.1 + 0.2 == 0.3 がFalseになるのはなぜ?

float はIEEE 754倍精度。10進数の小数を2進数で正確に表現できないため。math.isclose(...) で比較。正確な計算が必要なら decimal.Decimal


Q5. リストのコピーで全要素が共有される?

a = [[1, 2], [3, 4]]
b = a.copy()      # 浅いコピー
b[0].append(99)
print(a)          # [[1, 2, 99], [3, 4]] ← 共有

ネストがあるなら copy.deepcopy(a)


Q6. dict.get(key)dict[key] の違いは?

dict[key] はキーが無いと KeyErrordict.get(key) は無ければ Nonedict.get(key, default) は無ければ default


Q7. for/else のelseはいつ実行される?

break せずにループが正常終了したときだけ。「nobreak ブロック」と読み替えると分かりやすい。


Q8. import * を使うべきでない理由は?

  • どの名前がimportされるか分からない
  • 名前空間の汚染
  • IDE補完・静的解析が効かなくなる

慣例: from m import x, y, zimport m を使う。


Q9. __init__.py は空でもいいの?

良い。空でもパッケージとして認識されます。中に書くこともある:

  • 公開APIを集約(from .core import main など)
  • バージョン定義(__version__ = "1.0")
  • __all__from pkg import * の対象を制御

Q10. 巨大な if/elif を書きたくないとき何を使う?

  • ディスパッチテーブル(dict): {"a": handler_a, "b": handler_b}.get(key, default)()
  • 多態性(OOP): 共通メソッドで分岐をクラス側に移す
  • match(3.10+): 構造的パターンマッチング

Q11. リスト内包表記とジェネレータ式、どう使い分ける?

[x for x in ...]   即時にリストを作る → 何度も使う、サイズが小さい
(x for x in ...)   遅延評価           → 一度しか使わない、巨大データ

Q12. print でなく logging を使うべきと言われるのはなぜ?

  • レベル別フィルタ(INFO/WARN/ERROR)
  • 出力先切替(stdout/file/syslog)
  • フォーマット統一
  • 本番運用での制御性
import logging
logging.basicConfig(level=logging.INFO)
logging.info("起動しました")

Q13. なぜ True == 1 がTrueになる?

boolint の派生クラスだから。歴史的経緯で、Python 2.3でboolが追加された際、後方互換のためintから派生させた結果です。


Q14. asyncioで requests.get() を呼ぶとなぜ動かなくなる?

requests.get()同期I/Oイベントループをブロックして他のコルーチンが進まなくなる。

解決: httpx.AsyncClient などの非同期HTTPクライアントを使う、または asyncio.to_thread(blocking_func)


Q15. なぜ multiprocessingif __name__ == "__main__": が必要?

Windowsでは子プロセスが spawn で起動される際、親モジュールが再importされます。このとき再びサブプロセスが起動されて無限ループになる。

if __name__ == "__main__": で囲まれたコードは 直接実行時のみ動くので、再import時にはスキップされ、正常に動作します。


Q16. クロージャで外側の変数を書き換えたらエラーになる

def outer():
    x = 0
    def inner():
        x = x + 1   # UnboundLocalError
    inner()

理由: x = ... があるのでPythonは x をinnerのローカルと判断する。代入前に参照すると未定義エラー。

解決: nonlocal x(外側の関数の変数)または global x(モジュール変数)。


Q17. from collections import OrderedDict はもう不要?

Python 3.7+ で 通常のdictが挿入順を保持するようになったため、ほぼ不要。ただし OrderedDict には move_to_end() のような独自メソッドがあるので、それが必要なら使う。


Q18. なぜPythonは遅いと言われる?

主な理由:

  1. 動的型付け: 型情報が無いため、+ の度にメソッド解決が必要
  2. インタプリタ実行: バイトコードを逐次解釈(伝統的にJITがない)
  3. オブジェクトオーバーヘッド: 整数1つでもオブジェクトとしてヒープに存在

対策: NumPy/Cython/Numba/PyPy/Faster CPython/3.13 JIT


Q19. __slots__ は何のため?

通常Pythonのインスタンスは __dict__ で属性を保持しており、メモリを多く使う。__slots__ で属性名を固定すると __dict__ が作られず、メモリ効率と属性アクセス速度が向上する。

class Point:
    __slots__ = ("x", "y")
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2)
p.z = 3   # AttributeError(__slots__ にない属性は追加不可)

@dataclass(slots=True) で自動生成も可能(3.10+)。


Q20. なぜselfを明示的に書く?

Guidoの方針で、「インスタンスメソッドの第1引数として明示的に書く」のがPythonの流儀。Javaの暗黙の this よりも「メソッドは関数の糖衣構文」という構造が見えやすい。

class A:
    def hello(self):
        ...

a = A()
a.hello()        # 暗黙的にaがself
A.hello(a)       # 明示的に書いても等価

25. 図解: 名前空間と参照カウント

文字だけでは伝わりにくいPythonの挙動を、図で整理します。


25-1. 名前束縛モデル(変数 = 名札)

コード:
  a = [1, 2, 3]
  b = a

メモリ:
  +-------+         +------------+
  |   a   |---+---->| list 0xff  |
  +-------+   |     | [1, 2, 3]  |
              |     | refcount=2 |
  +-------+   |     +------------+
  |   b   |---+
  +-------+

aとbは同じオブジェクトを指している
del aまたはdel bでrefcountが減る
両方消えるとrefcount=0で解放される

25-2. ミュータブル変更の波及

b.append(4) を実行する前:
  +-------+        +-------------+
  |   a   |--+---->| list        |
  +-------+  |     | [1, 2, 3]   |
  |   b   |--+     +-------------+
  +-------+

b.append(4) を実行した後:
  +-------+        +-----------------+
  |   a   |--+---->| list            |
  +-------+  |     | [1, 2, 3, 4]    | ← b経由で変更がaにも反映
  |   b   |--+     +-----------------+
  +-------+

変数が同じオブジェクトを指している」状況で片方を変更すると、もう片方からも変更が見える。


25-3. LEGBスコープ

              ┌─────────────────────┐
              │ Built-in            │  print, len, range, ...
              │ ┌─────────────────┐ │
              │ │ Global (module) │ │  ファイル全体の変数
              │ │ ┌─────────────┐ │ │
              │ │ │ Enclosing   │ │ │  ネスト関数の外側関数
              │ │ │ ┌─────────┐ │ │ │
              │ │ │ │ Local   │ │ │ │  関数内
              │ │ │ │  x = 1  │ │ │ │
              │ │ │ └─────────┘ │ │ │
              │ │ └─────────────┘ │ │
              │ └─────────────────┘ │
              └─────────────────────┘

  名前を探す順: 内 → 外
  代入は内側に新規作成 → 外側を書き換えたければnonlocal / global

25-4. asyncioのイベントループ

    [スレッド1(イベントループ)]

  Task A: ──── await sleep(2) ───────── 再開 ──── 完了
  Task B: ──── await fetch ────────── 再開 ──── 完了
  Task C: ──── await sleep(1) ── 再開 ──── 完了

       ↑ サスペンド          ↑ ループが再開を判断

  各Taskはawaitのところで「中断」し、
  イベントループが「準備完了したものから順番に再開」する

  シングルスレッドだが、I/O待ちを重ねて並行できる

25-5. GILの動き(CPythonの伝統的モデル)

時間軸 →

  Thread A: ▓▓▓▓▓░░░░░▓▓▓▓▓░░░░░▓▓▓▓▓
  Thread B: ░░░░░▓▓▓▓▓░░░░░▓▓▓▓▓░░░░░
  Thread C: ░░░░░░░░░░░░░░░░░░░░░░░░░ (待機)

  ▓ = 実行中(GILを保持)
  ░ = 待機(GILを解放、または取得待ち)

  どの瞬間も実行中スレッドは1つだけ
  → CPUバウンドな純Pythonコードは並列にならない

26. 学習ロードマップ(30日)

体系的にPythonを学ぶための30日プラン。1日1〜2時間を想定。


Week 1(基礎)

  • Day 1: 環境構築(uvまたはvenv)、REPLに慣れる、PEP 8を読む
  • Day 2: 変数・データ型(int, float, str, bool, list, tuple, dict, set)
  • Day 3: 制御フロー(if、for、while、break/continue)
  • Day 4: 関数の基本(def、引数、return)
  • Day 5: 内包表記・ジェネレータ式
  • Day 6: 簡単なスクリプトを書く(CLIツール、ファイル処理)
  • Day 7: 復習、簡単なミニプロジェクト

Week 2(中級)

  • Day 8: モジュール・パッケージ・import
  • Day 9: クラスとインスタンス
  • Day 10: 継承・MROdataclass
  • Day 11: 例外処理(try/except/raise from)
  • Day 12: イテレータ・ジェネレータ
  • Day 13: コンテキストマネージャ(with)
  • Day 14: 復習、Webスクレイピングなどの実践

Week 3(応用・型)

Week 4(並行・パフォーマンス・実装)


30日後の到達目標

  • 1万行規模のコードベースを読み書きできる
  • 型ヒントを使って静的解析を通せる
  • pytestでテストを書ける
  • asyncioで簡単な並行Webアプリが書ける
  • pyproject.tomlでパッケージを公開できる

おすすめの実践プロジェクト

プロジェクト 学べる要素
Webスクレイパー requests/httpx, BeautifulSoup, asyncio
CLIツール click/typer, argparse, ファイルI/O
Web API(FastAPI) pydantic, async, テスト
データ分析ノートブック pandas, matplotlib, Jupyter
自作デコレータライブラリ デコレータ、関数オブジェクト
簡単なORM メタクラス、ディスクリプタ

27. 用語集

本ガイドに登場した主要な用語を50音順・アルファベット順で。


あ行

  • アンダースコア(_): 慣習的に「捨てる変数」「内部用」を示す。_x は内部、__x はクラス内のマングリング、__x__ はダンダー。
  • イテラブル: for で繰り返せるオブジェクト。__iter__ を持つ。
  • イテレータ: 1要素ずつ取り出せるオブジェクト。__next__ を持つ。
  • イベントループ: asyncioの中核。コルーチンを切り替えて並行に実行する。
  • インスタンス: クラスから作られたオブジェクトの個体。

か行

  • キーワード引数: 名前付きで渡す引数(f(x=1))。
  • クロージャ: 外側のスコープの変数を捕捉する関数。
  • コルーチン: async def で定義された関数。await で中断・再開できる。
  • コンテキストマネージャ: with 文に対応するオブジェクト。__enter__/__exit__ を持つ。

さ行

  • 参照カウント: CPythonのメモリ管理の基本。オブジェクトへの参照数を数えて0で解放。
  • ジェネレータ: yield を含む関数から生成されるイテレータ
  • ジェネリクス: 型をパラメータ化する仕組み(list[int])。
  • シングルトン: 1つしかインスタンスがないオブジェクト。None がその代表。
  • ストラクチュアルタイピング: 構造的部分型Protocol で実現される。

た行

  • ダックタイピング: 「アヒルのように歩けばアヒル」、必要な操作ができれば型は問わない。
  • タプル: イミュータブルなシーケンス((1, 2, 3))。
  • デコレータ: 関数を引数にとって新しい関数を返す関数@deco 構文で適用。
  • ディスクリプタ: 属性アクセスをカスタマイズするプロトコル。
  • 動的型付け: 値が型を持ち、変数は型を持たない。実行時に型が決まる。
  • ドット記法: obj.attr での属性アクセス。

な行

  • 内包表記: [x for x in ...] のような簡潔な構造。
  • 名前空間: 名前と値の対応表。モジュール・クラス・関数がそれぞれ持つ。

は行

  • バイトコード: Pythonのソースをコンパイルした中間表現。__pycache__ にキャッシュ。
  • ビルトイン: print, len など組み込みで使える関数・型。
  • 不変(イミュータブル): 作成後に内容が変わらないオブジェクト(int, str, tuple)。
  • プロトコル: 「特定のメソッドを持っていれば良い」という構造的な型契約。
  • プロパティ: メソッドを属性のように見せる仕組み(@property)。

ま行

  • ミュータブル: 内容が変えられるオブジェクト(list, dict, set)。
  • メタクラス: クラスを生成するクラス(type の派生)。
  • モジュール: 1つの .py ファイル。

や〜わ行

  • 遅延評価: ジェネレータのように「必要になるまで計算しない」アプローチ。

A〜Z

  • ABC: Abstract Base Class、抽象基底クラス@abstractmethod を強制できる。
  • ABI: Application Binary Interface。C拡張のバイナリ互換性。
  • AST: Abstract Syntax Tree、抽象構文木。ast モジュールで操作可能。
  • CPython: 公式のC言語実装。世界で使われるPythonのほぼすべて。
  • EAFP: Easier to Ask Forgiveness than Permission。「やってみてダメなら例外」のPythonic流儀。
  • GIL: Global Interpreter Lock。CPythonで同時にバイトコードを実行できるスレッドを1つに制限。
  • JIT: Just-In-Timeコンパイル。実行中にコードを機械語に変換する高速化技術。Python 3.13で実験的導入。
  • LBYL: Look Before You Leap。「飛ぶ前に見る」、事前条件チェック型のスタイル。
  • LEGB: Local → Enclosing → Global → Built-in。Pythonの名前解決順。
  • MRO: Method Resolution Order。多重継承での属性探索順。
  • PEP: Python Enhancement Proposal。仕様変更提案文書。
  • REPL: Read-Eval-Print-Loop。対話シェル。
  • TDZ: Temporal Dead Zone(JSの概念。Pythonには類似の挙動はあるが用語としては使わない)。
  • TOML: 設定ファイルフォーマット。pyproject.toml で使われる。
  • wheel: ビルド済みのバイナリパッケージ。.whl 拡張子。
  • WSGI / ASGI: Web Server Gateway Interface / Asynchronous SGI。Python Webフレームワークの標準インタフェース。

25-A. パフォーマンス最適化と Cython

Python のパフォーマンスボトルネックを改善する手法。

Profiling と ホットスポット

import cProfile
import pstats

def slow_function():
    result = []
    for i in range(1000000):
        result.append(i ** 2)
    return result

cProfile.run('slow_function()', 'profile_stats')
stats = pstats.Stats('profile_stats')
stats.strip_dirs().sort_stats('cumulative').print_stats(10)

NumPy による高速化

import numpy as np

# Pure Python: 遅い
result = [x ** 2 for x in range(1000000)]

# NumPy: 数十倍高速
arr = np.arange(1000000)
result = arr ** 2

Cython による C 化

# fast_math.pyx
cimport cython

@cython.boundscheck(False)
@cython.wraparound(False)
def fast_sum(double[:] arr):
    cdef double total = 0.0
    cdef int i, n = len(arr)
    for i in range(n):
        total += arr[i]
    return total
# setup.py
from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules=cythonize("fast_math.pyx")
)

実行:python setup.py build_ext --inplace でコンパイル。

25-B. 型チェックの実践(mypy / Pydantic)

mypy による静的型チェック

# main.py
from typing import Optional, List

def greet(name: str) -> str:
    return f"Hello, {name}"

def process_users(users: List[dict]) -> Optional[str]:
    if not users:
        return None
    return greet(users[0]["name"])

# エラー:int を str に渡している
greet(123)  # mypy error: Argument 1 to "greet" has incompatible type "int"; expected "str"

実行:mypy main.py

Pydantic によるランタイム検証

from pydantic import BaseModel, validator

class User(BaseModel):
    id: int
    name: str
    email: str
    age: Optional[int] = None
    
    @validator('age')
    def age_must_be_positive(cls, v):
        if v is not None and v < 0:
            raise ValueError("age must be positive")
        return v

# 有効
user = User(id=1, name="Alice", email="alice@example.com", age=30)

# エラー:name は str でない
try:
    User(id=1, name=123, email="invalid")
except ValueError as e:
    print(e)

25-C. 並行・並列処理

asyncio による非同期

import asyncio

async def fetch(url):
    await asyncio.sleep(1)  # I/O シミュレーション
    return f"Data from {url}"

async def main():
    urls = ["http://a.com", "http://b.com", "http://c.com"]
    tasks = [fetch(url) for url in urls]
    results = await asyncio.gather(*tasks)
    return results

asyncio.run(main())

multiprocessing による並列

from multiprocessing import Pool

def square(x):
    return x ** 2

if __name__ == "__main__":
    with Pool(4) as p:
        result = p.map(square, range(1000))

25-D. Decorator パターン

from functools import wraps
import time

def timing_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.time() - start:.2f}s")
        return result
    return wrapper

def retry(max_retries=3, delay=1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_retries - 1:
                        raise
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

@timing_decorator
@retry(max_retries=3)
def unreliable_api_call():
    import random
    if random.random() < 0.7:
        raise ConnectionError("API failed")
    return "Success"

まとめ

Pythonは、読みやすい構文、豊富な標準ライブラリ、データ分析・機械学習・自動化のエコシステムを持つ汎用言語です。実務では、動的型付けの柔軟さを活かしつつ、型ヒント、仮想環境、テスト、パッケージ管理で境界を整えることが大切です。

参考文献

公式・標準

講義・記事