holoviz / holoviz/param

Document how to create Parameterized Singleton

Open
#643 0 comments 0 reactions 0 assignees View on GitHub
doc type-feature
Dominant language
Python
Stars
521
Forks
86
Avg merge
1d 13h
Merged PRs (30d)
35

Description

# 🚀 Feature

Document how to create a `Parameterized` Singleton.

## Motivation

I'm trying to create an `AppStateWatcher` here https://github.com/Lightning-AI/lightning/pull/13531. For efficiency reasons only one instance should be created. To make it easy to use for users in their Panel apps I would like it to be a *singleton* as described in https://www.geeksforgeeks.org/singleton-pattern-in-python-a-complete-guide/.

When if followed the guide above it *did not work* for me. I saw strange behaviour when I had two windows open. Only one of the windows showed the update when I clicked a button. But I could trigger the the update from a button in both windows. At first this was quite confusing.

But I found out that the guide only describes how the `__new__` method should be implemented. But it turned out that something similar should be done for the `__init__` method. As far as I can see lots of guides and tutorials on singletons only describe the implementation of the `__new__` method.

I believe the singleton could be generally valuable. Normally in Panel when I want to share an instance across sessions I cache it in `pn.state.cache`. But here I would like to make the `AppStateWatcher` useful for other frameworks in lightning than Panel - thus the singleton came up as the answer.

### Does not work

```python
import param

class AppStateWatcher(param.Parameterized):
state = param.Parameter()
def __new__(cls):
# This makes the AppStateWatcher a *singleton*.
# The AppStateWatcher is a singleton to minimize the number of requests etc..
if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self):
super().__init__()

app1=AppStateWatcher()
print("app1", app1)

@param.depends(app1.param.state, watch=True)
def _update_a(state):
print(state)

app1.state="x"

app2=AppStateWatcher()

@param.depends(app2.param.state, watch=True)
def _update_b(state):
print(state)

app1.state="y"
app2.state="z"

print("app1", app1)
print("app2", app2)
```

```bash
$ python 'script.py'
app1
x
y
z
app1
app2
```

## Works

```python
import param

class AppStateWatcher(param.Parameterized):
state = param.Parameter()
def __new__(cls):
# This makes the AppStateWatcher a *singleton*.
# The AppStateWatcher is a singleton to minimize the number of requests etc..
if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self):
if not hasattr(self, "_init"):
super().__init__()
self._init = True

app1=AppStateWatcher()
print("app1", app1)

@param.depends(app1.param.state, watch=True)
def _update_a(state):
print(state)

app1.state="x"

app2=AppStateWatcher()

@param.depends(app2.param.state, watch=True)
def _update_b(state):
print(state)

app1.state="y"
app2.state="z"

print("app1", app1)
print("app2", app2)
```

```bash
$ python 'script.py'
app1
x
y
y
z
z
app1
app2
```

## Explanation

My hypothesis is that a second run of `__init__` resets the `watchers` and thus should be avoided.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.