Skip to content

Instantly share code, notes, and snippets.

@remram44
Last active December 18, 2015 03:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save remram44/5718325 to your computer and use it in GitHub Desktop.
Save remram44/5718325 to your computer and use it in GitHub Desktop.
Implicit self
import dis
import struct
import types
class instancemethod(object):
def __init__(self, func):
code = func.func_code
bytecode = code.co_code
# weird stuff here
def make_self_funcs():
container = [None]
def getter():
return container[0]
def setter(self):
container[0] = self
return getter, setter
getter, self.setter = make_self_funcs()
# witchcraft here
def blah():
return getter
cell = blah.func_closure[0]
# Rewrite bytecode
try:
global_number = func.func_code.co_names.index('self') # the global
except ValueError:
pass # Function does not use 'self'
else:
if func.func_closure is not None:
cell_number = len(func.func_closure) # the cell to use instead
else:
cell_number = 0
# LOAD_GLOBAL <global_number> 74 xx xx
# ->
# LOAD_DEREF <cell_number> 88 xx xx
# CALL_FUNCTION 0 83 00 00
# numbers are little endian
print "replacing..."
dis.dis(bytecode)
bytecode = bytecode.replace(
struct.pack('<BH', 0x74, global_number),
struct.pack('<BHBH', 0x88, cell_number, 0x83, 0))
print "with..."
dis.dis(bytecode)
# Create new function object
freevars = code.co_freevars + ('self',)
new_code = types.CodeType(
code.co_argcount,
code.co_nlocals,
code.co_stacksize,
code.co_flags | 0x10,
bytecode,
code.co_consts,
code.co_names,
code.co_varnames,
code.co_filename,
code.co_name,
code.co_firstlineno,
code.co_lnotab,
freevars,
code.co_cellvars)
if func.func_closure is None:
closure = (cell,)
else:
closure = func.func_closure + (cell,)
self.new_func = types.FunctionType(
new_code,
func.func_globals,
func.func_name,
func.func_defaults,
closure)
def __get__(self, instance, owner):
self.setter(instance)
return self.new_func
class A(object):
@instancemethod
def foo():
print A, self
A().foo()
@dutc
Copy link

dutc commented Jun 6, 2013

Interesting approach!

I think if you're willing to just construct a FunctionType yourself, then you don't need to fake the mutable closure with your cell + CALL_FUNCTION. I think you can then remove some black magic & make things a bit more concise.

from dis import dis
from types import FunctionType, CodeType
from opcode import opmap
from struct import pack

def instancemethod(func):
    if '__self__' not in func.func_code.co_names:
        return func # return unmolested

    self_index = func.func_code.co_names.index('__self__')
    closure = func.func_closure or ()
    cell_index = len(closure)
    bytecode = func.func_code.co_code.replace(
        pack('<BH', opmap['LOAD_GLOBAL'], self_index),
        pack('<BH', opmap['LOAD_DEREF'], cell_index))

    code = CodeType(
        func.func_code.co_argcount,
        func.func_code.co_nlocals,
        func.func_code.co_stacksize,
        func.func_code.co_flags & ~0x40,
        bytecode,
        func.func_code.co_consts,
        func.func_code.co_names,
        func.func_code.co_varnames,
        func.func_code.co_filename,
        func.func_code.co_name,
        func.func_code.co_firstlineno,
        func.func_code.co_lnotab,
        func.func_code.co_freevars + ('self',),
        func.func_code.co_cellvars)

    class funcwrapper(object):
        def __init__(self, func, code, closure):
            self.func, self.code, self.closure = func, code, closure

        def __get__(self, instance, owner):
            def closure():
                return instance
            return FunctionType(
                self.code,
                self.func.func_globals,
                self.func.func_name,
                self.func.func_defaults,
                self.closure + (closure.func_closure[0],))

    return funcwrapper(func, code, closure)

class Foo(object):
    def __init__(self, x):
        self.x = x

    @instancemethod
    def bar(y):
        return __self__.x * y

if __name__ == '__main__':
    foo, foo2 = Foo(2), Foo(3)
    assert foo.bar(10) is 20 and foo2.bar(10) is 30

@dutc
Copy link

dutc commented Jun 6, 2013

Also, note a very subtle error in the original that the above fixes. This is also more in line with how PyMethod_New works.

# using the original definition of instancemethod
class A(object):
    @instancemethod
    def foo():
        return self

a, b = A(), A()
a_foo, b_foo = a.foo, b.foo
assert a_foo() is a and b_foo() is b # AssertionError!

@remram44
Copy link
Author

remram44 commented Jun 6, 2013

I didn't test extensively, I was just wondering whether it could be done by only manipulating bytecode and not actual interpreter structures. Thinking about it, another problem with the original is that it's not reentrant (recursive calls would alter 'self' on the caller).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment