クラスを使い始めると、user.nameのようにドットで属性を読み書きする書き方にすぐ慣れますよね。ところが、読みたい属性の名前が変数に入っている場面に出会うと、急に手が止まってしまいます。
たとえば、画面で選ばれた項目名に応じて値を取り出したいときです。if文で項目ごとに分岐を書いていくと、項目が増えるたびにコードが長くなっていきます。
そんなときに役立つのが、属性の名前を文字列のまま扱えるgetattr()とsetattr()です。仲間のhasattr()とdelattr()も合わせると、属性の読み書きと確認、削除がひととおりそろいます。
この記事では、4つの関数の基本から実務での使いどころ、そして意外と知られていない落とし穴までを順番に見ていきます。サンプルの実行結果は、手元のPython 3.11で確認したものです。
属性を文字列で扱うとはどういうことか¶
最初に、そもそも属性とは何かをおさらいしておきましょう。属性とは、オブジェクトにくっついている名前付きのデータやメソッドのことです。
ふだん私たちは、user.nameのようにドットのあとへ名前を直接書いて属性にアクセスしています。この書き方はわかりやすい反面、名前をコードに固定で書き込むしかありません。
getattr()を使うと、同じことを getattr(user, "name") と書けます。名前が文字列になるので、変数に入れておいたり、ループで順番に差し替えたりできるようになります。
たとえるなら、ドットでのアクセスは決まった棚へ直接手を伸ばす動きです。一方のgetattr()は、棚の番号を書いたメモを係の人に渡して取ってきてもらうイメージに近いでしょう。
組み込み関数の全体像を先に押さえたい方は、こちらもどうぞ。 【関連記事】Python組み込み関数とは?初心者でもわかる徹底解説
4つの関数をひと目で比べる¶
ここで、今回登場する4つの関数を表で整理しておきます。どれもimportなしで使える組み込み関数です。
| 関数 | 役割 | ドットで書いた場合 | 属性がないとき |
|---|---|---|---|
| getattr(obj, "x") | 属性の値を読む | obj.x | AttributeError(デフォルト値があればそれを返す) |
| setattr(obj, "x", v) | 属性に値を入れる | obj.x = v | 新しく作られる |
| hasattr(obj, "x") | 属性があるか調べる | なし | Falseを返す |
| delattr(obj, "x") | 属性を削除する | del obj.x | AttributeError |
公式ドキュメントでも、getattr(x, "foobar") は x.foobar と同じ意味だと説明されています。つまり、新しい機能というより、ドットの書き方を関数の形に置きかえたものだと考えるとわかりやすいです。
基本の使い方をコードで確かめよう¶
言葉だけではイメージしにくいので、実際に動かしてみましょう。次のコードは、Userクラスに対して4つの関数を順番に使う例です。
class User:
def __init__(self, name, age):
self.name = name
self.age = age
user = User("Sato", 28)
# 属性名を変数に入れて読む
field = "name"
print(getattr(user, field)) # Sato
# 存在しない属性はデフォルト値で受け止める
print(getattr(user, "email", "未登録")) # 未登録
# 属性を追加する
setattr(user, "email", "sato@example.com")
print(user.email) # sato@example.com
# 属性があるかを調べる
print(hasattr(user, "age"), hasattr(user, "phone")) # True False
# 属性を削除する
delattr(user, "age")
print(hasattr(user, "age")) # False
ポイントは、getattr()の3つめの引数です。ここにデフォルト値を渡しておくと、属性がなくてもエラーにならず、その値が返ってきます。
デフォルト値を渡さないとどうなるか¶
では、3つめの引数を省略して、存在しない属性を読もうとするとどうなるのでしょうか。この場合は、ドットで書いたときと同じくAttributeErrorが発生します。
エラーメッセージは 'User' object has no attribute 'email' のような形で、どのクラスに何が足りないのかを教えてくれます。属性がないこと自体が想定外なら、あえてデフォルト値を渡さずにエラーで気づける形にしておくのも立派な選択です。
setattr()は新しい属性も作れる¶
setattr()は、既存の属性を書きかえるだけでなく、まだない属性を新しく作ることもできます。上のコードでemailを追加できたのは、この性質のおかげです。
ただし、どんなオブジェクトでも自由に属性を足せるわけではありません。たとえば組み込みのint型の値や、__slots__で属性を制限したクラスでは、新しい属性を追加しようとするとエラーになります。
実務でよく使う3つのパターン¶
基本がわかったところで、実際の開発でどんな場面に役立つのかを見ていきましょう。ここでは、私が現場でよく見かける3つの使い方を紹介します。
パターン1:メソッドを名前で呼び分ける¶
1つめは、文字列に応じて呼び出すメソッドを切りかえる使い方です。getattr()はメソッドも取り出せるので、取り出したものにカッコを付ければそのまま実行できます。
次の例では、出力形式の名前からto_csvやto_jsonといったメソッドを探して呼んでいます。
class Report:
def to_csv(self):
return "csv形式で出力しました"
def to_json(self):
return "json形式で出力しました"
report = Report()
for fmt in ["csv", "json", "pdf"]:
method = getattr(report, f"to_{fmt}", None)
if method is None:
print(f"{fmt}形式には対応していません")
else:
print(method())
実行すると、csvとjsonは出力され、pdfは対応していませんと表示されます。新しい形式を足したいときは、to_xmlのようなメソッドを1つ追加するだけで済みます。
if文を形式の数だけ並べる書き方と比べると、呼び出し側のコードが一切変わらないのが大きな利点です。
パターン2:辞書の中身を属性にまとめて入れる¶
2つめは、設定ファイルやAPIから受け取った辞書を、オブジェクトの属性へ一括で移す使い方です。キーと値のペアをループで回し、setattr()に渡していきます。
config = {"host": "localhost", "port": 8080, "debug": True}
class Settings:
pass
settings = Settings()
for key, value in config.items():
setattr(settings, key, value)
print(settings.host, settings.port, settings.debug) # localhost 8080 True
print(vars(settings)) # {'host': 'localhost', 'port': 8080, 'debug': True}
最後のvars()は、オブジェクトが持っている属性を辞書の形で見せてくれる関数です。setattr()でちゃんと入ったかを確かめたいときに便利なので、セットで覚えておくとよいでしょう。
辞書の基本については、こちらの記事で詳しく解説しています。 【関連記事】Pythonの辞書(dict)とは?キーと値でデータを管理する基本を初心者向けに解説
パターン3:あるかどうかで処理を分ける¶
3つめは、hasattr()で属性の有無を調べてから処理を分ける使い方です。受け取るオブジェクトの種類が複数あり、一部だけが特定のメソッドを持っているような場面で使われます。
ただ、Pythonでは先に調べるより、まず試して失敗したら対応する書き方が好まれることも多いです。この考え方の違いは、EAFPとLBYLという名前で知られています。
どちらのスタイルを選ぶべきか迷ったら、こちらを参考にしてください。 【関連記事】PythonのEAFP/LBYLとは?「許可より許しを求める」プログラミング哲学とは
hasattr()が隠してしまうバグに注意¶
便利なhasattr()ですが、知っておかないと痛い目を見る性質があります。実はhasattr()の中身は、getattr()を呼んでAttributeErrorが出るかどうかを見ているだけなのです。
これは公式ドキュメントにも明記されています。一見すると何の問題もなさそうですが、プロパティと組み合わせたときに困ったことが起こります。
次のコードを見てください。totalプロパティの中で、priceと書くべきところをpirceと打ち間違えています。
class Order:
def __init__(self, price):
self.price = price
@property
def total(self):
return self.pirce * 2 # priceのタイプミス
order = Order(500)
print(hasattr(order, "total")) # False
本来なら、タイプミスはAttributeErrorとして目に見える形で知らせてほしいところです。ところがhasattr()は、そのエラーを属性がないという意味に読みかえてしまい、静かにFalseを返します。
その結果、totalプロパティはちゃんと定義してあるのに、呼び出し側の分岐では存在しない扱いになります。エラーが出ないぶん、原因にたどり着くまでが遠いのが、この落とし穴のやっかいなところです。
私も以前、あるはずのメソッドが呼ばれない不具合を半日かけて追いかけたことがあります。最終的に、hasattr()で調べていたプロパティの中に、別の属性名の書き間違いが潜んでいたのが原因でした。それ以来、プロパティの有無を調べる場面では、hasattr()より直接アクセスしてエラーを見る書き方を選ぶようにしています。
プロパティの仕組みについては、こちらの記事が参考になります。 【関連記事】Pythonの@propertyとは?メソッドを属性の顔で使わせる書き方を初心者向けに解説
知っておきたい細かなルール¶
ここからは、使っているうちにつまずきやすい細かなルールを紹介します。どれも頻繁に出会うものではありませんが、知っていれば迷わずに済みます。
まず、アンダースコア2つで始まる属性の扱いです。クラスの中で self.__password と書いた属性は、名前マングリングという仕組みで _Account__password のような名前に変えられて保存されます。
そのため、hasattr(account, "__password") はFalseになります。getattr()で読みたい場合は、変換後の名前を自分で書く必要があります。
次に、setattr()ではドットでは書けない名前の属性も作れてしまいます。たとえば setattr(obj, "my-key", 1) は動きますが、ハイフンを含む名前は obj.my-key とは書けないので、getattr()でしか読めません。
これらのルールを表にまとめると、次のようになります。
| ケース | 何が起きるか | どうすればよいか |
|---|---|---|
| __で始まる属性 | 名前が変換されて保存される | 変換後の名前を使うか、そもそも外から触らない |
| ハイフンや空白を含む名前 | setattr()で作れるがドットでは読めない | 識別子として正しい名前だけを使う |
| プロパティ内のAttributeError | hasattr()がFalseを返す | 直接アクセスしてエラーを確認する |
| デフォルト値なしのgetattr() | AttributeErrorになる | 想定内ならデフォルト値を渡す |
名前マングリングの仕組みは、こちらで詳しく解説しています。 【関連記事】Pythonの名前マングリングとは?知らないとハマるクラス設計の落とし穴を徹底解説
使いすぎに気をつけたい理由¶
ここまで読むと、なんでもgetattr()で書きたくなるかもしれません。ですが、属性名を文字列で扱うことにはデメリットもあります。
いちばん大きいのは、エディタや型チェッカーの助けが届きにくくなる点です。user.nmae と書けばエディタがすぐに警告してくれますが、getattr(user, "nmae") の文字列の中身までは、多くの場合チェックしてくれません。
また、コードを読む人にとっても、どの属性が使われているのかを追いにくくなります。検索で属性名を探しても、文字列を組み立てている箇所は見つけにくいからです。
10年ほど開発に関わってきて感じるのは、属性名が最初から決まっているならドットで書くのがいちばん読みやすいということです。getattr()は、名前が実行するまでわからない場面のためにとっておきましょう。
どちらを選ぶかの目安を、表にまとめておきます。
| 状況 | おすすめ |
|---|---|
| 属性名がコードを書く時点で決まっている | ドットで書く |
| 属性名が変数やユーザー入力から来る | getattr()・setattr() |
| 属性がないかもしれず、代わりの値を使いたい | getattr()にデフォルト値を渡す |
| 外部から受け取った文字列でメソッドを呼ぶ | 呼んでよい名前を決めておき、getattr()で取り出す |
最後の行は特に大切です。ユーザーが入力した文字列をそのままgetattr()に渡すと、想定していないメソッドまで呼ばれてしまうおそれがあります。呼んでよい名前の一覧を先に決めておくことで、安全に使えます。
まとめ¶
今回は、属性を文字列で扱う4つの組み込み関数を見てきました。最後にポイントを振り返っておきましょう。
getattr()とsetattr()は、ドットでの読み書きを関数の形にしたものです。属性名を変数で渡せるので、メソッドの呼び分けや辞書からの一括設定がすっきり書けます。
getattr()には3つめの引数でデフォルト値を渡せます。属性がなくても止まらない処理にしたいときは、まずこの書き方を思い出してください。
一方で、hasattr()はプロパティ内のAttributeErrorまで飲み込んでしまいます。あるはずの属性がFalseになったら、直接アクセスしてエラーを確かめてみましょう。
まずは手元のクラスで、getattr()を使って属性を1つ読み出すところから試してみてください。
ここまでお読みいただきありがとうございました。