diff --git a/doc/reference/param/parameterized_helpers.md b/doc/reference/param/parameterized_helpers.md index 6fb6b42aa..52fe56ec3 100644 --- a/doc/reference/param/parameterized_helpers.md +++ b/doc/reference/param/parameterized_helpers.md @@ -17,5 +17,8 @@ discard_events edit_constant output + param.parameterized.register_reference_transform + param.parameterized.resolve_ref + param.parameterized.resolve_value script_repr ``` diff --git a/doc/user_guide/How_Param_Works.ipynb b/doc/user_guide/How_Param_Works.ipynb index c02624506..5f189748e 100644 --- a/doc/user_guide/How_Param_Works.ipynb +++ b/doc/user_guide/How_Param_Works.ipynb @@ -69,7 +69,7 @@ "__slots__ = ['name', 'default', 'doc',\n", " 'precedence', 'instantiate', 'constant', 'readonly',\n", " 'pickle_default_value', 'allow_None', 'per_instance',\n", - " 'watchers', 'owner', '_label']\n", + " 'watchers', 'owner', 'allow_refs', 'nested_refs', '_label']\n", "```\n", "\n", "In most cases, you can just treat a Parameter's existing slots like attributes of the Parameter class; they work just the same as regular attributes except for speed and storage space. However, if you add a _new_ attribute to a Parameter class, you have to make sure that you also add it to the `__slots__` defined for that Parameter class, or you'll either get an error or else the Parameter will get an unnecessary full `__dict__` object just to hold the one new attribute. " @@ -237,6 +237,135 @@ "You can re-execute the above code changing to `per_instance=True` and/or `instantiate=True` on Parameter `p` and see how the behavior differs. With `per_instance=True` (which would normally be the default), `a1` and `a2` would each have independent copies of the `Parameter` object, and with `instantiate=True`, each instance would get its own copy of the class's default value, making it immune to later changes at the class level." ] }, + { + "cell_type": "markdown", + "id": "b86f55e3-5ae5-4879-9b4c-273e859b6fec", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "Beyond the custom attribute access mechanisms of a single `Parameter`, Param can link together multiple Parameters. With the `allow_refs` option, a `Parameter` can act as a dynamic reference to another `Parameter`. This enables the values of two or more Parameters to stay in sync. Any change to the referenced Parameter's value is automatically reflected in all Parameters that reference it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fba9fbdc-0776-4cd3-8250-e00487593d57", + "metadata": {}, + "outputs": [], + "source": [ + "class B(Parameterized):\n", + " \n", + " p = Parameter(default=1, allow_refs=True)" + ] + }, + { + "cell_type": "markdown", + "id": "27beb80b-415f-41cb-b909-adb6822e7e9a", + "metadata": {}, + "source": [ + "Having declared a `Parameter` that allows references we can now pass the parameter `p` of `b1` to parameter `p` of `b2`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22a51c29-c286-4e0f-b1a6-1e02bc4149da", + "metadata": {}, + "outputs": [], + "source": [ + "b1 = B(p=14)\n", + "b2 = B(p=b1.param.p)" + ] + }, + { + "cell_type": "markdown", + "id": "5eab5e13-66df-4001-8d70-2cab70449641", + "metadata": {}, + "source": [ + "Inspecting `b2.p` we will see that `p` of `b2` now reflects the value of `b1.p`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fdc58565-f327-405f-958d-b49caa70adb3", + "metadata": {}, + "outputs": [], + "source": [ + "b2.p" + ] + }, + { + "cell_type": "markdown", + "id": "7322e310-9105-48c9-a0b8-41cb80bcf4e6", + "metadata": {}, + "source": [ + "Even when we update the value of `b1.p` the value of `b2.p` will reflect the change:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "961aebc0-45b5-458d-919c-1ba6a3c286e7", + "metadata": {}, + "outputs": [], + "source": [ + "b1.p = 7\n", + "\n", + "b2.p" + ] + }, + { + "cell_type": "markdown", + "id": "75c5eb72-3132-458c-9b30-b5cd1e64b63d", + "metadata": {}, + "source": [ + "Internally Param will [watch](Dependencies_and_Watchers.ipynb#watchers) all Parameters associated with the reference by calling `param.parameterized.resolve_ref` and then use `param.parameterized.resolve_value` to resolve the current value of the reference.\n", + "\n", + "If we explicitly set `b2.p` however the two values will become unsynced, i.e. the Watcher created when setting the reference will be removed:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e02cb21-a4ab-48fb-a516-da17feaf0da0", + "metadata": {}, + "outputs": [], + "source": [ + "b2.p = 3\n", + "\n", + "print(b1.p, b2.p)" + ] + }, + { + "cell_type": "markdown", + "id": "701e26c3-6eb0-4c76-b453-cc37be06ac41", + "metadata": {}, + "source": [ + "and any subsequent changes to `b1.p` will not be reflected by `b2`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50aedbea-7902-4f1a-8e98-6866a965eda8", + "metadata": {}, + "outputs": [], + "source": [ + "b1.p = 27\n", + "\n", + "b2.p" + ] + }, + { + "cell_type": "markdown", + "id": "07d982cd-c0f1-496f-8086-28a882266901", + "metadata": {}, + "source": [ + "If a new reference is assigned then the old Watcher will be removed a new one will be registered." + ] + }, { "cell_type": "markdown", "id": "c3e3d3aa", diff --git a/doc/user_guide/Parameters.ipynb b/doc/user_guide/Parameters.ipynb index 6f2ee440b..7e6163893 100644 --- a/doc/user_guide/Parameters.ipynb +++ b/doc/user_guide/Parameters.ipynb @@ -37,6 +37,8 @@ "- **instantiate**: Whether to deepcopy the default value into a Parameterized instance when it is created. False by default for Parameter and most of its subtypes, but some Parameter types commonly used with mutable containers default to `instantiate=True` to avoid interaction between separate Parameterized instances, and users can control this when declaring the Parameter (see below). \n", "- **per_instance**: whether a separate Parameter instance will be created for every Parameterized instance created. Similar to `instantiate`, but applies to the Parameter object rather than to its value.\n", "- **precedence**: Optional numeric value controlling whether this parameter is visible in a listing and if so in what order.\n", + "- **allow_refs**: Whether to allow the Parameter to accept references to other Parameters that will be dynamically resolved.\n", + "- **nested_refs**: Whether references should be resolved even when they are nested inside a container.\n", "\n", "Most of these settings (apart from **name**) are accepted as keyword arguments to the Parameter's constructor, with `default` mostly also accepted as the only positional argument:" ] @@ -784,6 +786,156 @@ "This approach can provide significant speedup and memory savings in certain cases, but should only be used for good reasons, since it can cause confusion for any code expecting instances to be independent as they have been declared." ] }, + { + "cell_type": "markdown", + "id": "2579c782-8dfd-4ded-995f-dc8498f4d275", + "metadata": {}, + "source": [ + "## Parameter references\n", + "\n", + "Building on the previous discussion about the dual roles of a `Parameter` - as both a value holder and a metadata container - let's explore how parameters can go a step further by acting as dynamic references to other parameters. When configured with `allow_refs=True`, a `Parameter` can serve as a live link to another `Parameter`, mirroring its current value. This capability enables more intricate relationships between parameters, allowing for automatic value synchronization and forming the basis for reactive programming." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "111e4911-4b73-40c3-ab57-70cd42eb7fb8", + "metadata": {}, + "outputs": [], + "source": [ + "class U(param.Parameterized):\n", + " \n", + " a = param.Number()\n", + " \n", + "class V(param.Parameterized):\n", + " \n", + " b = param.Number(allow_refs=True)\n", + " \n", + "u = U(a=3.14)\n", + "v = V(b=u.param.a)\n", + "\n", + "v.b" + ] + }, + { + "cell_type": "markdown", + "id": "324475cf-65a3-4902-8508-57af19697b5f", + "metadata": {}, + "source": [ + "By declaring that `V.b` allows references we have made it possible to pass the Parameter `U.a`, which means `v.b` will reflect the value of `u.a`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62016252-5a7d-4bb7-be7a-029ed1a47025", + "metadata": {}, + "outputs": [], + "source": [ + "u.a = 1.57\n", + "\n", + "v.b" + ] + }, + { + "cell_type": "markdown", + "id": "c6f62936-89bb-49e3-b197-cef43891b2f7", + "metadata": {}, + "source": [ + "This unidirectional link will be in effect until something else tries to set the value:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28094868-4f23-4399-be7b-da0c73854be7", + "metadata": {}, + "outputs": [], + "source": [ + "v.b = 14.1\n", + "u.a = 13.2\n", + "\n", + "v.b" + ] + }, + { + "cell_type": "markdown", + "id": "48878238-e062-4fd4-bb99-0e9020d9b26f", + "metadata": {}, + "source": [ + "In other words, if the value is overridden from the outside the link will be automatically removed.\n", + "\n", + "Simple references are resolved when `allow_refs=True` but to allow nested references we separately have to set `nested_refs=True`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d63d949d-79a2-471f-b0bb-232086e486bf", + "metadata": {}, + "outputs": [], + "source": [ + "class W(V):\n", + " \n", + " c = param.List(allow_refs=True, nested_refs=True)\n", + " \n", + "u1 = U(a=3)\n", + "u2 = U(a=13)\n", + "\n", + "w = W(c=[u1.param.a, u2.param.a]) \n", + "\n", + "w.c" + ] + }, + { + "cell_type": "markdown", + "id": "e7b3489f-4a75-4767-a7d2-b7066a976ce4", + "metadata": {}, + "source": [ + "When we modify either `u1.a` or `u2.a`, `w.c` will update:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2d9fc30-d5cb-49ac-aaf7-d4d6f1d74cb0", + "metadata": {}, + "outputs": [], + "source": [ + "u1.a = 7\n", + "\n", + "w.c" + ] + }, + { + "cell_type": "markdown", + "id": "ae2d21b3-c78b-4648-8622-282c237a1d64", + "metadata": {}, + "source": [ + "Note that `Parameter` types are not the only types of valid references. The full list of valid references include:\n", + "\n", + "- Class and instance `Parameter` objects\n", + "- Functions or methods annotated with `param.depends`\n", + "- Functions wrapped with `param.bind`\n", + "- Reactive expressions declared using `param.rx`\n", + "- Asynchronous generators\n", + "- Custom objects transformed into a valid reference with a hook registered with `param.parameterized.register_reference_transform`.\n", + "\n", + "There are two utility functions which allow resolving all parameters a reference depends on and the current value of the reference:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8cb30e85-5cfe-49d6-a641-f98c84a7acb2", + "metadata": {}, + "outputs": [], + "source": [ + "from param.parameterized import resolve_ref, resolve_value\n", + "\n", + "resolve_ref(u1.param.a), resolve_value(u1.param.a)" + ] + }, { "cell_type": "markdown", "id": "678b7a0e",