如何在不明确接受的情况下让 self 进入 Python 方法

发布于 2024-09-13 23:05:12 字数 486 浏览 3 评论 0原文

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

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

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

我已经通过使用装饰器成功地从方法签名中消除了 self(实际上,因为我已经在测试用例中使用了装饰器,所以我只是将其卷入其中),但是“self”并没有引用方法签名中的任何内容。测试用例方法。

我考虑过使用全局的 self,甚至想出了一个或多或少有效的实现,但我宁愿污染尽可能小的命名空间,这就是为什么我更愿意将变量直接注入到测试用例方法的本地命名空间。有什么想法吗?

I'm developing a documentation testing framework -- basically unit tests for PDFs. Tests are (decorated) methods of instances of classes defined by the framework, and these are located and instantiated at runtime and the methods are invoked to execute the tests.

My goal is to cut down on the amount of quirky Python syntax that the people who will write tests need to be concerned about, as these people may or may not be Python programmers, or even very much programmers at all. So I would like them to be able to write "def foo():" instead of "def foo(self):" for methods, but still be able to use "self" to access members.

In an ordinary program I would consider this a horrible idea, but in a domain-specific-languagey kind of program like this one, it seems worth a try.

I have successfully eliminated the self from the method signature by using a decorator (actually, since I am using a decorator already for the test cases, I would just roll it into that), but "self" does not then refer to anything in the test case method.

I have considered using a global for self, and even come up with an implementation that more or less works, but I'd rather pollute the smallest namespace possible, which is why I would prefer to inject the variable directly into the test case method's local namespace. Any thoughts?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

望笑 2024-09-20 23:05:12

我对这个问题的接受答案非常愚蠢,但我才刚刚开始。这是一个更好的方法。虽然这只是经过了很少的测试,但它有助于演示执行不正确操作的正确方法。它肯定可以在 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')

My accepted answer to this question was pretty dumb but I was just starting out. Here's a much better way. This is only scantily tested but it's good for a demonstration of the proper way to do this thing which is improper to do. It works on 2.6.5 for sure. I haven't tested any other versions but no opcodes are hardcoded into it so it should be about as portable as most other 2.x code.

add_self can be applied as a decorator but that would defeat the purpose (why not just type 'self'?) It would be easy to adapt the metaclass from my other answer to apply this function instead.

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')
梦在深巷 2024-09-20 23:05:12

这是一个单行方法装饰器,它似乎可以在不修改任何 可调用类型的特殊属性*标记为只读

# 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

请注意,除非您采取预防措施来防止它,否则 副作用 可能是它添加了一些条目 - 例如对 的引用>__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

Here's a one line method decorator that seems to do the job without modifying any 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

Note that unless you take precautions to prevent it, a side-effect of the eval() function may be it adding a few entries -- such as a reference to the __builtin__ module under the key __builtins__ -- automatically to the dict passed to it.

@kendall: Per your comment about how you're using this with methods being in container classes (but ignoring the injection of additional variables for the moment) -- is the following something like what you're doing? It's difficult for me to understand how things are split up between the framework and what the users write. It sounds like an interesting design pattern to me.

# 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
葬﹪忆之殇 2024-09-20 23:05:12

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

little upgrade for aaronasterling's solution( i haven't enough reputation to comment it ):

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

but both this solutions will work unpredictable if f function will be called recursively for different instance, so you have to clone it like this:

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')
執念 2024-09-20 23:05:12

诀窍是将“self”添加到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()

The trick is to add 'self' to f.func_globals. This works in python2.6. I really should get around to installing other versions to test stuff like this on. Sorry for the wall of code but I cover two cases: doing it with a metaclass and doing it with a decorator. For your usecase, I think the metaclass is better since the whole point of this exercise is to shield users from syntax.

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()
如此安好 2024-09-20 23:05:12

这可能是 装饰器 的一个用例 - 你给他们一个小的一组用来构建函数的乐高积木,复杂的框架内容通过 @testcase 或类似的东西通过管道传输。

编辑:您没有发布任何代码,因此这将是粗略的,但他们不需要编写方法。他们可以编写没有“self”的普通函数,并且您可以使用装饰器,就像我链接的文章中的示例一样:

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

This might be a use case for decorators - you give them a small set of lego bricks to build functions with, and the complicated framework stuff is piped in via @testcase or somesuch.

Edit: You didn't post any code, so this is going to be sketchy, but they don't need to write methods. They can write ordinary functions without "self", and you could use decorators like in this example from the article I linked:

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()"
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文