nikkie-ftnextの日記

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

openai-pythonの実装を見てPEP 661を知り、sentinel valuesの多様な実装が結びつきました(クラス・Enum・ウォルラス)

はじめに

背伸びのVenus1 nikkieです。

ライブラリopenai-pythonの素振りに励むnikkie氏。

「利用者が〇〇と書くだけで、なんでこんなことができちゃうんだろう」と気になったところは、ソースコード(裏の仕組み)も手短に確認します。
そんな中で新たに知ったsentinel valuesをエントリにします。

目次

openai-pythonの型ヒントを見ていて

例えばAssistantsをcreateするメソッド(client.beta.assistants.create)の型ヒント
https://github.com/openai/openai-python/blob/v1.2.3/src/openai/resources/beta/assistants/assistants.py#L40-L56

    def create(
        self,
        *,
        model: str,
        description: Optional[str] | NotGiven = NOT_GIVEN,

description引数の型NotGivenとデフォルト値NOT_GIVENってそれぞれなんだろう?と気になりました。
これらはVS Codeでのコーディング中、提示される型ヒントでもチラチラ見えていました。

import元をたどるとパッケージルートの_types.pyです。
https://github.com/openai/openai-python/blob/v1.2.3/src/openai/_types.py#L271-L293

NOT_GIVEN = NotGiven()ともあります2
デフォルト値にしていたNOT_GIVENNotGivenクラスのインスタンスでした(シングルトンのようです)

NotGivenクラスのコメントに興味深い一文が

Sentinel class used until PEP 0661 is accepted

PEP 661 – Sentinel Values を垣間見

Abstractを簡単に確認します(原文に即すというよりかは今の理解を書いています)。

sentinel value(直訳して、番兵の値)とは、唯一の予め指定されている値(Unique placeholder values

例えば

1つ目の例のようにNoneが使われることが多いですが、Noneをsentinel valueとは区別したい場合は別の値が使われます3

(PEPを離れますが)上で見たNOT_GIVENNoneをsentinel valueとは区別したい場合に該当しますね。
description引数には何らかの文字列かNoneが指定される可能性があり、いずれも指定されない場合はデフォルト値のNOT_GIVENとなります。

このPEPでは標準ライブラリ(stdlib)にsentinel valuesの実装を追加することを提案しています4
参照実装(Reference Implementation)は https://github.com/taleinat/python-stdlib-sentinels にあるとのことですが、PEPのstatusはDraftですし、議論がどうなっているか気になるところです。
ですが、今回はPEP 661の確認はここまでとします。

あれらって全部sentinel valuesの実装だったのか!

さて、PEP 661を覗いたことでこれまでに見た実装のいくつかがsentinel valuesでつながりました!

pandasで見かけたNoDefault

Enumを使ったsentinel valueの実装例です。

https://github.com/pandas-dev/pandas/blob/v2.1.3/pandas/_libs/lib.pyx#L2820-L2832

class _NoDefault(Enum):
    no_default = "NO_DEFAULT"

no_default = _NoDefault.no_default  # Sentinel indicating the default value.

pandasのコードではlib.no_defaultとして使われています。
https://github.com/search?q=repo%3Apandas-dev%2Fpandas%20no_default&type=code

気づいたきっかけはassert_almost_equalでした5
https://github.com/pandas-dev/pandas/blob/v1.4.2/pandas/_testing/asserters.py#L65

def assert_almost_equal(
    check_less_precise: bool | int | NoDefault = no_default,

なお、v2.1.3ではこの引数はなくなっていました。

Djangoで見かけた:=(ウォルラス)

同じ意図ですが、全然別の実装例Djangoにあります6

https://github.com/django/django/blob/4.2.5/django/core/management/utils.py#L164

def run_formatters(written_files, black_path=(sentinel := object())):

:=により、引数black_pathのデフォルト値はobject()が返すインスタンス(☆)です。
(☆)のインスタンスは変数sentinelでも指しています。
関数本体ではif black_path is sentinel:という分岐があり、そのブロックではblack_pathが指定されていないので、その場合の処理をしています

初見で「???」となりましたが、やりたいことはsentinel valuesというのが理解のきっかけとなりました。

終わりに

openai-python中のコメントをきっかけに、Pythonでsentinel valuesを実装するいくつかのやり方を見てきました

  • クラスを使う(openai-python
  • Enumを使う(pandas)
  • ウォルラス := を使う(Django

さて、Zen of Pythonにこういう一節がありますよね。

There should be one-- and preferably only one --obvious way to do it.

Zen of Pythonに対して自己矛盾しているように思われます7

また、PEP 661にはdrawback(欠陥)という語が登場しており8、いろいろなやり方がある(実際、標準ライブラリで実装されている)中でまずい実装もあるようです。

まとめる中で気になってきたので、PEP 661は時間をとって読んでみようと思います。

P.S. ミノ駆動本から、Noneに未設定という意味を持たせるのは悪いコードでは?

良いコード/悪いコードで学ぶ設計入門』9.6です。
未装備をNone(書籍ではJavaなのでnull)で表す悪しきコードを改善します。
Noneを使うのをやめて、未装備状態を表す定数を用意しました。

sentinel valuesについてもこの論は当てはまるのではないかと私は考えています。
気軽にNoneをsentinel valueの意味で使うよりは、標準ライブラリにある方が好ましいように思えます(Zen of Pythonとの矛盾を解決できるという点でも)


  1. この曲も今回取り上げたトピックもSVと略せますね

  2. https://github.com/openai/openai-python/blob/v1.2.3/src/openai/_types.py#L297
  3. However, sometimes an alternative sentinel value is needed, usually when it needs to be distinct from None.」(PEP 661 Abstract)
  4. This PEP proposes adding a utility for defining sentinel values, to be used in the stdlib and made publicly available as part of the stdlib.」(PEP 661 Abstract)
  5. pandasコードリーディング会#5 - connpassで読みました
  6. Django 4.1から環境変数PATHにblackが見つかれば、startprojectやstartappで作ったファイルがフォーマットされるようになってる〜〜!!! - nikkie-ftnextの日記 の宿題にも今回取り組んだ形になります
  7. 召喚した天使様「やり方は1つだけをスローガンとするPythonなのに、sentinel valuesはいくつもの実装方法があるとか、舐めているのですか
  8. However, the common implementations, including some in the stdlib, suffer from several significant drawbacks.」(PEP 661 Abstract)