2

我正在尝试为 Python 编写一个冻结装饰器。

思路如下:

(针对两条评论)

我可能错了,但我认为测试用例有两个主要用途。

  • 一种是测试驱动开发:理想情况下,开发人员在编写代码之前先编写案例。它通常有助于定义架构,因为该规则强制在开发之前定义真实的接口。人们甚至可能会认为,在某些情况下,在开发人员之间分配工作的人正在编写测试用例并用它来有效地说明他所考虑的规范。我没有任何使用这种测试用例的经验。

  • 第二个想法是,所有规模不错的项目和几个程序员都遭受代码损坏的困扰。以前可以正常工作的东西可能会因为看起来像无辜重构的变化而被破坏。虽然架构不错,但组件之间的松散耦合可能有助于对抗这种现象;如果您编写了一些测试用例以确保没有任何东西会破坏程序的行为,那么您晚上会睡得更好。

然而,没有人可以否认编写测试用例的开销。在第一种情况下,人们可能会争辩说测试用例实际上是在指导开发,因此不应被视为开销。

坦率地说,我是一个非常年轻的程序员,如果我是你,我在这个主题上的话并不真正有价值......无论如何,我认为大多数公司/项目都不是这样工作的,并且主要使用单元测试在第二种情况下...

换句话说,它的目的不是确保程序正常工作,而是检查它将来是否会正常工作。

通过使用这种冻结装饰器,无需编写测试成本即可满足此需求。

假设你有一个函数

def pow(n,k):
    if n == 0:  return 1
    else:       return n * pow(n,k-1)

它非常好,您想将其重写为优化版本。这是一个大项目的一部分。您希望它为几个值返回相同的结果。与其经历测试用例的痛苦,不如使用某种冻结装饰器。

第一次运行装饰器时,装饰器使用定义的 args(低于 0 和 7)运行函数并将结果保存在映射中( f --> args --> result )

@freeze(2,0)
@freeze(1,3)
@freeze(3,5)
@freeze(0,0)
def pow(n,k):
    if n == 0:  return 1
    else:       return n * pow(n,k-1)

下次执行程序时,装饰器将加载此映射并检查此函数对这些 args 的结果是否未更改。

我已经很快写了装饰器(见下文),但伤害了一些我需要你建议的问题......

from __future__ import with_statement
from collections import defaultdict
from types import GeneratorType
import cPickle

def __id_from_function(f):
    return ".".join([f.__module__, f.__name__])

def generator_firsts(g, N=100):
    try:
        if N==0:
            return []
        else:
            return  [g.next()] + generator_firsts(g, N-1)
    except StopIteration :
        return []

def __post_process(v):
    specialized_postprocess = [
        (GeneratorType, generator_firsts),
        (Exception,     str),
    ]
    try:
        val_mro = v.__class__.mro()
        for ( ancestor, specialized ) in specialized_postprocess:
            if ancestor in val_mro:
                return specialized(v)
        raise ""
    except:
        print "Cannot accept this as a value"
        return None

def __eval_function(f):
    def aux(args, kargs):
        try:
            return ( True, __post_process( f(*args, **kargs) ) )
        except Exception, e:
            return ( False, __post_process(e) )
    return aux

def __compare_behavior(f, past_records):
    for (args, kargs, result) in past_records:
        assert __eval_function(f)(args,kargs) == result

def __record_behavior(f, past_records, args, kargs):
    registered_args = [ (a, k) for (a, k, r) in past_records ]
    if (args, kargs) not  in registered_args:
        res = __eval_function(f)(args, kargs)
        past_records.append( (args, kargs, res) )

def __open_frz():
    try:
        with open(".frz", "r") as __open_frz:
            return cPickle.load(__open_frz)
    except:
        return defaultdict(list)

def __save_frz(past_records):
    with open(".frz", "w") as __open_frz:
        return cPickle.dump(past_records, __open_frz)


def freeze_behavior(*args, **kvargs):
    def freeze_decorator(f):
        past_records = __open_frz()
        f_id = __id_from_function(f)
        f_past_records = past_records[f_id]
        __compare_behavior(f, f_past_records)
        __record_behavior(f, f_past_records, args, kvargs)
        __save_frz(past_records)
        return f
    return freeze_decorator
  • 结果的转储和比较对于所有类型来说都不是微不足道的。现在我正在考虑使用一个函数(我在这里称之为后处理)来解决这个问题。基本上,我不是存储 res,而是存储 postprocess(res),然后比较 postprocess(res1)==postprocess(res2),而不是比较 res1 res2。让用户重载预定义的后处理函数很重要。我的第一个问题是: 你知道检查对象是否可转储的方法吗?

  • 为修饰的函数定义一个键是一件痛苦的事情。在以下片段中,我使用了功能模块及其名称。** 你能想出一个更聪明的方法来做到这一点。**

  • 下面的片段有点工作,但在测试和录制时打开和关闭文件。这只是一个愚蠢的原型......但是你知道打开文件,处理所有功能的装饰器,关闭文件的好方法......

  • 我打算为此添加一些功能。例如,添加定义一个可迭代对象以浏览一组参数、记录实际使用的参数等的可能性。您为什么期望这样的装饰器?

  • 一般来说,你会使用这样的功能,知道它的局限性......尤其是在尝试将它与 POO 一起使用时?

4

2 回答 2

3

“一般来说,你会使用这样的功能,知道它的局限性......?”

坦率地说——从来没有。

在任何情况下,我都不会以这种方式“冻结”函数的结果。

用例似乎基于两个错误的想法:(1)单元测试要么困难,要么复杂或昂贵;(2) 编写代码可能更简单,“冻结”结果并以某种方式使用冻结的结果进行重构。这没有帮助。事实上,冻结错误答案的真实可能性使这成为一个坏主意。

首先,关于“一致性与正确性”。使用简单的映射比使用一组复杂的装饰器更容易保存。

这样做而不是编写冻结装饰器。

print "frozen_f=", dict( (i,f(i)) for i in range(100) )

创建的字典对象将作为冻结结果集完美运行。没有装饰师。没有复杂性可言。

第二,关于“单元测试”。

单元测试的重点不是“冻结”一些随机结果。单元测试的重点是将真实结果与另一个开发的结果进行比较(更简单、更明显、性能不佳的方式)。通常单元测试会比较手工开发的结果。其他时候,单元测试使用明显但非常慢的算法来产生一些关键结果。

拥有测试数据的意义不在于它是“冻结”的结果。拥有测试数据的意义在于它是一个独立的结果。以不同的方式完成——有时由不同的人——确认该功能有效。

对不起。在我看来,这不是一个好主意。看起来它颠覆了单元测试的意图。


“但是,没有人可以否认编写测试用例的开销”

实际上,许多人会否认“开销”。在浪费时间和精力的意义上,这不是“开销”。对于我们中的一些人来说,单元测试是必不可少的。没有它们,代码可能会起作用,但只是偶然。有了他们,我们有充分的证据表明它确实有效;以及它适用的具体情况。

于 2009-03-17T15:04:36.007 回答
0

您是否希望实现不变量或后置条件?

您应该明确指定结果,这将消除大多数问题。

于 2009-03-17T14:59:44.893 回答