From the comments it seems that you have a possible solution by pickling the expression instead of the function. I just want to make clear that sympy's lambdify
function is really just creating a dynamically generated Python function and you can get the code for that function:
In [2]: import sympy as sm
...:
...: a,b = sm.symbols('a b')
...: f_fresnel = sm.integrate(sm.cos(a*b**2), (b,0,1))
...: F_fresnel = sm.lambdify((a,b), f_fresnel, "mpmath")
In [3]: import inspect
In [4]: print(inspect.getsource(F_fresnel))
def _lambdifygenerated(a, b):
return (mpf(1)/mpf(8))*sqrt(2)*sqrt(pi)*fresnelc(sqrt(2)*sqrt(a)/sqrt(pi))*gamma(mpf(1)/mpf(4))/(sqrt(a)*gamma(mpf(5)/mpf(4)))
You can just paste that function into your code directly if you want to be able to reuse it in a future script. It needs an import like from mpmath import sqrt, gamma, mpf, ...
. That way the future script does not need to import sympy, does not need to use pickle etc. This approach will be both faster and more robust.
The downsides of this approach are that if you are dynamically generating many of these functions then copying out the code might be error-prone but you could also automate that:
In [24]: code = inspect.getsource(sm.lambdify((a, b), f_fresnel, use_imps=True))
In [25]: code = code.replace('_lambdifygenerated', 'F_fresnel')
In [26]: with open('generated.py', 'a') as fout: # using append mode
...: fout.write('\n')
...: fout.write(code)
...:
In [27]: cat generated.py
def F_fresnel(a, b):
return (mpf(1)/mpf(8))*sqrt(2)*sqrt(pi)*fresnelc(sqrt(2)*sqrt(a)/sqrt(pi))*gamma(mpf(1)/mpf(4))/(sqrt(a)*gamma(mpf(5)/mpf(4)))
Clearly having code that rewrites your code on disk is risky so make sure you are careful if following something like this approach. The other downside is that this won't work if you are trying to pickle a complicated data structure that includes this F_fresnel
function as a part of that data structure.
As for why pickling fails the question is why pickling a function fails. After you have called lambdify
what you have is an ordinary function except that it is one that is dynamically generated. I think that in that situation pickle
will "pickle" the function by essentially just embedding the code to import that function. For example:
In [28]: import pickle
In [29]: pickle.dumps(sm.simplify)
Out[29]: b'\x80\x04\x95(\x00\x00\x00\x00\x00\x00\x00\x8c\x17sympy.simplify.simplify\x94\x8c\x08simplify\x94\x93\x94.'
I don't exactly know how to "read" a pickle in general but I can see pretty clearly that this is basically just storing the equivalent of the Python code
from sympy.simplify.simplify import simplify
That works if you pickle a function that is contained in a definite installed module because it means that pickle.loads
can just import the module and get the function from there. Unfortunately this approach does not work for dynamically generated functions like F_fresnel
because they are not defined in a module from which they can be imported (unless you copy out the code as I showed above). I don't know how dill
or cloudpickle
handle this as compared to pickle
but it's quite clear to me why pickle can't handle this:
In [30]: pickle.dumps(F_fresnel)
---------------------------------------------------------------------------
PicklingError Traceback (most recent call last)
Cell In[30], line 1
----> 1 pickle.dumps(F_fresnel)
PicklingError: Can't pickle <function _lambdifygenerated at 0x7f30a5db0f40>: attribute lookup _lambdifygenerated on __main__ failed
In [31]: _lambdifygenerated = F_fresnel
In [32]: pickle.dumps(F_fresnel)
Out[32]: b'\x80\x04\x95#\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x12_lambdifygenerated\x94\x93\x94.'
So pickle wants to store the code from __main__ import _lambdifygenerated
because _lambdifygenerated
is the name of the function in the code created by lambdify
regardless of what name e.g. F_fresnel
you have assigned it to. That fails because the function is defined in the __main__
module i.e. the main script and there is no object called _lambdifygenerated
there unless we assign it to that name. It would fail in many other ways though in practice because if you tried to load that pickle in a different script (a different __main__
) then it would not have the object to be imported.
Another solution would just be to wrap up the arguments to lambdify
in something that is pickleable and then pickle that:
class PickleLambdify:
def __init__(self, args, expression, **kwargs):
self.args = args
self.expression = expression
self.kwargs = kwargs
def regenerate(self):
return lambdify(self.args, self.expression, **self.kwargs)
Then you can do:
In [35]: F_fresnel_pickle = PickleLambdify((a,b), f_fresnel, modules="mpmath")
In [36]: pickle.dumps(F_fresnel_pickle)
Out[36]: b'\x80\x04\x95\x0e\x02\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x0ePickleLambdify\x94\x93\x94)\x81\x94}\x94(\x8c\x04args\x94\x8c\x11sympy.core.symbol\x94\x8c\x06Symbol\x94\x93\x94\x8c\x01a\x94\x85\x94\x81\x94h\x08\x8c\x01b\x94\x85\x94\x81\x94\x86\x94\x8c\nexpression\x94\x8c\x0esympy.core.mul\x94\x8c\x03Mul\x94\x93\x94(\x8c\x12sympy.core.numbers\x94\x8c\x08Rational\x94\x93\x94K\x01K\x08\x86\x94\x81\x94\x8c\x10sympy.core.power\x94\x8c\x03Pow\x94\x93\x94h\x14\x8c\x07Integer\x94\x93\x94K\x02\x85\x94\x81\x94h\x14\x8c\x04Half\x94\x93\x94)\x81\x94\x86\x94\x81\x94h\x1bh\x14\x8c\x02Pi\x94\x93\x94)\x81\x94h"\x86\x94\x81\x94h\x1bh\x0bh\x16J\xff\xff\xff\xffK\x02\x86\x94\x81\x94\x86\x94\x81\x94h\x1b\x8c\'sympy.functions.special.gamma_functions\x94\x8c\x05gamma\x94\x93\x94h\x16K\x05K\x04\x86\x94\x81\x94\x85\x94\x81\x94h\x14\x8c\x0bNegativeOne\x94\x93\x94)\x81\x94\x86\x94\x81\x94\x8c\'sympy.functions.special.error_functions\x94\x8c\x08fresnelc\x94\x93\x94h\x13h$h\x1bh\'h\x16J\xff\xff\xff\xffK\x02\x86\x94\x81\x94\x86\x94\x81\x94h\x1bh\x0bh"\x86\x94\x81\x94\x87\x94\x81\x94\x85\x94\x81\x94h0h\x16K\x01K\x04\x86\x94\x81\x94\x85\x94\x81\x94t\x94\x81\x94\x8c\x06kwargs\x94}\x94\x8c\x07modules\x94\x8c\x06mpmath\x94sub.'
In [37]: pickle.loads(pickle.dumps(F_fresnel_pickle))
Out[37]: <__main__.PickleLambdify at 0x7f30a5dbc290>
In [39]: pickle.loads(pickle.dumps(F_fresnel_pickle)).regenerate()
Out[39]: <function _lambdifygenerated(a, b)>
This way you can use pickle transparently with your function as part of a larger data structure but clearly at runtime unpickling this needs to import sympy, create the symbolic expressions and then call lambdify
again which is basically the same as what happens if you just pickle the expression yourself in the first place.