Como definir o valor padrão de um campo de modelo do Django para uma chamada de função / chamável (por exemplo, uma data relativa ao tempo de criação do objeto de modelo)

101

EDITADO:

Como posso definir o padrão de um campo Django para uma função que é avaliada cada vez que um novo objeto de modelo é criado?

Eu quero fazer algo como o seguinte, exceto que neste código, o código é avaliado uma vez e define o padrão para a mesma data para cada objeto de modelo criado, em vez de avaliar o código cada vez que um objeto de modelo é criado:

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



ORIGINAL:

Quero criar um valor padrão para um parâmetro de função de forma que seja dinâmico e seja chamado e definido toda vez que a função for chamada. Como eu posso fazer isso? por exemplo,

from datetime import datetime
def mydate(date=datetime.now()):
  print date

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

Especificamente, quero fazer isso em Django, por exemplo,

from datetime import datetime, timedelta
class MyModel(models.Model):
  # default to 1 day from now
  my_date = models.DateTimeField(default=datetime.now() + timedelta(days=1))
Rob Bednark
fonte
A questão é mal formulada: não tem nada a ver com padrões de parâmetros de função. Veja minha resposta.
Ned Batchelder
De acordo com o comentário de @NedBatchelder, editei minha pergunta (indicada como "EDITADA:") e deixei a original (indicada como "ORIGINAL:")
Rob Bednark

Respostas:

140

A pergunta está equivocada. Ao criar um campo de modelo no Django, você não está definindo uma função, então os valores padrão da função são irrelevantes:

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

Esta última linha não está definindo uma função; é invocar uma função para criar um campo na classe.

PRE Django 1.7

Django permite que você passe um callable como o padrão , e ele irá invocá-lo todas as vezes, assim como você deseja:

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

Django 1.7+

Observe que desde o Django 1.7, o uso de lambda como valor padrão não é recomendado (cf comentário @stvnw). A maneira correta de fazer isso é declarar uma função antes do campo e usá-la como um callable em default_value chamado arg:

from datetime import datetime, timedelta

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

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

Mais informações na resposta @simanas abaixo

Ned Batchelder
fonte
2
Obrigado @NedBatchelder. Isso é exatamente o que eu estava procurando. Eu não percebi que 'padrão' poderia exigir uma chamada. E agora eu vejo como minha pergunta foi realmente equivocada em relação à invocação de função versus definição de função.
Rob Bednark
25
Note que para Django> = 1.7 o uso de lambdas não é recomendado para opções de campo porque eles são incompatíveis com migrações. Ref: Docs , ticket
StvnW
4
Por favor, desmarque esta resposta, pois está errada para Djange> = 1,7. A resposta de @Simanas é absolutamente correta e, portanto, deve ser aceita.
AplusKminus
Como isso funciona com migrações? Todos os objetos antigos obtêm uma data com base na hora da migração? É possível definir um padrão diferente para ser usado com as migrações?
timthelion
3
E se eu quiser passar algum parâmetro para get_default_my_date?
Sadan A.
59

Fazer isso default=datetime.now()+timedelta(days=1)é absolutamente errado!

Ele é avaliado quando você inicia sua instância do django. Se você estiver usando o apache, provavelmente funcionará, porque em algumas configurações o apache revoga seu aplicativo django em todas as solicitações, mas ainda assim você pode se encontrar algum dia olhando seu código e tentando descobrir por que isso não foi calculado como você esperava .

A maneira certa de fazer isso é passar um objeto que pode ser chamado para o argumento padrão. Pode ser uma função datetime.today ou sua função personalizada. Então, ele é avaliado toda vez que você solicita um novo valor padrão.

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)
Simanas
fonte
21
Se meu padrão for baseado no valor de outro campo no modelo, é possível passar esse campo como um parâmetro como em algo semelhante get_deadline(my_parameter)?
YPCrumble
@YPCrumble, esta deve ser uma nova questão!
jsmedmar
2
Não. Isso não é possível. Você terá que usar uma abordagem diferente para fazer isso.
Simanas
Como você remove o deadlinecampo novamente enquanto exclui a get_deadlinefunção? Removi um campo com uma função para um valor padrão, mas agora o Django falha na inicialização após remover a função. Eu poderia editar manualmente a migração, o que estaria correto neste caso, mas e se você simplesmente alterasse a função padrão e quisesse remover a função antiga?
beruic
8

Há uma distinção importante entre os dois construtores DateTimeField a seguir:

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

Se você usar auto_now_add=Trueno construtor, a data e hora referenciada por my_date é "imutável" (definida apenas uma vez quando a linha é inserida na tabela).

Com auto_now=True, entretanto, o valor datetime será atualizado sempre que o objeto for salvo.

Isso foi definitivamente um problema para mim em um ponto. Para referência, os documentos estão aqui:

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

damzam
fonte
3
Você tem suas definições de auto_now e auto_now_add misturadas, é o contrário.
Michael Bates
@MichaelBates está melhor agora?
Hendy Irawan
4

Às vezes, você pode precisar acessar os dados do modelo após criar um novo modelo de usuário.

Aqui está como eu gero um token para cada novo perfil de usuário usando os primeiros 4 caracteres de seu nome de usuário:

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)))
CoreCreatives
fonte
3

Você não pode fazer isso diretamente; o valor padrão é avaliado quando a definição da função é avaliada. Mas existem duas maneiras de contornar isso.

Primeiro, você pode criar (e depois chamar) uma nova função a cada vez.

Ou, mais simplesmente, use um valor especial para marcar o padrão. Por exemplo:

from datetime import datetime
def mydate(date=None):
  if date is None:
    date = datetime.now()
  print date

Se Nonefor um valor de parâmetro perfeitamente razoável e não houver outro valor razoável que você possa usar em seu lugar, você pode apenas criar um novo valor que está definitivamente fora do domínio de sua função:

from datetime import datetime
class _MyDateDummyDefault(object):
  pass
def mydate(date=_MyDateDummyDefault):
  if date is _MyDateDummyDefault:
    date = datetime.now()
  print date
del _MyDateDummyDefault

Em alguns casos raros, você está escrevendo um meta-código que realmente precisa ser capaz de pegar absolutamente qualquer coisa, mesmo, digamos mydate.func_defaults[0],. Nesse caso, você deve fazer algo assim:

def mydate(*args, **kw):
  if 'date' in kw:
    date = kw['date']
  elif len(args):
    date = args[0]
  else:
    date = datetime.now()
  print date
abarnert
fonte
1
Observe que não há razão para realmente instanciar a classe de valor fictício - apenas use a própria classe como o valor fictício.
Amber
Você PODE fazer isso diretamente, apenas passe a função em vez do resultado da chamada da função. Veja minha resposta postada.
Não, você não pode. O resultado da função não é o mesmo tipo de coisa que os parâmetros normais, portanto, não pode ser razoavelmente usado como um valor padrão para esses parâmetros normais.
abarnert
1
Bem, sim, se você passar um objeto em vez de uma função, isso gerará uma exceção. O mesmo é verdadeiro se você passar uma função para um parâmetro esperando uma string. Não vejo como isso é relevante.
É relevante porque sua myfuncfunção é feita para pegar (e imprimir) um datetime; você o alterou, então não pode ser usado dessa forma.
abarnert
2

Passe a função como um parâmetro em vez de passar o resultado da chamada da função.

Ou seja, em vez disso:

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

Experimente isto:

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

fonte
Isso não funciona, porque agora o usuário não pode realmente passar um parâmetro - você tentará chamá-lo como uma função e lançar uma exceção. Nesse caso, você também pode simplesmente não aceitar parâmetros e print datetime.now()incondicionalmente.
abarnert
Tente. Você não está passando uma data, está passando a função datetime.now.
Sim, o padrão é passar a datetime.nowfunção. Mas qualquer usuário que passar um valor não padrão estará passando uma data e hora, não uma função. foo = datetime.now(); myfunc(foo)vai aumentar TypeError: 'datetime.datetime' object is not callable. Se eu realmente quisesse usar sua função, teria que fazer algo como myfunc(lambda: foo), que não é uma interface razoável.
abarnert
1
As interfaces que aceitam funções não são incomuns. Eu poderia começar a nomear exemplos do stdlib do python, se quiser.
2
Django já aceita um chamável exatamente como esta questão sugere.
Ned Batchelder