diff --git a/benchmarks/benchmarks/benchmarks.py b/benchmarks/benchmarks/benchmarks.py index 5700358b0..6a79f6244 100644 --- a/benchmarks/benchmarks/benchmarks.py +++ b/benchmarks/benchmarks/benchmarks.py @@ -635,3 +635,105 @@ def foo0(self): pass def time_trigger(self): self.p.x0 += 1 + + +# Parameter types exercised by the value get/set suites below, as +# (type, constructor kwargs, a valid replacement value). +_VALUE_TYPES = { + 'Parameter': (param.Parameter, {'default': 1.0}, 2.0), + 'Number': (param.Number, {'default': 1.0, 'bounds': (0, 100)}, 2.0), + 'String': (param.String, {'default': 'a'}, 'b'), + 'Boolean': (param.Boolean, {'default': True}, False), +} + + +def _value_class(ptype): + ptype_cls, kwargs, _ = _VALUE_TYPES[ptype] + + class P(param.Parameterized): + x = ptype_cls(**kwargs) + + return P + + +class ParameterizedGetValueSuite: + """ + Read a Parameter value, i.e. the Parameter.__get__ descriptor path. + + This is the single hottest operation in param: every attribute read on + a Parameterized object dispatches through it. + """ + + params = list(_VALUE_TYPES) + param_names = ['ptype'] + + def setup(self, ptype): + self.P = _value_class(ptype) + self.p = self.P() + # Warm up so the instance Parameter object already exists and this + # measures the steady state rather than first-touch instantiation. + self.p.x = _VALUE_TYPES[ptype][2] + self.p.x + + def time_instance(self, ptype): + self.p.x + + def time_class(self, ptype): + self.P.x + + +class ParameterizedSetValueSuite: + """ + Set a Parameter value, i.e. the Parameter.__set__ descriptor path, + including validation and (absent) watcher dispatch. + """ + + params = list(_VALUE_TYPES) + param_names = ['ptype'] + + def setup(self, ptype): + self.P = _value_class(ptype) + self.p = self.P() + self.value = _VALUE_TYPES[ptype][2] + # Force instance Parameter creation up front so this measures the + # steady state, not the one-off copy done on the first set. + self.p.x = self.value + + def time_instance(self, ptype): + self.p.x = self.value + + def time_class(self, ptype): + self.P.x = self.value + + +class ParameterizedFirstSetValueSuite: + """ + First set of a per_instance Parameter, which shallow-copies the class + Parameter object onto the instance (see ``_instantiate_param_obj``). + """ + + def setup(self): + self.P = _value_class('Number') + + def time_instantiate_and_first_set(self): + p = self.P() + p.x = 2.0 + + +class ParameterUnboundSlotSuite: + """ + Slot access on an unbound Parameter, where a slot still holding + ``Undefined`` falls back to ``_slot_defaults`` in + ``Parameter.__getattribute__``. + """ + + def setup(self): + self.p = param.Number(default=1.0) + + def time_undefined_slot(self): + # Never passed to the constructor, so resolved via _slot_defaults. + self.p.bounds + + def time_concrete_slot(self): + # Explicitly passed to the constructor, so no fallback. + self.p.default diff --git a/param/parameterized.py b/param/parameterized.py index 2e2444b31..b5517fa04 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -1929,19 +1929,20 @@ def __get__(self, obj: Parameterized | None, objtype: type[Parameterized] | None instance's value, if one has been set - otherwise produce the class's value (default). """ - if self.name is None: + # Slot reads go through Parameter.__getattribute__, so `name` is + # bound to a local rather than read twice. + name = self.name + if name is None: raise ValueError("Parameter name is not set") if obj is None: # e.g. when __get__ called for a Parameterized class - result = self.default - else: - # Attribute error when .values does not exist (_ClassPrivate) - # and KeyError when there's no cached value for this parameter. - try: - result = obj._param__private.values[self.name] - except (AttributeError, KeyError): - result = self.default - return result + return self.default + # Attribute error when .values does not exist (_ClassPrivate) + # and KeyError when there's no cached value for this parameter. + try: + return obj._param__private.values[name] + except (AttributeError, KeyError): + return self.default @instance_descriptor def __set__(self, obj: Parameterized | None, val: _T): @@ -1970,11 +1971,13 @@ def __set__(self, obj: Parameterized | None, val: _T): object stored in a constant or read-only Parameter (e.g. one item in a list). """ - if self.name is None: + # Slot reads go through Parameter.__getattribute__, so `name` is bound + # to a local and reused below rather than re-read from the slot. + name = self.name + if name is None: raise RuntimeError( "A parameter value cannot be set for an unbound parameter." ) - name = self.name if obj is not None and self.allow_refs and obj._param__private.initialized: syncing = name in obj._param__private.syncing @@ -1993,17 +1996,18 @@ def __set__(self, obj: Parameterized | None, val: _T): _old = NotImplemented # obj can be None if __set__ is called for a Parameterized class - if self.constant or self.readonly: - if self.readonly: + readonly = self.readonly + if self.constant or readonly: + if readonly: raise TypeError("Read-only parameter '%s' cannot be modified" % name) elif obj is None: _old = self.default self.default = val elif not obj._param__private.initialized: - _old = obj._param__private.values.get(self.name, self.default) - obj._param__private.values[self.name] = val + _old = obj._param__private.values.get(name, self.default) + obj._param__private.values[name] = val else: - _old = obj._param__private.values.get(self.name, self.default) + _old = obj._param__private.values.get(name, self.default) if val is not _old: raise TypeError("Constant parameter '%s' cannot be modified" % name) else: @@ -2011,10 +2015,11 @@ def __set__(self, obj: Parameterized | None, val: _T): _old = self.default self.default = val else: + private = obj._param__private # When setting a Parameter before calling super. - if not isinstance(obj._param__private, _InstancePrivate): + if not isinstance(private, _InstancePrivate): warnings.warn( - f"Setting the Parameter {self.name!r} to {val!r} before " + f"Setting the Parameter {name!r} to {val!r} before " f"the Parameterized class {type(obj).__name__!r} is fully " "instantiated is deprecated and will raise an error in " "a future version. Ensure the value is set after calling " @@ -2022,29 +2027,36 @@ def __set__(self, obj: Parameterized | None, val: _T): category=_ParamPendingDeprecationWarning, stacklevel=_find_stack_level(), ) - obj.__dict__['_param__private'] = _InstancePrivate( # pyright: ignore[reportIndexIssue] + private = _InstancePrivate( explicit_no_refs=type(obj)._param__private.explicit_no_refs ) - _old = obj._param__private.values.get(name, self.default) - obj._param__private.values[name] = val + obj.__dict__['_param__private'] = private # pyright: ignore[reportIndexIssue] + values = private.values + # Only fall back to reading the `default` slot when there is no + # value stored yet, since that read is not free. + try: + _old = values[name] + except KeyError: + _old = self.default + values[name] = val self._post_setter(obj, val) if obj is None: self._invalidate_init_cache() - - if obj is not None: - if not hasattr(obj, '_param__private') or not getattr(obj._param__private, 'initialized', False): + watchers = self.watchers.get("value") + else: + instance_private = getattr(obj, '_param__private', None) + if instance_private is None or not getattr(instance_private, 'initialized', False): return obj.param._update_deps(name) - if obj is None: - watchers = self.watchers.get("value") - elif name in obj._param__private.watchers: - watchers = obj._param__private.watchers[name].get('value') - if watchers is None: - watchers = self.watchers.get("value") - else: - watchers = None + instance_watchers = instance_private.watchers + if name in instance_watchers: + watchers = instance_watchers[name].get('value') + if watchers is None: + watchers = self.watchers.get("value") + else: + watchers = None obj = self.owner if obj is None and self.owner is not None else obj diff --git a/param/parameters.py b/param/parameters.py index a7a3fcfdd..47b80d751 100644 --- a/param/parameters.py +++ b/param/parameters.py @@ -628,6 +628,24 @@ def _initialize_generator(self, gen, obj: Parameterized | None = None): gen._saved_Dynamic_last = [] gen._saved_Dynamic_time = [] + def _resolve_dynamic( + self, obj: Parameterized | None, objtype: type[Parameterized] | None = None + ) -> tuple[t.Any, bool]: + """ + Return ``(value, is_dynamic)`` for this Parameter. + + Fetches the stored value once and, if it is a generator, asks it to + produce a value. Subclasses that need to know whether the value was + dynamically generated should use this rather than calling ``__get__`` + and ``_value_is_dynamic`` in turn, which resolves the value twice. + """ + gen = super().__get__(obj, objtype) + # Generators are always callable, so this avoids the more expensive + # hasattr() probe for the common case of a plain, static value. + if callable(gen) and hasattr(gen, '_Dynamic_last'): + return self._produce_value(gen), True + return gen, False + def __get__( self, obj: Parameterized | None, objtype: type[Parameterized] | None = None ) -> _T: @@ -636,12 +654,7 @@ def __get__( return that result, otherwise ask that result to produce a value and return it. """ - gen = super().__get__(obj, objtype) - - if not hasattr(gen,'_Dynamic_last'): - return gen - else: - return t.cast("_T", self._produce_value(gen)) + return t.cast("_T", self._resolve_dynamic(obj, objtype)[0]) @instance_descriptor def __set__(self, obj: Parameterized | None, val: _T): @@ -892,12 +905,11 @@ def __get__( ------- The value of the attribute, potentially after applying bounds checks. """ - result = super().__get__(obj, objtype) - - # Should be able to optimize this commonly used method by - # avoiding extra lookups (e.g. _value_is_dynamic() is also - # looking up 'result' - should just pass it in). - if self._value_is_dynamic(obj, objtype): + # Resolve the value and its dynamism in one pass. Calling + # super().__get__() and then _value_is_dynamic() would walk the + # descriptor chain twice, which is expensive on this very hot method. + result, is_dynamic = self._resolve_dynamic(obj, objtype) + if is_dynamic: self._validate(result) return result