如何在 Django QuerySet 中使用 Python 类型提示?

是否可以在 Django QuerySet 中使用 Python 类型提示指定记录类型? 类似于 QuerySet[SomeModel]

例如,我们有一个模型:

class SomeModel(models.Model):
smth = models.IntegerField()

我们想把这个模型的 QuerySet 传递给 func 中的 param:

def somefunc(rows: QuerySet):
pass

But how to specify type of records in QuerySet, like with List[SomeModel]:

def somefunc(rows: List[SomeModel]):
pass

但是用 QuerySet?

51458 次浏览

一种解决方案可能是使用 Union 类型类。

from typing import Union, List
from django.db.models import QuerySet
from my_app.models import MyModel


def somefunc(row: Union[QuerySet, List[MyModel]]):
pass

现在,当您切分 row参数时,它将知道返回的类型要么是 MyModel 的另一个列表,要么是 MyModel 的一个实例,同时还暗示 QuerySet类的方法也可用于 row参数。

我创建这个 helper 类是为了得到一个泛型类型提示:

from django.db.models import QuerySet
from typing import Iterator, Union, TypeVar, Generic


T = TypeVar("T")


class ModelType(Generic[T]):
def __iter__(self) -> Iterator[Union[T, QuerySet]]:
pass

然后像这样使用它:

def somefunc(row: ModelType[SomeModel]):
pass

这减少了我每次使用这种类型的噪音,使它在不同型号(如 ModelType[DifferentModel])之间可用。

这是一个改进的助手类或段。

from django.db.models import QuerySet
from typing import Iterator, TypeVar, Generic


_Z = TypeVar("_Z")


class QueryType(Generic[_Z], QuerySet):
def __iter__(self) -> Iterator[_Z]: ...

This class is used specifically for QuerySet object such as when you use filter in a query.
样本:

from some_file import QueryType


sample_query: QueryType[SampleClass] = SampleClass.objects.filter(name=name)

现在,解释器将 sample_query识别为一个 QuerySet对象,您将得到诸如 count()之类的建议,并且在遍历这些对象时,您将得到关于 SampleClass的建议

注意
这种类型提示的格式可以从 python3.6开始使用。


您还可以使用 提示,它具有专门针对 Django 的提示类。

有一个称为 django-stubs(名称为 跟随 PEP561)的特殊包来键入 django代码。

事情就是这样的:

# server/apps/main/views.py
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render


def index(request: HttpRequest) -> HttpResponse:
reveal_type(request.is_ajax)
reveal_type(request.user)
return render(request, 'main/index.html')

产出:

» PYTHONPATH="$PYTHONPATH:$PWD" mypy server
server/apps/main/views.py:14: note: Revealed type is 'def () -> builtins.bool'
server/apps/main/views.py:15: note: Revealed type is 'django.contrib.auth.models.User'

还有模特和 QuerySet:

# server/apps/main/logic/repo.py
from django.db.models.query import QuerySet


from server.apps.main.models import BlogPost


def published_posts() -> 'QuerySet[BlogPost]':  # works fine!
return BlogPost.objects.filter(
is_published=True,
)

产出:

reveal_type(published_posts().first())
# => Union[server.apps.main.models.BlogPost*, None]

如果导入注释模块,您实际上可以做您想做的事情:

from __future__ import annotations
from django.db import models
from django.db.models.query import QuerySet


class MyModel(models.Model):
pass


def my_function() -> QuerySet[MyModel]:
return MyModel.objects.all()

Neither MyPy nor the Python interpreter will complain or raise exceptions on this (tested on python 3.7). MyPy will probably be unable to type-check it, but if all you want is to document your return type, this should be good enough.

恕我直言,正确的方法是定义一个继承 QuerySet的类型,并为迭代器指定一个通用的返回类型。

    from django.db.models import QuerySet
from typing import Iterator, TypeVar, Generic, Optional


T = TypeVar("T")




class QuerySetType(Generic[T], QuerySet):  # QuerySet + Iterator


def __iter__(self) -> Iterator[T]:
pass


def first(self) -> Optional[T]:
pass


# ... add more refinements




Then you can use it like this:

users: QuerySetType[User] = User.objects.all()
for user in users:
print(user.email)  # typing OK!
user = users.first()  # typing OK!


我发现自己用 typing.Sequence解决了一个类似的问题:

from typing import Sequence




def print_emails(users: Sequence[User]):
for user in users:
print(user.email)




users = User.objects.all()




print_emails(users=users)

据我所知:

序列是支持 len ()和. (咒语)()的任何东西,与它的实际类型无关。

from typing import Iterable


def func(queryset_or_list: Iterable[MyModel]):
pass

查询集和模型实例列表都是可迭代对象。

from typing import (TypeVar, Generic, Iterable, Optional)
from django.db.models import Model
from django.db.models import QuerySet
_T = TypeVar("_T", bound=Model)




class QuerySetType(Generic[_T], QuerySet):
def __iter__(self) -> Iterable[_T]:
pass


def first(self) -> Optional[_T]:
pass

QuerySet is a good approach for function/method returning any 查询集 of any models. The Django queryset is iterable. But when the return type is very specific to one model, it may be better to use QuerySet[Model] over QuerySet.

示例: 筛选公司的所有活动用户

import datetime
from django.utils import timezone
from myapp.models import User
from collections.abc import Iterable


def get_active_users(company_id: int) -> QuerySet[User]:
one_month_ago = (timezone.now() - datetime.timedelta(days=30)).timestamp()
return User.objects.filter(company_id=company_id, is_active=True,
last_seen__gte=one_month_ago)

上述函数签名比 def get_active_users(company_id: int) -> QuerySet:更具可读性

当对其他方法调用返回的查询集时,类型检查器将报告,而 Iterable[User]也将正常工作。

def func() -> Iterable[User]:
return User.objects.all()


users = func()
users.filter(email__startswith='support')

MyPy 输出

"Iterable[User]" has no attribute "filter"