You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Django ORM层面计算大圆距离并解决相关报错

问题描述

我想通过筛选条件获取用户,其中一个核心条件是计算用户间的大圆距离。当前实现抛出错误:TypeError: the float() argument should be a string or a real number, not "F"。我清楚报错原因是F()表达式和geopy的distance函数不兼容,但不知道怎么修复。循环计算所有用户距离的方案性能太差,所以想用annotate实现数据库层面的计算。考虑过写自定义数据库函数,但不确定可行性;也试过GeoDjango,但没找到合适的距离函数。现在用geopy.distance.distance计算距离,作为初学者很困惑,想知道正确的计算参与者间距离的方法。

模型代码

class Participant(models.Model):
    GENDER_CHOICES = [
        ('M', ' Male'),
        ('F', 'Female'),
        ('O', 'Other'),
    ]
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="participant",)
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    gender = models.CharField(max_length=1, choices=GENDER_CHOICES)
    avatar = models.ImageField(upload_to='avatars/')
    email = models.EmailField(unique=True)
    likes = models.ManyToManyField('self', related_name='liked_by', symmetrical=False, blank=True)
    latitude = models.DecimalField(max_digits=9, decimal_places=6)
    longitude = models.DecimalField(max_digits=9, decimal_places=6)

视图代码

class ParticipantListViewTest(generics.ListAPIView):
    queryset = Participant.objects.prefetch_related("likes")
    serializer_class = ParticipantSerializer
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_fields = ['gender', 'first_name', 'last_name']
    search_fields = ['first_name', 'last_name', 'gender']
    ordering_fields = ['first_name', 'last_name', 'gender']

    def get_queryset(self):
        queryset = super().get_queryset()
        max_distance = self.request.query_params.get('distance', None)

        if max_distance:
            user = User.objects.get(id=5).participant

            queryset = queryset.annotate(fact_distance=distance(
                lonlat(user.longitude, user.latitude),
                lonlat(F("longitude"), F("latitude"))).km
                                         ).filter(fact_distance__lt=float(max_distance))
        return queryset
解决方案

核心问题解析

原代码报错的本质是:geopy的distance函数是在Python层面计算的,需要传入真实数值;但F("longitude")是Django的数据库层面表达式,不会立即求值,而是作为查询语句的一部分,所以geopy无法处理F()对象,导致类型错误。最优方案是让数据库直接计算距离,避免把全量数据拉到Python层处理。


方法1:用数据库原生函数计算(兼容多数数据库)

通用Haversine公式实现

如果不想依赖第三方扩展,可以基于Haversine公式自定义数据库函数,直接在SQL层面计算大圆距离:

from django.db.models import F, Func, Value, FloatField

class Haversine(Func):
    function = 'ACOS'
    template = """
        ACOS(
            COS(RADIANS(%(lat1)s)) * COS(RADIANS(%(lat2)s)) *
            COS(RADIANS(%(lon2)s) - RADIANS(%(lon1)s)) +
            SIN(RADIANS(%(lat1)s)) * SIN(RADIANS(%(lat2)s))
        ) * 6371
    """
    output_field = FloatField()

    def __init__(self, lat1, lon1, lat2, lon2, **kwargs):
        super().__init__(
            lat1=Value(lat1),
            lon1=Value(lon1),
            lat2=F(lat2),
            lon2=F(lon2),
            **kwargs
        )

# 修改视图的get_queryset方法
def get_queryset(self):
    queryset = super().get_queryset()
    max_distance = self.request.query_params.get('distance', None)

    if max_distance:
        user = User.objects.get(id=5).participant
        queryset = queryset.annotate(
            fact_distance=Haversine(
                lat1=user.latitude,
                lon1=user.longitude,
                lat2='latitude',
                lon2='longitude'
            )
        ).filter(fact_distance__lt=float(max_distance))
    return queryset

注:6371是地球平均半径(单位:公里),如果需要米为单位,替换为6371000即可。

PostgreSQL+PostGIS优化实现

如果使用PostgreSQL数据库,可以安装PostGIS扩展,用官方提供的ST_DistanceSphere函数(精度更高):

from django.db.models import F, Func, Value, FloatField

class Distance(Func):
    function = 'ST_DistanceSphere'
    output_field = FloatField()

def get_queryset(self):
    queryset = super().get_queryset()
    max_distance = self.request.query_params.get('distance', None)

    if max_distance:
        user = User.objects.get(id=5).participant
        queryset = queryset.annotate(
            fact_distance=Distance(
                Func(Value(user.longitude), Value(user.latitude), function='ST_MakePoint'),
                Func(F('longitude'), F('latitude'), function='ST_MakePoint'),
            ) / 1000  # 转成公里
        ).filter(fact_distance__lt=float(max_distance))
    return queryset

方法2:GeoDjango标准实现

如果项目可以引入GeoDjango,这是最规范的方案:

第一步:修改模型

将原有的latitude和longitude字段替换为PointField(地理坐标系):

from django.contrib.gis.db import models

class Participant(models.Model):
    # 保留其他字段不变
    location = models.PointField(geography=True)  # geography=True启用地理坐标系,自动计算大圆距离

运行迁移后,需要将原有经纬度数据批量导入到location字段中(可以写脚本处理)。

第二步:视图中计算距离

from django.contrib.gis.db.models.functions import Distance

def get_queryset(self):
    queryset = super().get_queryset()
    max_distance = self.request.query_params.get('distance', None)

    if max_distance:
        user = User.objects.get(id=5).participant
        # Distance返回单位为米,转成公里需乘以1000
        queryset = queryset.annotate(
            fact_distance=Distance('location', user.location)
        ).filter(fact_distance__lt=float(max_distance)*1000)
    return queryset

GeoDjango会自动调用数据库的地理函数,性能和精度都有保障。

内容的提问来源于stack exchange,提问作者Valentin Suyarov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 18:34:54