From e7928e1265b1d3bb0d937c0e3adcaf21185560cf Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 11 Mar 2015 12:37:10 +0100 Subject: [PATCH] [IMP] fields: add `__slots__` on all fields Introduce slots on all field classes for common attributes; slots take much less memory than a `__dict__`. The other attributes are stored in a dictionary `_attrs`; all fields with an empty value for `_attrs` (common case) share the same empty dictionary. This saves quite some memory (around 4.5Mb per registry), given the number of field instances created for a registry. Another mechanism is used for the default values of attributes, since slots cannot be assigned on classes. --- openerp/fields.py | 287 +++++++++++++++++++++++++++++++++------------- 1 file changed, 207 insertions(+), 80 deletions(-) diff --git a/openerp/fields.py b/openerp/fields.py index aaf6ba20f9c..b70d95709b0 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -79,11 +79,14 @@ class MetaField(type): MetaField.by_type[cls.type] = cls # compute class attributes to avoid calling dir() on fields + cls.default_attrs = {} cls.column_attrs = [] cls.related_attrs = [] cls.description_attrs = [] for attr in dir(cls): - if attr.startswith('_column_'): + if attr.startswith('_default_'): + cls.default_attrs[attr[9:]] = getattr(cls, attr) + elif attr.startswith('_column_'): cls.column_attrs.append((attr[8:], attr)) elif attr.startswith('_related_'): cls.related_attrs.append((attr[9:], attr)) @@ -254,69 +257,132 @@ class Field(object): """ __metaclass__ = MetaField - _attrs = None # dictionary of field attributes; it contains: + type = None # type of the field (string) + relational = False # whether the field is a relational one + + __slots__ = [ + '_attrs', # dictionary of field attributes; it contains: # - all attributes after __init__() # - free attributes only after set_class_name() - automatic = False # whether the field is automatically created ("magic" field) - inherited = False # whether the field is inherited (_inherits) - column = None # the column corresponding to the field - setup_done = False # whether the field has been set up + 'automatic', # whether the field is automatically created ("magic" field) + 'inherited', # whether the field is inherited (_inherits) + 'column', # the column corresponding to the field + 'setup_done', # whether the field has been set up - name = None # name of the field - type = None # type of the field (string) - relational = False # whether the field is a relational one - model_name = None # name of the model of this field - comodel_name = None # name of the model of values (if relational) + 'name', # name of the field + 'model_name', # name of the model of this field + 'comodel_name', # name of the model of values (if relational) - store = True # whether the field is stored in database - index = False # whether the field is indexed in database - manual = False # whether the field is a custom field - copy = True # whether the field is copied over by BaseModel.copy() - depends = () # collection of field dependencies - recursive = False # whether self depends on itself - compute = None # compute(recs) computes field on recs - compute_sudo = False # whether field should be recomputed as admin - inverse = None # inverse(recs) inverses field on recs - search = None # search(recs, operator, value) searches on self - related = None # sequence of field names, for related fields - related_sudo = True # whether related fields should be read as admin - company_dependent = False # whether `self` is company-dependent (property field) - default = None # default(recs) returns the default value + 'store', # whether the field is stored in database + 'index', # whether the field is indexed in database + 'manual', # whether the field is a custom field + 'copy', # whether the field is copied over by BaseModel.copy() + 'depends', # collection of field dependencies + 'recursive', # whether self depends on itself + 'compute', # compute(recs) computes field on recs + 'compute_sudo', # whether field should be recomputed as admin + 'inverse', # inverse(recs) inverses field on recs + 'search', # search(recs, operator, value) searches on self + 'related', # sequence of field names, for related fields + 'related_sudo', # whether related fields should be read as admin + 'company_dependent', # whether `self` is company-dependent (property field) + 'default', # default(recs) returns the default value - string = None # field label - help = None # field tooltip - readonly = False - required = False - states = None - groups = False # csv list of group xml ids - change_default = None # whether the field may trigger a "user-onchange" - deprecated = None # whether the field is ... deprecated + 'string', # field label + 'help', # field tooltip + 'readonly', # whether the field is readonly + 'required', # whether the field is required + 'states', # set readonly and required depending on state + 'groups', # csv list of group xml ids + 'change_default', # whether the field may trigger a "user-onchange" + 'deprecated', # whether the field is deprecated - inverse_fields = () # collection of inverse fields (objects) - computed_fields = () # fields computed with the same method as self - _triggers = () # invalidation and recomputation triggers + 'inverse_fields', # collection of inverse fields (objects) + 'computed_fields', # fields computed with the same method as self + 'related_field', # corresponding related field + '_triggers', # invalidation and recomputation triggers + ] + + # default values for slots + _default_automatic = False + _default_inherited = False + _default_column = None + _default_setup_done = False + + _default_name = None + _default_model_name = None + _default_comodel_name = None + + _default_store = True + _default_index = False + _default_manual = False + _default_copy = True + _default_depends = () + _default_recursive = False + _default_compute = None + _default_compute_sudo = False + _default_inverse = None + _default_search = None + _default_related = None + _default_related_sudo = True + _default_company_dependent = False + _default_default = None + + _default_string = None + _default_help = None + _default_readonly = False + _default_required = False + _default_states = None + _default_groups = False + _default_change_default = None + _default_deprecated = None + + _default_inverse_fields = () + _default_computed_fields = () + _default_related_field = None + _default__triggers = () def __init__(self, string=None, **kwargs): kwargs['string'] = string attrs = {key: val for key, val in kwargs.iteritems() if val is not None} self._attrs = attrs or EMPTY_DICT - def _set_attr(self, name, value): - """ Set the given field attribute, and add it to `_attrs` if necessary. """ - object.__setattr__(self, name, value) - if not hasattr(type(self), name): + def __getattr__(self, name): + """ Access non-slot field attribute. """ + if name == '_attrs': + raise AttributeError(name) + try: + return self._attrs[name] + except KeyError: + raise AttributeError(name) + + def __setattr__(self, name, value): + """ Set slot or non-slot field attribute. """ + try: + object.__setattr__(self, name, value) + except AttributeError: if self._attrs: self._attrs[name] = value else: self._attrs = {name: value} # replace EMPTY_DICT + def __delattr__(self, name): + """ Remove non-slot field attribute. """ + try: + del self._attrs[name] + except KeyError: + raise AttributeError(name) + def new(self, **kwargs): """ Return a field of the same type as `self`, with its own parameters. """ return type(self)(**kwargs) def set_class_name(self, cls, name): """ Assign the model class and field name of `self`. """ + for attr, value in self.default_attrs.iteritems(): + setattr(self, attr, value) + self.model_name = cls._name self.name = name @@ -346,7 +412,7 @@ class Field(object): self._attrs = EMPTY_DICT for attr, value in attrs.iteritems(): - self._set_attr(attr, value) + setattr(self, attr, value) if not self.string and not self.related: # related fields get their string from their parent field @@ -486,7 +552,7 @@ class Field(object): for attr, value in field._attrs.iteritems(): if attr not in self._attrs: - self._set_attr(attr, value) + setattr(self, attr, value) # special case for states: copy it only for inherited fields if not self.states and self.inherited: @@ -963,6 +1029,8 @@ class Field(object): class Boolean(Field): type = 'boolean' + __slots__ = [] + def convert_to_cache(self, value, record, validate=True): return bool(value) @@ -974,10 +1042,13 @@ class Boolean(Field): class Integer(Field): type = 'integer' - group_operator = None # operator for aggregating values + __slots__ = [ + 'group_operator', # operator for aggregating values + ] + + _default_group_operator = None _related_group_operator = property(attrgetter('group_operator')) - _column_group_operator = property(attrgetter('group_operator')) def convert_to_cache(self, value, record, validate=True): @@ -1005,8 +1076,14 @@ class Float(Field): cursor and returning a pair (total, decimal) """ type = 'float' - _digits = None # digits argument passed to class initializer - group_operator = None # operator for aggregating values + + __slots__ = [ + '_digits', # digits argument passed to class initializer + 'group_operator', # operator for aggregating values + ] + + _default__digits = None + _default_group_operator = None def __init__(self, string=None, digits=None, **kwargs): super(Float, self).__init__(string=string, _digits=digits, **kwargs) @@ -1045,8 +1122,11 @@ class Float(Field): class _String(Field): """ Abstract class for string fields. """ - translate = False + __slots__ = [ + 'translate', # whether the field is translated + ] + _default_translate = False _column_translate = property(attrgetter('translate')) _related_translate = property(attrgetter('translate')) _description_translate = property(attrgetter('translate')) @@ -1060,17 +1140,21 @@ class Char(_String): :param bool translate: whether the values of this field can be translated """ type = 'char' - size = None + + __slots__ = [ + 'size', # maximum size of values (deprecated) + ] + + _default_size = None + _column_size = property(attrgetter('size')) + _related_size = property(attrgetter('size')) + _description_size = property(attrgetter('size')) def _setup_regular(self, env): super(Char, self)._setup_regular(env) assert isinstance(self.size, (NoneType, int)), \ "Char field %s with non-integer size %r" % (self, self.size) - _column_size = property(attrgetter('size')) - _related_size = property(attrgetter('size')) - _description_size = property(attrgetter('size')) - def convert_to_cache(self, value, record, validate=True): if value is None or value is False: return False @@ -1083,6 +1167,7 @@ class Text(_String): :param translate: whether the value of this field can be translated """ type = 'text' + __slots__ = [] def convert_to_cache(self, value, record, validate=True): if value is None or value is False: @@ -1091,13 +1176,18 @@ class Text(_String): class Html(_String): type = 'html' - sanitize = True # whether value must be sanitized - strip_style = False # whether to strip style attributes + __slots__ = [ + 'sanitize', # whether value must be sanitized + 'strip_style', # whether to strip style attributes + ] + + _default_sanitize = True _column_sanitize = property(attrgetter('sanitize')) _related_sanitize = property(attrgetter('sanitize')) _description_sanitize = property(attrgetter('sanitize')) + _default_strip_style = False _column_strip_style = property(attrgetter('strip_style')) _related_strip_style = property(attrgetter('strip_style')) _description_strip_style = property(attrgetter('strip_style')) @@ -1112,6 +1202,7 @@ class Html(_String): class Date(Field): type = 'date' + __slots__ = [] @staticmethod def today(*args): @@ -1172,6 +1263,7 @@ class Date(Field): class Datetime(Field): type = 'datetime' + __slots__ = [] @staticmethod def now(*args): @@ -1246,6 +1338,7 @@ class Datetime(Field): class Binary(Field): type = 'binary' + __slots__ = [] class Selection(Field): @@ -1261,8 +1354,12 @@ class Selection(Field): `. """ type = 'selection' - selection = None # [(value, string), ...], function or method name - selection_add = None # [(value, string), ...] + + __slots__ = [ + 'selection', # [(value, string), ...], function or method name + ] + + _default_selection = None def __init__(self, selection=None, string=None, **kwargs): if callable(selection): @@ -1353,20 +1450,20 @@ class Selection(Field): class Reference(Selection): type = 'reference' - size = None - def __init__(self, selection=None, string=None, **kwargs): - super(Reference, self).__init__(selection=selection, string=string, **kwargs) + __slots__ = [ + 'size', # maximum size of values (deprecated) + ] + + _default_size = None + _related_size = property(attrgetter('size')) + _column_size = property(attrgetter('size')) def _setup_regular(self, env): super(Reference, self)._setup_regular(env) assert isinstance(self.size, (NoneType, int)), \ "Reference field %s with non-integer size %r" % (self, self.size) - _related_size = property(attrgetter('size')) - - _column_size = property(attrgetter('size')) - def convert_to_cache(self, value, record, validate=True): if isinstance(value, BaseModel): if ((not validate or value._name in self.get_values(record.env)) @@ -1392,8 +1489,14 @@ class Reference(Selection): class _Relational(Field): """ Abstract class for relational fields. """ relational = True - domain = None # domain for searching values - context = None # context for searching values + + __slots__ = [ + 'domain', # domain for searching values + 'context', # context for searching values + ] + + _default_domain = [] + _default_context = {} def _setup_regular(self, env): super(_Relational, self)._setup_regular(env) @@ -1461,9 +1564,16 @@ class Many2one(_Relational): fields or field extensions. """ type = 'many2one' - ondelete = 'set null' # what to do when value is deleted - auto_join = False # whether joins are generated upon search - delegate = False # whether self implements delegation + + __slots__ = [ + 'ondelete', # what to do when value is deleted + 'auto_join', # whether joins are generated upon search + 'delegate', # whether self implements delegation + ] + + _default_ondelete = 'set null' + _default_auto_join = False + _default_delegate = False def __init__(self, comodel_name=None, string=None, **kwargs): super(Many2one, self).__init__(comodel_name=comodel_name, string=string, **kwargs) @@ -1543,6 +1653,7 @@ class UnionUpdate(SpecialValue): class _RelationalMulti(_Relational): """ Abstract class for relational fields *2many. """ + __slots__ = [] def _update(self, records, value): """ Update the cached value of `self` for `records` with `value`. """ @@ -1666,10 +1777,17 @@ class One2many(_RelationalMulti): the case of related fields or field extensions. """ type = 'one2many' - inverse_name = None # name of the inverse field - auto_join = False # whether joins are generated upon search - limit = None # optional limit to use upon read - copy = False # o2m are not copied by default + + __slots__ = [ + 'inverse_name', # name of the inverse field + 'auto_join', # whether joins are generated upon search + 'limit', # optional limit to use upon read + ] + + _default_inverse_name = None + _default_auto_join = False + _default_limit = None + _default_copy = False # o2m are not copied by default def __init__(self, comodel_name=None, inverse_name=None, string=None, **kwargs): super(One2many, self).__init__( @@ -1731,10 +1849,18 @@ class Many2many(_RelationalMulti): """ type = 'many2many' - relation = None # name of table - column1 = None # column of table referring to model - column2 = None # column of table referring to comodel - limit = None # optional limit to use upon read + + __slots__ = [ + 'relation', # name of table + 'column1', # column of table referring to model + 'column2', # column of table referring to comodel + 'limit', # optional limit to use upon read + ] + + _default_relation = None + _default_column1 = None + _default_column2 = None + _default_limit = None def __init__(self, comodel_name=None, relation=None, column1=None, column2=None, string=None, **kwargs): @@ -1777,6 +1903,7 @@ class Many2many(_RelationalMulti): class Serialized(Field): """ Minimal support for existing sparse and serialized fields. """ type = 'serialized' + __slots__ = [] def convert_to_cache(self, value, record, validate=True): return value or {} @@ -1785,11 +1912,11 @@ class Serialized(Field): class Id(Field): """ Special case for field 'id'. """ type = 'integer' + __slots__ = [] - string = 'ID' - store = True - #: Can't write this! - readonly = True + _default_string = 'ID' + _default_store = True + _default_readonly = True def to_column(self): self.column = fields.integer(self.string)