導入
はじめまして、株式会社みらい翻訳エンジニアリング部の@reonyanarticleです。
大分遅れてしまいましたが、みらい翻訳アドベントカレンダーの大トリ、25日を担当します。
2025年10月、Effective Python 第3版が出ました。
自分はEffective Python 第1版から持っているので、「もう第3版!?」と、つい感慨深くなってしまいました。
改めて第1版(日本語版)の発売日を見ると2016年で、ほぼ10年前です。時間が経つのは早いですね……。
第1版の章立てを眺めていると、普通にPython2.7の話が混ざっています。
自分がPythonを始めたころは、ちょうどPython2.7とPython3.6が同居していた時代でした。
「どっちのprintなんだっけ」「strってバイト列?Unicode?」「この比較って通るの?」みたいな、今思い出すと習い始めは混乱していたことばかりでした。
昔を懐かしんで、改めてEffective Python 第1版を読んでいたらふと、「昔のPython、今見ると“まじか”な仕様が多すぎない?」と思いました。
というわけで、2025年の今あえてPython2.7を振り返りつつ、Python3(手元の最新版としては3.13系を想定)と比較していきます。
全部は追いきれないので、今回は 関数・型・クラスに絞って思想の変化が見えるポイントを拾っていきます。
準備
2025年12月現在、Python2.7はサポート終了して久しいため、uvではインストールできません。
そこでこの記事ではDockerでPython2.7環境を用意します。
注意:この記事を読んで試した結果について、こちらでは責任を負いません。自己責任でお願いします。
実行するコマンド例です。
docker pull --platform=linux/amd64 python:2.7-slim
docker run -it --rm --platform=linux/amd64 python:2.7-slim bash
これでPython2.7が準備できました。
比較の方針
細かい差分を全部追うとキリがないので、この記事では次の3つに分けます。
- 関数:「文」っぽいものが減って、関数/イテレータに寄っていった話
- 型:異種型の暗黙比較が消え、型ヒントが増え、書き方も整理された話
- クラス:「実装を読まないと分からない」から「宣言的に書ける」へ寄っていった話
関数編
printが“文”だった
Python2の「関数まわり」で、まず触れたくなるのはやっぱりprintです。
Python2では、printは関数ではなく「文(statement)」でした。
Pythonにおける「文」とは
ざっくり言うと、Pythonには「式(expression)」と「文(statement)」があります。
- 式:値を返せます(評価結果があるので、代入や引数に渡すことができます)
- 文:値を返しません(実行はできるけど、評価結果として扱えません)
printはPython2では文なので、式として扱えないところがポイントです。
Python2のprintの味わい深い例
>>> print "Hello"
Hello
>>> print ("Hello")
Hello
>>> print ("Hello", "world")
('Hello', 'world')
>>> x = print ("Hello")
File "<stdin>", line 1
x = print ("Hello")
^
SyntaxError: invalid syntax
この挙動、初見だとかなり罠です。
-
print ("Hello")は「関数呼び出し」ではなく、単に"Hello"という式をprintしているだけです -
print ("Hello", "world")は タプル("Hello", "world")をprintしているので('Hello', 'world')になります - そして文なので、
x = print (...)みたいに代入できません
Python3での変更点(PEP3105)
Python3ではprintがprint()関数になりました。
これ自体は「まあそうだよね」なのですが、PEP3105を読むと “なぜそうしたか” がはっきり書かれていて面白いです。
PEP3105では、printが文であることについて次のような論点が挙げられています(要約):
- アプリケーションレベルの機能なのに、
printだけが専用の構文(文)を持っている -
logging等に置き換えたいときに、文だと置換が面倒 -
>>streamなど特殊構文が進化の足かせになりやすい - 区切り文字や末尾文字(改行など)の指定が文だとやりにくい
そして仕様として、sep / end / file を持つprint()関数のシグネチャが提案されます。
def print(*args, sep=' ', end='\n', file=None)
また、互換性の説明として、Python2の print ("Hello", "world") がタプル表示になる例も、PEP中でそのまま紹介されています。
さらに「Python3化の計画メモ」であるPEP3100にも、printを関数に置き換える方針が明記されています。
ここは、Pythonが「構文を増やすのは最後の手段」という姿勢で整理されていった例としても読みやすいです。
map/filterがlistを返していた
次は、mapとfilterの話です。
Python2のmapは「結果のリストを返すもの」でした。
>>> numbers = [1, 2, 3]
>>> evens = map(lambda x: x * 2, numbers)
>>> type(evens)
<type 'list'>
この挙動は分かりやすいです。
ただし、分かりやすさの代わりに「スケールしない」ポイントが出てきます。
Python2におけるmap/filterの問題点
mapがリストを返すということは、裏でこういうことをしています。
- 入力の全要素に対して関数を適用しきる
- その結果を全部メモリ上に確保する
- それから返す
なので、
- 最初の数件だけ欲しい
- 条件を満たしたら途中で止めたい
- 入力が巨大、入力が無限列(ジェネレータ)
みたいな状況で、一気に苦しくなります。
Python3での変更点
Python3ではmapやfilterはイテレータを返します。
>>> numbers = [1, 2, 3]
>>> evens = map(lambda x: x * 2, numbers)
>>> type(evens)
<class 'map'>
>>> next(evens)
2
呼び出した時点では計算せず、必要になったときに少しずつ計算されます。
これで、
- 評価タイミングを呼び出し側がコントロールできる
- 巨大データをストリーミング的に扱える
-
mapを合成してもムダに中間リストが増えない
といった恩恵が出ます。
この「イテレータ/ジェネレータを基本にする」方向性は、PEP3100や、What’s New in Python3.0でも明記されています。
What’s Newでは「list(map(...))で一時しのぎはできるが、リスト内包表記やforループの方が良いことも多い」という話まで、わりと踏み込んで書かれています。
型の話
比較演算子<について
次は、Pythonにおける型の扱いがどう変わったかを見ていきます。
例として、比較演算子<を見てみます。
1 < "a"
Python3ではこうなります。
>>> 1 < "a"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'int' and 'str'
「intとstrは比較できないのでTypeError」は、ある意味イメージ通りです。
ところがPython2は違います。
>>> 1 < "a"
True
Python2の比較演算子の仕様
Python2.7の公式ドキュメント(Built-in Types)には、異なる型同士の比較について次のように書かれています(要旨):
- 異なる型(数値型どうし等を除く)は基本的に
equalにはならない - ただし異種型でもソートできるように、一貫してはいるが任意の順序(ordered consistently but arbitrarily)が与えられる
- CPython実装詳細として、(数値型を除き)異種型の順序は型名で決まる/比較できない同型はアドレス順になることがある
つまり、1 < "a" が True になるのは「意味のある比較だから」ではなく、ソートを成立させるための任意の順序付けの結果です。
この手の挙動は、デバッグするときにだいたい心を削ってきます。
Python3での方針
Python3.0では比較ルールが単純化され、What’s New in Python3.0では次の方針が明記されています(要約):
- 意味のある自然な順序(meaningful natural ordering)がないなら、
<,<=,>,>=はTypeErrorを投げる - その結果として、型が混ざったリストのソートは「そもそも意味がない」ので成立しなくなる
- ただし
==と!=は別扱いで、比較不能な異なる型同士は常に unequal(これはOK) -
list.sort()/sorted()からcmp引数が消え、keyを使う -
cmp()や__cmp__()も廃止され、rich comparison(__lt__など)へ誘導される
PEP3100にも「== / != 以外の異種型比較は例外にする」という方針が箇条書きで書かれています。
ここは、The Zen of Pythonの「Explicit is better than implicit」「In the face of ambiguity, refuse the temptation to guess」あたりの空気が、仕様に落ちてきた例として読みやすいです。
型アノテーションの話
比較の話は「実行時の型の扱い」でしたが、次は「読み手にとっての型」を見ます。
PEP484について
PEP484では、型ヒントを導入しつつも、
- Pythonは動的型付け言語のままである
- 型ヒントを必須にするつもりはない
という非ゴール(Non-goals)が明確に書かれています。
つまり、Python3は「型を強制する」方向ではなく、
「型を書けるようにして、ツール(型チェッカー等)に任せられる余地を増やす」方向に舵を切った、ということだと理解しています。
typing.Listからlistへ(PEP585)
型アノテーションが入った当初、コンテナ型はこう書く必要がありました。
from typing import List
def f(numbers: List[int]) -> List[int]:
...
当時は組み込みのlistがジェネリックではなかったので、型引数を受け取れるのはtyping.List側だけでした。
しかしこの書き方は、実行時の型(list)と注釈の型(typing.List)がズレていて、学習コストもツール実装も増えがちです。
そこでPEP585で、list[int] のように組み込みコレクションをそのままジェネリックにする方向に整理されました。
PEP585では、typingに並行したコレクション階層が存在していたこと自体が、経緯として説明されています。
Python3.9以降では、次の書き方ができます。
def f(numbers: list[int]) -> list[int]:
...
見た目がシンプルになっただけでなく、「注釈と実行時の型を一致させる」方向に寄せて、特別扱いを減らしているのが分かります。
クラスの話
最後にクラスです。まずはPython2っぽいクラスを書いてみます。
class User(object):
def __init__(self, id, name):
self.id = id
self.name = name
この時代のクラスは、もちろん悪いわけではないのですが、読み手にとっては情報が不足しがちです。
- フィールドは
__init__を読まないと分からない -
idやnameの型は分からない - 比較・表示・等価性は基本的に自前実装
- クラスが「何を表すか」は文脈頼りになりがち
つまり、クラスの意味を理解するには「実装を読んで推測する」比率が高くなります。
dataclassでクラスが一気に“宣言的”になった(PEP557)
Python3.7でdataclassが入り、状況が大きく変わりました。
from dataclasses import dataclass
@dataclass
class User:
id: int
name: str
これだけで「このクラスは何を持つか」が宣言できます。
さらに必要なら、frozen=True や order=True のように、ふるまいも宣言できます。
PEP557では、dataclassが何をするかがかなり明確に書かれています。
- PEP526の型注釈(Variable Annotations)を使って、フィールドを見つける
-
__init__/repr/ 比較メソッド等を自動生成する - ただしデコレータは「普通のクラス」にメソッドを足して返すだけ(特別な新型のオブジェクトにしない)
またPEP557の「どこまでやるか」の線引きも面白くて、
「PEP484/526を超える値の検証(validation)や変換はスコープ外」だと明言されています。
この “線引き” が、そのまま次の話につながります。
境界ではpydanticが刺さる
dataclassや型ヒントは「構造」を表現するのが得意ですが、境界で必要になるのは「入力の検証・変換」です。
ここで代表例として登場するのがpydanticです。
現行のPydanticドキュメントでは「pure, canonical Python3.9+」で型注釈をベースに検証できる、と明記されています。
またPEP593のtyping.Annotatedで「型+メタデータ(制約など)」を同じ場所に書けるようになり、
PEP681のdataclass_transformは「dataclassっぽい振る舞いをするライブラリ」を型チェッカーが理解するための枠組みを整えています。
最後に:Coding Agent時代に、あえて昔を振り返る理由
ここまでPython2.7とPython3の差分を、関数・型・クラスの観点でざっと見てきました。
もちろん全然足りません。unicode/str問題、long型、例外周り、dictのビュー化など、積もる話はたくさんあります。
ただ、今回の範囲だけでも「Pythonがどんな方向に寄っていったか」は割と見えます。
- 特殊な構文や暗黙の挙動を減らす(
printの関数化、比較の単純化など) - eagerなリスト生成を減らし、イテレータを基本に寄せる(
map/filterなど) - “書ける型”を増やし、ツールに任せる余地を増やす(PEP484/526/585)
- クラスを宣言的に書けるようにして、ノイズを減らす(
dataclass)
Coding Agentが当たり前になってきた今でも、最終的に責任を持つのは人間です。
そのときに「なぜこの書き方が良いのか」「どこが暗黙で危ないのか」を言語仕様と思想のレベルで説明できると、判断が速くなります。
そして自分も最新の言語使用と思想を学ぶためにEffective Python第3版を読んで年を越そうと思います。
ありがとうございました。
参考文献
-
Effective Python 第1版 : https://amzn.asia/d/bu2z4K0
-
Effective Python 第2版 : https://amzn.asia/d/ecci2kP
-
Python2とPython3の違いまとめ : https://qiita.com/cstoku/items/353fd4b0fd9ed17dc152
-
PEP3100: Miscellaneous Python3.0 Plans : https://peps.python.org/pep-3100/
-
PEP3105: Make print a function : https://peps.python.org/pep-3105/
-
What’s New in Python3.0 : https://docs.python.org/3/whatsnew/3.0.html
-
Python2.7 docs: Built-in Type : https://stackless.readthedocs.io/en/2.7-slp/library/stdtypes.html
-
PEP20: The Zen of Python : https://peps.python.org/pep-0020/
-
PEP484: Type Hints : https://peps.python.org/pep-0484/
-
PEP526: Syntax for Variable Annotations : https://peps.python.org/pep-0526/
-
PEP585: Type Hinting Generics in Standard Collections : https://peps.python.org/pep-0585/
-
PEP557: Data Classes : https://peps.python.org/pep-0557/
-
PEP593: Flexible function and variable annotations : https://peps.python.org/pep-0593/
-
PEP681: Data Class Transforms : https://peps.python.org/pep-0681/
-
Pydantic documentation : https://docs.pydantic.dev/latest/