{ "cells": [ { "cell_type": "markdown", "id": "33cd4c67", "metadata": {}, "source": [ "# Dependencies and Watchers\n", "\n", "As outlined in the [Dynamic Parameters](Dynamic_Parameters.ipynb) guide, Param can be used in multiple ways, including as a static set of typed attributes, dynamic attributes that are computed when they are read (`param.Dynamic` parameters, a \"pull\" or \"get\" model), and using explicitly expressed chains of actions driven by events at the Parameter level (a \"push\" or \"set\" model described in this notebook). \n", "\n", "Unlike Dynamic Parameters, which calculate values when parameters are _accessed_, the dependency and watcher interface allows events to be triggered when parameters are _set_. With this interface, parameters and methods can declare that they should be updated or invoked when a given parameter is modified, spawning a cascading series of events that update settings to be consistent, adapt values as appropriate for a change, or invoke computations such as updating a displayed object when a value is modified. This approach is well suited to a GUI interface, where a user interacts with a single widget at a time but other widgets or displays need to be updated in response. The\n", "[Dynamic Parameters](Dynamic_Parameters.ipynb) approach, in contrast, is well suited when Parameters update either on read or in response to a global clock or counter, such as in a simulation or machine-learning iteration.\n", "\n", "This user guide is structured as three main sections:\n", "\n", "- [Dependencies](#dependencies): High-level dependency declaration via the `@param.depends()` decorator\n", "- [Watchers](#watchers): Low-level watching mechanism via `.param.watch()`.\n", "- [Using dependencies and watchers](#using-dependencies-and-watchers): Utilities and tools for working with events created using either dependencies or watchers." ] }, { "cell_type": "markdown", "id": "12e54858", "metadata": {}, "source": [ "## Dependencies\n", "\n", "Param's `depends` decorator allows a programmer to express that a given computation \"depends\" on a certain set of parameters. For instance, if you have parameters whose values are interlinked, it's easy to express that relationship with `depends`:" ] }, { "cell_type": "code", "execution_count": null, "id": "3001f2d9", "metadata": {}, "outputs": [], "source": [ "import param\n", "\n", "class C(param.Parameterized):\n", " _countries = {'Africa': ['Ghana', 'Togo', 'South Africa'],\n", " 'Asia' : ['China', 'Thailand', 'Japan', 'Singapore'],\n", " 'Europe': ['Austria', 'Bulgaria', 'Greece', 'Switzerland']}\n", " \n", " continent = param.Selector(default='Asia', objects=list(_countries.keys()))\n", " country = param.Selector(objects=_countries['Asia'])\n", " \n", " @param.depends('continent', watch=True)\n", " def _update_countries(self):\n", " countries = self._countries[self.continent]\n", " self.param['country'].objects = countries\n", " if self.country not in countries:\n", " self.country = countries[0]\n", "\n", "c = C()\n", "c.country, c.param.country.objects" ] }, { "cell_type": "code", "execution_count": null, "id": "93e1c6e6", "metadata": {}, "outputs": [], "source": [ "c.continent='Africa'\n", "c.country, c.param.country.objects" ] }, { "cell_type": "code", "execution_count": null, "id": "8258470a", "metadata": {}, "outputs": [], "source": [ "c" ] }, { "cell_type": "markdown", "id": "e2f371c2", "metadata": {}, "source": [ "As you can see, here Param updates the allowed and current values for `country` whenever someone changes the `continent` parameter. This code relies on the dependency mechanism to make sure these parameters are kept appropriately synchronized:\n", "\n", "1. First, we set up the default continent but do not declare the `objects` and `default` for the `country` parameter. This is because this parameter is dependent on the `continent` and therefore it is easy to set up values that are inconsistent and makes it difficult to override the default continent since changes to both parameters need to be coordinated. \n", "2. Next, if someone chooses a different continent, the list of countries allowed needs to be updated, so the method `_update_countries()` that (a) looks up the countries allowed for the current continent, (b) sets that list as the allowed objects for the `country` parameter, and (c) selects the first such country as the default country.\n", "3. Finally, we expressed that the `_update_countries()` method depends on the `continent` parameter. We specified `watch=True` to direct Param to invoke this method immediately, whenever the value of `continent` changes. We'll see [examples of watch=False](#watch-false-dependencies) later. Importantly we also set `on_init=True`, which means that when instance is created the `self._update_countries()` method is automatically called setting up the `country` parameter appropriately. This avoids having to declare a `__init__` method to manually call the method ourselves and the potentially brittle process of setting up consistent defaults." ] }, { "cell_type": "markdown", "id": "b14b37b6", "metadata": {}, "source": [ "### Dependency specs\n", "\n", "The example above expressed a dependency of `_update_countries` on this object's `continent` parameter. A wide range of such dependency relationships can be specified:\n", "\n", "1. **Multiple dependencies**: Here we had only one parameter in the dependency list, but you can supply any number of dependencies (`@param.depends('continent', 'country', watch=True)`).\n", "2. **Dependencies on nested parameters**: Parameters specified can either be on this class, or on nested Parameterized objects of this class. Parameters on this class are specified as the attribute name as a simple string (like `'continent'`). Nested parameters are specified as a dot-separated string (like `'handler.strategy.i'`, if this object has a parameter `handler`, whose value is an object `strategy`, which itself has a parameter `i`). If you want to depend on some arbitrary parameter elsewhere in Python, just create an `instantiate=False` (and typically read-only) parameter on this class to hold it, then here you can specify the path to it on _this_ object.\n", "3. **Dependencies on metadata**: By default, dependencies are tied to a parameter's current value, but dependencies can also be on any of the declared metadata about the parameter (e.g. a method could depend on `country:constant`, triggering when someone changes whether that parameter is constant, or on `country:objects` (triggering when the objects list is replaced (not just changed in place as in appending). The available metadata is listed in the `__slots__` attribute of a Parameter object (e.g. \n", "`p.param.continent.__slots__`). \n", "4. **Dependencies on any nested param**: If you want to depend on _all_ the parameters of a nested object `n`, your method can depend on `'n.param'` (where parameter `n` has been set to a Parameterized object).\n", "5. **Dependencies on a method name**: Often you will want to break up computation into multiple chunks, some of which are useful on their own and some which require other computations to have been done as prerequisites. In this case, your method can declare a dependency on another method (as a string name), which means that it will now watch everything that method watches, and will then get invoked after that method is invoked.\n", "\n", "We can see examples of all these dependency specifications in class `D` below:" ] }, { "cell_type": "code", "execution_count": null, "id": "6df85b16", "metadata": {}, "outputs": [], "source": [ "class D(param.Parameterized):\n", " x = param.Number(7)\n", " s = param.String(\"never\")\n", " i = param.Integer(-5)\n", " o = param.Selector(objects=['red', 'green', 'blue'])\n", " n = param.ClassSelector(default=c, class_=param.Parameterized, instantiate=False) \n", " \n", " @param.depends('x', 's', 'n.country', 's:constant', watch=True)\n", " def cb1(self):\n", " print(f\"cb1 x={self.x} s={self.s} \"\n", " f\"param.s.constant={self.param.s.constant} n.country={self.n.country}\")\n", "\n", " @param.depends('n.param', watch=True)\n", " def cb2(self):\n", " print(f\"cb2 n={self.n}\")\n", "\n", " @param.depends('x', 'i', watch=True)\n", " def cb3(self):\n", " print(f\"cb3 x={self.x} i={self.i}\")\n", "\n", " @param.depends('cb3', watch=True)\n", " def cb4(self):\n", " print(f\"cb4 x={self.x} i={self.i}\")\n", "\n", "d = D()\n", "d" ] }, { "cell_type": "markdown", "id": "80365d7f", "metadata": {}, "source": [ "Here we have created an object `d` of type `D` with a unique ID like `D00003`. `d` has various parameters, including one nested Parameterized object in its parameter `n`. In this class, the nested parameter is set to our earlier object `c`, using `instantiate=False` to ensure that the value is precisely the same earlier object, not a copy of it. You can verify that it is the same object by comparing e.g. `name='C00002'` in the repr for the subobject in `d` to the name in the repr for `c` in the previous section; both should be e.g. `C00002`.\n", "\n", "Dependencies are stored declaratively so that they are accessible for other libraries to inspect and use. E.g. we can now examine the dependencies for the decorated callback method `cb1`:" ] }, { "cell_type": "code", "execution_count": null, "id": "c13bd79d", "metadata": {}, "outputs": [], "source": [ "dependencies = d.param.method_dependencies('cb1')\n", "[f\"{o.inst.name}.{o.pobj.name}:{o.what}\" for o in dependencies]" ] }, { "cell_type": "markdown", "id": "7548280e", "metadata": {}, "source": [ "Here we can see that method `cb1` will be invoked for any value changes in `d`'s parameters `x` or `s`, for any value changes in `c`'s parameter `country`, and a change in the `constant` slot of `s`. These dependency relationships correspond to the specification `@param.depends('x', 's', 'n.country', 's:constant', watch=True)` above.\n", "\n", "Now, if we change `x`, we can see that Param invokes `cb1`:" ] }, { "cell_type": "code", "execution_count": null, "id": "77caf9c1", "metadata": {}, "outputs": [], "source": [ "d.x = 5" ] }, { "cell_type": "markdown", "id": "066453bf", "metadata": {}, "source": [ "`cb3` and `cb4` are also invoked, because `cb3` depends on `x` as well, plus `cb4` depends on `cb3`, inheriting all of `cb3`'s dependencies.\n", "\n", "If we now change `c.country`, `cb1` will be invoked since `cb1` depends on `n.country`, and `n` is currently set to `c`:" ] }, { "cell_type": "code", "execution_count": null, "id": "17394110", "metadata": {}, "outputs": [], "source": [ "c.country = 'Togo'" ] }, { "cell_type": "markdown", "id": "477ab1d1", "metadata": {}, "source": [ "As you can see, `cb2` is also invoked, because `cb2` depends on _all_ parameters of the subobject in `n`. \n", "\n", "`continent` is also a parameter on `c`, so `cb2` will also be invoked if you change `c.continent`. Note that changing `c.continent` itself invokes `c._update_countries()`, so in that case `cb2` actually gets invoked _twice_ (once for each parameter changed on `c`), along with `cb1` (watching `n.country`):" ] }, { "cell_type": "code", "execution_count": null, "id": "acc4c4cb", "metadata": {}, "outputs": [], "source": [ "c.continent = 'Europe'" ] }, { "cell_type": "markdown", "id": "a973b325", "metadata": {}, "source": [ "Changing metadata works just the same as changing values. Because `cb1` depends on the `constant` slot of `s`, it is invoked when that slot changes:" ] }, { "cell_type": "code", "execution_count": null, "id": "a6403a93", "metadata": {}, "outputs": [], "source": [ "d.param.s.constant = True" ] }, { "cell_type": "markdown", "id": "68c82e25", "metadata": {}, "source": [ "Importantly, if we replace a sub-object on which we have declared dependencies, Param automatically rebinds the dependencies to the new object:" ] }, { "cell_type": "code", "execution_count": null, "id": "f6195bdd", "metadata": {}, "outputs": [], "source": [ "d.n = C()" ] }, { "cell_type": "markdown", "id": "af05eb1f", "metadata": {}, "source": [ "Note that if the values of the dependencies on the old and new object are the same, no event is fired. \n", "\n", "Additionally the previously bound sub-object is now no longer connected:" ] }, { "cell_type": "code", "execution_count": null, "id": "9d52086e", "metadata": {}, "outputs": [], "source": [ "c.continent = 'Europe'" ] }, { "cell_type": "markdown", "id": "6e4d5b80", "metadata": {}, "source": [ "### `watch=False` dependencies\n", "\n", "The previous examples all supplied `watch=True`, indicating that Param itself should watch for changes in the dependency and invoke that method when a dependent parameter is set. If `watch=False` (the default), `@param.depends` declares that such a dependency exists, but does not automatically invoke it. `watch=False` is useful for setting up code for a separate library like [Panel](https://panel.holoviz.org) or [HoloViews](https://holoviews.org) to use, indicating which parameters the external library should watch so that it knows when to invoke the decorated method. Typically, you'll want to use `watch=False` when that external library needs to do something with the return value of the method (a functional approach), and use `watch=True` when the function is [side-effecty](https://en.wikipedia.org/wiki/Side_effect_(computer_science)), i.e. having an effect just from running it, and not normally returning a value.\n", "\n", "For instance, consider this Param class with methods that return values to display:" ] }, { "cell_type": "code", "execution_count": null, "id": "7f1ebe63", "metadata": {}, "outputs": [], "source": [ "class Mul(param.Parameterized):\n", " a = param.Number(5, bounds=(-100, 100))\n", " b = param.Number(-2, bounds=(-100, 100))\n", "\n", " @param.depends('a', 'b')\n", " def view(self):\n", " return str(self.a*self.b)\n", "\n", " def view2(self):\n", " return str(self.a*self.b)\n", "\n", "prod = Mul(name='Multiplier')" ] }, { "cell_type": "markdown", "id": "00ff51ea", "metadata": {}, "source": [ "You could run this code manually:" ] }, { "cell_type": "code", "execution_count": null, "id": "fc51ed2b", "metadata": {}, "outputs": [], "source": [ "prod.a = 7\n", "prod.b = 10\n", "prod.view()" ] }, { "cell_type": "markdown", "id": "50ab0942", "metadata": {}, "source": [ "Or you could pass the parameters and the `view` method to Panel, and let Panel invoke it as needed by following the dependency chain:" ] }, { "cell_type": "code", "execution_count": null, "id": "05f1bb0d", "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "pn.extension()" ] }, { "cell_type": "code", "execution_count": null, "id": "050b268a", "metadata": {}, "outputs": [], "source": [ "pn.Row(prod.param, prod.view)" ] }, { "cell_type": "markdown", "id": "acda7cc7", "metadata": {}, "source": [ "Panel creates widgets for the parameters, runs the `view` method with the default values of those parameters, and displays the result. As long as you have a live Python process running (not just a static HTML export of this page as on param.holoviz.org), Panel will then watch for changes in those parameters due to the widgets and will re-execute the `view` method to update the output whenever one of those parameters changes. Using the dependency declarations, Panel is able to do all this without ever having to be told separately which parameters there are or what dependency relationships there are. \n", "\n", "How does that work? A library like Panel can simply ask Param what dependency relationships have been declared for the method passed to it:" ] }, { "cell_type": "code", "execution_count": null, "id": "3536291d", "metadata": {}, "outputs": [], "source": [ "[o.name for o in prod.param.method_dependencies('view')]" ] }, { "cell_type": "markdown", "id": "899a41e8", "metadata": {}, "source": [ "Note that in this particular case the `depends` decorator could have been omitted, because Param conservatively assumes that any method _could_ read the value of any parameter, and thus if it has no other declaration from the user, the dependencies are assumed to include _all_ parameters (including `name`, even though it is constant):" ] }, { "cell_type": "code", "execution_count": null, "id": "a8bef58c", "metadata": {}, "outputs": [], "source": [ "[o.name for o in prod.param.method_dependencies('view2')]" ] }, { "cell_type": "markdown", "id": "f8383036", "metadata": {}, "source": [ "Conversely, if you want to declare that a given method does not depend on any parameters at all, you can use `@param.depends()`. \n", "\n", "Be sure not to set `watch=True` for dependencies for any method you pass to an external library like Panel to handle, or else that method will get invoked _twice_, once by Param itself (discarding the output) and once by the external library (using the output). Typically you will want `watch=True` for a side-effecty function or method (typically not returning a value), and `watch=False` (the default) for a function or method with a return value, and you'll need an external library to do something with that return value." ] }, { "cell_type": "markdown", "id": "1169f3b6", "metadata": {}, "source": [ "### `@param.depends` with function objects\n", "\n", "The `depends` decorator can also be used with bare functions, in which case the specification should be an actual Parameter object, not a string. The function will be called with the parameter(s)'s value(s) as positional arguments:" ] }, { "cell_type": "code", "execution_count": null, "id": "60a085b7", "metadata": {}, "outputs": [], "source": [ "@param.depends(c.param.country, d.param.i, watch=True)\n", "def g(country, i):\n", " print(f\"g country={country} i={i}\")\n", "\n", "c.country = 'Greece'" ] }, { "cell_type": "code", "execution_count": null, "id": "a7105a92", "metadata": {}, "outputs": [], "source": [ "d.i = 6" ] }, { "cell_type": "markdown", "id": "cd198a2e", "metadata": {}, "source": [ "Here you can see that in addition to the classmethods starting with `cb` previously set up to depend on the country, setting `c`'s `country` parameter or `d`'s `i` parameter now also invokes function `g`, passing in the current values of the parameters it depends on whenever the function gets invoked. `g` can then make a side effect happen such as updating any other data structure it can access that needs to be changed when `country` or `i` changes. \n", "\n", "Using `@param.depends(..., watch=False)` with a function allows providing bound standalone functions to an external library for display, just as in the `.view` method above.\n", "\n", "Of course, you can still invoke `g` with your own explicit arguments, which does not invoke any watching mechanisms:" ] }, { "cell_type": "code", "execution_count": null, "id": "c4e160c1", "metadata": {}, "outputs": [], "source": [ "g('USA', 7)" ] }, { "cell_type": "markdown", "id": "00370f00", "metadata": {}, "source": [ "## Watchers\n", "\n", "The `depends` decorator is built on Param's lower-level `.param.watch` interface, registering the decorated method or function as a `Watcher` object associated with those parameter(s). If you're building or using a complex library like Panel, you can use the low-level Parameter watching interface to set up arbitrary chains of watchers to respond to parameter value or metadata setting:\n", "\n", "- `obj.param.watch(fn, parameter_names, what='value', onlychanged=True, queued=False, precedence=0)`:
Create and register a `Watcher` that will invoke the given callback `fn` when the `what` item (`value` or one of the Parameter's slots) is set (or more specifically, changed, if `onlychanged=True`). If `queued=True`, delays calling any events triggered during this callback's execution until all processing of the current events has been completed (breadth-first Event processing rather than the default depth-first processing). The `precedence` declares a precedence level for the Watcher that determines the priority with which the callback is executed. Lower precedence levels are executed earlier. Negative precedences are reserved for internal Watchers, i.e. those set up by `param.depends`. The `fn` will be invoked with one or more `Event` objects that have been triggered, as positional arguments. Returns a `Watcher` object, e.g. for use in `unwatch`.\n", "\n", "- `obj.param.watch_values(fn, parameter_names, what='value', onlychanged=True, queued=False, precedence=0)`:
Easier-to-use version of `obj.param.watch` specific to watching for changes in parameter values. Same as `watch`, but hard-codes `what='value'` and invokes the callback `fn` using keyword arguments _param_name_=_new_value_ rather than with a positional-argument list of `Event` objects.\n", "\n", "- `obj.param.unwatch(watcher)`:
Remove the given `Watcher` (typically obtained as the return value from `watch` or `watch_values`) from those registered on this `obj`.\n", "\n", "To see how to use `watch` and `watch_values`, let's make a class with parameters `a` and `b` and various watchers with corresponding callback methods:" ] }, { "cell_type": "code", "execution_count": null, "id": "9f076d1b", "metadata": {}, "outputs": [], "source": [ "def e(e):\n", " return f\"(event: {e.name} changed from {e.old} to {e.new})\"\n", "\n", "class P(param.Parameterized):\n", " a = param.Integer(default=0)\n", " b = param.Integer(default=0)\n", " \n", " def __init__(self, **params):\n", " super().__init__(**params)\n", " self.param.watch(self.run_a1, ['a'], queued=True, precedence=2)\n", " self.param.watch(self.run_a2, ['a'], precedence=1)\n", " self.param.watch(self.run_b, ['b'])\n", "\n", " def run_a1(self, event):\n", " self.b += 1\n", " print('a1', self.a, e(event))\n", "\n", " def run_a2(self, event):\n", " print('a2', self.a, e(event))\n", "\n", " def run_b(self, event):\n", " print('b', self.b, e(event))\n", " \n", "p = P()\n", "\n", "p.a = 1" ] }, { "cell_type": "markdown", "id": "6cdbd8e5", "metadata": {}, "source": [ "Here, we have set up three Watchers, each invoking a method on `P` when either `a` or `b` changes. The first Watcher invokes `run_a1` when `a` changes, and in turn `run_a1` changes `b`. Since `queued=True` for `run_a1`, `run_b` is not invoked while `run_a1` executes, but only later once both `run_a1` and `run_a2` have completed (since both Watchers were triggered by the original event `p.a=1`).\n", "\n", "Additionally we have set a higher `precedence` value for `run_a1` which results in it being executed **after** `run_a2`.\n", "\n", "Here we're using data from the `Event` objects given to each callback to see what's changed; try `help(param.parameterized.Event)` for details of what is in these objects (and similarly try the help for `Watcher` (returned by `watch`) or `PInfo` (returned by `.param.method_dependencies`))." ] }, { "cell_type": "code", "execution_count": null, "id": "cef96eae", "metadata": {}, "outputs": [], "source": [ "#help(param.parameterized.Event)\n", "#help(param.parameterized.Watcher)\n", "#help(param.parameterized.PInfo)" ] }, { "cell_type": "markdown", "id": "c06d1a13", "metadata": {}, "source": [ "## Using dependencies and watchers\n", "\n", "Whether you use the `watch` or the `depends` approach, Param will store a set of `Watcher` objects on each `Parameterized` object that let it manage and process `Event`s. Param provides various context managers, methods, and Parameters that help you work with Watchers and Events:\n", "\n", "- [`batch_call_watchers`](#batch-call-watchers): context manager accumulating and eliding multiple Events to be applied on exit from the context \n", "- [`discard_events`](#discard-events): context manager silently discarding events generated while in the context\n", "- [`.param.trigger`](#param-trigger): method to force creation of an Event for this Parameter's Watchers without a corresponding change to the Parameter\n", "- [`.param.watchers`](#param-watchers): writable property to access the instance watchers\n", "- [Event Parameter](#event-parameter): Special Parameter type providing triggerable transient Events (like a momentary push button)\n", "- [Async executor](#async-executor): Support for asynchronous processing of Events, e.g. for interfacing to external servers\n", "\n", "Each of these will be described in the following sections." ] }, { "cell_type": "markdown", "id": "9356f733", "metadata": {}, "source": [ "### `batch_call_watchers`\n", "\n", "Context manager that accumulates parameter changes on the supplied object and dispatches them all at once when the context is exited, to allow multiple changes to a given parameter to be accumulated and short-circuited, rather than prompting serial changes from a batch of parameter setting:" ] }, { "cell_type": "code", "execution_count": null, "id": "905456c3", "metadata": {}, "outputs": [], "source": [ "with param.parameterized.batch_call_watchers(p):\n", " p.a = 2\n", " p.a = 3\n", " p.a = 1\n", " p.a = 5" ] }, { "cell_type": "markdown", "id": "9c25515a", "metadata": {}, "source": [ "Here, even though `p.a` is changed four times, each of the watchers of `a` is executed only once, with the final value. One of those events then changes `b`, so `b`'s watcher is also executed once.\n", "\n", "If we set `b` explicitly, `b`'s watcher will be invoked twice, once for the explicit setting of `b`, and once because of the code `self.b += 1`:" ] }, { "cell_type": "code", "execution_count": null, "id": "6c7fe2df", "metadata": {}, "outputs": [], "source": [ "with param.parameterized.batch_call_watchers(p):\n", " p.a = 2\n", " p.b = 8\n", " p.a = 3\n", " p.a = 1\n", " p.a = 5" ] }, { "cell_type": "markdown", "id": "1deb1709", "metadata": {}, "source": [ "If all you need to do is set a batch of parameters, you can use `.update` instead of `batch_call_watchers`, which has the same underlying batching mechanism:" ] }, { "cell_type": "code", "execution_count": null, "id": "e511a92a", "metadata": {}, "outputs": [], "source": [ "p.param.update(a=9,b=2)" ] }, { "cell_type": "markdown", "id": "acc97f01", "metadata": {}, "source": [ "### `discard_events`\n", "\n", "Context manager that discards any events within its scope that are triggered on the supplied parameterized object. Useful for making silent changes to dependent parameters, e.g. in a setup phase. If your dependencies are meant to ensure consistency between parameters, be careful that your manual changes in this context don't put the object into an inconsistent state!" ] }, { "cell_type": "code", "execution_count": null, "id": "cf129b7c", "metadata": {}, "outputs": [], "source": [ "with param.parameterized.discard_events(p):\n", " p.a = 2\n", " p.b = 9" ] }, { "cell_type": "markdown", "id": "ffc9a462", "metadata": {}, "source": [ "(Notice that none of the callbacks is invoked, despite all the Watchers on `p.a` and `p.b`.)" ] }, { "cell_type": "markdown", "id": "67437fe6", "metadata": {}, "source": [ "### `.param.trigger`\n", "\n", "Usually, a Watcher will be invoked only when a parameter is set (and only if it is changed, by default). What if you want to trigger a Watcher in other cases? For instance, if a parameter value is a mutable container like a list and you add or change an item in that container, Param's `set` method will never be invoked, because in Python the container itself is not changed when the contents are changed. In such cases, you can trigger a watcher explicitly, using `.param.trigger(*param_names)`. Triggering does not affect parameter values, apart from the special parameters of type Event (see [below](#Event-Parameter:)).\n", "\n", "For instance, if you set `p.b` to the value it already has, no callback will normally be invoked:" ] }, { "cell_type": "code", "execution_count": null, "id": "c2b06db2", "metadata": {}, "outputs": [], "source": [ "p.b = p.b" ] }, { "cell_type": "markdown", "id": "9fb9a5a8", "metadata": {}, "source": [ "But if you explicitly trigger parameter `b` on `p`, `run_b` will be invoked, even though the value of `b` is not changing:" ] }, { "cell_type": "code", "execution_count": null, "id": "b086d010", "metadata": {}, "outputs": [], "source": [ "p.param.trigger('b')" ] }, { "cell_type": "markdown", "id": "621e183e", "metadata": {}, "source": [ "If you trigger `a`, the usual series of chained events will be triggered, including changing `b`:" ] }, { "cell_type": "code", "execution_count": null, "id": "827ef750", "metadata": {}, "outputs": [], "source": [ "p.param.trigger('a')" ] }, { "cell_type": "markdown", "id": "fc3cb5ba", "metadata": {}, "source": [ "### `.param.watchers`\n", "\n", "For more advanced purposes it can be useful to inspect all the watchers set up on an instance, in which case you can use `inst.param.watchers` to obtain a dictionary with the following structure: `{parameter_name: {what: [Watcher(), ...], ...}, ...}`" ] }, { "cell_type": "code", "execution_count": null, "id": "0b2ae598", "metadata": {}, "outputs": [], "source": [ "p.param.watchers" ] }, { "cell_type": "markdown", "id": "42ffa0a0", "metadata": {}, "source": [ "### `Event` Parameter\n", "\n", "An Event Parameter is a special Parameter type whose value is intimately linked to the triggering of events for Watchers to consume. Event has a Boolean value, which when set to `True` triggers the associated watchers (as any Parameter does) but then is automatically set back to `False`. \n", "\n", "Conversely, if events are triggered directly on a `param.Event` via `.trigger`, the value is transiently set to True (so that it's clear which of many parameters being watched may have changed), then restored to False when the triggering completes. An Event parameter is thus like a momentary switch or pushbutton with a transient True value that normally serves only to launch some other action (e.g. via a `param.depends` decorator or a watcher), rather than encapsulating the action itself as `param.Action` does. " ] }, { "cell_type": "code", "execution_count": null, "id": "8498624d", "metadata": {}, "outputs": [], "source": [ "class Q(param.Parameterized):\n", " e = param.Event()\n", " \n", " @param.depends('e', watch=True)\n", " def callback(self):\n", " print(f'e=={self.e}')\n", " \n", "q = Q()\n", "q.e = True" ] }, { "cell_type": "code", "execution_count": null, "id": "317b053e", "metadata": {}, "outputs": [], "source": [ "q.e" ] }, { "cell_type": "code", "execution_count": null, "id": "b423aa8b", "metadata": {}, "outputs": [], "source": [ "q.param.trigger('e')" ] }, { "cell_type": "code", "execution_count": null, "id": "bb5b83ae", "metadata": {}, "outputs": [], "source": [ "q.e" ] }, { "cell_type": "markdown", "id": "8f17a138", "metadata": {}, "source": [ "### Async executor\n", "\n", "Param's events and callbacks described above are all synchronous, happening in a clearly defined order where the processing of each function blocks all other processing until it is completed. Watchers can also be used with the Python3 asyncio [`async`/`await`](https://docs.python.org/3/library/asyncio-task.html) support to operate asynchronously. The asynchronous executor can be defined on `param.parameterized.async_executor` by default it will start an [asyncio](https://docs.python.org/3/library/asyncio.html) event loop or reuse the one that is running. This allows you to use coroutines and other asynchronous functions as `.param.watch` callbacks.\n", "\n", "As an example we can watch results accumulate:" ] }, { "cell_type": "code", "execution_count": null, "id": "03ece21a", "metadata": {}, "outputs": [], "source": [ "import param, asyncio, aiohttp\n", "\n", "class Downloader(param.Parameterized):\n", " url = param.String()\n", " results = param.List()\n", " \n", " def __init__(self, **params):\n", " super().__init__(**params)\n", " self.param.watch(self.fetch, ['url'])\n", "\n", " async def fetch(self, event):\n", " async with aiohttp.ClientSession() as session:\n", " async with session.get(event.new) as response:\n", " img = await response.read()\n", " self.results.append((event.new, img))\n", "\n", "f = Downloader()\n", "n = 7\n", "for index in range(n):\n", " f.url = f\"https://picsum.photos/800/300?image={index}\"\n", "\n", "f.results" ] }, { "cell_type": "markdown", "id": "4744d8c7", "metadata": {}, "source": [ "When you execute the above cell, you will normally get `[]`, indicating that there are not yet any results available. \n", "\n", "What the code does is to request 10 different images from an image site by repeatedly setting the `url` parameter of `Downloader` to a new URL. Each time the `url` parameter is modified, because of the `self.param.watch` call, the `self.fetch` callback is invoked. Because it is marked `async` and uses `await` internally, the method call returns without waiting for results to be available. Once the `await`ed results are available, the method continues with its execution and a tuple (_image_name_, _image_data_) is added to the `results` parameter.\n", "\n", "If you need to have all the results available (and have an internet connection!), you can wait for them:" ] }, { "cell_type": "code", "execution_count": null, "id": "38da7bb2", "metadata": {}, "outputs": [], "source": [ "print(\"Waiting: \", end=\"\")\n", "while len(f.results)