9

私はドキュメント テスト フレームワークを開発しています。基本的には、PDF の単体テストです。テストは、フレームワークによって定義されたクラスのインスタンスの (装飾された) メソッドであり、これらは実行時に配置およびインスタンス化され、テストを実行するためにメソッドが呼び出されます。

私の目標は、テストを作成する人々が心配する必要がある風変わりな Python 構文の量を削減することです。これらの人々は Python プログラマーである場合もそうでない場合もあり、まったくプログラマーである場合もあります。したがって、メソッドに対して「def foo(self):」の代わりに「def foo():」を記述できるようにしたいと考えていますが、「self」を使用してメンバーにアクセスできるようにしたいと考えています。

通常のプログラムでは、これはひどい考えだと思いますが、このようなドメイン固有言語の種類のプログラムでは、試してみる価値があるようです。

デコレータを使用してメソッド シグネチャから self を削除することに成功しました (実際には、テスト ケースに既にデコレータを使用しているため、デコレータにロールインするだけです)。テストケースメソッド。

私は自分自身にグローバルを使用することを検討し、多かれ少なかれ機能する実装を考え出しましたが、可能な限り最小の名前空間を汚染したいので、変数をテストケースメソッドのローカルに直接挿入することを好みます名前空間。何かご意見は?

4

5 に答える 5

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')
于 2010-08-10T22:42:02.110 に答える
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')
于 2010-08-11T01:28:12.427 に答える
4

以下は、 Read-only とマークされた Callable types* の Special 属性を変更せずにジョブを実行するように見える 1 行のメソッド デコレーターです。

# 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()への参照など、いくつかのエントリが渡されたものに自動的に追加される可能性があることに注意してください。__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
于 2010-11-11T00:22:55.210 に答える
3

トリックは、 に「self」を追加することf.func_globalsです。これはpython2.6で動作します。このようなものをテストするには、他のバージョンをインストールする必要があります。コードの壁で申し訳ありませんが、メタクラスで行う場合とデコレータで行う場合の 2 つのケースを取り上げます。あなたのユースケースでは、この演習の全体的なポイントはユーザーを構文から保護することであるため、メタクラスの方が優れていると思います。

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()
于 2010-08-11T00:32:28.757 に答える
2

これは、デコレータのユースケースかもしれません。機能を構築するためのレゴブロックの小さなセットをデコレータ@testcaseに提供し、複雑なフレームワークのものは、またはそのようなものを介してパイプされます。

編集:コードを投稿していないため、大雑把になりますが、メソッドを記述する必要はありません。彼らは「自己」なしで通常の関数を書くことができ、私がリンクした記事のこの例のようにデコレータを使うことができます:

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()"
于 2010-08-10T22:39:56.023 に答える