how to fit a method belonging to an instance with pymc3? - pymc3

I failed to fit a method belonging to an instance of a class, as a Deterministic function, with PyMc3. Can you show me how to do that ?
For simplicity, my case is summarised below with a simple example. In reality, my constraint is that everything is made through a GUI and actions like ‘find_MAP’ should be inside methods linked to pyqt buttons.
I want to fit the function ‘FunctionIWantToFit’ over the data points. Problem, the following code:
import numpy as np
import pymc3 as pm3
from scipy.interpolate import interp1d
import theano.tensor as tt
import theano.compile
class cprofile:
def __init__(self):
self.observed_x = np.array([0.3,1.4,3.1,5,6.8,9,13.4,17.1])
self.observations = np.array([6.25,2.75,1.25,1.25,1.5,1.75,1.5,1])
self.x = np.arange(0,18,0.5)
#theano.compile.ops.as_op(itypes=[tt.dscalar,tt.dscalar,tt.dscalar],
otypes=[tt.dvector])
def FunctionIWantToFit(self,t,y,z):
# can be complicated but simple in this example
# among other things, this FunctionIWantToFit depends on a bunch of
# variables and methods that belong to this instance of the class cprofile,
# so it cannot simply be put outside the class ! (like in the following example)
val=t+y*self.x+z*self.x**2
interp_values = interp1d(self.x,val)
return interp_values(self.observed_x)
def doMAP(self):
model = pm3.Model()
with model:
t = pm3.Uniform("t",0,5)
y = pm3.Uniform("y",0,5)
z = pm3.Uniform("z",0,5)
MyModel = pm3.Deterministic('MyModel',self.FunctionIWantToFit(t,y,z))
obs = pm3.Normal('obs',mu=MyModel,sd=0.1,observed=self.observations)
start = pm3.find_MAP()
print('start: ',start)
test=cprofile()
test.doMAP()
gives the following error:
Traceback (most recent call last):
File "<ipython-input-15-3dfb7aa09f84>", line 1, in <module>
runfile('/Users/steph/work/profiles/GUI/pymc3/so.py', wdir='/Users/steph/work/profiles/GUI/pymc3')
File "/Users/steph/anaconda/lib/python3.5/site-packages/spyder/utils/site/sitecustomize.py", line 866, in runfile
execfile(filename, namespace)
File "/Users/steph/anaconda/lib/python3.5/site-packages/spyder/utils/site/sitecustomize.py", line 102, in execfile
exec(compile(f.read(), filename, 'exec'), namespace)
File "/Users/steph/work/profiles/GUI/pymc3/so.py", line 44, in <module>
test.doMAP()
File "/Users/steph/work/profiles/GUI/pymc3/so.py", line 38, in doMAP
MyModel = pm3.Deterministic('MyModel',self.FunctionIWantToFit(x,y,z))
File "/Users/steph/anaconda/lib/python3.5/site-packages/theano/gof/op.py", line 668, in __call__
required = thunk()
File "/Users/steph/anaconda/lib/python3.5/site-packages/theano/gof/op.py", line 912, in rval
r = p(n, [x[0] for x in i], o)
File "/Users/steph/anaconda/lib/python3.5/site-packages/theano/compile/ops.py", line 522, in perform
outs = self.__fn(*inputs)
TypeError: FunctionIWantToFit() missing 1 required positional argument: 'z'
What’s wrong ?
remark 1: I systematically get an error message concerning the last parameter of ‘FunctionIWantToFit’. here it’s ‘z’ but if I remove z from the signature, the error message concerns ‘y’ (identical except from the name of the variable). if I add a 4th variable ‘w’ in the signature, the error message concerns ‘w’ (identical except from the name of the variable).
rk2: it looks like I missed something very basic in ‘theano’ or ‘pymc3’, because when I put ‘FunctionIWantToFit’ outside the class, it works. See the following example.
class cprofile:
def __init__(self):
self.observations = np.array([6.25,2.75,1.25,1.25,1.5,1.75,1.5,1])
def doMAP(self):
model = pm3.Model()
with model:
t = pm3.Uniform("t",0,5)
y = pm3.Uniform("y",0,5)
z = pm3.Uniform("z",0,5)
MyModel = pm3.Deterministic('MyModel',FunctionIWantToFit(t,y,z))
obs = pm3.Normal('obs',mu=MyModel,sd=0.1,observed=self.observations)
start = pm3.find_MAP()
print('start: ',start)
#theano.compile.ops.as_op(itypes=[tt.dscalar,tt.dscalar,tt.dscalar],
otypes=[tt.dvector])
def FunctionIWantToFit(t,y,z):
observed_x = np.array([0.3,1.4,3.1,5,6.8,9,13.4,17.1])
x = np.arange(0,18,0.5)
val=t+y*x+z*x**2
interp_values = interp1d(x,val)
return interp_values(observed_x)
test=cprofile()
test.doMAP()
gives:
Warning: gradient not available.(E.g. vars contains discrete variables). MAP estimates may not be accurate for the default parameters. Defaulting to non-gradient minimization fmin_powell.
WARNING:pymc3:Warning: gradient not available.(E.g. vars contains discrete variables). MAP estimates may not be accurate for the default parameters. Defaulting to non-gradient minimization fmin_powell.
Optimization terminated successfully.
Current function value: 1070.673818
Iterations: 4
Function evaluations: 179
start: {'t_interval_': array(-0.27924150484602733), 'y_interval_': array(-9.940000425802811), 'z_interval_': array(-12.524909223913992)}
Except that I don’t know how to do that without big modifications in several modules, since the real ‘FunctionIWantToFit’ depends on a bunch of variables and methods that belong to this instance of the class profile.
In fact I 'm not even sure I know how to do that since ‘FunctionIWantToFit’ should then have objects in arguments (that I currently use via self) and I'm not sure how to do that with the theano decorator.
So I would prefer to avoid this solution... unless necessary. then I need explanations on how to implement it...
added on april 9, 2017:
Even without the interpolation question, it doesn't work because I must have missed something obvious with theano and/or pymc3. Please can you explain the problem ? I just want to compare model and data. First, it's such a shame being stuck to pymc2. ; second, I'm sure I'm not the only one with such a basic problem.
For example, let's consider variations around this very basic code:
import numpy as np
import theano
import pymc3
theano.config.compute_test_value = 'ignore'
theano.config.on_unused_input = 'ignore'
class testclass:
x = np.arange(0,18,0.5)
observed_x = np.array([0.3,1.4,3.1,5,6.8,9,13.4,17.1])
observations = np.array([6.25,2.75,1.25,1.25,1.5,1.75,1.5,1])
def testfunc(self,t,y,z):
t2 = theano.tensor.dscalar('t2')
y2 = theano.tensor.dscalar('y2')
z2 = theano.tensor.dscalar('z2')
val = t2 + y2 * self.observed_x + z2 * self.observed_x**2
f = theano.function([t2,y2,z2],val)
return f
test=testclass()
model = pymc3.Model()
with model:
t = pymc3.Uniform("t",0,5)
y = pymc3.Uniform("y",0,5)
z = pymc3.Uniform("z",0,5)
with model:
MyModel = pymc3.Deterministic('MyModel',test.testfunc(t,y,z))
with model:
obs = pymc3.Normal('obs',mu=MyModel,sd=0.1,observed=test.observations)
this code fails at the last line with the error message: TypeError: unsupported operand type(s) for -: 'TensorConstant' and 'Function'
if I change 'testfunc' into:
def testfunc(self,t,y,z):
t2 = theano.tensor.dscalar('t2')
y2 = theano.tensor.dscalar('y2')
z2 = theano.tensor.dscalar('z2')
val = t2 + y2 * self.observed_x + z2 * self.observed_x**2
f = theano.function([t2,y2,z2],val)
fval = f(t,y,z,self.observed_x)
return fval
the code fails at the 'MyModel =' line with error TypeError: ('Bad input argument to theano function with name "/Users/steph/work/profiles/GUI/pymc3/theanotest170409.py:32" at index 0(0-based)', 'Expected an array-like object, but found a Variable: maybe you are trying to call a function on a (possibly shared) variable instead of a numeric array?')
if I go back to the original 'testfunc' but change the last 'with model' lines with:
with model:
fval = test.testfunc(t,y,z)
obs = pymc3.Normal('obs',mu=fval,sd=0.1,observed=test.observations)
the error is the same as the first one.
I presented here only 3 tries but I would like to underline that I tried many many combinations, simpler and simpler until these ones, during hours. I have the feeling pymc3 shows a huge change of spirit, compared to pymc2, that I didn't get and is poorly documented...

Ok, let's do this by parts. First I'll explain the error messages that you got, and then I'll tell you how I would proceed.
On the first question, the direct reason why you're getting a complaint on the missing parameters is because your function, defined inside the class, takes as input (self, t, y, z), while you're declaring it in the op decorator as having only three inputs (t, y, z). You would have to declare the inputs as being four in your decorator to account for the class instance itself.
On "added on april 9, 2017:", the first code will not work because the output of test.testfunc(t,y,z) is a theano function itself. pymc3.Deterministic is expecting it to output theano variables (or python variables). Instead, make test.testfun output val = t2 + y2 * self.observed_x + z2 * self.observed_x**2 directly.
Then, on "if I change 'testfunc' into:", you get that error because of the way pymc3 is trying to work with theano functions. Long story short, the problem is that when pymc3 is making use of this function, it will send it theano variables, while fval is expecting numerical variables (numpy arrays or other). As in the previous paragraph, you just need to output val directly: no need to compile any theano function for this.
As for how I would proceed, I would try to declare the class instance as input to the theano decorator. Unfortunately, I can't find any documentation on how to do this and it might actually be impossible (see this old post, for example).
Then I would try to pass everything the function needs as inputs and define it outside of the class. This could be quite cumbersome and if it needs methods as input, then you run into additional problems.
Another way of doing this is to create a child class of theano.gof.Op whose init method takes your class (or rather an instance of it) as input and then define your perform() method. This would look something like this:
class myOp(theano.gof.Op):
""" These are the inputs/outputs you used in your as_op
decorator.
"""
itypes=[tt.dscalar,tt.dscalar,tt.dscalar]
otypes=[tt.dvector]
def __init__(self, myclass):
""" myclass would be the class you had from before, which
you called cprofile in your first block of code."""
self.myclass = myclass
def perform(self,node, inputs, outputs):
""" Here you define your operations, but instead of
calling everyting from that class with self.methods(), you
just do self.myclass.methods().
Here, 'inputs' is a list with the three inputs you declared
so you need to unpack them. 'outputs' is something similar, so
the function doesn't actually return anything, but saves all
to outputs. 'node' is magic juice that keeps the world
spinning around; you need not do anything with it, but always
include it.
"""
t, y, z = inputs[0][0], inputs[0][1], inputs[0][2]
outputs[0][0] = t+y*self.myclass.x+z*self.myclass.x**2
myop = myOp(myclass)
Once you have done this, you can use myop as your Op for the rest of your code. Note that some parts are missing. You can check my example for more details.
As for the example, you do not need to define the grad() method. Because of this, you can do all operations in perform() in pure python, if that helps.
Alternatively, and I say this with a smirk on my face, if you have access to the definition of the class you're using, you can also make it inherit from theano.gof.Op, create the perform() method (as in my other example, where you left a message) and try to use it like that. It could create conflicts with whatever else you're doing with that class and it's probably quite hard to get right, but might be fun to try.

theano.compile.ops.as_op is just a short-hand for defining simple Theano Ops. If you want to code more involved ones, it is better to define it in a separate class. Objects of this class could of course take a reference to an instance of your cprofile if that really is necessary.
http://deeplearning.net/software/theano/extending/extending_theano.html

I finally converged toward the successful code below:
import numpy as np
import theano
from scipy.interpolate import interp1d
import pymc3 as pm3
theano.config.compute_test_value = 'ignore'
theano.config.on_unused_input = 'ignore'
class cprofile:
observations = np.array([6.25,2.75,1.25,1.25,1.5,1.75,1.5,1])
x = np.arange(0,18,0.5)
observed_x = np.array([0.3,1.4,3.1,5,6.8,9,13.4,17.1])
def doMAP(self):
model = pm3.Model()
with model:
t = pm3.Uniform("t",0,5)
y = pm3.Uniform("y",0,5)
z = pm3.Uniform("z",0,5)
obs=pm3.Normal('obs',
mu=FunctionIWantToFit(self)(t,y,z),
sd=0.1,observed=self.observations)
start = pm3.find_MAP()
print('start: ',start)
class FunctionIWantToFit(theano.gof.Op):
itypes=[theano.tensor.dscalar,
theano.tensor.dscalar,
theano.tensor.dscalar]
otypes=[theano.tensor.dvector]
def __init__(self, cp):
self.cp = cp # note cp is an instance of the 'cprofile' class
def perform(self,node, inputs, outputs):
t, y, z = inputs[0], inputs[1], inputs[2]
xxx = self.cp.x
temp = t+y*xxx+z*xxx**2
interpolated_concentration = interp1d(xxx,temp)
outputs[0][0] = interpolated_concentration(self.cp.observed_x)
testcp=cprofile()
testcp.doMAP()
thanks to the answer by Dario because I was too slow to understand the first answer by myself. I get it retrospectively but I strongly think the pymc3 doc is painfully unclear. It should contain very simple and illustrative examples.
However I didn’t succed in doing anything that work following the comment by Chris. Could anyone explain and/or give an example ?
One more thing: I don’t know whether my example above is efficient or could be simplified. In particular it gives me the impression the instance ‘testcp’ is copied twice in memory. More comments/answers are welcome to go further.

Related

Cannot iterate over AbstractOrderedScalarSet before it has been constructed (initialized)

I have just started with pyomo and Python, and trying to create a simple model but have a problem with adding a constraint.
I followed the following example from GitHub
https://github.com/brentertainer/pyomo-tutorials/blob/master/introduction/02-lp-pyomo.ipynb
import pandas as pd
import pyomo.environ as pe
import pyomo.opt as po
#DATA
T=3;
CH=2;
time = ['t{0}'.format(t+1) for t in range(T)]
CHP=['CHP{0}'.format(s+1) for s in range(CH)]
#Technical characteristic
heat_maxprod = {'CHP1': 250,'CHP2': 250} #Only for CHPS
#MODEL
seq=pe.ConcreteModel
### SETS
seq.CHP = pe.Set(initialize = CHP)
seq.T = pe.Set(initialize = time)
### PARAMETERS
seq.heat_maxprod = pe.Param(seq.CHP, initialize = heat_maxprod) #Max heat production
### VARIABLES
seq.q_DA=pe.Var(seq.CHP, seq.T, domain=pe.Reals)
### CONSTRAINTS
##Maximum and Minimum Heat Production
seq.Heat_DA1 = pe.ConstraintList()
for t in seq.T:
for s in seq.CHP:
seq.Heat_DA1.add( 0 <= seq.q_DA[s,t])
seq.Heat_DA2 = pe.ConstraintList()
for t in seq.T:
for s in seq.CHP:
seq.Heat_DA2.add( seq.q_DA[s,t] <= seq.heat_maxprod[s])
### OBJECTIVE
seq.obj=Objective(expr=sum( seq.C_fuel[s]*(seq.rho_heat[s]*seq.q_DA[s,t]) for t in seq.T for s in seq.CHP))
When I run the program I am getting the following error:
RuntimeError: Cannot iterate over AbstractOrderedScalarSet 'AbstractOrderedScalarSet' before it has been constructed (initialized): 'iter' is an attribute on an Abstract component and cannot be accessed until the component has been fully constructed (converted to a Concrete component) using AbstractModel.create_instance() or AbstractOrderedScalarSet.construct().
Can someone, please, help with an issue? Thanks!
P.S. I know that the resulting answer for the problem is zero, I just want to make it work in terms of correct syntaxis.
In this line of code:
seq=pe.ConcreteModel
You are missing parenthesis. So, I think you are just creating an alias for the function instead of calling it.
Try:
seq=pe.ConcreteModel()

What is a def __call__(self,in_data) function used for in python and where is the in_data getting its value

I am actually a student and my teacher gave me this code to understand and use this code so i can continue to learn machine learning etc. I get that it is an AND function and i get what it is printing , the thing i cant understan is the def call(self,in_data): function and where does the "in_data" gets its value from? Because in my point of view all i see it's a blank variable that somehow helps me to complete the code. Also changing the value of the weights in the init part didnt actually change anything in this code and i am wondering why. Here is the whole code i was given to study. Thanks for helping!
import numpy as np
class Perceptron:
def __init__(self,input_length,weights=None):
if weights is None:
self.weights= np.ones(input_length)*0.5
else:
self.weights=weights
#staticmethod
def unit_step_function(x):
if x>0.5:
return 1
return 0
def __call__(self,in_data):
weighted_input=self.weights*in_data
weighted_sum=weighted_input.sum()
return Perceptron.unit_step_function(weighted_sum)
p = Perceptron(2,np.array([0.5,0.5]))
for x in [np.array([0,0]),np.array([0,1]),np.array([1,0]),np.array([1,1])]:
y=p(np.array(x))
print(x,y)
__call__ is called when you try to invoke the object as though it were a function. You can see that being done at the bottom. p is a Perceptron object, but it's being treated as a function when you write
y = p(np.array(x))
So where is the data coming from? It's the np.array(x) that's being passed in.
You can read more about the "dunder"/"magic" methods here.
I'd actually argue that this is an abuse of __call__ though. I wouldn't consider a Perceptron to be inherently function-like. I think using a normal method would be clearer:
class Perceptron:
. . .
def give_input(self, in_data): # Instead of __call__
weighted_input = self.weights*in_data
weighted_sum = weighted_input.sum()
return Perceptron.unit_step_function(weighted_sum)
for x in [np.array([0,0]),np.array([0,1]),np.array([1,0]),np.array([1,1])]:
y = p.give_input(np.array(x)) # Just a typical method call now

unbound method <method> must be called with <class> instance as first argument

I would like to provide default behaviour for a class as illustrated below.
import numpy as np
class Test:
def __init__(self, my_method=None):
self.my_method = my_method or np.min
Test().my_method([1, 2, 3]) # >>> 1
The code works as expected. To keep all the default values together for easier code maintenance I wanted to change the code to
import numpy as np
class Test:
default_method = np.min
def __init__(self, my_method=None):
self.my_method = my_method or Test.default_method
Test().my_method([1, 2, 3]) # >>> TypeError
but the call to my_method fails with the error message unbound method amin() must be called with Test instance as first argument (got list instance instead). Oddly, the code works as expected if I use the builtin min rather than np.min, i.e. the following works as expected.
import numpy as np
class Test:
default_method = min # no np.
def __init__(self, my_method=None):
self.my_method = my_method or Test.default_method
Test().my_method([1, 2, 3]) # >>> 1
What am I missing?
Any function stored as an attribute on a class object is treated as a method by Python. On Python 2, that means it requires the first argument to be an instance of the class (which will be passed automatically if the attribute is requested via an instance). On Python 3, unbound methods no longer check their arguments in that way (so your code would work as written).
To work around the issue on Python 2, try wrapping the default_method value with staticmethod:
class Test(object):
default_method = staticmethod(np.min)
#...
This might not be a bad idea even on Python 3, since you'll also be able to use self.default_method rather than explicitly naming the class.
As for why the code worked with min but not np.min, that's because they are implemented differently. You can see that from their types:
>>> type(min)
<class 'builtin_function_or_method'>
>>> type(np.min)
<class 'function'>
Regular functions (like np.min) act as descriptors when they're attributes of a class (thus getting the "binding" behavior that was causing your issue). Builtin functions like min don't support the descriptor protocol, so the issue doesn't come up.

python 2.7 or 3.2(classes and instances)

I'm a beginner of python. My question is while compiling a project using python, how to make a user-input variable an attribute.
For example:
class supermarket:
num=int(input('enter a no.'))
def __init__(self,num):
self.ini=''
def odd_even(self,num):
if num%2==0:
self.ini='even'
else:
self.ini='odd'
#calling
pallavi=supermarket()
pallavi.(num)
Here, it's showing the error that there is no attribute called num.
What should I do?
This is just a summary and leaves a lot out, but basically, your num should go inside the __init__() call as self.num. So:
class supermarket:
def __init__(self):
self.ini = ''
self.num = int(input('enter a no.'))
# etc.
Then to access the attribute:
pallavi = supermarket()
pallavi.num # No parentheses needed
There's lots more to classes in Python that I don't have time to go into right now, but I'll touch on one thing: until you know what you're doing, all assignments in a class should go inside a function, not in the class definition itself. If you have a statement with a = sign in it that's in the class, not in a function (like the num=int(input("enter a no.")) statement in your example), it's going to fail and you won't understand why.
The reason why goes into the difference between "class variables" and "instance variables", but it might be too soon for you to wrestle with that concept. Still, it might be worth taking a look at the Python tutorial's chapter on classes. If you don't understand parts of that tutorial, don't worry about it yet -- just learn a few concepts, keep on writing code, then go back later and read the tutorial again and a few more concepts may become clear to you.
Good luck!
You have numerous problems here:
num = int(input(...)) assigns a class attribute - this code runs when the class is defined, not when an instance is created, and the attribute will be shared by all instances of the class;
Despite defining a second num parameter to __init__, you call pallavi = supermarket() without passing the argument;
Also, why is num a parameter of odd_even - if it's an attribute, access it via self; and
pallavi.(num) is not correct Python syntax - attribute access syntax is object.attr, the parentheses are a SyntaxError.
I think what you want is something like:
class Supermarket(): # note PEP-8 naming
# no class attributes
def __init__(self, num):
self.num = num # assign instance attribute
self.ini = 'odd' if num % 2 else 'even' # don't need separate method
#classmethod # method of the class, rather than of an instance
def from_input(cls):
while True:
try:
num = int(input('Enter a no.: ')) # try to get an integer
except ValueError:
print("Please enter an integer.") # require valid input
else:
return cls(num) # create class from user input
This separates out the request for user input from the actual initialisation of the instance, and would be called like:
>>> pallavi = Supermarket.from_input()
Enter a no.: foo
Please enter an integer.
Enter a no.: 12
>>> pallavi.num
12
>>> pallavi.ini
'even'
As you mention 3.2 and 2.7, note that input should be replaced with raw_input when using 2.x.

Extending SWIG builtin classes

The -builtin option of SWIG has the advantage of being faster, and of being exempt of a bug with multiple inheritance.
The setback is I can't set any attribute on the generated classes or any subclass :
-I can extend a python builtin type like list, without hassle, by subclassing it :
class Thing(list):
pass
Thing.myattr = 'anything' # No problem
-However using the same approach on a SWIG builtin type, the following happens :
class Thing(SWIGBuiltinClass):
pass
Thing.myattr = 'anything'
AttributeError: type object 'Thing' has no attribute 'myattr'
How could I work around this problem ?
I found a solution quite by accident. I was experimenting with metaclasses, thinking I could manage to override the setattr and getattr functions of the builtin type in the subclass.
Doing this I discovered the builtins already have a metaclass (SwigPyObjectType), so my metaclass had to inherit it.
And that's it. This alone solved the problem. I would be glad if someone could explain why :
SwigPyObjectType = type(SWIGBuiltinClass)
class Meta(SwigPyObjectType):
pass
class Thing(SWIGBuiltinClass):
__metaclass__ = Meta
Thing.myattr = 'anything' # Works fine this time
The problem comes from how swig implemented the classes in "-builtin" to be just like builtin classes (hence the name).
builtin classes are not extensible - try to add or modify a member of "str" and python won't let you modify the attribute dictionary.
I do have a solution I've been using for several years.
I'm not sure I can recommend it because:
It's arguably evil - the moral equivalent of casting away const-ness in C/C++
It's unsupported and could break in future python releases
I haven't tried it with python3
I would be a bit uncomfortable using "black-magic" like this in production code - it could break and is certainly obscure - but at least one giant corporation IS using this in production code
But.. I love how well it works to solve some obscure features we wanted for debugging.
The original idea is not mine, I got it from:
https://gist.github.com/mahmoudimus/295200 by Mahmoud Abdelkader
The basic idea is to access the const dictionary in the swig-created type object as a non-const dictionary and add/override any desired methods.
FYI, the technique of runtime modification of classes is called monkeypatching, see https://en.wikipedia.org/wiki/Monkey_patch
First - here's "monkeypatch.py":
''' monkeypatch.py:
I got this from https://gist.github.com/mahmoudimus/295200 by Mahmoud Abdelkader,
his comment: "found this from Armin R. on Twitter, what a beautiful gem ;)"
I made a few changes for coding style preferences
- Rudy Albachten April 30 2015
'''
import ctypes
from types import DictProxyType, MethodType
# figure out the size of _Py_ssize_t
_Py_ssize_t = ctypes.c_int64 if hasattr(ctypes.pythonapi, 'Py_InitModule4_64') else ctypes.c_int
# python without tracing
class _PyObject(ctypes.Structure):
pass
_PyObject._fields_ = [
('ob_refcnt', _Py_ssize_t),
('ob_type', ctypes.POINTER(_PyObject))
]
# fixup for python with tracing
if object.__basicsize__ != ctypes.sizeof(_PyObject):
class _PyObject(ctypes.Structure):
pass
_PyObject._fields_ = [
('_ob_next', ctypes.POINTER(_PyObject)),
('_ob_prev', ctypes.POINTER(_PyObject)),
('ob_refcnt', _Py_ssize_t),
('ob_type', ctypes.POINTER(_PyObject))
]
class _DictProxy(_PyObject):
_fields_ = [('dict', ctypes.POINTER(_PyObject))]
def reveal_dict(proxy):
if not isinstance(proxy, DictProxyType):
raise TypeError('dictproxy expected')
dp = _DictProxy.from_address(id(proxy))
ns = {}
ctypes.pythonapi.PyDict_SetItem(ctypes.py_object(ns), ctypes.py_object(None), dp.dict)
return ns[None]
def get_class_dict(cls):
d = getattr(cls, '__dict__', None)
if d is None:
raise TypeError('given class does not have a dictionary')
if isinstance(d, DictProxyType):
return reveal_dict(d)
return d
def test():
import random
d = get_class_dict(str)
d['foo'] = lambda x: ''.join(random.choice((c.upper, c.lower))() for c in x)
print "and this is monkey patching str".foo()
if __name__ == '__main__':
test()
Here's a contrived example using monkeypatch:
I have a class "myclass" in module "mystuff" wrapped with swig -python -builtin
I want to add an extra runtime method "namelen" that returns the length of the name returned by myclass.getName()
import mystuff
import monkeypatch
# add a "namelen" method to all "myclass" objects
def namelen(self):
return len(self.getName())
d = monkeypatch.get_class_dict(mystuff.myclass)
d['namelen'] = namelen
x = mystuff.myclass("xxxxxxxx")
print "namelen:", x.namelen()
Note that this can also be used to extend or override methods on builtin python classes, as is demonstrated in the test in monkeypatch.py: it adds a method "foo" to the builtin str class that returns a copy of the original string with random upper/lower case letters
I would probably replace:
# add a "namelen" method to all "myclass" objects
def namelen(self):
return len(self.getName())
d = monkeypatch.get_class_dict(mystuff.myclass)
d['namelen'] = namelen
with
# add a "namelen" method to all "myclass" objects
monkeypatch.get_class_dict(mystuff.myclass)['namelen'] = lambda self: return len(self.getName())
to avoid extra global variables