web-dev-qa-db-ja.com

型ヒントsqlalchemyクエリ結果

Sqlalchemyクエリが返すオブジェクトの種類がわかりません。

entries = session.query(Foo.id, Foo.date).all()

エントリ内の各オブジェクトのタイプはsqlalchemy.util._collections.resultのようですが、pythonインタプリタのクイックfrom sqlalchemy.util._collections import resultはImportErrorを発生させます。

私が最終的にやろうとしているのは、この関数のヒントを入力することです:

def my_super_function(session: Session) -> ???:
    entries = session.query(Foo.id, Foo.date).all()
    return entries

???の代わりに何を入れればよいですか? mypy(この場合)はList[Tuple[int, str]]で問題ないようです。確かに、エントリにタプルであるかのようにアクセスできますが、たとえばentry.dateでもアクセスできます。

10
JPFrancoia

クラスもインポートできないことに不思議に思いました。答えはかなり長く、私がそれをどのように解決したかを説明してきたので、我慢してください。

Query.all()Queryオブジェクト自体でlist()を呼び出します:

_def all(self):
    """Return the results represented by this ``Query`` as a list.
    This results in an execution of the underlying query.
    """
    return list(self)
_

...リストはオブジェクトを反復処理するため、 Query.__iter__()

_def __iter__(self):
    context = self._compile_context()
    context.statement.use_labels = True
    if self._autoflush and not self._populate_existing:
        self.session._autoflush()
    return self._execute_and_instances(context)
_

... Query._execute_and_instances() メソッドの結果を返します:

_def _execute_and_instances(self, querycontext):
    conn = self._get_bind_args(
        querycontext, self._connection_from_session, close_with_result=True
    )

    result = conn.execute(querycontext.statement, self._params)
    return loading.instances(querycontext.query, result, querycontext)
_

これはクエリを実行し、 sqlalchemy.loading.instances() 関数の結果を返します。その関数には this line があり、単一エンティティ以外のクエリに適用されます。

_keyed_Tuple = util.lightweight_named_Tuple("result", labels)
_

...そして、その行の後にprint(keyed_Tuple)を挿入すると、上記で述べた型である_<class 'sqlalchemy.util._collections.result'>_が出力されます。つまり、そのオブジェクトが何であれ、それは sqlalchemy.util._collections.lightweight_named_Tuple() 関数からのものです。

_def lightweight_named_Tuple(name, fields):
    hash_ = (name,) + Tuple(fields)
    tp_cls = _lw_tuples.get(hash_)
    if tp_cls:
        return tp_cls

    tp_cls = type(
        name,
        (_LW,),
        dict(
            [
                (field, _property_getters[idx])
                for idx, field in enumerate(fields)
                if field is not None
            ]
            + [("__slots__", ())]
        ),
    )

    tp_cls._real_fields = fields
    tp_cls._fields = Tuple([f for f in fields if f is not None])

    _lw_tuples[hash_] = tp_cls
    return tp_cls
_

したがって、重要な部分は このステートメント です。

_tp_cls = type(
    name,
    (_LW,),
    dict(
        [
            (field, _property_getters[idx])
            for idx, field in enumerate(fields)
            if field is not None
        ]
        + [("__slots__", ())]
    ),
)
_

...組み込みのtype()クラスを呼び出します。

3つの引数で、新しい型オブジェクトを返します。これは本質的にクラスステートメントの動的形式です。

クラス_sqlalchemy.util._collections.result_をインポートできないのはこのためです。クラスはクエリ時にのみ作成されるためです。この理由は、列名(つまり、名前付きタプル属性)は、クエリが実行されるまでわからないためです。

From python docstypeの署名は次のとおりです:type(name, bases, dict)ここで:

名前文字列はクラス名であり、___name___属性になります。 basesタプルは基本クラスを項目化し、___bases___属性になります。 dict辞書は、クラス本体の定義を含む名前空間であり、___dict___属性になるように標準辞書にコピーされます。

ご覧のとおり、type()lightweight_named_Tuple()に渡されるbases引数は_(_LW,)_です。したがって、動的に作成された名前付きタプル型は、インポート可能なクラスである _sqlalchemy.util._collections._LW_ から継承されます。

_from sqlalchemy.util._collections import _LW

entries = session.query(Foo.id, Foo.date).all()
for entry in entries:
    assert isinstance(entry, _LW)  # True
_

...したがって、関数をアンダースコアで始まる内部クラスに入力するのが適切な形式かどうかはわかりませんが、__LW_は、Tupleから継承される_sqlalchemy.util._collections.AbstractKeyedTuple_から継承します。タプルのリストなので、_List[Tuple[int, str]]_の現在の型指定が機能するのはこのためです。だからあなたの選択を取り、__LW_、AbstractKeyedTupleTupleはすべて、関数が返すものの正しい表現になります。

8
SuperShoot