代码之家  ›  专栏  ›  技术社区  ›  kindall

如何让self进入Python方法而不显式接受它

  •  8
  • kindall  · 技术社区  · 14 年前

    我正在开发一个文档测试框架——基本上是针对PDF的单元测试。测试是由框架定义的类实例的(修饰的)方法,这些方法在运行时被定位和实例化,并且调用这些方法来执行测试。

    我的目标是减少编写测试的人需要关注的奇怪的Python语法的数量,因为这些人可能是Python程序员,也可能不是,甚至完全是程序员。因此,我希望他们能够为方法编写“def foo():”而不是“def foo(self):”,但仍然能够使用“self”访问成员。

    在一个普通的程序中,我会认为这是一个可怕的想法,但在一个特定领域的语言类程序中,像这样的一个,它似乎值得一试。

    我已经通过使用decorator成功地从方法签名中消除了self(实际上,因为我已经在测试用例中使用了decorator,所以我只想将它卷到其中),但是“self”并不引用测试用例方法中的任何东西。

    5 回复  |  直到 14 年前
        1
  •  4
  •   martineau    13 年前

    这里有一个单行方法修饰符,它似乎不需要修改任何 Special attributes of Callable types* marked Read-only :

    # 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
    

    side-effect eval() 函数可能是它添加了一些条目——比如对 __builtin__ 钥匙下的模块 __builtins__ dict 传递给它。

    @kendall:根据您关于如何在容器类中使用这个方法的评论(但目前忽略了额外变量的注入)--下面的内容与您所做的类似吗?对于我来说,很难理解框架和用户编写的东西是如何分开的。我觉得这是一个有趣的设计模式。

    # 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
    
        2
  •  6
  •   aaronasterling    14 年前

    add_self

    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')
    
        3
  •  5
  •   Odomontois    14 年前

    aaronasterling的解决方案几乎没有升级(我没有足够的声誉来评论它):

    def wrap(f):
        @functools.wraps(f)
        def wrapper(self,*arg,**kw):
            f.func_globals['self'] = self        
            return f(*arg,**kw)
        return wrapper
    

    但这两种解决方案都无法预测,如果f函数将针对不同的实例递归调用,那么您必须像这样克隆它:

    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')
    
        4
  •  3
  •   aaronasterling    14 年前

    f.func_globals . 这在python2.6中有效。我真的应该去安装其他版本来测试像这样的东西。对不起,代码墙,但我涉及两种情况:使用元类和装饰器。对于您的用例,我认为元类更好,因为这个练习的重点是保护用户不受语法的影响。

    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()
    
        5
  •  2
  •   chryss    14 年前

    这可能是 decorators -你给他们一小套乐高积木来构建函数,复杂的框架材料通过管道输入 @testcase 或者类似的。

    编辑: 您没有发布任何代码,所以这将是粗略的,但他们不需要编写方法。它们可以编写没有“self”的普通函数,您可以使用decorator,如我链接的文章中的示例所示:

    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()"