Cómo establecer el valor predeterminado de un campo de modelo de Django en una función llamada / invocable (por ejemplo, una fecha relativa a la hora de creación del objeto del modelo)

101

EDITADO:

¿Cómo puedo establecer el valor predeterminado de un campo de Django en una función que se evalúa cada vez que se crea un nuevo objeto de modelo?

Quiero hacer algo como lo siguiente, excepto que en este código, el código se evalúa una vez y establece el valor predeterminado en la misma fecha para cada objeto modelo creado, en lugar de evaluar el código cada vez que se crea un objeto modelo:

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:

Quiero crear un valor predeterminado para un parámetro de función de modo que sea dinámico y se llame y configure cada vez que se llame a la función. ¿Cómo puedo hacer eso? p.ej,

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

Específicamente, quiero hacerlo en Django, por ejemplo,

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
fuente
La pregunta está mal redactada: no tiene nada que ver con los valores predeterminados de los parámetros de función. Mira mi respuesta.
Ned Batchelder
Según el comentario de @ NedBatchelder, edité mi pregunta (indicada como "EDITADO:") y dejé el original (indicado como "ORIGINAL:")
Rob Bednark

Respuestas:

140

La pregunta está equivocada. Al crear un campo de modelo en Django, no está definiendo una función, por lo que los valores predeterminados de la función son 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 línea no define una función; está invocando una función para crear un campo en la clase.

PRE Django 1.7

Django le permite pasar un invocable como predeterminado , y lo invocará cada vez, tal como desee:

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+

Tenga en cuenta que desde Django 1.7, no se recomienda el uso de lambda como valor predeterminado (consulte el comentario de @stvnw). La forma correcta de hacer esto es declarar una función antes del campo y usarla como invocable en valor_predeterminado llamado 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)

Más información en la respuesta de @simanas a continuación

Ned Batchelder
fuente
2
Gracias @NedBatchelder. Esto es exactamente lo que estaba buscando. No me di cuenta de que 'predeterminado' podría tomar un invocable. Y ahora veo cómo mi pregunta estaba equivocada con respecto a la invocación de funciones frente a la definición de funciones.
Rob Bednark
25
Tenga en cuenta que para Django> = 1.7 no se recomienda el uso de lambdas para las opciones de campo porque son incompatibles con las migraciones. Ref: Docs , ticket
StvnW
4
Desmarque esta respuesta, ya que es incorrecta para Djange> = 1.7. La respuesta de @Simanas es absolutamente correcta y, por lo tanto, debe aceptarse.
AplusKminus
¿Cómo funciona esto con las migraciones? ¿Todos los objetos antiguos obtienen una fecha basada en el momento de la migración? ¿Es posible establecer un valor predeterminado diferente para usar con las migraciones?
timthelion
3
¿Qué pasa si quiero pasar algún parámetro a get_default_my_date?
Sadan A.
59

¡Hacer esto default=datetime.now()+timedelta(days=1)está absolutamente mal!

Se evalúa cuando inicia su instancia de django. Si está bajo apache, probablemente funcionará, porque en algunas configuraciones apache revoca su aplicación django en cada solicitud, pero aún puede encontrarse algún día revisando su código y tratando de descubrir por qué esto no se calcula como esperaba .

La forma correcta de hacer esto es pasar un objeto invocable al argumento predeterminado. Puede ser una función datetime.today o su función personalizada. Luego, se evalúa cada vez que solicita un nuevo valor predeterminado.

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
fuente
21
Si mi valor predeterminado se basa en el valor de otro campo en el modelo, ¿es posible pasar ese campo como parámetro como en algo así get_deadline(my_parameter)?
YPCrumble
@YPCrumble ¡esta debería ser una nueva pregunta!
jsmedmar
2
No Eso no es posible. Tendrá que utilizar un enfoque diferente para hacerlo.
Simanas
¿Cómo se puede deadlinevolver a eliminar el campo y al mismo tiempo eliminar la get_deadlinefunción? He eliminado un campo con una función para un valor predeterminado, pero ahora Django se bloquea al iniciar después de eliminar la función. Podría editar manualmente la migración, lo que estaría bien en este caso, pero ¿qué pasa si simplemente cambia la función predeterminada y desea eliminar la función anterior?
beruic
8

Hay una distinción importante entre los siguientes dos constructores DateTimeField:

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

Si utiliza auto_now_add=True en el constructor, la fecha y hora a la que hace referencia my_date es "inmutable" (solo se establece una vez cuando la fila se inserta en la tabla).

Con auto_now=True embargo, con el valor de fecha y hora se actualizará cada vez que se guarde el objeto.

Esto definitivamente fue un problema para mí en un momento. Como referencia, los documentos están aquí:

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

Damzam
fuente
3
Tiene sus definiciones de auto_now y auto_now_add mezcladas, es al revés.
Michael Bates
@MichaelBates mejor ahora?
Hendy Irawan
4

A veces, es posible que deba acceder a los datos del modelo después de crear un nuevo modelo de usuario.

Así es como genero un token para cada nuevo perfil de usuario usando los primeros 4 caracteres de su nombre de usuario:

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
fuente
3

No puedes hacer eso directamente; el valor predeterminado se evalúa cuando se evalúa la definición de la función. Pero hay dos formas de evitarlo.

Primero, puede crear (y luego llamar) una nueva función cada vez.

O, más simplemente, use un valor especial para marcar el predeterminado. Por ejemplo:

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

Si Nonees un valor de parámetro perfectamente razonable, y no hay otro valor razonable que pueda usar en su lugar, puede crear un nuevo valor que definitivamente está fuera del dominio de su función:

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

En algunos casos raros, se está escribiendo meta-código que realmente hace falta ser capaz de tomar absolutamente nada, ni siquiera, por ejemplo, mydate.func_defaults[0]. En ese caso, debe hacer algo como esto:

def mydate(*args, **kw):
  if 'date' in kw:
    date = kw['date']
  elif len(args):
    date = args[0]
  else:
    date = datetime.now()
  print date
abarnert
fuente
1
Tenga en cuenta que no hay ninguna razón para crear una instancia de la clase de valor ficticio; solo use la clase en sí como valor ficticio.
Ámbar
PUEDE hacer esto directamente, simplemente pase la función en lugar del resultado de la llamada a la función. Vea mi respuesta publicada.
No, no puedes. El resultado de la función no es el mismo tipo de cosas que los parámetros normales, por lo que no se puede usar razonablemente como valor predeterminado para esos parámetros normales.
abarnert
1
Bueno, sí, si pasa un objeto en lugar de una función, generará una excepción. Lo mismo es cierto si pasa una función a un parámetro que espera una cadena. No veo cómo eso es relevante.
Es relevante porque su myfuncfunción está hecha para tomar (e imprimir) a datetime; lo ha cambiado para que no se pueda utilizar de esa forma.
abarnert
2

Pase la función como parámetro en lugar de pasar el resultado de la llamada a la función.

Es decir, en lugar de esto:

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

Prueba esto:

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

fuente
Esto no funciona, porque ahora el usuario no puede pasar un parámetro; intentarás llamarlo como una función y lanzar una excepción. En cuyo caso, es mejor que no acepte parámetros e print datetime.now()incondicionalmente.
abarnert
Intentalo. No está pasando una fecha, está pasando la función datetime.now.
Sí, el valor predeterminado es pasar la datetime.nowfunción. Pero cualquier usuario que pase un valor no predeterminado pasará una fecha y hora, no una función. foo = datetime.now(); myfunc(foo)levantará TypeError: 'datetime.datetime' object is not callable. Si realmente quisiera usar su función, tendría que hacer algo como myfunc(lambda: foo), que no es una interfaz razonable.
abarnert
1
Las interfaces que aceptan funciones no son inusuales. Podría comenzar a nombrar ejemplos del stdlib de python, si lo desea.
2
Django ya acepta un invocable exactamente como sugiere esta pregunta.
Ned Batchelder