Python __getattr__とは?呼ばれるタイミング・__getattribute__との違いと無限再帰の防ぎ方

Pythonの__getattr__は、通常の方法で属性が見つからなかったときだけ呼ばれる特殊メソッドです。存在しない属性へのアクセスを横取りして既定値を返したり、別のオブジェクトに処理を任せたりできますが、見つからない場合は必ずAttributeErrorを送出する必要があります。

よく似た名前の組み込み関数getattr()(属性名を文字列で指定して取り出す関数)とは別物です。getattr()の使い方はPythonのgetattr()とは?で解説しています。この記事では、Python 3.12で実際に動かした結果をもとに、__getattr__が呼ばれるタイミング、__getattribute__との違い、無限再帰やcopyで壊れる落とし穴、モジュールの__getattr__までを解説します。

__getattr__は「見つからなかったときだけ」呼ばれる

class Config:
    def __init__(self):
        self.x = 1
    def __getattr__(self, name):
        print("__getattr__:", name)
        return f"既定({name})"

c = Config()
print(c.x)
print(c.y)
print(getattr(c, "z"))

実行結果:

1
__getattr__: y
既定(y)
__getattr__: z
既定(z)

c.xは通常の探索で見つかるので、__getattr__は呼ばれません。呼ばれるのはc.yのように見つからなかったときだけです。組み込み関数のgetattr(c, "z")も同じ仕組みで動くので、見つからなければ__getattr__に回ります。

公式ドキュメントでは、__getattr__はデフォルトの属性アクセスがAttributeErrorで失敗したときに呼ばれ、計算した値を返すかAttributeErrorを送出すべきだと説明されています。ここでいう失敗には、__getattribute__()が見つけられなかった場合と、プロパティ(__get__())の中でAttributeErrorが起きた場合の2つが含まれます。後者は落とし穴になるので後で取り上げます。

__getattribute__との違い

両方を定義して、呼ばれる順番を確かめます。

class Always:
    def __init__(self):
        self.x = 1
    def __getattribute__(self, name):
        print("__getattribute__:", name)
        return object.__getattribute__(self, name)
    def __getattr__(self, name):
        print("__getattr__:", name)
        return 0

a = Always()
print(a.x)
print(a.y)

実行結果:

__getattribute__: x
1
__getattribute__: y
__getattr__: y
0
__getattr__ __getattribute__
呼ばれるタイミング 見つからなかったときだけ すべての属性アクセスで毎回
呼ばれる順番 __getattribute__が失敗したあと 最初
実装の難しさ 比較的安全 誤ると何も動かなくなる
主な用途 既定値、委譲、遅延読み込み アクセスのログ、全属性の制御

ほとんどの用途は__getattr__で足ります。__getattribute__を使うのは、存在する属性へのアクセスまで横取りしたい場合に限ります。

落とし穴1:self.○○を書くと無限再帰

class Bad:
    def __getattr__(self, name):
        return self.data[name]

try:
    Bad().foo
except RecursionError as e:
    print("RecursionError:", e)

実行結果:

RecursionError: maximum recursion depth exceeded

self.dataも存在しないので、また__getattr__が呼ばれ、その中でまたself.data……と自分を呼び続けます。

落とし穴2:copyやpickleで無限再帰になる

普段は__init__でself.dataを作っているので問題が出なくても、copy.copy()やpickleで壊れることがあります。

import copy
class Wrapper:
    def __init__(self, data):
        self.data = data
    def __getattr__(self, name):
        return self.data[name]

w = Wrapper({"a": 1})
print(w.a)
try:
    copy.copy(w)
except RecursionError as e:
    print("copy.copy -> RecursionError")

実行結果:

1
copy.copy -> RecursionError

copyは__init__を呼ばずに新しいオブジェクトを作り、そのうえで__setstate__などの特殊メソッドがあるかを調べます。このときself.dataがまだないため、落とし穴1と同じ再帰が起きます。

安全な書き方

self.__dict__を直接見る、特殊メソッド名(__で始まる名前)は即座に断る、見つからなければAttributeErrorを送出する、の3点を守ります。

import copy
class Wrapper:
    def __init__(self, data):
        self.data = data
    def __getattr__(self, name):
        if name.startswith("__"):
            raise AttributeError(name)
        try:
            return self.__dict__["data"][name]
        except KeyError:
            raise AttributeError(f"{type(self).__name__!r} has no attribute {name!r}") from None

w = Wrapper({"a": 1})
w2 = copy.copy(w)
print(w2.a)
try:
    w.zz
except AttributeError as e:
    print("AttributeError:", e)
print(hasattr(w, "zz"))

実行結果:

1
AttributeError: 'Wrapper' has no attribute 'zz'
False

self.__dict__は通常の探索で必ず見つかるので、__getattr__を再帰させません。

落とし穴3:Noneを返すとhasattr()が常にTrueになる

class Silent:
    def __getattr__(self, name):
        return None

print(hasattr(Silent(), "なんでも"))

実行結果:

True

hasattr()は属性を取り出してみて、AttributeErrorが出るかどうかで判定しています。__getattr__が何でもNoneを返すと、どんな名前でも「ある」ことになり、hasattr()で分岐している標準ライブラリやフレームワークの動作まで狂います。hasattr()についてはPythonのhasattr()の記事も参考にしてください。

落とし穴4:プロパティの中のミスが隠れる

公式ドキュメントの説明どおり、プロパティの中でAttributeErrorが起きると、そのプロパティ名で__getattr__が呼ばれます。

class User:
    def __init__(self):
        self.first = "Taro"
    @property
    def full(self):
        return self.first + " " + self.lsat   # last のつづり間違い
    def __getattr__(self, name):
        print("__getattr__:", name)
        raise AttributeError(f"no attribute {name!r}")

try:
    User().full
except AttributeError as e:
    print("AttributeError:", e)

実行結果:

__getattr__: lsat
__getattr__: full
AttributeError: no attribute 'full'

本当の原因はlsatのつづり間違いなのに、エラーメッセージは「fullがない」になっています。プロパティと__getattr__を同じクラスで使うときは、このすり替わりを知っておくとデバッグが速くなります。

使いどころ1:辞書を属性のように読む

class Response:
    def __init__(self, data):
        self._data = data
    def __getattr__(self, name):
        try:
            return self.__dict__["_data"][name]
        except KeyError:
            raise AttributeError(name) from None

r = Response({"status": "ok", "count": 3})
print(r.status)
print(r.count)

実行結果:

ok
3

ただし、エディタの補完が効かず、つづり間違いが実行時まで分かりません。キーが決まっているならTypedDictや@dataclassのほうが安全です。

使いどころ2:別のオブジェクトに処理を任せる(委譲)

class Logged:
    def __init__(self, obj):
        self._obj = obj
    def __getattr__(self, name):
        attr = getattr(self.__dict__["_obj"], name)
        print("委譲:", name)
        return attr

lst = Logged([3, 1, 2])
lst.append(0)
lst.sort()
print(lst._obj)
try:
    len(lst)
except TypeError as e:
    print("TypeError:", e)
print(lst.__len__())

実行結果:

委譲: append
委譲: sort
[0, 1, 2, 3]
TypeError: object of type 'Logged' has no len()
委譲: __len__
4

普通のメソッドは中身のリストに任せられますが、len(lst)はTypeErrorになりました。len()や+などが使う特殊メソッドは、インスタンスではなくクラスから直接探されるため、__getattr__を通りません(特殊メソッド検索)。lst.__len__()と明示的に書けば__getattr__を通ります。ラッパーでlen()や演算子も使いたいなら、__len__などをクラスに定義する必要があります。

モジュールの__getattr__(廃止予定の名前を案内する)

Python 3.7から、モジュールのトップレベルに__getattr__(name)関数を書くと、そのモジュールにない名前へのアクセスを横取りできます(公式ドキュメント)。よく使われるのは、古い関数名に警告を出しつつ新しい関数を返す使い方です。

# mylib.py
import warnings
def new_func():
    return "new"
def __getattr__(name):
    if name == "old_func":
        warnings.warn("old_func は廃止予定です。new_func を使ってください", DeprecationWarning, stacklevel=2)
        return new_func
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
# 呼び出し側
import warnings
warnings.simplefilter("always")
import mylib
print(mylib.old_func())
try:
    mylib.nothing
except AttributeError as e:
    print("AttributeError:", e)

実行結果:

main.py:5: DeprecationWarning: old_func は廃止予定です。new_func を使ってください
  print(mylib.old_func())
new
AttributeError: module 'mylib' has no attribute 'nothing'

(1行目のファイル名と行番号は、呼び出し側のスクリプトによって変わります。)

よくある質問

Q. __getattr__と__getattribute__の違いは何ですか?

__getattr__は通常の探索で属性が見つからなかったときだけ呼ばれ、__getattribute__はすべての属性アクセスで毎回呼ばれます。両方ある場合は__getattribute__が先に呼ばれ、AttributeErrorになったときだけ__getattr__が呼ばれます。

Q. __getattr__とgetattr()の違いは何ですか?

getattr()は属性名を文字列で指定して値を取り出す組み込み関数で、__getattr__はクラスに定義して見つからない属性へのアクセスを横取りする特殊メソッドです。getattr()で見つからない属性を取り出そうとすると、そのクラスの__getattr__が呼ばれます。

Q. __getattr__でRecursionErrorになるのはなぜですか?

__getattr__の中でself.dataのように存在しない属性を読むと、また__getattr__が呼ばれて自分を呼び続けるためです。self.__dict__を直接読み、特殊メソッド名は即座にAttributeErrorにすると防げます。copyやpickleは__init__を呼ばずにオブジェクトを作るので、そこで初めて発生することもあります。

Q. __getattr__で見つからないときは何を返せばいいですか?

何も返さず、AttributeErrorを送出します。Noneを返すとhasattr()がどんな名前でもTrueを返すようになり、標準ライブラリやフレームワークの判定が狂います。

まとめ

  • __getattr__は属性が見つからなかったときだけ呼ばれる(__getattribute__は毎回)
  • 中でself.○○を読むと無限再帰。self.__dict__を使い、__で始まる名前は断る
  • copyやpickleは__init__を通らないので、そこで初めて壊れることがある
  • 見つからなければAttributeErrorを送出する。Noneを返すとhasattr()が狂う
  • プロパティ内のAttributeErrorは、プロパティ名の__getattr__呼び出しにすり替わる
  • len()などの特殊メソッドは__getattr__を通らない
  • モジュールにも__getattr__を書ける(3.7以降)

あわせて読みたい

対面・無料|東京・永田町で毎週土曜日13時

1回の参加で、プログラミングの構造がわかる

ゼロからはじめるPython入門講座

8年間・累計600回開催/これまで全国で2万人以上が参加

「やはり対面で聞けるのはとても気づきがあり、価値があると思います」

参加者の声

  • 本当に初心者からの講座です。プログラミングに触ったことがなくて大丈夫です
  • 対面だから、その場で聞けます。つまずいた所を持ち帰らずに解決できます
  • 講座は1時間。そのあとも残って学習を続けられ、講師にも質問できます
  • 独学で挫折した方も。糸口が見つかって、講座のあと会員になる方が多くいます

無料で参加する|開催日程を見る

参加費は無料です