[PYTHON] Summary of how to share state with multiple functions

When programming, I often want to "share state with multiple functions".

At this time, it is better to choose the best from various solutions than to use global variables or bucket relay the pointer without thinking.

Therefore, I will summarize the method of sharing the state with multiple functions, along with the advantages and disadvantages, as much as I can think of.

Use Python as the sample code, and use Racket (a type of Scheme) when you can't write in Python. because I like.

Global variables

state = "initial state"

def fuga0():
    #Change after referring to the state
    global state
    print(f"fuga0: {state}")
    state = "another state"
    return "result of fuga0"

def fuga1():
    #Refer to the state
    print(f"fuga1: {state}")

def hoge():
    print(fuga0())
    fuga1()
    print(f"last: {state}")

hoge()

Result (the following is omitted)


fuga0: initial state
result of fuga0
fuga1: another state
last: another state

Not a very recommended writing style.

Bucket relay pointer

from dataclasses import dataclass
from typing import Any

#A pointer to which this guy is bucket relayed
@dataclass
class Box:
    value: Any

def fuga0(box):
    print(f"fuga0: {box.value}")
    box.value = "another state"
    return "result of fuga0"

def fuga1(box):
    print(f"fuga1: {box.value}")

def hoge(box):
    print(fuga0(box))
    fuga1(box)
    print(f"last: {box.value}")

b = Box("initial state")

hoge(b)

In the case of a constant, the value should be bucket-relayed instead of the pointer.

Receives the current state as an argument and returns the new state as a return value

It is a so-called functional writing style, and the state does not change in the code.

def fuga0(state):
    print(f"fuga0: {state}")
    return "result of fuga0", "another state"

def fuga1(state):
    print(f"fuga1: {state}")

def hoge(state):
    result, new_state = fuga0(state)
    print(result)
    fuga1(new_state)
    print(f"last: {new_state}")

hoge("initial")

Object-orientation

A common practice in object-oriented languages. Make the state you want to share a member variable.

class Hoge:
    def __init__(self):
        self._state = "initial state"
    
    def _fuga0(self):
        print(f"fuga0: {self._state}")
        self._state = "another state"
        return "result of fuga0"
    
    def _fuga1(self):
        print(f"fuga0: {self._state}")
    
    def __call__(self):
        print(self._fuga0())
        self._fuga1()
        print(f"last: {self._state}")

hoge = Hoge()
hoge()

closure

Close to object-oriented.

def create_hoge():
    state = "initial"
    
    def fuga0():
        nonlocal state
        print(f"fuga0: {state}")
        state = "another state"
        return "result of fuga0"
    
    def fuga1():
        print(f"fuga1: {state}")
    
    def hoge():
        print(fuga0())
        fuga1()
        print(f"last: {state}")
    
    return hoge

create_hoge()()

Glocal Variable

Pattern of my thoughts. Create a variable that can be used only in with. See the link for details.

param.py


_param = None
_initialized = False

@contextmanager
def parametrize(data):
    global _param
    global _initialized
    before = _param
    before_initialized = _initialized
    _param = data
    _initialized = True
    try:
        yield
    finally:
        _param = before
        _initialized = before_initialized

def set_param(data):
    if _initialized:
        global _param
        _param = data
    else:
        raise RuntimeError
    
def get_param():
    if _initialized:
        return _param
    else:
        raise RuntimeError
from param import get_param, set_param, parametrize

def fuga0():
    print(f"fuga0: {get_param()}")
    set_param("another state")
    return "result of fuga0"

def fuga1():
    print(f"fuga0: {get_param()}")

def hoge():
    print(fuga0())
    fuga1()
    print(f"last: {get_param()}")

with parametrize("initial state"):
    hoge()

State monad

What is a monad? What is a state monad? Please google for something like that.

Let's write with Racket. I'm a little unsure about the implementation of applicative ...

;Definition of state monads from here
(require (prefix-in base: racket/base))
(require data/functor)
(require data/applicative)
(require data/monad)

(struct result (value state) #:transparent)

(struct state (f)
  #:methods gen:functor
  [(define (map g x)
     (state (λ (s)
              (match-define (result v ss) (run x s))
              (result (g v) ss))))]
  #:methods gen:applicative
  [(define (pure _ x)
     (state (λ (s) (result x s))))
   (define (apply f xs)
     (define (get-args xs s)
       (match xs
         [(cons x rest)
          (match-define (result xv xs) (run x s))
          (match-define (result args argss) (get-args rest xs))
          (result (cons xv args) argss)]
         [_ (result `() s)]))
     (state (λ (s)
              (match-define (result fv fs) (run f s))
              (match-define (result args argss) (get-args xs fs))
              (result (base:apply fv args) argss))))]
  #:methods gen:monad
  [(define (chain f x)
     (state (λ (s)
              (match-define (result xv xs) (run x s))
              (match-define (result fv fs) (run (f xv) xs))
              (result fv fs))))])

(define (run m s) ((state-f m) s))

(define get
  (state (λ (s) (result s s))))

(define (set ns)
  (state (λ (s) (result s ns))))
;Definition up to here
  
(define fuga0
  (do [x <- get]
      (pure (printf "fuga0: ~a\n" x))
      (set "another state")
      (pure "result of fuga0")))

(define fuga1
  (do [x <- get]
      (pure (printf "fuga1: ~a\n" x))))

(define hoge
  (do [x <- fuga0]
      (pure (displayln x))
      fuga1
      [y <- get]
      (pure (printf "last: ~a\n" y))))

(run hoge "initial state")

If it's a constant, use the Reader monad.

It's quite a penance in Python, but if you do your best, it looks like this.

#State monad definition from here
from typing import Callable, TypeVar, Generic, Tuple


S = TypeVar("S") #State type
R = TypeVar("R") #Return type
A = TypeVar("A") #New return type

class State(Generic[S, R]):
    def __init__(self, f: Callable[[S], Tuple[S, R]]):
        self._f = f
    
    def run(self, state: S) -> Tuple[S, R]:
        return self._f(state)
    
    @staticmethod
    def of(value: R):
        return State(lambda s: (value, s))
    
    def flatmap(self, g: Callable[[R], State[S, A]]) -> State[S, A]:
        def _new(state):
            f_ret, f_state = self.run(state)
            return g(f_ret).run(f_state)
        return State(_new)
    
    def map(self, g: Callable[[R], A]) -> State[S, A]:
        return self.flatmap(lambda x: State.of(g(x)))
    
    def then(self, m: State[S, A]) -> State[S, A]:
        return self.flatmap(lambda _: m)
    
    def value(self, v: A) -> State[S, A]:
        return self.map(lambda _: v)

get_m = State(lambda s: (s, s))

def set_m(new_state):
    return State(lambda s: (s, new_state))
#State monad definition so far

fuga0 = (get_m
    .map(lambda v: print(f"fuga0: {v}"))
    .then(set_m("another_state"))
    .value("result of fuga0"))

fuga1 = (get_m
    .map(lambda v: print(f"fuga1: {v}")))

hoge = (fuga0
    .map(print)
    .then(fuga1)
    .then(get_m)
    .map(lambda v: print(f"last: {v}")))

hoge.run("initial state")

Limited continuation

What is limited continuation? So why can we handle state changes? Please refer to Asai-sensei's tutorial.

(require racket/control)

(define (get)
  (shift k
         (λ (x)
           ((k x) x))))

(define (set s)
  (shift k
         (λ (x)
           ((k x) s))))

(define (run m s)
  ((reset
    (let ([ret (m)])
      (λ (_) ret))) s))


(define (fuga0)
  (printf "fuga0: ~a\n" (get))
  (set "another state")
  "result of fuga0")

(define (fuga1)
  (printf "fuga1: ~a\n" (get)))

(define (hoge)
  (displayln (fuga0))
  (fuga1)
  (printf "last: ~a\n" (get)))

(run hoge "initial state")

Summary

Everybody say everyone differently.

Instead of sticking to what programmers know, increase the number of tools and use them properly.

Also, there is contextvars in the Python standard library. I didn't mention it this time because it seems to have been created with asynchrony in mind.

Recommended Posts

Summary of how to share state with multiple functions
Summary of how to use pandas.DataFrame.loc
Summary of how to use pyenv-virtualenv
Summary of how to use csvkit
[Python] Summary of how to use split and join functions
[Python] Summary of eval / exec functions + How to write character strings with line breaks
[Python] Summary of how to use pandas
How to title multiple figures with matplotlib
Here's a brief summary of how to get started with Django
[Python2.7] Summary of how to use unittest
Summary of how to use Python list
[Python2.7] Summary of how to use subprocess
Summary of how to write AWS Lambda
Summary of how to build a LAMP + Wordpress environment with Sakura VPS
Summary of how to import files in Python 3
[Python] How to draw multiple graphs with Matplotlib
Summary of how to use MNIST in Python
How to specify attributes with Mock of python
How to implement "named_scope" of RubyOnRails with Django
[Java] How to switch between multiple versions of Java
How to return multiple indexes with index method
Summary of how to read numerical data with python [CSV, NetCDF, Fortran binary]
[Blender] Summary of how to install / update / uninstall add-ons
How to share folders with Docker and Windows with tensorflow
How to display multiple images of galaxies in tiles
How to output CSV of multi-line header with pandas
How to infer MAP estimate of HMM with PyStruct
[Python] Summary of how to specify the color of the figure
How to infer MAP estimate of HMM with OpenGM
How to learn structured SVM of ChainCRF with PyStruct
[IPython] How to Share IPython Notebook
How to update with SQLAlchemy?
How to cast with Theano
How to separate strings with','
How to RDP with Fedora31
How to Delete with SQLAlchemy?
How to embed multiple embeds in one message with Discord.py
[python] Summary of how to retrieve lists and dictionary elements
How to enable Read / Write of net.Conn with context with golang
[Linux] [C / C ++] Summary of how to get pid, ppid, tid
How to display a list of installable versions with pyenv
Comparison of how to use higher-order functions in Python 2 and 3
Summary of how to write .proto files used in gRPC
How to cancel RT with tweepy
How to extract features of time series data with PySpark Basics
Python: How to use async with
How to handle multiple versions of CUDA in the same environment
How to deal with "You have multiple authentication backends configured ..." (Django)
Connect to multiple databases with SQLAlchemy
How to get the ID of Type2Tag NXP NTAG213 with nfcpy
How to use virtualenv with PowerShell
How to deal with imbalanced data
How to install python-pip with ubuntu20.04LTS
How to deal with imbalanced data
Basic grammar of Python3 system (how to use functions, closures, lambda functions)
How to get started with Python
How to connect to Cloud Firestore from Google Cloud Functions with python code
How to deal with DistributionNotFound errors
How to get started with Django
10 functions of "language with battery" python
How to monitor the execution status of sqlldr with the pv command