Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions benchmarks/benchmarks/benchmarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
78 changes: 45 additions & 33 deletions param/parameterized.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -1993,58 +1996,67 @@ 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:
if obj is None:
_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 "
"`super().__init__(**params)` in the constructor.",
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

Expand Down
36 changes: 24 additions & 12 deletions param/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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):
Expand Down Expand Up @@ -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

Expand Down