← melang

Python

melangの各noteで他言語と比較した、Pythonの設計をまとめたページ。ここに書かれている内容は、各noteの比較表から自動で集めたものです。

型

値は型を持つが、検査は主に実行時。

age = 20
age = 20
age = "Alice"  # 再代入で型が変わる
age + 10       # 実行して初めて TypeError になる

変数自体には型がなく、値の側に型が紐づく。型の不整合は、その値を実際に使った時点で初めて顕在化する。

型注釈の位置

postfix構文の型ヒントを後付けできるが、実行時には無視される。

age: int = 20
age: int = 20
name = "Alice"  # 型ヒントなしでも書ける(動的型付け)

def greet(name: str) -> str:  # 引数・戻り値ともpostfixで明示できる
    return f"Hello, {name}"

変数・引数・戻り値のすべてに`: Type`というpostfixの型ヒントを書けるが、これはあくまで静的解析ツール(mypy等)のためのアノテーションで、実行時の型チェックには影響しない。「型注釈の構文を持つこと」と「静的型付けであること」が別問題であることを示す例。

型変換

型名を関数のように呼んで変換し、失敗は例外で表す。

n = int(s)
n = int("42")
f = float(n)
s = str(n)

int("abc")  # ValueError
int(3.9)    # 3(小数部は切り捨て)

try:
    n = int(text)
except ValueError:
    n = None

`int(x)`, `float(x)`, `str(x)`のように型名で変換する。失敗は`ValueError`で表され、`try`/`except`で処理する。整数は任意精度なので数値の範囲外による失敗は起きないが、失敗可能性は関数の型からは分からない。

暗黙の型変換

数値の型階層内では暗黙変換されるが、他の型とは変換されない。

x = 10
x = 10
y = 1.5
x + y   # 3.5(intがfloatへ暗黙変換されて演算される)
x + "1" # TypeError(数値と文字列の暗黙変換はない)

数値の型階層(int -> float -> complex)内では暗黙変換が行われるが、数値と文字列のような異なる種類の型の間では暗黙変換は起きず、実行時にエラーになる。

Enum / Sum Type

enum.Enumはpayloadを持たず、網羅性チェックも基本ない。

class Status(Enum):
    PENDING = "pending"
from enum import Enum

class Status(Enum):
    PENDING = "pending"
    RUNNING = "running"

match status:
    case Status.PENDING:
        ...
    case Status.RUNNING:
        ...
    # 網羅チェックはmypy等の型チェッカに頼る必要がある

標準ライブラリの`enum.Enum`はpayloadを持たず、`match`文自体に網羅性チェックは基本的にない。型チェッカ(`mypy`等)で`assert_never`などを使えば擬似的に検証できる。

値の不変性

list・dict・オブジェクトは変更できる。不変な型は個別に用意されている。

@dataclass(frozen=True) / tuple / frozenset
from dataclasses import dataclass, replace

@dataclass(frozen=True)
class User:
    name: str
    tags: tuple = ()

a = User("alice")
# a.name = "bob"             # FrozenInstanceError
b = replace(a, name="bob")   # 新しい値を生成

tags = ["a"]
tags.append("admin")         # OK: listは変更できる
NAME = "x"; NAME = "y"       # OK: 定数を宣言する仕組みはない

`list`・`dict`・`set`・通常のクラスのインスタンスは変更でき、それが一般的な使い方である。`tuple`・`frozenset`・`str`・`frozen=True`のdataclassは不変だが、`frozen`は浅く、中身が`list`なら変更できる。定数を宣言する仕組みは言語にない。

名前付き引数

位置引数とキーワード引数を併用でき、`*`でキーワード専用にもできる。

def foo(width: int, height: int = 0)
def foo(width: int, height: int = 0): ...
foo(width=10, height=20) # キーワード引数
foo(10, height=20)       # 位置引数と併用可

def bar(*, width: int, height: int): ... # * より後ろはキーワード専用
bar(width=10, height=20)
# bar(10, 20) # NG: キーワード専用引数は位置渡しできない

位置引数とキーワード引数を併用でき、`*`を使うと以降の引数をキーワード専用にできる。デフォルト引数と組み合わせて、オプションが多いAPIでもよく使われる。

関数の引数

すべての引数がオブジェクトへの参照。変更可能な型は共有される。

def f(n, items):
def f(n, items):
    n = 1            # 呼び出し側には影響しない(再束縛)
    items.append(1)  # 呼び出し側から見える(同じlist)

xs = []
f(0, xs)
print(xs)  # [1]

「オブジェクト参照渡し」と呼ばれ、値渡しとも参照渡しとも言い切れない。同じ引数でも、`int`や`str`のような不変な型は影響せず、`list`や`dict`のような可変な型は影響する。可変か不変かを知らないと、呼び出しの結果を予測できない。

if 文/式

`if`はstatement。値の分岐は条件式`a if cond else b`で書き、この形では`else`が必須。

if cond: ... else: ... / a if cond else b
x = 1 if cond else 2   # 条件式はelseが必須

if cond:
    process()

`if`はstatementで、値を返す分岐は条件式(`a if cond else b`)として別に用意されている。この形では`else`を省略できず、「値の分岐は`else`が必須」という点はmelangと同じ考え方になる。ただし、手続きの`else`は自由に使える。

I/O の明示

型ヒントは任意で、I/Oを示す標準の仕組みもない。

def find_user(id: UserId) -> User: ...
def find_user(id: UserId) -> User:
    return db.query(id)

def get_user(id: UserId) -> User:
    return find_user(id)  # 呼べる。型チェッカーも止めない

`async def`は非同期関数を示すが、型ヒントと同じく強制力がなく、実行時にもI/Oの有無を判別する仕組みはない。

言語名

イギリスのコメディ番組名に由来し、動物のパイソンとは無関係。

開発者Guido van RossumがMonty Python's Flying Circus(イギリスのコメディ番組)のファンだったことに由来し、ヘビの意味ではない。ただし現在ではヘビをモチーフにしたロゴ・グッズ・コミュニティ文化が定着しており、由来と現在のイメージが乖離した例。