Python中的声明性验证

2021-05-05 20:53:32

在过去的两年里,我对Pureescript和Haskell都会合理舒适。我学到了许多新的东西,同时进入纯粹的功能编程生态系统,并且许多这是其他范例可以应用于其他范式。不幸的是,纯粹的FP Worldcan感觉有点像另一个维度 - 许多编程问题都有散开解决方案,但“常规”编程的世界不了解素图。

一种这样的模式被称为“适用式验证”,但我将简单地调用“声明性验证”。在这篇文章中,我将为使用这项技术提供一些动机,然后在Python实现这些想法的一个小图书馆。

我们的许多程序接受用户的输入。我们通常需要在继续处理之前验证此单一,而在错误的情况下,请通知用户任何问题。有几种用于执行此类无验证的技术,但最常见的是写一些遍布输入的命令代码,并建立错误列表。如果没有错误,则提供的iny有效,否则它不是。我们可以在一个指示验证是否成功的对象中包装我们的validationReturn。

@ DataClass类有效:value:任何def is_valid():返回true @ dataclass类无效:value:def is_valid():返回false def validate_name(名称,错误):如果不是isinstance(名称,str)或name == "" :错误。附加("名称必须是非空字符串")def validate_age(年龄,错误):如果不是isinstance(年龄,int):错误。附加("年龄必须是int")elif年龄< 10:错误。附加("年龄必须至少10")def验证(数据):errors = [] validate_name(数据。获取("名称"),错误)validate_age(数据。get( "年龄"),错误)如果不是错误:返回有效(数据)else:返回无效(错误)

虽然这种方法有效,但是当我们有验证依赖于之前的结果时,事情会变得复杂。例如,我们要添加一个新的,更复杂的规则,说明如果名称绘制,则年龄必须至少为40.为了做到这一点,才需要出现姓名和年龄并有适当的类型。但是,我们没有方便的方法来“重用”此逻辑的现有validate_name和validate_age函数。一种方法是在本地重新检查,如果Qoypes不正确,已经添加了错误。

def validate_drew(数据,错误):if(不是isinstance(数据。获得("名称"),str)或不isinstance("年龄"),int)) :返回Elif数据。得到("名称")==" drew"和数据。得到("年龄")< 40:错误。附录("德鲁必须老")

这不是很大的,因为现在我们在Twoplaces中重复了实例检查。我们也可以确保错误列表中不存在特定错误,但这会将此验证耦合到以前过验证中暴露的错误。

可以使用“解析”方法来克服有状态验证方法的缺陷。也就是说,我们声明性地描述了我们预期并返回错误,如果我们的数据不符合TheSeppectations,则返回错误的形状和类型。在Parse后,这种方法在解析后有很好的记录,不验证。倒进是一个很好的状态验证的替代方案,但这种风格的桨式(通常称为Monadic解析)确实有一个缺点 - 它一旦达到第一个错误就会停止处理。我们希望在我们用户的无效输入上收集尽可能多的信息。

我们可以采取另一种方法,使我们提供了ParsingApproach的可融合性以及有状态方法的错误积累。这个人传统上称为“适用式验证”。

我们将提供两个主要功能以及我们现有的有效和无效类型。

validate_into允许我们调用提供的函数,其中包含Otguments列表,假设所有参数都有效。否则,它累积在任何无效参数中的错误。

And_then允许我们假设函数的验证的验证的另一个“阶段”是有效的。如果函数的主题无效,我们什么都不做。

您可以将validate_into视为构建验证普通和验证的一个“阶段”,并将两个阶段连接在一起。 Astage内的任何验证都会累积其错误,但如果阶段失败,我们将无法为任何较小的阶段运行。这意味着当给定阶段取决于不适治阶段的有效值时,我们应该仅将Outvalidations分解为阶段。

让我们使用这两个功能从上面重新实现我们的验证。首先,我们将定义一个人类,我们将在其中放置有效数据。

def validate_name(名称):如果不是isinstance(姓名,str)或name =="" :返回无效(["名称必须是非空字符串"])else:返回有效(姓名)def validate_age(年龄):如果不是isinstance(年龄,int):返回无效([&# 34;年龄必须是整数"])Elif年龄< 10:返回无效(["年龄必须至少为10"])else:返回有效(年龄)def validate_drew(person):如果是人。 name ==" drew"和人。年龄< 40:返回无效([" draw旧"])else:返回有效(人)def验证(数据):return validate_into(person,validate_name(数据。get("姓名" )),validate_age(数据。得到("年龄")),)。 And_then(validate_drew)

这里有几件事要注意到。首先,每个验证函数都进行隔离。其次,没有输入数据的突变发生。每个都函数验证,然后返回有效或无效值。最后,请注意,每个无效返回错误列表。这允许我们的绯意发生。

验证({"姓名":none,"年龄":"你好",})#=>无效(value = [#'名称必须是非空字符串',#'年龄必须是整数'])验证({"姓名":& #34; drew","年龄":38,})#=>无效(value = [' drew旧的'])验证({"姓名":"简","年龄":38,} )#=>有效(价值=人(名称=' Jane',年龄= 38))

请注意,我们的验证第二阶段,即vightate_drew,Canassume所有输入都在第一阶段后有效。因此,我们并不确保重新检查任何关于我们具体验证的名称或年龄类型的内容(Drew需要旧的)。如果我们为PersonConstructor添加了新参数,请注意添加新验证的效力如何是多么难看。

我们可能会认为,支持此代码的库会非常复杂。在练习中,它非常简单。我们使用的标准Library之外的唯一功能是来自工具库库的咖喱,但如果我们想删除依赖,我们可以自己重新落实咖喱。

从DataClasses从Functools导入DataClass从Toolz导入Curry从键入导入键键入导入任何@ dataclass类有效:value:任何def is_valid(self):返回true def应用(self,其他):如果是另一个。 is_valid():返回有效(self。值(其他值))else:返回其他def和_then(self,f):返回f(self.value)@ dataclass类无效:value:任何def is_valid(self):返回假def申请(自我,其他):如果是的话。 IS_VALID():返回自我否则:返回无效(SELE。值+其他。值)def和_then(self,f):返回自我def validate_into(f,* args):返回减少(lambda a,b:a。申请) b),args,有效(咖喱(f)))

上面的代码是我们的图书馆的整体。 AND_THEN函数呈呈态度直截了当。如果我们尝试将验证的新阶段连锁有效值,我们简单地调用提供的功能,其中有效的价值占据了您的有效价值。如果我们尝试将验证的新阶段链接到无效值,我们只是忽略提供的功能并返回自我。

validate_into函数感觉更复杂,因此让我们描述一步一步的ITIS。首先,我们咖喱提供的功能。这不一致,因为我们将在Timeas上应用一个参数,我们确定每个参数是否有效。我们还将此咖喱函数介绍了一个有效的包装器,因为它在查看AnyGuments之前在有效状态下开始。然后,一个接一个地,我们将下一个参数应用于我们的“函数sofar”。在该参数有效和“到目前为止”是isvalid的情况下,我们只是用参数调用函数并重新包装它在有效状态。如果“到目前为止”是有效的,但新参数无效,我们makethe新的“函数到目前为止”无效结果。最后,重要的是,如果“函数到目前为止”已经无效,我们提供了一个新的InvalidArgument,我们会连接错误并重新包装导致无效。

使用这些简单的工具,我们可以编写复杂的深刻嵌套的验证器。我们的验证器的灵活性很简单,因为它们只不过是功能。 WeCan将它们放在一个包中,并在我们的代码库中轻松分享常用的验证器(思考validate_presence)。

这篇文章中的任何内容都是新的。我正在重新实现来自许多其他Ecosystemsin Python的想法,使它们更加平衡。申请风格验证是纯粹的功能编程世界的大量想法,即在更多主流语言中更广泛的认可和采用。