nikkie-ftnextの日記

イベントレポートや読書メモを発信

openai-pythonでAPIのレスポンスをresponse.choicesともresponse["choices"]とも書けるのはなぜ? OpenAIObjectは辞書を継承したクラスだったのです

はじめに

『かがみの孤城』円盤発売まであと6️⃣日、nikkieです。

openai-pythonライブラリに関する小ネタです。

目次

APIのレスポンスの扱い方

動作環境です。

では、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 で失敗したとき (略) に呼び出されます。

デフォルトの属性アクセスとはobjx属性を持っているときのアクセスという理解です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を送出する」という実装です。

kstr型の値ですから、アンダースコアで始まる文字列ならば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』を踏まえるとOpenAIObjectUserDictで実装するのがよいのかもしれません。
プルリクチャンスだ!


  1. クラス定義でx属性を実装していないとしても、実行時にインスタンスx属性に代入して持たせることもできます
  2. GPTのAPIがレスポンスを返すまでにかかった時間が分かるようです。便利ですね
  3. 感想ブログがこちらです。
  4. 親クラスが辞書なので、getメソッドを持ちますね