Selfless Classes, Godless Hacks
[Previously, on selfless classes](/post/selfless-classes): ```python def selfless(cls): for name, function in vars(cls).items(): if not callable(function): continue setattr(cls, name, add_self(function)) return cls def add_self(function): ... # Do something ``` That's a decorator that iterates over a class's methods, and replaces them with a version of themselves that gets ``self`` automatically. The way we did this was by noticing that when ``self`` wasn't part of their signature, it was considered a global variable, and adding it to their global scope. This time, we're going to add it to their local scope - and then rewrite their code to use it instead. ![pause](/media/images/pause.png) Let's start with a simpler example. We saw that this code actually ``NameError``s: ```python def f(): locals()['x'] = 1 print(x) ``` And if we disassemble it, we can see why: ```python >>> import dis >>> dis.disassemble(f) 0 LOAD_CONST 1 (1) 3 LOAD_GLOBAL 0 (locals) 6 CALL_FUNCTION 0 9 LOAD_CONST 2 ('x') 12 STORE_SUBSCR 13 LOAD_GLOBAL 1 (x) 16 PRINT_ITEM 17 PRINT_NEWLINE 18 LOAD_CONST 0 (None) 21 RETURN_VALUE ``` So if we change the ``LOAD_GLOBAL`` instruction to ``LOAD_FAST`` (that's what "LOAD_LOCAL" is called), it should work. First, let's write a normal disassembly function (seriously you guys, who wrote ``dis``? Its API is really lousy). ```python # NOTE: this should work on Python 2.7-3.5; in Python 3.6 they # changed the bytecode, and I haven't had the chance catch up. def disassemble(function): bytecode = io.BytesIO(function.__code__.co_code) while True: # Read the next opcode. op = bytecode.read(1) # If we're done, break. if not op: break op, = struct.unpack('<b', op) arg = None # If it's a high opcode, then it also has a 2-byte argument. if op > opcode.HAVE_ARGUMENT: arg, = struct.unpack('<h', bytecode.read(2)) yield op, arg ``` Alright, let's do this! ``` def f(): locals()['x'] = 1 print(x) # f.__code__.co_varnames holds local variable names. # f.__code__.co_names holds global variable names and attributes. # Add x as a local variable. varnames = ('x',) bytecode =  for op, arg in disassemble(f): # If we get LOAD_GLOBAL 1 (x) change it to LOAD_STORE 0 (there are no # local variables, so us adding 'x' makes it the 0th local variable). if ( op == opcode.opmap['LOAD_GLOBAL'] and f.__code__.co_names[arg] == 'x' ): op, arg = opcode.opmap['LOAD_FAST'], 0 bytecode.append(struct.pack('<b', op)) if arg is not None: bytecode.append(struct.pack('<h', arg)) # Creating a code object is a bit tiresome: code_args = [ f.__code__.co_argcount, f.__code__.co_nlocals, f.__code__.co_stacksize, f.__code__.co_flags, b''.join(bytecode), f.__code__.co_consts, f.__code__.co_names, varnames, f.__code__.co_filename, f.__code__.co_name, f.__code__.co_firstlineno, f.__code__.co_lnotab, f.__code__.co_freevars, f.__code__.co_cellvars, ] # Python 3 code object difference: if hasattr(f.__code__, 'co_kwonlyargcount'): code_args.insert(1, f.__code__.co_kwonlyargcount) f.__code__ = types.CodeType(*code_args) ``` ```python >>> f() 1 ``` It works! ![pause](/media/images/pause.png) Doing this to a real function is a little bit more complicated: adding ``self`` to ``co_varnames`` will shift the index of all the other local variables, so we'd have to fix the argument in the opcodes that use them; and removing ``self`` from ``co_names`` will shift all the global variables and attribute names that come after it, so we'd have to fix their opcodes, too. This is what we end up with: ```python def add_self(function): code = function.__code__ index = code.co_names.index('self') bytecode =  for op, arg in disassemble(function): # Increase argument of LOAD/STORE_FAST (because we're adding self to varnames). if op in (opcode.opmap['LOAD_FAST'], opcode.opmap['STORE_FAST']): arg += 1 # Change LOAD_GLOBAL of self to LOAD_FAST. elif op == opcode.opmap['LOAD_GLOBAL'] and arg == index: op, arg = opcode.opmap['LOAD_FAST'], 0 # Change STORE_GLOBAL of self to STORE_FAST. elif op == opcode.opmap['STORE_GLOBAL'] and arg == index: op, arg = opcode.opmap['STORE_FAST'], 0 # Decrease argument of LOAD/STORE_GLOBAL/NAME/ATTR if it's after index (because we're removing self from names). elif op in (opcode.opmap['LOAD_NAME'], opcode.opmap['STORE_NAME'], opcode.opmap['LOAD_ATTR'], opcode.opmap['STORE_ATTR'], opcode.opmap['LOAD_GLOBAL'], opcode.opmap['STORE_GLOBAL']): if arg > index: arg -= 1 bytecode.append(struct.pack('<b', op)) if arg is not None: bytecode.append(struct.pack('<h', arg)) names = tuple(name for name in code.co_names if name != 'self') varnames = ('self',) + code.co_varnames code_args = [ code.co_argcount + 1, # Add self to arguments. code.co_nlocals + 1, # Add self to locals. code.co_stacksize, code.co_flags, b''.join(bytecode), # Change bytecode. code.co_consts, names, # Remove self from names. varnames, # Add self to varnames. code.co_filename, code.co_name, code.co_firstlineno, code.co_lnotab, code.co_freevars, code.co_cellvars, ] # Python 3 code object difference: if hasattr(code, 'co_kwonlyargcount'): code_args.insert(1, code.co_kwonlyargcount) function.__code__ = types.CodeType(*code_args) return function ``` And lo and behold: ```python @selfless class A: def __init__(x): self.x = x def f(): return self.x + 1 a = A(1) assert a.x == 1 assert a.f() == 2 ``` ![pause](/media/images/pause.png) After a long time, I've finally come to terms with my ``self``, and found peace: maybe that's what it means to grow up. Treating ``self`` like a standard parameter (and thus, having to add it to every signature of every method) makes Python *as a whole* better, because there are *no special cases*. Methods are just functions, and the entire data model is so much simpler because of that: in fact, once you get descriptors, you've basically mastered Python. So selfish classes are better, after all; good call, Guido. As always, you can find the code on Github, in the [never\_use\_this](https://github.com/dan-gittik/selfless/tree/master/never_use_this) directory.