如何将Django模型字段的默认值设置为函数调用/可调用对象(例如,相对于模型对象创建时间的日期)

136

编辑:

我该如何将 Django 字段的默认设置为一个在创建新模型对象时每次都会计算的函数?

我想做类似于以下代码的事情,但在此代码中,该代码只被计算一次,并将默认设置为每个创建的模型对象相同的日期,而不是每次创建模型对象时都计算代码:

from datetime import datetime, timedelta

class MyModel(models.Model):
    # default to 1 day from now
    my_datetime = models.DateTimeField(default=datetime.now() + timedelta(days=1))

原文:

我想为一个函数参数创建一个默认值,使其在每次调用函数时都是动态的,并且会被调用和设置。我该如何做到这一点?例如:

from datetime import datetime

def mydatetime(date=datetime.now()):
    print date

mydatetime() 
mydatetime() # prints the same thing as the previous call; but I want it to be a newer value

具体来说,我想在Django中实现,例如:

from datetime import datetime, timedelta

class MyModel(models.Model):
    # default to 1 day from now
    my_datetime = models.DateTimeField(default=datetime.now() + timedelta(days=1))

问题措辞不当:它与函数参数默认值无关。请参见我的答案。 - Ned Batchelder
根据@NedBatchelder的评论,我编辑了我的问题(标为“已编辑:”),并保留原始问题(标为“原始问题:”) - Rob Bednark
应该接受这个回答:https://dev59.com/pGcs5IYBdhLWcg3wrmDC - Kai - Kazuya Ito
应该接受https://dev59.com/pGcs5IYBdhLWcg3wrmDC作为正确答案。 - Super Kai - Kazuya Ito
7个回答

176
问题是误导性的。在Django中创建模型字段时,您并没有定义一个函数,因此函数默认值是无关紧要的:
from datetime import datetime, timedelta
class MyModel(models.Model):
    # default to 1 day from now
    my_datetime = models.DateTimeField(default=datetime.now() + timedelta(days=1))

这最后一行并不是在定义一个函数,而是调用一个函数来在类中创建一个字段。

在这种情况下,datetime.now() + timedelta(days=1)将被评估一次,并存储为默认值。

Django 1.7之前

Django [允许您将可调用对象作为默认值传递][1],并且它将每次都像您想要的那样调用它:

from datetime import datetime, timedelta
class MyModel(models.Model):
    # default to 1 day from now
    my_datetime = models.DateTimeField(default=lambda: datetime.now() + timedelta(days=1))

Django 1.7+

请注意,自Django 1.7以来,不建议将lambda用作默认值(参见@stvnw的评论)。正确的方法是在字段之前声明一个函数,并将其用作default_value命名参数中的可调用项:

from datetime import datetime, timedelta

# default to 1 day from now
def get_default_my_datetime():
    return datetime.now() + timedelta(days=1)

class MyModel(models.Model):
    my_date = models.DateTimeField(default=get_default_my_date)

以下是 @simanas 回答中的更多信息。 [1]: https://docs.djangoproject.com/en/dev/ref/models/fields/#default


3
谢谢 @NedBatchelder。这正是我在寻找的。我没有意识到 'default' 可以使用可调用对象。现在我明白了我的问题确实关乎函数调用和函数定义的区别。 - Rob Bednark
34
请注意,对于Django版本大于等于1.7的情况下,不推荐使用lambda表达式来设置字段选项,因为这会导致无法进行数据库迁移。参考:文档工单 - StvnW
8
请取消勾选此答案,因为它在Django>=1.7版本中是错误的。@Simanas的答案是完全正确的,因此应该被接受。 - AplusKminus
1
这个在迁移时是如何工作的?所有旧对象是否都会根据迁移时间获得日期?是否可以设置不同的默认值用于迁移? - timthelion
12
如果我想向 get_default_my_date 传递一些参数,该怎么办? - Sadan A.
显示剩余2条评论

78

这样做default=datetime.now()+timedelta(days=1)是绝对错误的!

它会在启动Django实例时被计算。如果您使用的是Apache服务器,可能会工作,因为在某些配置中,Apache会在每个请求上撤消您的Django应用,但仍然有一天你可能需要查看代码并尝试弄清楚为什么计算结果不符合您的预期。

正确的方法是将可调用对象传递给默认参数。它可以是datetime.today函数或您自定义的函数。然后每次请求新的默认值时都会计算该函数。

def get_deadline():
    return datetime.today() + timedelta(days=20)

class Bill(models.Model):
    name = models.CharField(max_length=50)
    customer = models.ForeignKey(User, related_name='bills')
    date = models.DateField(default=datetime.today)
    deadline = models.DateField(default=get_deadline)

31
如果我的默认值基于模型中另一个字段的值,那么是否可以将该字段作为参数传递,就像get_deadline(my_parameter)这样? - YPCrumble
1
@YPCrumble 这应该是一个新问题! - jsmedmar
3
不行,那是不可能的。你需要使用一些不同的方法来完成它。 - Simanas
你如何在删除 get_deadline 函数的同时再次删除 deadline 字段?我已经使用一个函数删除了一个字段的默认值,但是现在在删除该函数后,Django 在启动时崩溃了。在这种情况下,我可以手动编辑迁移,这也可以接受,但是如果您只是更改了默认函数并想要删除旧函数怎么办? - beruic
default=datetime.today在DJango版本3.2中不起作用,我的数据库是MySQL,运行您的代码后仍将默认值设置为None。 - Pankaj

8

以下两个DateTimeField构造函数之间有一个重要的区别:

my_datetime = models.DateTimeField(auto_now=True)
my_datetime = models.DateTimeField(auto_now_add=True)

如果在构造函数中使用auto_now_add=True,则my_date引用的日期时间是“不可变的”(仅在将行插入表时设置一次)。

然而,使用auto_now=True,每次保存对象时日期时间值都会更新。

这对我来说确实是一个陷阱。有关参考,请查看文档:

https://docs.djangoproject.com/en/dev/ref/models/fields/#datetimefield


3
你把auto_now和auto_now_add的定义搞混了,实际情况应该是相反的。 - Michael Bates
@MichaelBates 现在好了吗? - Hendy Irawan
default=datetime.today 在 DJango 3.2 版本中无法正常工作,我的数据库是 MySQL,运行您的代码后仍将默认值设置为 None。 - Pankaj

6
有时候在创建新用户模型后,您可能需要访问模型数据。
以下是我使用用户名的前四个字符为每个新用户配置文件生成令牌的方法:
from django.dispatch import receiver
class Profile(models.Model):
    auth_token = models.CharField(max_length=13, default=None, null=True, blank=True)


@receiver(post_save, sender=User) # this is called after a User model is saved.
def create_user_profile(sender, instance, created, **kwargs):
    if created: # only run the following if the profile is new
        new_profile = Profile.objects.create(user=instance)
        new_profile.create_auth_token()
        new_profile.save()

def create_auth_token(self):
    import random, string
    auth = self.user.username[:4] # get first 4 characters in user name
    self.auth_token =  auth + ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits + string.ascii_lowercase) for _ in range(random.randint(3, 5)))

3

你不能直接这样做;默认值在函数定义时被计算。但是有两种方法可以解决这个问题。

首先,你可以每次创建(然后调用)一个新的函数。

或者更简单地,只需使用特殊值来标记默认值。例如:

from datetime import datetime

def mydatetime(date=None):
    if datetime is None:
        datetime = datetime.now()
    print datetime

如果在参数中使用 None 是一种非常合理的情况,并且没有其他可以替代的合理值,那么您可以创建一个明确在函数域之外的新值。
from datetime import datetime

class _MyDateTimeDummyDefault(object):
    pass

def mydatetime(date=_MyDateDummyTimeDefault):
    if datetime is _MyDateDummyTimeDefault:
        datetime = datetime.now()
    print datetime

del _MyDateDummyTimeDefault

在一些罕见的情况下,您正在编写真正需要能够接受任何东西的元代码,甚至是像mydate.func_defaults[0]这样的代码。在这种情况下,您必须执行以下操作:
def mydatetime(*args, **kw):
    if 'datetime' in kw:
        datetime = kw['datetime']
    elif len(args):
        datetime = args[0]
    else:
        datetime = datetime.now()
    print datetime

1
注意,实际上没有必要实例化虚拟值类-只需使用类本身作为虚拟值即可。 - Amber
你可以直接这样做,只需传递函数而不是函数调用的结果。请参阅我的回答。 - user462356
不可以。函数的结果与普通参数的类型不同,因此不能合理地将其用作这些普通参数的默认值。 - abarnert
1
如果您传递一个对象而不是函数,它将引发异常。如果您将函数传递到期望字符串的参数中,情况也是如此。我不明白这与问题的相关性。 - user462356
它甚至不够优雅。我的解决方案保留了API本身;而你的则用一些不寻常和不符合Python风格的东西来替换它。这就是为什么你会在标准库中看到像我这样的代码,而像你这样的代码则无处可见。 - abarnert
显示剩余2条评论

1
将函数作为参数传递,而不是传递函数调用的结果。
也就是说,不要这样做:
def myfunc(datetime=datetime.now()):
    print datetime

试试这个:

def myfunc(datetime=datetime.now):
    print datetime()

这样做是行不通的,因为现在用户实际上无法传递参数——您将尝试将其作为函数调用并抛出异常。在这种情况下,您可能只需不带参数并无条件地print datetime.now() - abarnert
我认为要求传入一个函数的接口并不过分。用户需要传入一个生成日期的函数,而不是一个日期。例如,lambda: datetime.now() + timedelta(days=1)。 - user462356
我甚至不知道该说什么。没有人会期望有这样的界面,或者喜欢它,或者记得如何使用它。这太愚蠢了。 - abarnert
1
接受函数的接口并不罕见。如果您愿意,我可以开始列举一些 Python 标准库中的例子。 - user462356
2
Django已经像这个问题建议的那样接受可调用对象。 - Ned Batchelder
显示剩余6条评论

0

您应该将get_my_datetime设置为没有()的形式,它会返回当前日期和时间(+1天),并将其作为默认值添加到DateTimeField()中,如下所示。*不要使用()设置get_my_datetime(),因为当Django服务器启动时,默认的日期和时间不会改变,并且您应该使用Django的timezone.now(),因为Python的datetime.now()TIME_ZONE = 'UTC'(这是在settings.py中的默认设置)时无法正确保存UTC到数据库中:

from datetime import timedelta
from django.utils import timezone

def get_my_datetime(): # ↓ Don't use "datetime.now()"
    return timezone.now() + timedelta(days=1)

class MyModel(models.Model):
    # default to 1 day from now
    my_datetime = models.DateTimeField(default=get_my_date)             
                        # Don't set "get_my_datetime()" ↑

而且,就像你的代码一样,不要设置datetime.now() + timedelta(days=1),因为默认的日期和时间是在Django服务器启动时确定的(未更改):
from datetime import datetime, timedelta

class MyModel(models.Model):
    # default to 1 day from now              # ↓ ↓ ↓ ↓ ↓ ↓ Don't set ↓ ↓ ↓ ↓ ↓ ↓
    my_datetime = models.DateTimeField(default=datetime.now() + timedelta(days=1))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接