From d2a26c1a90e837777dabdf3d67ceec4d2a70fb86 Mon Sep 17 00:00:00 2001 From: Adam Chainz Date: Thu, 15 Dec 2016 18:42:44 +0000 Subject: [PATCH] Optimized Model instantiation a bit. * Avoid some unnecessary attribute lookups, e.g. access signals directly rather than from module * Alias some repeat accesses inside the method to use the slightly faster local lookups * Use tuple to iterate remaining kwargs as it's faster to construct * Cache Field.get_default() to avoid running through all the logic on every call * Use a cached list of the properties on the model class to avoid repeat isinstance() calls --- django/db/models/base.py | 60 +++++++++++++++++------------ django/db/models/fields/__init__.py | 17 ++++++-- django/db/models/options.py | 11 ++++++ 3 files changed, 60 insertions(+), 28 deletions(-) diff --git a/django/db/models/base.py b/django/db/models/base.py index 6b4eb25009..53b761f45a 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -16,7 +16,6 @@ from django.db import ( DEFAULT_DB_ALIAS, DJANGO_VERSION_PICKLE_KEY, DatabaseError, connection, connections, router, transaction, ) -from django.db.models import signals from django.db.models.constants import LOOKUP_SEP from django.db.models.deletion import CASCADE, Collector from django.db.models.fields.related import ( @@ -25,6 +24,9 @@ from django.db.models.fields.related import ( from django.db.models.manager import Manager from django.db.models.options import Options from django.db.models.query import Q +from django.db.models.signals import ( + class_prepared, post_init, post_save, pre_init, pre_save, +) from django.db.models.utils import make_model_tuple from django.utils import six from django.utils.deprecation import RemovedInDjango20Warning @@ -366,7 +368,7 @@ class ModelBase(type): manager.auto_created = True cls.add_to_class('objects', manager) - signals.class_prepared.send(sender=cls) + class_prepared.send(sender=cls) def _requires_legacy_default_manager(cls): # RemovedInDjango20Warning opts = cls._meta @@ -465,7 +467,13 @@ class ModelState(object): class Model(six.with_metaclass(ModelBase)): def __init__(self, *args, **kwargs): - signals.pre_init.send(sender=self.__class__, args=args, kwargs=kwargs) + # Alias some things as locals to avoid repeat global lookups + cls = self.__class__ + opts = self._meta + _setattr = setattr + _DEFERRED = DEFERRED + + pre_init.send(sender=cls, args=args, kwargs=kwargs) # Set up the storage for instance state self._state = ModelState() @@ -474,27 +482,27 @@ class Model(six.with_metaclass(ModelBase)): # overrides it. It should be one or the other; don't duplicate the work # The reason for the kwargs check is that standard iterator passes in by # args, and instantiation for iteration is 33% faster. - if len(args) > len(self._meta.concrete_fields): + if len(args) > len(opts.concrete_fields): # Daft, but matches old exception sans the err msg. raise IndexError("Number of args exceeds number of fields") if not kwargs: - fields_iter = iter(self._meta.concrete_fields) + fields_iter = iter(opts.concrete_fields) # The ordering of the zip calls matter - zip throws StopIteration # when an iter throws it. So if the first iter throws it, the second # is *not* consumed. We rely on this, so don't change the order # without changing the logic. for val, field in zip(args, fields_iter): - if val is DEFERRED: + if val is _DEFERRED: continue - setattr(self, field.attname, val) + _setattr(self, field.attname, val) else: # Slower, kwargs-ready version. - fields_iter = iter(self._meta.fields) + fields_iter = iter(opts.fields) for val, field in zip(args, fields_iter): - if val is DEFERRED: + if val is _DEFERRED: continue - setattr(self, field.attname, val) + _setattr(self, field.attname, val) kwargs.pop(field.name, None) # Now we're left with the unprocessed fields that *must* come from @@ -539,28 +547,28 @@ class Model(six.with_metaclass(ModelBase)): # field.name instead of field.attname (e.g. "user" instead of # "user_id") so that the object gets properly cached (and type # checked) by the RelatedObjectDescriptor. - if rel_obj is not DEFERRED: - setattr(self, field.name, rel_obj) + if rel_obj is not _DEFERRED: + _setattr(self, field.name, rel_obj) else: - if val is not DEFERRED: - setattr(self, field.attname, val) + if val is not _DEFERRED: + _setattr(self, field.attname, val) if kwargs: - for prop in list(kwargs): + property_names = opts._property_names + for prop in tuple(kwargs): try: # Any remaining kwargs must correspond to properties or # virtual fields. - if (isinstance(getattr(self.__class__, prop), property) or - self._meta.get_field(prop)): - if kwargs[prop] is not DEFERRED: - setattr(self, prop, kwargs[prop]) + if prop in property_names or opts.get_field(prop): + if kwargs[prop] is not _DEFERRED: + _setattr(self, prop, kwargs[prop]) del kwargs[prop] except (AttributeError, FieldDoesNotExist): pass if kwargs: raise TypeError("'%s' is an invalid keyword argument for this function" % list(kwargs)[0]) super(Model, self).__init__() - signals.post_init.send(sender=self.__class__, instance=self) + post_init.send(sender=cls, instance=self) @classmethod def from_db(cls, db, field_names, values): @@ -816,8 +824,10 @@ class Model(six.with_metaclass(ModelBase)): cls = cls._meta.concrete_model meta = cls._meta if not meta.auto_created: - signals.pre_save.send(sender=origin, instance=self, raw=raw, using=using, - update_fields=update_fields) + pre_save.send( + sender=origin, instance=self, raw=raw, using=using, + update_fields=update_fields, + ) with transaction.atomic(using=using, savepoint=False): if not raw: self._save_parents(cls, using, update_fields) @@ -829,8 +839,10 @@ class Model(six.with_metaclass(ModelBase)): # Signal that the save is complete if not meta.auto_created: - signals.post_save.send(sender=origin, instance=self, created=(not updated), - update_fields=update_fields, raw=raw, using=using) + post_save.send( + sender=origin, instance=self, created=(not updated), + update_fields=update_fields, raw=raw, using=using, + ) save_base.alters_data = True diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index f1f68dbe7a..7434ac19ff 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -92,6 +92,10 @@ def _empty(of_cls): return new +def return_None(): + return None + + @total_ordering @python_2_unicode_compatible class Field(RegisterLookupMixin): @@ -771,13 +775,18 @@ class Field(RegisterLookupMixin): """ Returns the default value for this field. """ + return self._get_default() + + @cached_property + def _get_default(self): if self.has_default(): if callable(self.default): - return self.default() - return self.default + return self.default + return lambda: self.default + if not self.empty_strings_allowed or self.null and not connection.features.interprets_empty_strings_as_nulls: - return None - return "" + return return_None + return six.text_type # returns empty string def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH, limit_choices_to=None): """Returns choices with a default blank choices included, for use diff --git a/django/db/models/options.py b/django/db/models/options.py index fd0b317865..335289beea 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -883,3 +883,14 @@ class Options(object): @has_auto_field.setter def has_auto_field(self, value): pass + + @cached_property + def _property_names(self): + """ + Return a set of the names of the properties defined on the model. + Internal helper for model initialization. + """ + return frozenset({ + attr for attr in + dir(self.model) if isinstance(getattr(self.model, attr), property) + })