Repository: streamlit/streamlit

Issue:
st.cache_resource (v 1.18) vs st.experimental_singleton (v 1.17): attributeerror with cache_resource 
### Checklist

- [X] I have searched the [existing issues](https://github.com/streamlit/streamlit/issues) for similar issues.
- [X] I added a very descriptive title to this issue.
- [X] I have provided sufficient information below to help reproduce this issue.

### Summary

AttributeError
Traceback:
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\scriptrunner\script_runner.py", line 565, in _run_script
    exec(code, module.__dict__)
File "C:\Users\A260219\Jupyter\bloomberg\prova.py", line 2126, in <module>
    ottimizzazione(num_variabili, fun_varianza, ptf_weights_equally, expected_returns, lista_asset_pairplot)
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\caching\cache_utils.py", line 186, in __call__
    return self._get_or_create_cached_value(args, kwargs)
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\caching\cache_utils.py", line 211, in _get_or_create_cached_value
    return self._handle_cache_miss(cache, value_key, func_args, func_kwargs)
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\caching\cache_utils.py", line 265, in _handle_cache_miss
    computed_value = self._info.func(*func_args, **func_kwargs)
File "C:\Users\A260219\Jupyter\bloomberg\prova.py", line 1555, in ottimizzazione
    st.write(pd.DataFrame(data=np.round(ptf_weights.reshape([1, num_variabili])*100, decimals=1), columns=dati_ritorni.columns, index=["Pesi"]))
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\metrics_util.py", line 311, in wrapped_func
    result = non_optional_func(*args, **kwargs)
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\elements\write.py", line 200, in write
    self.dg.dataframe(arg)
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\metrics_util.py", line 311, in wrapped_func
    result = non_optional_func(*args, **kwargs)
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\elements\dataframe_selector.py", line 116, in dataframe
    return self.dg._arrow_dataframe(
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\metrics_util.py", line 311, in wrapped_func
    result = non_optional_func(*args, **kwargs)
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\elements\arrow.py", line 117, in _arrow_dataframe
    return self.dg._enqueue("arrow_data_frame", proto)
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\delta_generator.py", line 563, in _enqueue
    caching.save_element_message(
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\caching\__init__.py", line 50, in save_element_message
    CACHE_RESOURCE_MESSAGE_REPLAY_CTX.save_element_message(
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\caching\cached_message_replay.py", line 293, in save_element_message
    raise AttributeError

### Reproducible Code Example

```Python
@st.cache_resource  ##al momento non funzionante con la funzione di cui di seguito e la versione 1.18 di Streamlit (ma st.experimental_singleton funzionava nella 1.17)
    def ottimizzazione(num_variabili: int, _fun_varianza: Callable[[np.array], float], ptf_weights_equally: np.array, expected_returns: np.array, lista_asset_pairplot: list) -> None:
        
        #l'underscore serviva per dire a streamlit di non fare l'hash della funzione
        fun_varianza=_fun_varianza

        #qui si specificano i vincoli (min, max) per le singole variabili (NB: il numero di vincoli deve essere esattamente uguale al numero di variabili)
        bnds = tuple([(valore_minimo_per_singola_asset_class, valore_massimo_per_singola_asset_class) for i in range(num_variabili)])

        st.write("--------------------------------------------------------------------")
        st.write("Elaborazione portafoglio di minima varianza")
        #di seguito la chiamata all'ottimizzatore con argomenti: la funzione-oggetto da minimizzare, X0 ovvero un initial guess delle variabili, il metodo scelto ('SLSQP' é
        #l'unico metodo che supporta ogni tipo di vincolo), i bounds sui valori delle singole variabili, i vincoli su piú variabili simultaneamente, infine le opzioni
        opt={'maxiter': maxiter, 'disp': True, 'ftol': 1e-12, 'eps': 1.4901161193847656e-08} #valori di default 'eps': 1.4901161193847656e-08, 'ftol': 1e-06 (si tratta rispettivamente della step size (eps) usata per l'approssimazione numerica della jacobiana ovvero il vettore del gradiente, e dell'obbiettivo di precisione (ftol) sulla funzione obbiettivo)
        res = minimize(fun_varianza, x0=ptf_weights_equally, method='SLSQP', bounds=bnds, constraints=st.session_state.vincoli_generali, options=opt)

        ptf_weights=res.x #attribuisce al vettore pesi i pesi ottimali calcolati da minimize (relativi al portafoglio di minima varianza)

        st.write("Di seguito i pesi degli asset in portafoglio espressi in percentuale:")
        st.write(pd.DataFrame(data=np.round(ptf_weights.reshape([1, num_variabili])*100, decimals=1), columns=dati_ritorni.columns, index=["Pesi"]))

        ptf_min_var_return=(ptf_weights.T).dot(expected_returns) #calcola il rendimento del portafoglio ottimale di minima varianza
        ptf_min_var_variance=res.fun #attribuisce ad apposita variabile il valore finale minimizzato della funzione obbiettivo (ovvero la varianza dei rendimenti di portafoglio)

        st.write("La deviazione standard annualizzata percentuale del portafoglio é %0.03f" % (np.sqrt(ptf_min_var_variance)*np.sqrt(periodicity)*100))
        st.write("Il rendimento percentuale del portafoglio é %0.03f" % (ptf_min_var_return*100))

        ##di seguito si calcola il portafoglio di massimo rendimento (da fare per il caso in cui vi siano vincoli complessi)
        #qui si specifica la funzione da minimizzare per il portafoglio di massimo rendimento
        def fun_rendimento(ptf_weights: np.array) -> float:
            return -(ptf_weights.T).dot(expected_returns) #si minimizza l'opposto del rendimento per massimizzarlo

        st.write("--------------------------------------------------------------------")
        st.write("Elaborazione portafoglio di massimo rendimento")

        #di seguito la massimizzazione del rendimento (minimizza l'opposto) con vincoli invariati
        res_return = minimize(fun_rendimento, x0=ptf_weights_equally, method='SLSQP', bounds=bnds, constraints=st.session_state.vincoli_generali, options=opt)

        #estrae il valore finale del rendimento (massimizato); ovviamente va invertito di segno perché la funzione obbiettivo é l'opposto del rendimento
        ptf_max_risk_return=-res_return.fun

        st.write("Di seguito i pesi degli asset in portafoglio espressi in percentuale:")
        st.write(pd.DataFrame(data=np.round((res_return.x).reshape([1, num_variabili])*100, decimals=1), columns=dati_ritorni.columns, index=["Pesi"]))

        st.write("Il rendimento percentuale del portafoglio é %0.03f" % (ptf_max_risk_return*100))

        step=(ptf_max_risk_return-ptf_min_var_return)/n_step

        #si specifica la matrice dei pesi di portafoglio con shape variabili*portafogli
        ptf_weights_matrix=np.zeros([num_variabili, (n_step+1)])
        ptf_weights_matrix[:, 0]=ptf_weights #il primo portafoglio é quello di minima varianza di cui giá abbiamo calcolato i pesi

        #si specifica il vettore delle varianze dei vari portafogli sulla frontiera efficiente
        variance_vector=np.zeros([(n_step+1),])
        variance_vector[0]=ptf_min_var_variance

        #si specifica il vettore dei rendimenti dei vari portafogli sulla frontiera efficiente
        returns_vector=np.zeros([(n_step+1),])
        returns_vector[0]=ptf_min_var_return

        desired_return=ptf_min_var_return+step

        #la funzione destinata ad essere usata per il vincolo sul rendimento nella costruzione
        #del resto della frontiera
        def vincolo_rendimento(ptf_weights: np.array) -> float:
            return (ptf_weights.T).dot(expected_returns) - desired_return

        for i in range(n_step):
            st.write("--------------------------------------------------------------------")
            st.write("Elaborazione portafoglio n.ro %s" % (i+1))
            #qui si specificano i vincoli (generali, ovvero su piú variabili) nella forma di un dictionary che contiene la specifica del tipo ('eq' per equality ed 
            #'ineq' per inequality) e la funzione del vincolo (che verrá posta uguale a zero per i vincoli di equality e non-negativa per i vincoli di inequality)
                                
            res = minimize(fun_varianza, x0=ptf_weights, method='SLSQP', bounds=bnds,
                        constraints=[i for i in st.session_state.vincoli_generali] + [{'type': 'eq', 'fun': vincolo_rendimento}], #aggiunge il vincolo sul rendimento desiderato
                        options=opt)
            
            ptf_weights=res.x
            ptf_weights_matrix[:, i+1]=ptf_weights
            st.write("Di seguito i pesi degli asset in portafoglio espressi in percentuale:")
            st.write(pd.DataFrame(data=np.round(ptf_weights.reshape([1, num_variabili])*100, decimals=1), columns=dati_ritorni.columns, index=["Pesi"]))
            variance_vector[i+1]=res.fun
            st.write("La deviazione standard annualizzata percentuale del portafoglio é %0.03f" % (np.sqrt(variance_vector[i+1])*np.sqrt(periodicity)*100))
            returns_vector[i+1]=(ptf_weights.T).dot(expected_returns)
            st.write("Il rendimento percentuale del portafoglio é %0.03f" % (returns_vector[i+1]*100))
            desired_return=desired_return+step

        st.write("---------------------------------------------------------------------------------------------")
        st.write("A seguire un riepilogo della composizione dei portafogli componenti la frontiera di Markowitz:")
        sintesi_composizione_portafogli_markowitz=pd.DataFrame(data=np.round(ptf_weights_matrix*100, decimals=2), #i pesi in portafoglio
                                                            columns=[("Ptf n.ro "+str(i)) for i in range(n_step+1)],
                                                            index=dati_ritorni.columns, dtype="float")
        sintesi_composizione_portafogli_markowitz.to_csv("sintesi_composizione_portafogli_markowitz.csv", sep=";", decimal=".")
        st.write(sintesi_composizione_portafogli_markowitz)
        st.write("Di seguito un riepilogo di rendimenti, rischio ed efficienza dei portafogli componenti la frontiera di Markowitz:")
        sintesi_rendimento_rischio_portafogli_markowitz=pd.DataFrame(data=np.round([returns_vector*100, #i rendimenti
                                                                                    np.sqrt(variance_vector)*np.sqrt(periodicity)*100, #le deviazioni standard annualizzate
                                                                                    np.sqrt(variance_vector)*np.sqrt(periodicity)*2.566*100, #il VaR parametrico t-Student 6df con livello di confidenza 99%, orizzonte annuale, espresso in percentuale
                                                                                    np.divide((returns_vector*100-risk_free_rate*100), np.sqrt(variance_vector)*np.sqrt(periodicity)*100)], #gli sharpe ratio
                                                                                    decimals=2),
                                                                                    columns=[("Ptf n.ro "+str(i)) for i in range(n_step+1)],
                                                                                    index=["Rendimenti (%)", "Deviazioni Standard (%)", "VaR 99% t-Student 6df (%)", "Sharpe Ratio"],
                                                                                    dtype="float")
        sintesi_rendimento_rischio_portafogli_markowitz.to_csv("sintesi_rendimento_rischio_portafogli_markowitz.csv", sep=";", decimal=".")
        st.write(sintesi_rendimento_rischio_portafogli_markowitz)

        #concateniamo i dati essenziali della frontiera di Markowitz, inframezzando una riga vuota e le intestazioni di colonna prima della parte rendimento-rischio
        empty_row=pd.Series([None for _ in range(len(sintesi_composizione_portafogli_markowitz.columns.values.tolist()))], index=sintesi_composizione_portafogli_markowitz.columns.values.tolist(), name=" ")
        sintesi_composizione_portafogli_markowitz_format=sintesi_composizione_portafogli_markowitz.append(empty_row, ignore_index=False)
        intestazione_row=pd.Series([f"Ptf n.ro {i}" for i in range(len(sintesi_composizione_portafogli_markowitz.columns.values.tolist()))], index=sintesi_composizione_portafogli_markowitz.columns.values.tolist(), name=" ")
        sintesi_composizione_portafogli_markowitz_format=sintesi_composizione_portafogli_markowitz_format.append(intestazione_row, ignore_index=False)
        st.session_state.riassuntivo_markowitz=pd.concat([sintesi_composizione_portafogli_markowitz_format, sintesi_rendimento_rischio_portafogli_markowitz], axis=0)

        #plotto il grafico con la composizione della frontiera di Markowitz
        fig=px.area(sintesi_composizione_portafogli_markowitz.T, \
                    x=sintesi_composizione_portafogli_markowitz.T.index, \
                    y=sintesi_composizione_portafogli_markowitz.T.columns, \
                    labels = dict(index = "N.ro Portafoglio", value = "Percentuale in portafoglio"), \
                    title="Andamento composizione frontiera di Markowitz")
        st.plotly_chart(fig, use_container_width=True)

        #calcolo le deviazioni standard annualizzate
        std_dev_annualized_vector=np.sqrt(variance_vector)*np.sqrt(periodicity)

        #plotto la frontiera di Markowitz
        fig=px.line(x=std_dev_annualized_vector*100, \
                    y=returns_vector*100, \
                    labels = dict(x = "Deviazione standard annualizzata di portafoglio (%)", y = "Rendimento annuale (%)"), \
                    title="Frontiera di Markowitz")
        st.plotly_chart(fig, use_container_width=True)

        st.write("Di seguito la matrice di correlazione:")
        #calcolo la matrice di correlazione
        df_corr=pd.DataFrame.corr(pd.DataFrame(data=returns, columns=dati_ritorni.columns), method='pearson')
        #plotto la heatmap
        fig=px.imshow(df_corr)
        st.plotly_chart(fig, use_container_width=True)

        if plotting_grafico_pairplot=="y":

            st.write("Di seguito il grafico pairplot:")
            #la riga di codice seguente elimina eventuali indici che fossero superiori al consentito (ad esempio 12 o 8 su un numero di variabili/asset pari a 8)
            lista_asset_pairplot=[lista_asset_pairplot[i] for i in range(len(lista_asset_pairplot)) if lista_asset_pairplot[i]<num_variabili]
            df_ritorni=pd.DataFrame(data=returns, columns=dati_ritorni.columns)
            df_ritorni=df_ritorni.loc[:, [dati_ritorni.columns[i] for i in lista_asset_pairplot]]
            ##per debug
            #st.write(df_ritorni)
            #creo uno scatter plot
            fig_scatter = px.scatter_matrix(df_ritorni, width=1500, height=1000)
            #cambiamo dimensione ed opacitá dei punti
            fig_scatter.update_traces(marker={"size": 4,"opacity":0.7})
            #stampo a schermo lo scatterplot (nota bene il mancato uso del metodo show)
            fig_scatter


        if uso_resampled_frontiers:

            ##di seguito il codice per il calcolo delle resampled frontiers

            #crea un vettore numpy contenente le medie attese delle variabili, ovvero in questo caso i rendimenti periodicizzati (ovvero trasformati da annuali in settimanali o mensili)
            means=(1+expected_returns)**(1/periodicity)-1 #esempio di come si possa vettorializzare il calcolo (velocizzandolo)

            #inizializza la matrice 3d che contiene i pesi di tutte le frontiere efficienti
            ptf_weights_3darray=np.zeros([num_variabili, (n_step+1), n_scenari])

            #inizializza le matrici destinate a contenere varianze e ritorni (da resampling) delle varie frontiere efficienti
            variance_vector_2darray=np.zeros([(n_step+1), n_scenari])
            returns_vector_2darray=np.zeros([(n_step+1), n_scenari])

            #inizializza le matrici destinate a contenere di volta in volta le covarianze ed i
            #rendimenti ricampionati dei singoli scenari
            covariance_matrix_sampling=np.zeros([num_variabili, num_variabili])
            returns_sampling=np.zeros([num_variabili])

            #crea matrice vuota n*num_variabili destinata ad accogliere i returns periodali ricampionati con t-Student multivariata
            output_t=np.empty([n, num_variabili])

            if plottare_andamenti_simulati=="y":
                #crea matrice vuota n*num_variabili*n_scenari destinata alla storicizzazione dei dati contenuti nella matrice di cui sopra
                output_t_3d=np.empty([n, num_variabili, n_scenari])

            #qui si specifica la funzione da minimizzare che deve essere espressa come funzione di un array numpy
            #notare che ora si usa la covarianza calcolata sui rendimenti campionati, ovvero covariance_matrix_sampling
            def fun_varianza_resampling(ptf_weights: np.array) -> float:
                return ((ptf_weights.T).dot(covariance_matrix_sampling)).dot(ptf_weights) #w*VAR*w ovvero la varianza di portafoglio

            #notare che si usa returns_sampling in luogo di expected_returns (perché si usano i rendimenti dello scenario)
            def vincolo_rendimento_resampling(ptf_weights: np.array) -> float:
                return (ptf_weights.T).dot(returns_sampling) - desired_return

            #qui si specifica la funzione da minimizzare per il portafoglio di massimo rendimento
            #notare che si usa return_sampling ovvero i rendimenti ricampionati in luogo degli expected returns originali
            def fun_rendimento_resampling(ptf_weights: np.array) -> float:
                return -(ptf_weights.T).dot(returns_sampling) #si minimizza l'opposto del rendimento per massimizzarlo

            #inizializza le grandezze destinate a tenere traccia di eventuali anomalie nell'ottimizzatore di scipy (minimize)
            anomalies_count=0
            anomalies_status=[]

            ##da qui si parte con la generazione degli scenari (via simulazioni monte carlo) e successivo calcolo delle relative
            ##frontiere efficienti
            #annuncio ed inizializzo barra avanzamento per le simulazioni
            st.write("\n")
            st.write("\n")
            st.write("\n")
            st.write("\n")
            st.write("\n")
            st.write("Avanzamento elaborazione scenari e relative frontiere ricampionate (Resampled Frontiers)")
            barra_avanzamento_simulazioni = st.progress(0)
            st.write("\n")

            for z in range(n_scenari):
                
                #aggiorno barra avanzamento simulazioni
                barra_avanzamento_simulazioni.progress(float((z+1)/n_scenari))

                #genera n estrazioni (vettori) da una gaussiana multivariata con medie nulle e covarianza covariance_matrix
                #(ergo una matrice n*num_variabili, ovvero estrazioni*variabili)
                output_n=np.random.multivariate_normal(np.zeros(num_variabili), covariance_matrix, n)
                
                ##codice seguente é necessario se si vuole trasformare l'estrazione di cui sopra in una estrazione da t-student multivariata
                #degree of freedom di una t-student multivariata da cui si vuole estrarre campioni
                df=6
                
                #n estrazioni da distribuzione chi quadro che servono per il fattore di correzione del vettore dell'estrazione dalla gaussiana
                w=np.random.chisquare(df, n) #attenzione che w é un vettore di dimensione n, come del resto il fattore_correzione a seguire
                
                #il fattore di correzione é pari a radice_quadrata(df/estrazione_chi_q)
                fattore_correzione=np.sqrt(w/df) #np.sqrt funziona element-wise
                fattore_correzione=fattore_correzione**(-1) #inverte per ottenere radice_quadrata(df/estrazione_chi_q) ovvero il fattore di correzione da moltiplicare per l'estrazione dalla normale
                    
                #a seguire la moltiplicazione di ciascuna estrazione dalla gaussiana per il fattore di correzione corrispondente (element-wise)
                for i in range(n):
                    output_t[i, :]=np.multiply(output_n[i, :], fattore_correzione[i]) #np.multiply funziona element-wise
                    #occorre risommare il vettore delle medie in quanto le estrazioni sono fatte da distribuzioni con medie nulle
                    output_n[i, :]=means+output_n[i, :] 
                    #nel caso della t-student si normalizza anche per la deviazione standard della t-student standardizzata (che NON é 1 come invece nel caso della gaussiana standardizzata)
                    output_t[i, :]=means+output_t[i, :]/(np.sqrt(df/(df-2))) #serve quindi per avere una matrice covarianza finale (calcolata sul sample multivariato estratto) asintoticamente uguale alla matrice covariance_matrix originale
                
                if plottare_andamenti_simulati=="y":
                    #storicizza i rendimenti ricampionati con t-Student ad uso successivo (grafici)
                    output_t_3d[:, :, z]=output_t
                
                #calcola i nuovi rendimenti medi (dello scenario z)
                returns_sampling=np.mean(output_t, axis=0)
                
                #calcola la nuova matrice covarianza a valere sui rendimenti da scenario z
                covariance_matrix_sampling=np.cov(output_t, rowvar=False)
                
                #riannualizza i rendimenti
                returns_sampling=(1+returns_sampling)**(periodicity)-1 
                
                #qui si specificano i vincoli (min, max) per le singole variabili (NB: il numero di vincoli deve essere esattamente uguale al numero di variabili)
                bnds = tuple([(valore_minimo_per_singola_asset_class, valore_massimo_per_singola_asset_class) for i in range(num_variabili)])
                
                #di seguito la chiamata all'ottimizzatore con argomenti: la funzione-oggetto da minimizzare, X0 ovvero un initial guess delle variabili, il metodo scelto ('SLSQP' é
                #l'unico metodo che supporta ogni tipo di vincolo), i bounds sui valori delle singole variabili, i vincoli su piú variabili simultaneamente, infine le opzioni
                opt={'maxiter': maxiter, 'disp': False, 'ftol': 1e-12, 'eps': 1.4901161193847656e-08} #valori di default 'eps': 1.4901161193847656e-08, 'ftol': 1e-06 (si tratta rispettivamente della step size (eps) usata per l'approssimazione numerica della jacobiana ovvero il vettore del gradiente, e dell'obbiettivo di precisione (ftol) sulla funzione obbiettivo)
                res = minimize(fun_varianza_resampling, x0=ptf_weights_equally, method='SLSQP', bounds=bnds, constraints=st.session_state.vincoli_generali, options=opt)
                
                #di seguito il codice che serve per tenere traccia di eventuali anomalie nell'ottimizzatore di scipy (minimize)
                if res.success!=True:
                    anomalies_count+=1
                    anomalies_status.append(res.message)
                
                ptf_weights=res.x #attribuisce al vettore pesi i pesi ottimali calcolati da minimize (relativi al portafoglio di minima varianza)
                
                ptf_min_var_return=(ptf_weights.T).dot(returns_sampling) #calcola il rendimento del portafoglio ottimale di minima varianza
                ptf_min_var_variance=res.fun #attribuisce ad apposita variabile il valore finale minimizzato della funzione obbiettivo (ovvero la varianza dei rendimenti di portafoglio)
                
                ##di seguito si calcola il portafoglio di massimo rendimento (da fare per il caso in cui vi siano vincoli complessi)
                
                #di seguito la massimizzazione del rendimento (minimizza l'opposto) con vincoli invariati
                res_return = minimize(fun_rendimento_resampling, x0=ptf_weights_equally, method='SLSQP', bounds=bnds, constraints=st.session_state.vincoli_generali, options=opt)
                
                #di seguito il codice che serve per tenere traccia di eventuali anomalie nell'ottimizzatore di scipy (minimize)
                if res_return.success!=True:
                    anomalies_count+=1
                    anomalies_status.append(res.message)
                
                #estrae il valore finale del rendimento (massimizato); ovviamente va invertito di segno perché la funzione obbiettivo é l'opposto del rendimento
                ptf_max_risk_return=-res_return.fun
                
                #come in precedenza calcola lo step di aumento dei rendimenti da aggiungere per ogni nuovo portafoglio da ottimizzare sulla frontiera
                step=(ptf_max_risk_return-ptf_min_var_return)/n_step
                    
                ptf_weights_3darray[:, 0, z]=ptf_weights #il primo portafoglio é quello di minima varianza di cui giá abbiamo calcolato i pesi
                    
                #la matrice (resampled frontiers) delle varianze con shape n_step+1, n_scenari
                variance_vector_2darray[0, z]=ptf_min_var_variance
                
                #la matrice (resampled frontiers) dei rendimenti con shape n_step+1, n_scenari
                returns_vector_2darray[0, z]=ptf_min_var_return
                
                desired_return=ptf_min_var_return+step
                
                for i in range(n_step):
                    
                    #si minimizza la varianza ricampionata sotto il vincolo dei rendimenti ricampionati       
                    res = minimize(fun_varianza_resampling, x0=ptf_weights, method='SLSQP', bounds=bnds,
                                constraints=[i for i in st.session_state.vincoli_generali] + [{'type': 'eq', 'fun': vincolo_rendimento_resampling}], #aggiunge il vincolo sul rendimento desiderato
                                options=opt)
                    
                    #di seguito il codice che serve per tenere traccia di eventuali anomalie nell'ottimizzatore di scipy (minimize)
                    if res.success!=True:
                        anomalies_count+=1
                        anomalies_status.append(res.message)
                    
                    ptf_weights=res.x
                    
                    ptf_weights_3darray[:, i+1, z]=ptf_weights #si salvano i pesi di portafoglio nel cubo di dimensioni num_variabili, (n_step+1), n_scenari
                            
                    variance_vector_2darray[i+1, z]=res.fun
                    
                    returns_vector_2darray[i+1, z]=(ptf_weights.T).dot(returns_sampling)
                    
                    desired_return=desired_return+step

            #si inizializza la matrice dei pesi dei portafogli sulla frontiera ricampionata
            ptf_weights_resampled=np.zeros([num_variabili, (n_step+1)])

            #a seguire l'averaging dei pesi lungo la direzione degli scenari
            for j in range(n_step+1):
                for a in range(num_variabili):
                    ptf_weights_resampled[a, j]=np.average(ptf_weights_3darray[a, j, :])

            #si inizializzano vettori contenenti varianze e rendimenti della resampled frontier
            varianze_frontiera_ricampionata=np.zeros([(n_step+1)])
            rendimenti_frontiera_ricampionata=np.zeros([(n_step+1)])

            #qui si popolano i vettori di cui sopra
            #si tenga ovviamente presente che si usa varianze e rendimenti "storici" (o comunque
            #quelli usati nell'ottimizzazione di Markowitz)
            for s in range(n_step+1):
                varianze_frontiera_ricampionata[s]=((ptf_weights_resampled[:, s].T).dot(covariance_matrix)).dot(ptf_weights_resampled[:, s])
                rendimenti_frontiera_ricampionata[s]=(ptf_weights_resampled[:, s].T).dot(expected_returns)

            #si passa alle deviazioni standard annualizzate
            dev_std_frontiera_ricampionata=(np.sqrt(varianze_frontiera_ricampionata))*np.sqrt(periodicity)

            st.write("---------------------------------------------------------------------------------------------")
            st.write("A seguire un riepilogo della composizione dei portafogli componenti la resampled frontier:")
            sintesi_composizione_portafogli_resampled_frontiers=pd.DataFrame(data=np.round(ptf_weights_resampled*100, decimals=2), #i pesi in portafoglio
                                                                            columns=[("Ptf n.ro "+str(i)) for i in range(n_step+1)],
                                                                            index=dati_ritorni.columns, dtype="float")
            sintesi_composizione_portafogli_resampled_frontiers.to_csv("sintesi_composizione_portafogli_resampled_frontiers.csv", sep=";", decimal=".")
            st.write(sintesi_composizione_portafogli_resampled_frontiers)
            st.write("Di seguito un riepilogo di rendimenti, rischio ed efficienza dei portafogli componenti la resampled frontier:")
            sintesi_rendimento_rischio_portafogli_resampled_frontiers=pd.DataFrame(data=np.round([rendimenti_frontiera_ricampionata*100, #i rendimenti
                                                                                                dev_std_frontiera_ricampionata*100, #le deviazioni standard annualizzate
                                                                                                dev_std_frontiera_ricampionata*2.566*100, #il VaR parametrico t-Student 6df con livello di confidenza 99%, orizzonte annuale, espresso in percentuale
                                                                                                np.divide((rendimenti_frontiera_ricampionata*100-risk_free_rate*100), dev_std_frontiera_ricampionata*100)], #gli sharpe ratio
                                                                                                decimals=2),
                                                                                                columns=[("Ptf n.ro "+str(i)) for i in range(n_step+1)],
                                                                                                index=["Rendimenti (%)", "Deviazioni Standard (%)", "VaR 99% t-Student 6df (%)", "Sharpe Ratio"],
                                                                                                dtype="float")
            sintesi_rendimento_rischio_portafogli_resampled_frontiers.to_csv("sintesi_rendimento_rischio_portafogli_resampled_frontiers.csv", sep=";", decimal=".")
            st.write(sintesi_rendimento_rischio_portafogli_resampled_frontiers)

            #concateniamo i dati essenziali della Frontiera Ricampionata, inframezzando una riga vuota e le intestazioni di colonna prima della parte rendimento-rischio
            empty_row=pd.Series([None for _ in range(len(sintesi_composizione_portafogli_resampled_frontiers.columns.values.tolist()))], index=sintesi_composizione_portafogli_resampled_frontiers.columns.values.tolist(), name=" ")
            sintesi_composizione_portafogli_resampled_frontiers_format=sintesi_composizione_portafogli_resampled_frontiers.append(empty_row, ignore_index=False)
            intestazione_row=pd.Series([f"Ptf n.ro {i}" for i in range(len(sintesi_composizione_portafogli_resampled_frontiers.columns.values.tolist()))], index=sintesi_composizione_portafogli_resampled_frontiers.columns.values.tolist(), name=" ")
            sintesi_composizione_portafogli_resampled_frontiers_format=sintesi_composizione_portafogli_resampled_frontiers_format.append(intestazione_row, ignore_index=False)
            st.session_state.riassuntivo_resampled=pd.concat([sintesi_composizione_portafogli_resampled_frontiers_format, sintesi_rendimento_rischio_portafogli_resampled_frontiers], axis=0)

            #plotto il grafico con la composizione della Frontiera Ricampionata
            fig=px.area(sintesi_composizione_portafogli_resampled_frontiers.T, \
                        x=sintesi_composizione_portafogli_resampled_frontiers.T.index, \
                        y=sintesi_composizione_portafogli_resampled_frontiers.T.columns, \
                        labels = dict(index = "N.ro Portafoglio", value = "Percentuale in portafoglio"), \
                        title="Andamento composizione Frontiera Ricampionata")
            st.plotly_chart(fig, use_container_width=True)

            #calcola le deviazioni standard annualizzate
            std_dev_annualized_vector_resampled=np.sqrt(variance_vector_2darray)*np.sqrt(periodicity)
            returns_vector_resampled=returns_vector_2darray

            ##facciamo uno stack dei dati per il grafico a seguire
            ##shape degli array numpy coinvolti di seguito
            #std_dev_annualized_vector_resampled: shape portafogli*scenari
            #returns_vector_resampled: shape portafogli*scenari
            #shape del risultato: portafogli*scenari*tipo_variabile (dove tipo_variabile é pari a 2, ovvero: deviazioni standard e rendimenti)
            array_resampled_frontiers=np.stack([std_dev_annualized_vector_resampled*100, returns_vector_resampled*100], axis=2)

            #creiamo e visualizziamo il grafico inerente le singole frontiere di Markowitz ricampionate
            fig = go.Figure([   
                        go.Scatter(
                            x=array_resampled_frontiers[:, i, 0],
                            y=array_resampled_frontiers[:, i, 1],
                            mode='lines',
                            #marker=dict(color="#444"),
                            line=dict(width=1),
                            showlegend=False,
                            hoverinfo='none'
                            ) for i in range(n_scenari)])
            fig.update_layout(
                yaxis_title='Return',
                xaxis_title='Standard Deviation',
                title='Resampled Frontiers',
                xaxis={"tickformat": ".2f"},
                yaxis={"tickformat": ".2f"},
                width=1500, 
                height=1000
            )
            if n_scenari<=1000 or forza_plotting_interattivo:
                st.plotly_chart(fig, staticPlot=True, use_container_width=True) #possibile aggiungere render_mode='auto' (oppure "webgl" o "svg")
            else:
                fig_png=pio.to_image(fig.update_layout(template='plotly_dark'), format="png", width=1500, height=1000, scale=None, validate=True, engine='auto')
                st.image(fig_png)

            #creiamo e visualizziamo il grafico inerente le singole frontiere di Markowitz ricampionate
            #rappresentate sotto forma di punti invece che linee continue
            fig = go.Figure([   
                        go.Scatter(
                            x=array_resampled_frontiers[:, i, 0],
                            y=array_resampled_frontiers[:, i, 1],
                            mode='markers',
                            marker=dict(color='LightSkyBlue', size=3),
                            opacity=0.5,  #effetto gradevole ma peggiora le performance
                            #line=dict(width=1),
                            showlegend=False,
                            hoverinfo='none'
                            ) for i in range(n_scenari)])
            fig.update_layout(
                yaxis_title='Return',
                xaxis_title='Standard Deviation',
                title='Resampled Frontiers (singoli portafogli)',
                xaxis={"tickformat": ".2f"},
                yaxis={"tickformat": ".2f"},
                width=1500, 
                height=1000
            )
            if n_scenari<=1000 or forza_plotting_interattivo:
                st.plotly_chart(fig, staticPlot=True, use_container_width=True) #possibile aggiungere render_mode='auto' (oppure "webgl" o "svg")
            else:
                fig_png=pio.to_image(fig.update_layout(template='plotly_dark'), format="png", width=1500, height=1000, scale=None, validate=True, engine='auto')
                st.image(fig_png)

            #plottiamo la frontiera di Markowitz vs la Frontiera Ricampionata nel caso i rendimenti attesi e le volatilitá e correlazioni
            #siano perfette (nel qual caso ovviamente Markowitz si trova necessariamente sopra la Frontiera Ricampionata)
            fig = go.Figure([   
                        go.Scatter(
                            name='Frontiera di Markowitz',
                            x=std_dev_annualized_vector*100,
                            y=returns_vector*100,
                            mode='lines',
                            line=dict(width=1),
                            showlegend=True
                            ),
                        go.Scatter(
                            name='Frontiera Ricampionata',
                            x=dev_std_frontiera_ricampionata*100,
                            y=rendimenti_frontiera_ricampionata*100,
                            mode='lines',
                            line=dict(width=1),
                            showlegend=True,
                            hoverinfo='none'
                            )
                            ])
            fig.update_layout(
                yaxis_title='Return',
                xaxis_title='Standard Deviation',
                title='Resampled Frontiers vs Markowitz',
                xaxis={"tickformat": ".2f"},
                yaxis={"tickformat": ".2f"},
                width=1500, 
                height=1000
            )
            st.plotly_chart(fig, staticPlot=True, use_container_width=True)

            #di seguito si ricalcolano rendimenti e rischi di tutti i portafogli simulati
            #ipotizzando che lo scenario base sia corretto (dunque usando i suoi rendimenti
            #attesi e la sua matrice di covarianza)
            if plotting_grafico_ptf_simulati_spazio_originario=="y":
                #inizializza le matrici destinate a contenere varianze e ritorni (da resampling) delle varie frontiere efficienti ricalcolate sullo scenario base (rendimenti attesi e matrice covarianza originali)
                variance_vector_2darray_recalc=np.zeros([(n_step+1), n_scenari])
                returns_vector_2darray_recalc=np.zeros([(n_step+1), n_scenari])
                
                #effettua il ricalcolo per singolo portafoglio
                for s in range(n_scenari):
                    for p in range(n_step+1):
                        variance_vector_2darray_recalc[p, s]=fun_varianza(ptf_weights_3darray[:, p, s])
                        returns_vector_2darray_recalc[p, s]=-fun_rendimento(ptf_weights_3darray[:, p, s]) #si usa il segno meno perchè la funzione restituiva l'opposto causa uso con il minimizer
                
                #calcola le deviazioni standard annualizzate
                std_dev_ann_vector_2darray_recalc=np.sqrt(variance_vector_2darray_recalc)*np.sqrt(periodicity)

                ##shape degli array numpy coinvolti di seguito
                #array_resampled_frontiers_spazio_originario=np.stack([(std_dev_ann_vector_2darray_recalc.flatten())*100, (returns_vector_2darray_recalc.flatten())*100], axis=2)

                #creiamo e visualizziamo il grafico inerente le singole frontiere di Markowitz ricampionate
                fig = go.Figure([   
                            go.Scatter(
                                x=(std_dev_ann_vector_2darray_recalc.flatten())*100,
                                y=(returns_vector_2darray_recalc.flatten())*100,
                                mode='markers',
                                marker=dict(color='LightSkyBlue', size=3),
                                opacity=0.5, #effetto gradevole ma peggiora le performance
                                showlegend=False,
                                hoverinfo='none'
                                )])
                fig.update_layout(
                    yaxis_title='Return',
                    xaxis_title='Standard Deviation',
                    title="Portafogli simulati in spazio rischio-rendimento originario",
                    xaxis={"tickformat": ".2f"},
                    yaxis={"tickformat": ".2f"},
                    width=1500, 
                    height=1000
                )
                if n_scenari<=1000 or forza_plotting_interattivo:
                    st.plotly_chart(fig, staticPlot=True, use_container_width=True) #possibile aggiungere render_mode='auto' (oppure "webgl" o "svg")
                else:
                    fig_png=pio.to_image(fig.update_layout(template='plotly_dark'), format="png", width=1500, height=1000, scale=None, validate=True, engine='auto')
                    st.image(fig_png)

            #si recuperano e si plottano gli andamenti di portafoglio nei diversi portafogli/scenari
            if plottare_andamenti_simulati=="y":
                
                for n_ro_ptf in range(0, n_step+1, step_ptf_resamp_graph):
                    #inizializza la matrice destinata a contenere i rendimenti di portafoglio relativi ai diversi scenari
                    portfolio_returns_scenari=np.zeros([n, n_scenari])
                    
                    #qui si calcolano i rendimenti di portafoglio partendo dai rendimenti ricampionati degli asset ed i relativi pesi (da frontiera ricampionata)
                    for z in range(n_scenari):
                        portfolio_returns_scenari[:, z]=(output_t_3d[:, :, z]).dot(ptf_weights_resampled[:, n_ro_ptf])
                    
                    #di seguito si setta il numero di periodi di tempo (settimane o mesi) in modo tale che l'orizzonte temporale del grafico non superi l'anno
                    tempo_grafico=min(n, periodicity)
                    
                    #inizializza la matrice destinata ad accogliere i valori del portafoglio nel suo andamento temporale e per i diversi scenari
                    andamento_portafoglio=np.zeros([tempo_grafico+1, n_scenari])
                    
                    #ovviamente si parte dal valore di 100 ad inizio periodo
                    andamento_portafoglio[0, :]=np.array([100 for i in range(n_scenari)])
                    
                    #di seguito si calcola l'andamento del portafoglio nel tempo e per i diversi scenari
                    for z in range(n_scenari):
                        for t in range(tempo_grafico):
                            andamento_portafoglio[t+1, z]=andamento_portafoglio[t, z]*(1+portfolio_returns_scenari[t, z])

                    #plottiamo gli andamenti di portafoglio
                    fig = go.Figure([   
                                    go.Scatter(
                                        #name='Resampled Frontier',
                                        #x=andamento_portafoglio[:, i],
                                        y=andamento_portafoglio[:, i],
                                        mode='lines',
                                        line=dict(width=0.5, color='#c44e52'),  #'#c44e52' oppure '#d62728'
                                        showlegend=False,
                                        hoverinfo='none'
                                        ) for i in range(n_scenari)])
                    fig.update_layout(
                        yaxis_title="Valore del portafoglio (fatto 100 iniziale)",
                        xaxis_title="Tempo",
                        title=f"Portafoglio simulato n.ro {n_ro_ptf} (da Resampled Fr.)",
                        width=1500, 
                        height=1000
                    )
                    st.plotly_chart(fig, staticPlot=True, use_container_width=True)
```


### Steps To Reproduce

please read only the error


### Expected Behavior

cache the function

### Current Behavior

AttributeError
Traceback:
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\scriptrunner\script_runner.py", line 565, in _run_script
    exec(code, module.__dict__)
File "C:\Users\A260219\Jupyter\bloomberg\prova.py", line 2126, in <module>
    ottimizzazione(num_variabili, fun_varianza, ptf_weights_equally, expected_returns, lista_asset_pairplot)
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\caching\cache_utils.py", line 186, in __call__
    return self._get_or_create_cached_value(args, kwargs)
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\caching\cache_utils.py", line 211, in _get_or_create_cached_value
    return self._handle_cache_miss(cache, value_key, func_args, func_kwargs)
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\caching\cache_utils.py", line 265, in _handle_cache_miss
    computed_value = self._info.func(*func_args, **func_kwargs)
File "C:\Users\A260219\Jupyter\bloomberg\prova.py", line 1555, in ottimizzazione
    st.write(pd.DataFrame(data=np.round(ptf_weights.reshape([1, num_variabili])*100, decimals=1), columns=dati_ritorni.columns, index=["Pesi"]))
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\metrics_util.py", line 311, in wrapped_func
    result = non_optional_func(*args, **kwargs)
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\elements\write.py", line 200, in write
    self.dg.dataframe(arg)
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\metrics_util.py", line 311, in wrapped_func
    result = non_optional_func(*args, **kwargs)
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\elements\dataframe_selector.py", line 116, in dataframe
    return self.dg._arrow_dataframe(
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\metrics_util.py", line 311, in wrapped_func
    result = non_optional_func(*args, **kwargs)
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\elements\arrow.py", line 117, in _arrow_dataframe
    return self.dg._enqueue("arrow_data_frame", proto)
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\delta_generator.py", line 563, in _enqueue
    caching.save_element_message(
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\caching\__init__.py", line 50, in save_element_message
    CACHE_RESOURCE_MESSAGE_REPLAY_CTX.save_element_message(
File "C:\Users\A260219\Anaconda3\envs\lavoro\lib\site-packages\streamlit\runtime\caching\cached_message_replay.py", line 293, in save_element_message
    raise AttributeError

### Is this a regression?

- [X] Yes, this used to work in a previous version.

### Debug info

- Streamlit version: 1.18
- Python version: 3.10
- Operating System:  windows
- Browser: chrome
- Virtual environment: -


### Additional Information

no problems at all with st.experimental_singleton (streamlit version 1.17)
but with st.cache_resource in streamlit 1.18 i get the error above
the function is very very large and computationally expensive, but the behavior was very fine with st.experimental_singleton and streamlit version 1.17

### Are you willing to submit a PR?

- [ ] Yes, I am willing to submit a PR!


Discussion:
please help
to reproduce the error try this simple code:

import streamlit as st
import pandas as pd


df=pd.DataFrame({"col1": [1 , 2, 3, 4], "col2": ["pino", "gino", "lucullo", "augusto"]})

st.write(df)

@st.cache_resource
def stampa_df(df: pd.DataFrame) -> None:
    st.write(df)

stampa_df(df)
Thank you @francesco12357 for opening this issue. 
I can confirm that this is a regression issue. 

Code snippet:

```python
import streamlit as st
import pandas as pd

df = pd.DataFrame(
    {"col1": [1, 2, 3, 4], "col2": ["pino", "gino", "lucullo", "augusto"]}
)

st.write(df)


@st.cache_resource
def stampa_df(df_param: pd.DataFrame) -> None:
    st.write(df_param)


stampa_df(df)
```

For now, if it is possible, I suggest you refactor the code to use the `@st.cache_resource` decorated function without widgets call and move them outside of a decorated function. 
Thanks


Il giorno 12 feb 2023, alle ore 18:12, Karen Javadyan ***@***.***> ha scritto:

﻿

Thank you @francesco12357<https://github.com/francesco12357> for opening this issue.
I can confirm that this is a regression issue.

Code snippet:

import streamlit as st
import pandas as pd

df = pd.DataFrame(
    {"col1": [1, 2, 3, 4], "col2": ["pino", "gino", "lucullo", "augusto"]}
)

st.write(df)


@st.cache_resource
def stampa_df(df_param: pd.DataFrame) -> None:
    st.write(df_param)


stampa_df(df)

For now, if it is possible, I suggest you refactor the code to use the @st.cache_resource decorated function without widgets call and move them outside of a decorated function.

—
Reply to this email directly, view it on GitHub<https://github.com/streamlit/streamlit/issues/6103#issuecomment-1427083871>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/ARJ422OWGTE5OB5HR3S2P33WXEKYTANCNFSM6AAAAAAUYWQ42U>.
You are receiving this because you were mentioned.Message ID: ***@***.***>

Hi @francesco12357 , FYI, we are deep diving into the issue and hoping to get a fix.

Resolving patch:
diff --git a/lib/streamlit/runtime/caching/cached_message_replay.py b/lib/streamlit/runtime/caching/cached_message_replay.py
--- a/lib/streamlit/runtime/caching/cached_message_replay.py
+++ b/lib/streamlit/runtime/caching/cached_message_replay.py
@@ -281,9 +281,11 @@ def save_element_message(
         if not runtime.exists():
             return
         if len(self._cached_message_stack) >= 1:
-
             id_to_save = self.select_dg_to_save(invoked_dg_id, used_dg_id)
-            if isinstance(element_proto, Widget):
+            # Arrow dataframes have an ID but only set it when used as data editor
+            # widgets, so we have to check that the ID has been actually set to
+            # know if an element is a widget.
+            if isinstance(element_proto, Widget) and element_proto.id:
                 wid = element_proto.id
                 # TODO replace `Message` with a more precise type
                 if not self._registered_metadata:


Test patch:
diff --git a/lib/tests/streamlit/runtime/caching/common_cache_test.py b/lib/tests/streamlit/runtime/caching/common_cache_test.py
--- a/lib/tests/streamlit/runtime/caching/common_cache_test.py
+++ b/lib/tests/streamlit/runtime/caching/common_cache_test.py
@@ -967,3 +967,12 @@ def test_dynamic_widget_replay(self):
         sr4 = sr3.get("checkbox")[0].uncheck().run()
         sr5 = sr4.get("button")[0].click().run()
         assert sr5.get("text")[0].value == "['foo']"
+
+
+class WidgetReplayTest(InteractiveScriptTests):
+    def test_arrow_replay(self):
+        """Regression test for https://github.com/streamlit/streamlit/issues/6103"""
+        script = self.script_from_filename(__file__, "arrow_replay.py")
+
+        sr = script.run()
+        assert len(sr.get("exception")) == 0
diff --git a/lib/tests/streamlit/runtime/caching/test_data/arrow_replay.py b/lib/tests/streamlit/runtime/caching/test_data/arrow_replay.py
new file mode 100644
--- /dev/null
+++ b/lib/tests/streamlit/runtime/caching/test_data/arrow_replay.py
@@ -0,0 +1,32 @@
+# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""A small script that reproduces the bug from #6103"""
+
+import pandas as pd
+
+import streamlit as st
+
+df = pd.DataFrame(
+    {"col1": [1, 2, 3, 4], "col2": ["pino", "gino", "lucullo", "augusto"]}
+)
+
+st.write(df)
+
+
+@st.cache_resource
+def stampa_df(df_param: pd.DataFrame) -> None:
+    st.write(df_param)
+
+
+stampa_df(df)