【发布时间】:2010-10-13 19:38:21
【问题描述】:
我正在尝试为 Python 编写冻结装饰器。
思路如下:
(回应两位cmets)
我可能错了,但我认为有两个主要用途 测试用例。
一是测试驱动开发: 理想情况下,开发人员在编写代码之前先编写案例。 它通常有助于定义架构,因为这门学科 强制在开发之前定义真正的接口。 人们甚至可以认为,在某些情况下, 在开发人员之间分派工作是编写测试用例和 用它来有效地说明他心目中的规范。 我没有任何使用这种测试用例的经验。
第二个是所有项目都有一个像样的想法 大小和几个程序员正在遭受损坏的代码。 过去可以工作的东西可能会因更改而损坏 这看起来像是一个无辜的重构。 虽然架构很好,但组件之间的松散耦合可能 有助于对抗这种现象;你会睡得更好 晚上如果你写了一些测试用例来确保 没有什么会破坏你的程序的行为。
然而, 没有人可以否认编写测试用例的开销。在里面 第一种情况可能会争辩说测试用例实际上是指导 开发,因此不应被视为开销。
坦率地说,我是一个非常年轻的程序员,如果我是 你,我在这个问题上的话并不真正有价值...... 无论如何,我认为大多数公司/项目都行不通 像这样,并且单元测试主要用于第二个 案例...
换句话说,而不是确保程序是 工作正常,它的目的是检查它是否会 以后也一样。
无需编写测试成本即可满足此需求, 通过使用这个冷冻装饰器。
假设你有一个函数
def pow(n,k):
if n == 0: return 1
else: return n * pow(n,k-1)
非常好,您想将其重写为优化版本。 这是一个大项目的一部分。您希望它返回相同的结果 为了一些价值。 与其经历测试用例的痛苦,不如使用一些 一种冷冻装饰器。
第一次运行装饰器时, 装饰器使用定义的参数运行函数(低于 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。 让用户重载预定义的后处理函数很重要。 我的第一个问题是: 您知道检查对象是否可转储的方法吗?
为修饰的函数定义一个键是一件很痛苦的事。在下面的sn-ps 我正在使用功能模块及其名称。 ** 你能想出一个更聪明的方法来做到这一点。 **
下面的 sn-ps 有点工作,但在测试和录制时打开和关闭文件。这只是一个愚蠢的原型...但是您知道打开文件、处理所有功能的装饰器、关闭文件的好方法...
我打算为此添加一些功能。例如,添加定义的可能性 用于浏览一组参数、记录实际使用的参数等的迭代。 你为什么会期待这样的装饰师?
一般来说,你会使用这样的功能吗,知道它的局限性……尤其是在尝试将它与 POO 一起使用时?
【问题讨论】:
-
你应该详细说明为什么;不清楚为什么你会想要这个而不是单元测试。 -- 另外,当函数故意改变时更新 .frz 文件听起来很痛苦。
-
"一般来说,你会使用这样的功能吗...?"绝不。对不起,但这个描述对我来说毫无意义。用例是模糊的。您只是在文件中腌制函数结果吗?
-
感谢您的 cmets!让我在主帖中回答。
-
@Deestan : 关于改变,确实很痛苦!!!我还不知道如何让用户简单地“解冻”界面。 (不过,我可以看到一两种方法),如果您对此有任何聪明的想法,我也想听听。
标签: python testing decorator freeze