How to get self into a Python method without explicitly accepting it
我正在开发一个文档测试框架——基本上是PDF的单元测试。测试是由框架定义的类实例的(修饰)方法,这些方法在运行时定位和实例化,并调用这些方法来执行测试。
我的目标是减少编写测试的人需要关注的奇怪的Python语法的数量,因为这些人可能是或不是Python程序员,甚至是非常多的程序员。所以我希望他们能够为方法编写"def foo():"而不是"def foo(self):",但是仍然能够使用"self"访问成员。
在一个普通的程序中,我会认为这是一个可怕的想法,但是在一个特定于领域的语言类型的程序中,像这样的程序,似乎值得一试。
我已经通过使用一个修饰器成功地从方法签名中消除了self(实际上,因为我已经在测试用例中使用了一个修饰器,所以我将把它滚到这个方法中),但是"self"不会引用测试用例方法中的任何内容。
我曾经考虑过使用一个全局for self,甚至想出一个或多或少可以工作的实现,但是我宁愿污染尽可能小的名称空间,这就是为什么我更愿意将变量直接注入到测试用例方法的本地名称空间中。有什么想法吗?
Aaronasterling的解决方案几乎没有升级(我没有足够的声誉对此发表评论):
1 2 3 4 5 6 | def wrap(f): @functools.wraps(f) def wrapper(self,*arg,**kw): f.func_globals['self'] = self return f(*arg,**kw) return wrapper |
但是,如果将为不同的实例递归调用f函数,这两个解决方案都将工作不可预知,因此您必须像这样克隆它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import types class wrap(object): def __init__(self,func): self.func = func def __get__(self,obj,type): new_globals = self.func.func_globals.copy() new_globals['self'] = obj return types.FunctionType(self.func.func_code,new_globals) class C(object): def __init__(self,word): self.greeting = word @wrap def greet(name): print(self.greeting+' , ' + name+ '!') C('Hello').greet('kindall') |
我对这个问题的回答很愚蠢,但我刚开始。这是一个更好的方法。这只是很少的测试,但它有利于演示正确的方法来做这件事,这是不适当的。它当然在2.6.5上工作。我没有测试任何其他版本,但没有硬编码的操作码,所以它应该和大多数其他2.x代码一样可移植。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | import opcode import types def instructions(code): """Iterates over a code string yielding integer [op, arg] pairs If the opcode does not take an argument, just put None in the second part """ code = map(ord, code) i, L = 0, len(code) extended_arg = 0 while i < L: op = code[i] i+= 1 if op < opcode.HAVE_ARGUMENT: yield [op, None] continue oparg = code[i] + (code[i+1] << 8) + extended_arg extended_arg = 0 i += 2 if op == opcode.EXTENDED_ARG: extended_arg = oparg << 16 continue yield [op, oparg] def write_instruction(inst): """Takes an integer [op, arg] pair and returns a list of character bytecodes""" op, oparg = inst if oparg is None: return [chr(op)] elif oparg <= 65536L: return [chr(op), chr(oparg & 255), chr((oparg >> 8) & 255)] elif oparg <= 4294967296L: # The argument is large enough to need 4 bytes and the EXTENDED_ARG opcode return [chr(opcode.EXTENDED_ARG), chr((oparg >> 16) & 255), chr((oparg >> 24) & 255), chr(op), chr(oparg & 255), chr((oparg >> 8) & 255)] else: raise ValueError("Invalid oparg: {0} is too large".format(oparg)) def add_self(f): """Add self to a method Creates a new function by prepending the name 'self' to co_varnames, and incrementing co_argcount and co_nlocals. Increase the index of all other locals by 1 to compensate. Also removes 'self' from co_names and decrease the index of all names that occur after it by 1. Finally, replace all occurrences of `LOAD_GLOBAL i,j` that make reference to the old 'self' with 'LOAD_FAST 0,0'. Essentially, just create a code object that is exactly the same but has one more argument. """ code_obj = f.func_code try: self_index = code_obj.co_names.index('self') except ValueError: raise NotImplementedError("self is not a global") # The arguments are just the first co_argcount co_varnames varnames = ('self', ) + code_obj.co_varnames names = tuple(name for name in code_obj.co_names if name != 'self') code = [] for inst in instructions(code_obj.co_code): op = inst[0] if op in opcode.haslocal: # The index is now one greater because we added 'self' at the head of # the tuple inst[1] += 1 elif op in opcode.hasname: arg = inst[1] if arg == self_index: # This refers to the old global 'self' if op == opcode.opmap['LOAD_GLOBAL']: inst[0] = opcode.opmap['LOAD_FAST'] inst[1] = 0 else: # If `self` is used as an attribute, real global, module # name, module attribute, or gets looked at funny, bail out. raise NotImplementedError("Abnormal use of self") elif arg > self_index: # This rewrites the index to account for the old global 'self' # having been removed. inst[1] -= 1 code += write_instruction(inst) code = ''.join(code) # type help(types.CodeType) at the interpreter prompt for this one new_code_obj = types.CodeType(code_obj.co_argcount + 1, code_obj.co_nlocals + 1, code_obj.co_stacksize, code_obj.co_flags, code, code_obj.co_consts, names, varnames, '<OpcodeCity>', code_obj.co_name, code_obj.co_firstlineno, code_obj.co_lnotab, code_obj.co_freevars, code_obj.co_cellvars) # help(types.FunctionType) return types.FunctionType(new_code_obj, f.func_globals) class Test(object): msg = 'Foo' @add_self def show(msg): print self.msg + msg t = Test() t.show('Bar') |
下面是一个单行方法修饰器,它似乎在不修改任何可调用类型*的特殊属性(标记为只读)的情况下完成了工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # method decorator -- makes undeclared 'self' argument available to method injectself = lambda f: lambda self: eval(f.func_code, dict(self=self)) class TestClass: def __init__(self, thing): self.attr = thing @injectself def method(): print 'in TestClass::method(): self.attr = %r' % self.attr return 42 test = TestClass("attribute's value") ret = test.method() print 'return value:', ret # output: # in TestClass::method(): self.attr ="attribute's value" # return value: 42 |
请注意,除非您采取预防措施,否则
@肯德尔:根据你对如何在容器类中使用方法的评论(但暂时忽略了额外变量的注入),下面是你正在做的事情吗?对于我来说,很难理解框架和用户所写内容之间是如何划分的。我觉得这是个有趣的设计模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | # method decorator -- makes undeclared 'self' argument available to method injectself = lambda f: lambda self: eval(f.func_code, dict(self=self)) class methodclass: def __call__(): print 'in methodclass::__call__(): self.attr = %r' % self.attr return 42 class TestClass: def __init__(self, thing): self.attr = thing method = injectself(methodclass.__call__) test = TestClass("attribute's value") ret = test.method() print 'return value:', ret # output # in methodclass::__call__(): self.attr ="attribute's value" # return value: 42 |
诀窍是在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | import new, functools class TestMeta(type): def __new__(meta, classname, bases, classdict): for item in classdict: if hasattr(classdict[item], '__call__'): classdict[item] = wrap(classdict[item]) return type.__new__(meta, classname, bases, classdict) def wrap(f): @functools.wraps(f) def wrapper(self): f.func_globals['self'] = self return f() return wrapper def testdec(f): @functools.wraps(f) def wrapper(): return f() return wrapper class Test(object): __metaclass__ = TestMeta message = 'You can do anything in python' def test(): print self.message @testdec def test2(): print self.message + ' but the wrapper funcion can\'t take a self argument either or you get a TypeError' class Test2(object): message = 'It also works as a decorator but (to me at least) feels better as a metaclass' @wrap def test(): print self.message t = Test() t2 = Test2() t.test() t.test2() t2.test() |
这可能是装饰师的一个用例——你给他们一小套乐高积木来构建功能,而复杂的框架材料则通过
编辑:您没有发布任何代码,所以这将是粗略的,但他们不需要编写方法。他们可以在不使用"self"的情况下编写普通函数,您可以使用本文链接的文章中的装饰器:
1 2 3 4 5 6 7 8 9 10 11 12 | class myDecorator(object): def __init__(self, f): print"inside myDecorator.__init__()" f() # Prove that function definition has completed def __call__(self): print"inside myDecorator.__call__()" @myDecorator def aFunction(): print"inside aFunction()" |