如何在Python方法中获取self而不显式传递它

11 投票
5 回答
11757 浏览
提问于 2025-04-16 02:36

我正在开发一个文档测试框架,简单来说就是为PDF文件做单元测试。测试是框架中定义的类的实例方法,这些方法在运行时被找到并实例化,然后被调用来执行测试。

我的目标是减少那些编写测试的人需要关注的奇怪Python语法,因为这些人可能不是Python程序员,甚至可能根本就不是程序员。所以我希望他们能写“def foo():”而不是“def foo(self):”来定义方法,但仍然能够使用“self”来访问类的成员。

在普通程序中,我会觉得这是个糟糕的主意,但在像这样的特定领域语言的程序中,似乎值得一试。

我已经成功地通过使用装饰器去掉了方法签名中的self(实际上,由于我已经在测试案例中使用了装饰器,我只是把它合并到那个装饰器中),但这样一来,测试案例方法中的“self”就不再指向任何东西了。

我考虑过使用全局变量来代替self,甚至想出了一个大致可行的实现,但我更希望能尽量减少命名空间的污染,这就是我更倾向于直接将变量注入到测试案例方法的局部命名空间中的原因。你有什么想法吗?

5 个回答

5

这是对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')
6

我之前对这个问题的回答其实挺傻的,但那时候我刚开始学。现在我有了一个更好的方法。这种方法测试得不多,但可以用来演示正确的做法,虽然这个做法本身其实不太合适。这个方法在2.6.5版本上肯定能用。我没测试过其他版本,但里面没有硬编码的操作码,所以应该和大多数其他2.x版本的代码一样,能很方便地移植。

add_self可以作为一个装饰器使用,但那样就失去了意义(那还不如直接写'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')
4

这里有一个一行的函数装饰器,它似乎能完成任务,而且不会修改任何被标记为只读的可调用类型的特殊属性:

# 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

需要注意的是,除非你采取一些措施来防止,否则使用eval()函数可能会有一个副作用,就是它会自动在传给它的dict中添加一些条目,比如在__builtins__这个键下添加对__builtin__模块的引用。

@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

撰写回答