はじめに
『かがみの孤城』円盤発売まであと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
で実装するのがよいのかもしれません。
プルリクチャンスだ!