はじめに
『かがみの孤城』円盤発売まであと6️⃣日、nikkieです。
openai-pythonライブラリに関する小ネタです。
目次
- はじめに
- 目次
- APIのレスポンスの扱い方
- OpenAIObjectは辞書を継承している
- OpenAIObjectインスタンスで.を使えるのは__getattr__を実装しているから
- __getattr__のうち、例外送出の実装に目を向ける
- 終わりに
- P.S. 『ロバストPython』5章より、辞書の継承は微妙かも
APIのレスポンスの扱い方
動作環境です。
- Python 3.10.9
- openai 0.27.8
では、ChatGPTとおしゃべりしましょう。
>>> import openai >>> response = openai.ChatCompletion.create(model="gpt-3.5-turbo", messages=[{"role": "user", "content": "こんにちは。あなたの名前は?"}], temperature=0) >>> type(response) <class 'openai.openai_object.OpenAIObject'>
「こんにちは。あなたの名前は?」というプロンプトへの回答はレスポンスに含まれていて、以下のようにアクセスできます:
>>> response["choices"][0]["message"]["content"] 'こんにちは。私はAIアシスタントです。名前はありません。何かお手伝いできますか?' >>> response.choices[0].message.content 'こんにちは。私はAIアシスタントです。名前はありません。何かお手伝いできますか?'
[]でも.でもアクセスできるのは便利ですね!
では、どのようにしてこれが可能になっているのでしょうか?
私、気になります!とソースコードを見てみました。
OpenAIObjectは辞書を継承している
https://github.com/openai/openai-python/blob/v0.27.8/openai/openai_object.py#L11
class OpenAIObject(dict):
辞書を継承しているので、[]でアクセスできるわけですね。
ちなみにobj["key"]のように[]を使ったとき、objに実装されている__getitem__が呼び出されています。
https://docs.python.org/ja/3/reference/datamodel.html#object.__getitem__
self[key]の値評価 (evaluation) を実現するために呼び出されます。マップ 型の場合は、 key に誤りがある場合(コンテナに含まれていない場合)、 KeyError を送出しなければなりません。
OpenAIObjectインスタンスで.を使えるのは__getattr__を実装しているから
obj.xのように.を使ったとき、雑に言うと、objに実装されている__getattr__が呼び出されます。
https://docs.python.org/ja/3/reference/datamodel.html#object.__getattr__
デフォルトの属性アクセスが AttributeError で失敗したとき (略) に呼び出されます。
デフォルトの属性アクセスとはobjがx属性を持っているときのアクセスという理解です1。
OpenAIObjectの場合、ソースコードの中にchoices属性の設定は見つからないようなので、デフォルトの属性アクセスが失敗し__getattr__が呼ばれます。
実装は以下です。
https://github.com/openai/openai-python/blob/v0.27.8/openai/openai_object.py#L55-L61
def __getattr__(self, k): if k[0] == "_": raise AttributeError(k) try: return self[k] except KeyError as err: raise AttributeError(*err.args)
見てください、return self[k]!
(辞書なので持っている)__getitem__メソッドを呼び出しています!
ここからresponse["choices"]とresponse.choicesが同じデータを返すことが分かります。
.でつないでresponse.choices[0].message.contentと書けるのはself[k]が返したオブジェクトもまたOpenAIObjectになるからという理解です。
先日のエントリでも、APIのレスポンスから再帰的にOpenAIObjectが作られることを確認しました。
OpenAIObjectはリストとOpenAIObjectを組み合わせているわけです。
__getattr__のうち、例外送出の実装に目を向ける
もう一度__getattr__メソッドの定義を見てみましょう。
今度注目したいのは「k[0] == "_"であればAttributeErrorを送出する」という実装です。
kはstr型の値ですから、アンダースコアで始まる文字列ならばAttributeErrorを送出という実装です。
「もしや他の言語で言うプライベートな属性に近い実装を試みている?」と思いましたが、__getattr__のドキュメントを思い出すとその期待のようには動いていなさそうです。
例えば、OpenAIObjectには_response_msという属性があります2。
https://github.com/openai/openai-python/blob/v0.27.8/openai/openai_object.py#L30
これにアクセスするとAttributeErrorが送出される!と思いきや、値が返ってきます。
>>> response._response_ms
1662
なぜかというと、__getattr__が呼び出されるのはデフォルトの属性アクセスが失敗したときだから、という理解です。
_response_msという属性はデフォルトのアクセスで成功するので、__getattr__は呼び出されません。
デフォルトのアクセスで失敗する属性については、アンダースコアで始まるとAttributeErrorが送出されます。
>>> response._choices Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/.../venv/lib/python3.10/site-packages/openai/openai_object.py", line 57, in __getattr__ raise AttributeError(k) AttributeError: _choices
終わりに
openai-pythonライブラリでresponse.choicesともresponse["choices"]とも書ける理由を見てきました。
OpenAIObjectに__getattr__を実装しているのでresponse.choicesと書けるOpenAIObjectは辞書を継承しているのでresponse["choices"]と書ける
__getattr__は__getitem__(evaluation)を呼び出す実装なので、どちらで書いても同じデータにアクセスできます!
「アンダースコアで始まる属性にはアクセスできないってこと!?」と思って浮足立ちましたが、デフォルトの属性アクセスが失敗したときに__getattr__が呼ばれるという言語仕様により、Pythonにプライベートはないというのを今回も思い知りました。
P.S. 『ロバストPython』5章より、辞書の継承は微妙かも
実装を見ていて思い出したのが、『ロバストPython』の5章3。
dictを継承する例が紹介されます。
dictが持っている__getitem__を、継承先のクラスでオーバーライドするという例です。
書籍では、継承先のクラスのインスタンスのgetメソッド4を呼び出したときに、オーバーライドした__getitem__が使われないと指摘しています。
辞書を継承してメソッドをオーバライドしても、そのメソッドが辞書のその他のメソッドから呼び出されるという保証はない。(5.4.2)
(なぜ保証がないのか、詳しくは書籍をどうぞ)
書籍でのオススメはUserDictの使用です。
https://docs.python.org/ja/3/library/collections.html#collections.UserDict
__getattr__は辞書にないメソッドなのでOpenAIObjectの実装はワークしていますが、『ロバストPython』を踏まえるとOpenAIObjectはUserDictで実装するのがよいのかもしれません。
プルリクチャンスだ!