【问题标题】:How to use Python type hints with Django QuerySet?如何在 Django QuerySet 中使用 Python 类型提示?
【发布时间】:2017-07-12 20:34:28
【问题描述】:

是否可以使用 Python 类型提示在 Django QuerySet 中指定记录类型? QuerySet[SomeModel] 之类的东西?

例如,我们有模型:

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

我们想将该模型的 QuerySet 作为参数传递给 func:

def somefunc(rows: QuerySet):
    pass

但是如何在 QuerySet 中指定记录的类型,比如List[SomeModel]:

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

但是使用查询集?

【问题讨论】:

标签: python django type-hinting


【解决方案1】:

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

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 上可用论据。

【讨论】:

【解决方案2】:

有一个名为django-stubs(名称follows 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'

还有模特和QuerySets:

# 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]

【讨论】:

    【解决方案3】:

    我创建了这个帮助类来获得泛型类型提示:

    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])。

    【讨论】:

      【解决方案4】:

      这是 Or Duan 的一个改进的辅助类。

      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]: ...
      

      此类专门用于QuerySet 对象,例如当您在查询中使用filter 时。
      示例:

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

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

      注意
      这种类型提示的格式从python3.6 开始提供。


      你也可以使用django_hint,它有专门针对 Django 的提示类。

      【讨论】:

      • 如果有人在使用 VSCode,请快速注意;如果响应类型被单引号包围(不知道原因),则此解决方案非常有效; def my_method(self) -> 'QueryType[MyModel]': ...
      【解决方案5】:

      恕我直言,正确的做法是定义一个继承 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
      
      
      

      那么你可以这样使用它:

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

      【讨论】:

        【解决方案6】:

        QuerySet 是返回任何模型的任何 queryset 的函数/方法的好方法。 Django 查询集是可迭代的。但是当返回类型非常特定于一个模型时,使用QuerySet[Model] 比使用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"
        

        【讨论】:

          【解决方案7】:
          from typing import Iterable
          
          def func(queryset_or_list: Iterable[MyModel]): 
              pass
          

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

          【讨论】:

            【解决方案8】:

            如果你导入注解模块,你实际上可以做你想做的事:

            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()
            

            MyPy 和 Python 解释器都不会对此抱怨或引发异常(在 python 3.7 上测试)。 MyPy 可能无法对其进行类型检查,但如果您只想记录返回类型,这应该足够了。

            【讨论】:

              【解决方案9】:

              我发现自己使用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() 和 .getitem() 的东西,与它的实际类型无关。

              【讨论】:

                【解决方案10】:
                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
                

                【讨论】:

                  猜你喜欢
                  • 2017-05-12
                  • 1970-01-01
                  • 1970-01-01
                  • 2020-01-19
                  • 2022-09-30
                  • 2020-02-28
                  • 2023-03-29
                  相关资源
                  最近更新 更多