关于变量:如何在不显式接受Python方法的情况下使自己进入该方法

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代码一样可移植。

add_self可以用作装饰,但这会破坏目的(为什么不只是键入'self'?)可以很容易地从我的另一个答案中调整元类来应用这个函数。

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

请注意,除非您采取预防措施,否则eval()功能的一个副作用可能是它会自动添加一些条目,例如对传递给它的dict的键__builtins__下的__builtin__模块的引用。

@肯德尔:根据你对如何在容器类中使用方法的评论(但暂时忽略了额外变量的注入),下面是你正在做的事情吗?对于我来说,很难理解框架和用户所写内容之间是如何划分的。我觉得这是个有趣的设计模式。

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


诀窍是在f.func_globals中添加"self"。这在python2.6中有效。我真的应该开始安装其他版本来测试类似的东西。对于代码墙感到抱歉,但我涉及两种情况:使用元类执行和使用装饰器执行。对于您的用例,我认为元类更好,因为本练习的重点是保护用户不受语法影响。

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


这可能是装饰师的一个用例——你给他们一小套乐高积木来构建功能,而复杂的框架材料则通过@testcase或其他类似的方式导入。

编辑:您没有发布任何代码,所以这将是粗略的,但他们不需要编写方法。他们可以在不使用"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()"