Cells
A cell is an object with a value and a set of observers that react to changes in its value. You’ll see exactly what that means in a moment.
There are a number of ways to create cells. The simplest cell is the
constant cell, created with the live_cells.value
function,
which holds a constant value.
import live_cells as lc
a = lc.value(1)
b = lc.value('hello world')
Mutable Cells
Mutable cells, created with live_cells.mutable
, which takes
the initial value of the cell, have a value property that can be set
directly.
import live_cells as lc
a = lc.mutable(0)
print(a.value) # Prints 0
# Set the value of a to 3
a.value = 3
print(a.value) # Prints 3
Observing Cells
When the value of a cell changes, its observers are notified of the
change. The simplest way to demonstrate this is to set up a watch
function using live_cells.watch
:
import live_cells as lc
a = lc.mutable(0)
b = lc.mutable(1)
lc.watch(lambda: print(f'{a()}, {b()}'))
a.value = 5 # Prints 5, 1
b.value = 10 # Prints 5, 10
live_cells.watch
takes a watch function and registers it to be
called when the values of the cells referenced within it change. In
the example above, a watch function that prints the values of cells
a
and b
is defined. This function is called automatically when the
value of either a
or b
changes.
There are a couple of points to keep in mind when using live_cells.watch
:
The watch function is called once immediately when
live_cells.watch
is called, to determine which cells are referenced by it.live_cells.watch
automatically tracks which cells are referenced within the watch function and calls it when their values change. This works even when the cells are referenced conditionally.
Attention
Within a watch function, the values of cells are referenced using
the function call syntax, e.g. a()
, rather than accessing the
value
property directly.
Every call to live_cells.watch
adds a new watch function:
import live_cells as lc
a = lc.mutable(0)
b = lc.mutable(1)
lc.watch(lambda: print(f'{a()}, {b()}'))
lc.watch(lambda: print(f'A = {a()}'))
# Prints: 20, 1
# Also prints: A = 20
a.value = 20
# Prints 20, 10
b.value = 10
This results in the following being printed:
20, 1
A = 20
20, 20
In this example, the second watch function only observes the value of
a
. Change the value of a
results in both the first and second
watch function being called. Changing the value of b
results in
only the first watch function being called, since the second watch
function does not reference b
and hence is not observing it.
live_cells.watch
returns a watch handle (CellWatcher
),
which provides a stop
method that deregisters the watch
function. When the stop
method is called, the watch function is no
longer called when the values of the cells it is observing change.
import live_cells as lc
a = lc.mutable(0)
watcher = lc.watch(lambda: print(f'A = {a()}'))
# Prints A = 1
a.value = 1
# Prints A = 2
a.value = 2
watcher.stop()
# Doesn't print anything
a.value = 3
Tip
A watch function with more than one expression can be defined by using
live_cells.watch
as a decorator:import live_cells as lc a = lc.mutable(0) @lc.watch def watcher(): print(f'A = {a()}') print(f'A + 1 = {a() + 1}') # Prints: # A = 2 # A + 1 = 3 a.value = 2
The decorated function is registered as a watch function that observes the cells referenced within it. The watch handle can be accessed by the name of the decorated function. For example, the watch function in the previous example can be stopped with the following:
watcher.stop()
live_cells.watch
also takes an optional schedule
argument,
which if not None is a function that is called when the watch
function should be called, with the watch callback passed as an
argument. The schedule
function should schedule the callback
function to it, to be called at a later stage. If schedule
is
None, the watch function is called immediately when the cells
referenced within it change.
import gevent
import live_cells as lc
a = lc.mutable()
@lc.watch(schedule=gevent.spawn)
def watch():
print(f'{a()}')
In this example a watch function observing cell a
is defined. When
the value of a
changes, the watch function is not called
immediately but is scheduled to run on a gevent green thread, using gevent.spawn
.
Important
The watch function sees the values of the cells, which are
referenced within it, as they are at the time the schedule
function is called and not at the time when the watch function is
actually run. This guarantees that the watch function will not
“miss” updates if it runs after the values of the cells are changed
again.
Computed Cells
A computed cell is a cell with a value that is defined as a function of the values of one or more argument cells. Whenever the value of an argument cell changes, the value of the computed cell is recomputed.
Computed cells are defined using live_cells.computed
, which
takes the value computation function of the cell:
import live_cells as lc
a = lc.mutable(1)
b = lc.mutable(2)
sum = lc.computed(lambda: a() + b())
In this example, sum
is a computed cell with the value defined as
the sum of cells a
and b
. The value of sum
is recomputed
whenever the value of either a
or b
changes. This is
demonstrated below:
lc.watch(lambda: print(f'The sum is {sum()}'))
a.value = 3 # Prints: The sum is 5
b.value = 4 # Prints: The sum is 7
In this example:
A watch function observing the
sum
cell is defined.The value of
a
is set to3
, which:Causes the value of
sum
to be recomputed.Calls the watch function defined in 1.
The value of
b
is set to4
, which likewise also results in the value ofsum
being recomputed and the watch function being called.
Tip
A computed cell with more than one expression can be defined by
using live_cells.computed
as a decorator:
import live_cells as lc
n = lc.mutable(2)
@lc.computed
def factorial_n():
result = 1
while n() > 1:
result *= n
n -= 1
return result
lc.watch(lambda: print(f'n! = {factorial_n()}'))
The computed cell is defined with the decorated function as the compute function. The cell can then be referenced using the name of the decorated function.
The live_cells.computed
function takes a changes_only
argument which allows you to control whether the computed cell
notifies its observers if its value hasn’t changed after a
recomputation. By default this is False
, which means the computed
cell notifies its observers whenever it is recomputed. If
changes_only
is True
, the cell only notifies its observers if
the new value of the cell is not equal to its previous value.
This is demonstrated with the following example:
import live_cells as lc
a = lc.mutable(0)
b = lc.computed(lambda: a % 2, changes_only=True)
lc.watch(lambda: print(f'{b()}'))
a.value = 1
a.value = 3
a.value = 5
a.value = 6
a.value = 8
This results in the following being printed to standard output:
0
1
0
Notice that only three lines are printed to standard output, even
though the value of the computed cell argument a
was changed five
times.
If changes_only=True
is omitted from the definition of b
, the
following is printed to standard output:
0
1
1
1
0
0
Notice that a new line is printed to standard output whenever the
value of a
, which is an argument of b
is changed. This is
because b
notifies its observers whenever the value of its
argument a
has changed, even if b
‘s new value is equal to
its previous value.
Batch Updates
The values of multiple mutable cells can be set simultaneously in a
batch update. The effect of this is that while the values of the
cells are changed on setting the value
property, the observers of
the cells are only notified after all the cell values have been set.
Batch updates are performed using the live_cells.batch
context
manager:
import live_cells as lc
a = lc.mutable(0)
b = lc.mutable(1)
lc.watch(lambda: print(f'a = {a()}, b = {b()}'))
# This only prints: a = 15, b = 3
with lc.batch():
a.value = 15
b.value = 3
In the example above, the values of a
and b
are set to 15
and 3
respectively, with a batch update. The watch function,
which observes both a
and b
is only called once when exiting
the context managed by live_cells.batch
.
As a result the following is printed:
a = 0, b = 1
a = 15, b = 3
a = 0, b = 1
is printed when the watch function is first defined.a = 15, b = 3
is printed when exiting the context managed bylive_cells.batch
.