捍卫Pyramid的设计

不时地,挑战各个方面 Pyramid 设计已提交。为了给后面的讨论提供背景,我们在这里详细介绍了一些设计决策和权衡。在某些情况下,我们承认该框架可以做得更好,并且我们描述了未来将采取哪些步骤来改进它。在其他人中,我们只是按照说明提交挑战,显然你不能总是取悦所有人。

Pyramid提供了多种方法

Python 流行文化的一个经典是“tioowtdi”(“只有一种方法”,一种轻蔑的、不加掩饰的引用Perl的“timtowndi”,它是“有多种方法可以做到这一点”的首字母缩写)。

Pyramid 无论好坏,都是一个“timtowtdi”系统。例如,它包含多个将URL解析为 view callable 通过 url dispatchtraversal . 存在多种配置方法: imperative configurationconfiguration decorationZCML (可选地通过 pyramid_zcml )它适用于多种不同类型的持久性和模板化系统。等等。然而,这些重叠的做事方式的存在并不是没有理由和目的的:我们有许多受众要服务,我们相信Timtowtdi实际上是在Web框架级别 防止 在PythonWebCommunity的更高级别上,这是一组更加阴险和有害的重复。

Pyramid 开始的时候 repoze.bfg 由一个拥有多年历史的团队撰写 Zope 经验。理念 traversal 还有路 view lookup 作品完全是从Zope偷来的。授权子系统由 Pyramid 是Zope的派生。应用程序可以 扩展的 没有复刻也是一个zope衍生物。

这些功能的实现是 必修的 允许 Pyramid 作者以他们习惯的方式为客户构建面包和黄油CMS型系统。除了Zope本身之外,没有其他系统具有这样的特性,而且Zope本身也开始显示出它的时代迹象。我们被它早期设计错误的后果所阻碍。Zope缺乏文档也很难解决。很难雇用聪明的人来处理Zope应用程序,因为没有一个全面的文档集可以在一个可消费的地方解释“一切”,而且它太大,并且自我矛盾,无法正确地记录。以前 repoze.bfg 在开发过程中,它的作者显然四处寻找适合该法案的其他框架。但非Zope框架没有。所以我们开始建造 repoze.bfg .

然而,我们的研究结果表明,尽管事实上 one 框架具有我们所需要的所有特性,许多现有的框架都有很好的想法,有时非常引人注目。特别地, URL dispatch 是将URL映射到代码的更直接的机制。

因此,尽管我们找不到一个框架,除了Zope,它符合我们的需求,而且当我们将许多Zope思想融入到BFG中时,我们也模拟了我们在其他框架(如 url dispatch )在bfg的首次公开发布之后,随着时间的推移,添加了一些功能来支持对系统中的各种zope ism过敏的人,例如使用 imperative configurationconfiguration decoration ,而不是单独使用 ZCML 以及取消 interface 物体。很快我们就清楚我们有一个非常通用的系统,并且开始吸引非Zope用户以及前Zope用户。

在推广的结果中,BFG与塔架1的特征集共享了90%的特征集,因此具有非常相似的目标市场。因为它们非常相似,所以在两个系统之间进行选择对于一个没有党派偏见的开发人员来说是一种挫折。考虑到这两个框架的相似性,塔架和BFG开发社区也很奇怪能与同一组用户竞争。因此,塔架和BFG小组开始合作,形成合并计划。BFG缺少的功能(尤其是 view handler 增加了类、flash消息传递和其他一些小的缺失位,以使前挂架用户更加熟悉。结果是 Pyramid .

python web框架空间目前被臭名昭著地割据了。我们真的希望 Pyramid 将吸引至少两个目前非常独特的用户:塔架和BFG用户。通过将塔架和BFG的最佳概念统一到一个单一的代码库中,并将其祖先的不良概念抛在脑后,我们将能够更好地巩固我们的努力,共享更多的代码,并将我们的努力作为一个整体而不是毫无意义地进行竞争。我们希望能够缩短包心态,从而导致 大得多 重复的工作,由相互竞争但极其相似的应用程序和库表示,每个应用程序和库都建立在一个与另一个不兼容的特定低级堆栈上。我们还将把可信的python web框架的选择减少至少一个。我们还希望通过提供所需的功能来吸引其他社区(如Zope和TurboGears)的用户,同时允许以熟悉的方式进行操作。实现这些目标的一些功能重叠是可以预料的,也是不可避免的,至少如果我们的目标是在更高的层次上防止无意义的重复。如果我们的工作做得足够好,不同的受众将能够共存和合作,而不是在想象中的网络框架DMZ中互相攻击。

Pyramid使用Zope组件体系结构(“ZCA”)注册表

Pyramid 使用A Zope Component Architecture (zca)“组件注册表”作为其 application registry 在引擎盖下面。这是一些参数点。 Pyramid 是一个 Zope 系谱,所以它的开发人员在它的开始使用ZCA注册表是很自然的。但是,我们理解使用ZCA注册表会带来问题和后果,我们已经尽力解决这些问题和后果。下面是一个关于 Pyramid ZCA注册表的使用及其使用涉及的权衡。

问题

可以用来访问ZCA组件注册表中数据的全局API不是特别漂亮或直观,有时它只是一个简单的钝器。同样,使用ZCA全局API的代码的临时源代码阅读器的概念负载也有点高。考虑一个ZCA新手读取执行典型“未命名实用程序”查找的代码,使用 zope.component.getUtility() 全球API:

1from pyramid.interfaces import ISettings
2from zope.component import getUtility
3settings = getUtility(ISettings)

运行此代码后, settings 将是一本 Python 字典。但任何平民都不可能仅仅通过阅读代码就知道这一点。上面的代码位有很多理解问题,这是显而易见的。

首先,“效用”是什么?好吧,就本次讨论而言,就上述代码而言,这并不重要。如果你真的想知道,你可以读 this . 然而,这种代码的读者仍然需要理解这个概念才能解析它。这是第一个问题。

第二,这是什么 ISettings 事情?这是一个 interface . 这很重要吗?不完全是这样,我们只是将它用作基于其标识作为标记的某些查找的键:它表示一个具有字典API的对象,但在这个上下文中并不十分重要。这是第二个问题。

第三,什么是 getUtility 功能呢?它正在查找 ISettings “实用程序”应该返回…嗯,一个实用程序。注意我们是如何建立起对 interface 以及“效用”的概念来回答这个问题:到目前为止的一个坏迹象。还要注意,答案是圆形的,a 真正地 坏征兆。

第四,在哪 getUtility 寻找数据?当然是“组件注册”。什么是组件注册表?问题4。

第五,假设你买了一个神奇的登记处 is 这个注册表? 人本 ……”“?”在这种情况下,这是最好的答案(更具体的答案需要了解内部的知识)。可以有多个注册表吗?对。所以在 哪一个 登记处找到登记了吗?当然是“当前”的注册。依据 Pyramid ,当前注册表是线程局部变量。使用查询本地线程的API可以理解它如何在非本地工作。

你现在接受了这样一个事实,那就是有一个注册中心就在附近。但是如何填充注册表呢?为什么,通过调用指令的代码 config.add_view . 然而,在这种特殊情况下, ISettings 由框架本身在引擎盖下生成:它不存在于任何用户配置中。这是非常难以理解的。问题6。

显然,这里有一些认知负载需要由代码的读者来承担,代码扩展了 Pyramid 框架由于使用了zca,即使他们已经是专业的python程序员和web应用领域的专家。这是次优。

改善

第一,主要改进: Pyramid 不希望应用程序开发人员理解ZCA概念或其任何API . 如果一个 应用 开发人员需要在创建 Pyramid 应用程序,我们在某个轴上失败了。

相反,框架隐藏了zca注册表在特殊用途的API函数后面的存在,这些函数 do 使用ZCA API。以 request.authenticated_userid 函数,返回当前请求中存在的用户ID或 None 如果当前请求中没有用户ID。应用程序开发人员这样称呼它:

1userid = request.authenticated_userid

他们现在有了当前的用户ID。

然而,在其领导下 authenticated_userid 是这样的:

 1def authenticated_userid(request):
 2    """ Return the userid of the currently authenticated user or
 3    ``None`` if there is no security policy in effect or there
 4    is no currently authenticated user. """
 5
 6    registry = request.registry # the ZCA component registry
 7    policy = registry.queryUtility(ISecurityPolicy)
 8    if policy is None:
 9        return None
10    return policy.authenticated_userid(request)

使用这样的包装器,我们总是努力向应用程序开发人员隐藏ZCA API。应用程序开发人员不应该知道ZCA API;他们应该调用一个包含一些与域相关的对象的python函数作为参数,并且它应该返回一个结果。下面的推论是,任何已使用 Pyramid 也不需要理解ZCA API。

向应用程序开发人员和代码阅读器隐藏ZCA API是增强域特定性的一种形式。任何应用程序开发人员都不需要了解Web框架如何完成其工作的小型、详细的机制。人们希望处理的概念更接近他们所工作的领域。例如,Web开发人员希望了解 用户 不是 公用事业 . Pyramid 将zca用作实现细节,而不是向最终用户公开的特性。

但是,与应用程序开发人员不同, 框架开发人员 ,包括想要覆盖的人 Pyramid 通过预先预订的框架插件(如遍历或视图查找)提供功能, must 了解ZCA注册表API。

Pyramid 框架开发人员非常关注zca注册表API的概念性加载问题,因此 replacement registry implementation 已命名 repoze.component 实际上是开发出来的。虽然这个包有一个完全功能和测试良好的注册表实现,并且它的API比ZCA注册表API好得多,但是对它的工作很大程度上被放弃了,并且它没有在 Pyramid . 我们继续使用ZCA注册表 Pyramid 因为它最终证明更适合。

备注

我们继续使用zca注册表,而不是放弃它,而是在 repoze.component 很大程度上是因为接口的zca概念提供了接口层次结构的使用,这在许多场景(如上下文类型继承)中都很有用。想到一个标记类型,类似于一个允许此功能的界面,似乎它只是在重新设计轮子。

让框架开发人员和扩展器理解ZCA注册表API是一种权衡。我们( Pyramid 开发人员)喜欢ZCA注册中心提供的特性,我们很久以前就承担了理解它做什么和如何工作的重任。作者 Pyramid 深入理解ZCA,并能像阅读任何其他代码一样轻松地阅读使用它的代码。

但我们认识到,可能希望扩展框架的开发人员对ZCA注册表API的使用不如最初的开发人员那么熟悉。所以为了善待第三方 Pyramid 框架开发人员,我们在沙地上画了一些线。

在所有核心代码中,我们都使用了zca全局API函数,例如 zope.component.getUtilityzope.component.getAdapter ,异常而不是规则。因此,而不是:

1from pyramid.interfaces import ISecurityPolicy
2from zope.component import getUtility
3policy = getUtility(ISecurityPolicy)

Pyramid 代码通常用于:

1from pyramid.interfaces import ISecurityPolicy
2from pyramid.threadlocal import get_current_registry
3registry = get_current_registry()
4policy = registry.getUtility(ISecurityPolicy)

虽然后者更为冗长,但也可以说它使事情变得更加明显。所有的 Pyramid 核心代码使用此模式,而不是ZCA全局API。

理论基础

以下是有关 Pyramid 决定使用ZCA注册表:

  • 历史。回答这个问题的一个重要部分是“历史”。大部分的设计 Pyramid 直接从 Zope . Zope使用ZCA注册表来执行一些技巧。 Pyramid 模仿这些技巧,并且,因为zca注册表对这组技巧很有效, Pyramid 用于同样的目的。例如, Pyramid 地图A request 到A view callable 使用 traversal 几乎完全是从Zope中提取的。ZCA注册表在如何完成此视图映射请求的详细信息中起着重要作用。

  • 特征。ZCA组件注册表本质上提供了可以被视为类似于超级字典的东西,它允许进行比基于单个键检索值更复杂的查找。其中一些查找功能对最终用户非常有用,例如能够注册仅在上下文是某类对象或上下文实现某些对象时才找到的视图 interface .

  • 奇点。只有一个地方“应用程序配置”位于 Pyramid 应用程序:在组件注册表中。组件注册表根据配置回答运行时框架向其提出的问题。 应用程序 . 注:“一个应用程序”与“一个进程”不同;多个独立配置的副本相同 Pyramid 应用程序能够在同一进程空间中运行。

  • 可组合性。可以强制填充ZCA组件注册表,也可以使用现有机制通过使用配置文件(zcml,通过可选的 pyramid_zcml 包装)。我们不需要从头开始编写前端来利用配置文件驱动的注册表填充。

  • 可插拔性。使用zca注册表允许通过一个定义良好且被广泛理解的插件体系结构进行框架扩展。只要框架开发人员和扩展人员了解ZCA注册表,就可以扩展 Pyramid 几乎是武断的。例如,构建一个同时注册多个视图的指令相对容易,允许应用程序开发人员在编写的代码中使用该指令作为“宏”。这在某种程度上是一个区别于其他(非Zope)框架的特性。

  • 可测试性。在框架代码中明智地使用ZCA注册表使测试该代码稍微容易一些。我们不使用MonkeyPatching或其他工具来注册模拟对象进行测试,而是通过ZCA注册注入依赖项,然后在代码中使用查找来查找模拟对象。

  • 速度。对于一组特定的复杂查找方案,zca注册表非常快速 Pyramid 用途,经过多年优化,仅用于这些目的。ZCA注册表包含用于此目的的可选C代码,显然没有(或很少)错误。

  • 生态系统。许多现有的Zope包可以用于 Pyramid 由于我们使用了ZCA注册表,几乎没有(或没有)更改。

结论

如果你只 开发应用程序 使用 Pyramid 这里没什么可抱怨的。您永远不需要了解zca注册表API;使用文档化的 Pyramid 代替API。但是,您可能是一个不阅读API文档的应用程序开发人员。相反,您阅读原始源代码,并且因为您没有阅读API文档,所以您甚至不知道什么函数、类和方法 form 这个 Pyramid 应用程序编程接口。因此,您现在已经编写了使用内部构件的代码,并且已经将自己画到了概念的角落,需要使用实现细节与一些ZCA进行斗争。如果这是你,很难有很多同情你。您要么需要熟悉我们如何使用ZCA注册表,要么只需要使用文档化的API;这就是我们将它们作为API进行文档化的原因。

如果你 延伸发展 Pyramid (创建新的指令,使用一些更模糊的Hook,如中所述 使用钩子 或在 Pyramid 核心代码),您将面临需要了解至少一些ZCA概念。在某些地方,它被毫不掩饰地使用,并将永远存在。我们知道这很奇怪,但是如果你花时间去读一些关于它的文章,这也是很有用的,也是可以理解的。

Pyramid“鼓励使用zcml”

ZCML 是一种配置语言,可用于配置 Zope Component Architecture 注册表 Pyramid 用于应用程序配置。人们常常声称Pyramid“需要zcml”。

它不在。 Pyramid 1.0,zcml不作为核心的一部分发货;而是在 pyramid_zcml 附加包,完全可选。完全不需要ZCML来使用 Pyramid 也不是任何其他类型的框架声明性前端到应用程序配置。

Pyramid是横穿的,我不喜欢横穿

Pyramidtraversal 是将URL路径解析为 resource 资源树中的对象。有些人对这个概念感到不舒服,认为它是错误的。谢天谢地,如果你使用 Pyramid 而且你不想用资源树来建模你的应用程序,你根本不需要使用它。代替使用 URL dispatch 将URL路径映射到视图。

有些人认为遍历是单方面错误的观点是可以理解的。那些认为这是错误的人几乎总是把他们所有的数据放在一个关系数据库中。关系数据库天生就不具有层次结构,因此不可能像树一样遍历关系数据库。

然而,那些认为遍历单方错误的人忽略了考虑到许多持久性机制 are 分层的。示例包括文件系统、LDAP数据库、 ZODB (或其他类型的图形)数据库、XML文档和Python模块名称空间。将前端建模为一个层次数据存储的图形通常比较方便,可以使用遍历将视图应用于以下对象: are 正在遍历树中的资源(例如,在zodb的情况下)或至少代表它们的资源(例如,在文件系统中的文件包装器的情况下)。

此外,许多网站结构自然是分级的,即使驱动它们的数据不是这样的。例如,报纸网站通常是极其分级的:节内节内节,无限。如果您希望您的URL指示此结构,并且结构是不确定的(嵌套节的数量可以是“n”而不是某个固定的数字),那么资源树是一种很好的建模方法,即使后端是关系数据库。在这种情况下,资源树只是一个站点结构。

遍历还提供了比URL调度更好的应用程序可组合性,因为它不依赖于URL匹配的固定顺序。您可以围绕视图到资源的映射组合一组不同的功能(稍后再添加),这比尝试获得正确的URL模式匹配顺序更具预见性。

但这一点最终还没有定论。如果不想使用遍历,则不必使用。请改用URL调度。

Pyramid做URL调度,我不喜欢URL调度

Pyramidurl dispatch 是将URL路径解析为 view 可通过对一组有序的路由定义执行模式匹配来调用。按顺序检查路由定义:第一个匹配的模式用于将URL与可调用视图关联。

有些人对这个概念感到不舒服,认为它是错误的。这些人通常都沉浸在 Zope . Zope不提供任何机制,除了 traversal 将代码映射到URL。这主要是因为Zope实际上需要使用 ZODB ,这是一个分层对象存储。Zope还支持关系数据库,但通常调用数据库的代码位于Zodb对象图中的某个位置(或者至少是 view 与对象图中的节点相关),需要遍历才能到达此代码。

我认为URL调度最终是有用的,即使您也希望使用遍历。实际上你可以 结合 URL调度和遍历 Pyramid (见 结合遍历和URL调度 )这种用法的一个例子是:如果您想在对象图(或任何管理界面)上模拟Zope2的“ZopeManagementInterface”UI之类的东西,可以注册类似于 config.add_route('manage', '/manage/*traverse') 然后通过使用 route_name 对A的论证 view 配置,例如, config.add_view('.some.callable', context=".some.Resource", route_name='manage') . 如果你用这种方式把事情联系起来,然后有人走到,例如, /manage/ob1/ob2 ,它们可能会显示一个管理界面,但会 /ob1/ob2 将用默认的对象视图呈现它们。如果你很聪明(也可能是受虐狂),那么在这些混合配置中你还可以使用其他的技巧。

另外,如果您是一个讨厌URL调度的人,如果您被要求编写一个必须使用某些遗留关系数据库结构的应用程序,您可能会发现使用URL调度对于视图和URL路径之间的一次性关联非常有用。有时向对象图中添加一个节点,有效地表示一些代码的入口点,这是毫无意义的。你只需使用一条路线就可以完成。如果路由匹配,将调用与该路由关联的视图。如果没有匹配的路线, Pyramid 返回到使用遍历。

但这一点最终还没有定论。如果你使用 Pyramid ,而且您真的不想使用URL调度,您根本不需要使用它。相反,使用 traversal 专门用于将URL路径映射到视图,就像在 Zope .

Pyramid视图不接受任意关键字参数

许多web框架(zope、turbogears、pylons 1.x、django)允许它们的变体 view callable 接受任意关键字或位置参数,这些参数使用 request.POSTrequest.GET 或路由匹配字典。例如,django视图将接受与关联的“urlconf”中的信息匹配的位置参数,例如 r'^polls/(?P<poll_id>\d+)/$

1def aview(request, poll_id):
2    return HttpResponse(poll_id)

Zope同样允许您向通过遍历找到的资源对象的任何方法添加任意关键字和位置参数:

1from persistent import Persistent
2
3class MyZopeObject(Persistent):
4    def aview(self, a, b, c=None):
5        return '%s %s %c' % (a, b, c)

当作为可发布调用的结果调用此方法时,将搜索zope请求对象的get和post命名空间,以查找与请求中的位置参数和关键字参数的名称匹配的键,并调用该方法(如果可能),并使用其中提到的值填充其参数列表。涡轮齿轮和塔架1.x的运行方式类似。

走出盒子, Pyramid 配置为不具有这些功能。默认情况下 Pyramid 查看可调用文件始终只接受 request 没有其他论据。其基本原理是,这种参数规范在积极地进行匹配时可能会代价高昂,并且 Pyramid 将绩效作为其主要目标之一。因此,我们决定让人们在默认情况下通过询问视图可调用体中的请求对象来获取信息,而不是提供将其解包到视图参数列表中的魔力。

然而,作为 Pyramid 1.0A9,用户代码可以影响视图可调用文件的调用方式,从而可以组成一个以任意参数调用的系统视图外可调用文件。见 使用视图映射器 .

Pyramid提供的“轨道”太少

通过设计, Pyramid 不是一个特别固执己见的网络框架。它有一个相对节俭的特性集。它不包含内置ORM,也不包含任何特定的数据库绑定。它不包含表单生成框架。它没有管理Web用户界面。它没有内置的文本索引。它并不规定如何安排代码。

这种固执己见的功能存在于构建的应用程序和框架中。 顶上 属于 Pyramid . 它的目的是为了更高层次的系统出现 Pyramid 作为基地。

Pyramid提供了太多的“轨道”

Pyramid 提供一些其他Web框架不具备的功能。如果您正在构建一个简单的定制Web应用程序,那么这些功能是用于可能对您没有意义的用例的:

这些特性对 Pyramid . 这个 Pyramid 作者通常被委托构建CMS风格的应用程序。此类应用程序通常是框架式的,因为它们有多个部署。每个部署都需要稍微不同的子应用程序组合,并且框架和子应用程序通常需要 可扩展的 . 由于应用程序有不止一个部署,可插性和可扩展性非常重要,因为维护应用程序的多个分支(每个部署一个分支)是非常不可取的。因为扩展一个使用 traversal 在一个使用 URL dispatch ,每个部署使用 resource tree 由域模型对象的持久树组成,并使用 traversal 映射 view callable 树中资源的代码。资源树包含非常精细的安全声明,因为资源由不同的用户集拥有和访问。接口用于使单元测试和实现的可替换性变得更容易。

在定制的Web应用程序中,通常只有一个规范部署,因此不可能有多个代码复刻。不需要扩展性;代码只是在适当的地方更改了。安全性要求通常不那么具体。对于这样的应用程序,使用上面列出的功能通常是多余的。

如果你不喜欢这些功能,并不意味着你不能或不应该使用 Pyramid . 它们都是可选的,而且花了很多时间来确保您不需要预先了解它们。您可以使用 Pyramid 完全是通过忽略上面的特性来定制的。在构建了一个定制的Web应用程序之后,您可能会发现这些功能很方便,因为它必须部署在多个位置,所以突然变得流行并需要可扩展性。

Pyramid太大了

Pyramid 压缩后的tarball大于2MB。一定很大!”

不,我们只是把文件、测试代码和脚手架一起装运。下面是包树的子目录中包含的内容的细分:

文档/

3.6MB

Pyramid/测试/

1.3MB

Pyramid/脚手架/

133kb

Pyramid/(除了 pyramid/testspyramid/scaffolds

812kb

在包中大约34k行的python代码中,在正常操作期间实际上有机会执行的代码(不包括测试和脚手架python文件)约占10000行。

Pyramid有太多依赖项

随着时间的推移,我们在减少Pyramid所具有的包装依赖性方面取得了很多进展。Pyramid1.2有15个。Pyramid1.3和1.4有12个。截至本文撰写之日,Pyramid1.5的当前版本只有7个。这个数字不太可能变小。

python 3的一个端口在pyramid 1.3中完成,通过迫使我们做出更好的打包决策,帮助我们摆脱了大量的依赖性。去除1.5中Pyramid核心中的变色龙和Mako模板系统依赖性,让我们摆脱了剩下的大部分。

Pyramid“欺骗”以获得速度

其他Web框架作者在不同时间提出的投诉 Pyramid “欺骗”以获得业绩。一个声称的作弊机制是我们使用 zope.interface 快速查找。另一个声称的作弊机制是宗教上对外来功能调用的回避。

如果有作弊这样的事情来获得更好的表现,我们希望尽可能多地作弊。我们优化 Pyramid 积极地。这是有代价的。核心代码有一些可以用更可读性来表示的部分。作为一个改进,我们对这些部分进行了大量的评论。

Pyramid的术语错误(“MVC”)。

“我是MVC Web框架用户,我很困惑。 Pyramid 调用控制器视图!它没有任何控制器。”

如果您在这个阵营中,您可能已经开始期待关于现有“MVC”框架如何使用其术语的事情了。例如,您可能期望模型是ORM模型,控制器是具有映射到URL的方法的类,视图是模板。 Pyramid 实际上,每一个概念都有,而且每一个可能 作品 几乎与您现有的“MVC”Web框架完全相同。我们只是不使用MVC术语,因为我们无法将它在Web框架空间中的使用与历史现实相平衡。

人们非常希望通过使用类似的术语为Web应用程序提供与普通桌面GUI平台相同的属性,并为通用Web框架中的各种组件如何连接在一起提供一些参考框架。但在作者看来,“MVC”在总体上与网络不太匹配。引用 Model-View-Controller Wikipedia entry

尽管MVC有不同的风格,但控制流程通常如下:

用户以某种方式与用户界面交互(例如,按下鼠标按钮)。

控制器通常通过注册的处理程序或回调处理来自用户界面的输入事件,并将事件转换为适当的用户操作,这对于模型来说是可以理解的。

控制器将用户操作通知模型,可能导致模型状态的更改。(例如,控制器更新用户的购物车。) [5]

视图查询模型以生成适当的用户界面(例如,视图列出购物车的内容)。请注意,视图从模型中获取自己的数据。

控制器(在某些实现中)可能会向视图发出一条常规指令,以呈现自己。在其他视图中,该视图由状态更改模型(观察者)自动通知,该模型需要屏幕更新。

用户界面等待进一步的用户交互,这将重新启动循环。

对于作者来说,似乎有人编辑了这个维基百科的定义,用最通用的术语来扭曲地表达概念,以解释当前网络框架使用“MVC”这个术语的原因。我怀疑MVC模式的原始作者是否会同意这样一个宽泛的定义。但是 即使如此 似乎大多数MVC Web框架都无法满足这种错误的一般定义。

例如,您的模板(视图)是否总是像“注意视图从模型中获取自己的数据”中声明的那样直接查询模型?大概不会。我的“控制器”倾向于这样做,按摩数据以便“视图”(模板)更容易使用。当您的“控制器”返回JSON时,您会怎么做?您的控制器是否使用模板生成JSON?如果不是,那么“视图”是什么?大多数MVC风格的GUI Web框架都连接了某种事件系统,允许视图在模型更改时进行检测。网络在其当前的形式中没有这样的功能;它实际上只是拉动而已。

因此,为了不把欲望误认为现实,而不是试图把网络的方栓塞进“MVC”的圆孔中,我们只是简单地说,有两件事:资源和观点。资源树表示站点结构,视图表示资源。这些模板实际上只是任何给定视图的实现细节。视图不需要模板来返回响应。没有“控制器”;它只是不存在。“模型”要么由资源树表示,要么由完全独立于框架的“域模型”(如SQLAlchemy模型)表示。在我们看来,考虑到网络当前的限制,这似乎更为合理。

Pyramid应用程序是可扩展的;我不相信应用程序的可扩展性

任何 Pyramid 遵循某些约束编写的应用程序是 可扩展的 . 此功能将在中讨论 Pyramid 文件章节名称 扩展现有 Pyramid 应用高级配置 . 通过使用 Zope Component Architecture 在内部 Pyramid .

在此上下文中,“可扩展”是指:

  • 应用程序的行为可以在特定的 部署 而不需要部署人员修改原始应用程序的源。

  • 最初的开发人员不需要在应用程序创建时预测任何可扩展性插件点,以允许重写或扩展基本的应用程序行为。

  • 最初的开发人员可以选择预期一组特定于应用程序的插点,这些插点可以由部署人员挂接。如果他们选择使用ZCA提供的设施,那么最初的开发人员不需要对引入这样一个插件的机制进行非常认真的思考。

许多开发人员似乎认为创建可扩展的应用程序是不值得的。相反,他们建议为每个部署修改给定应用程序的源以重写行为更为合理。关于版本控制分支和合并的许多讨论通常都会随之而来。

很明显,不需要使每个应用程序都可扩展。大多数Web应用程序只有一个部署,因此根本不需要可扩展。但是,一些Web应用程序有多个部署,而其他应用程序则有 many 部署。例如,通用内容管理系统(CMS)可能具有基本功能,需要为特定部署扩展这些功能。CMS可以部署在许多地方的许多组织中。此CMS的一些部署可以由第三方集中部署,并作为一个组进行管理。通过预定的插件点为每次部署扩展这样一个系统要比持续保持系统的每个软件分支与某些上游源同步容易得多。上游开发人员可能会以这样的方式更改代码:您对相同代码库的更改会与他们的代码库发生细微的冲突。在部署的整个生命周期中反复合并这些更改可能很困难且耗时,而且能够以一种不太具侵入性的方式修改特定部署的应用程序通常很有用。

如果你不想考虑 Pyramid 应用程序的可扩展性,您根本不需要。您可以完全忽略可扩展性。但是,如果您遵循 扩展现有 Pyramid 应用 你不需要 make 您的应用程序可扩展。在框架中编写的任何应用程序 is 在基本级别自动扩展。部署人员用来扩展它的机制必然是粗糙的。通常,视图、路由和资源可以被覆盖。但是对于大多数次要的(甚至是一些主要的)定制,这些通常是唯一需要覆盖的插件。如果应用程序没有完全按照部署要求执行,部署人员通常可以重写视图、路由或资源,并快速使其按他们希望的方式执行。 原开发商不一定预期 . 下面是一些示例场景,演示了这种功能的好处。

  • 如果部署需要不同的样式,则部署程序可以在定义覆盖的单独Python包中覆盖主模板和CSS。

  • 如果部署需要应用程序页来做不同的事情,或者公开更多或不同的信息,那么部署程序可能会覆盖在单独的Python包中呈现该页的视图。

  • 如果部署需要附加功能,则部署程序可以向覆盖包添加视图。

只要上游包的基本设计没有改变,这些类型的修改通常可以在上游包的许多版本中生存,而无需重新访问。

从外部扩展应用程序并不是万能的,它会带来一系列类似于分支和合并的风险。有时,上游的重大变化会导致您重新访问和更新某些修改。但是,您不需要经常处理无意义的文本合并冲突,当需要更新上游包时,对上游包进行细微的更改通常会导致这些冲突,因为如果从外部扩展应用程序,则不会进行文本合并。您的修改也将被包含在一个规范的、明确定义的地方,不管它有什么价值。

对应用程序进行分支并不断合并以获得新的特性和错误修复显然是有用的。你可以用 Pyramid 应用程序和任何应用程序一样有用。但是应用程序的部署 Pyramid 即使应用程序没有提前定义任何插头点,也可以避免这种需要。竞争性Web框架的发起人可能会摒弃这一特性,转而支持分支和合并,因为在他们选择的框架中编写的应用程序无法以类似的基本方式进行扩展。

同时 Pyramid 应用程序从根本上是可扩展的,即使您不考虑特定的可扩展性来编写它们,如果您有一定的冒险精神,也可以更进一步。如果你了解更多关于 Zope Component Architecture ,您可以选择在开发应用程序时使用它公开其他更特定于域的配置插件。你所暴露的插接点不必像自动提供的那样粗糙。 Pyramid 本身。例如,您可以编写自己的指令,以配置一组视图以用于预烘焙目的(例如, restview 或者类似的),当其他人在 includeme 他们的定制包。这是有代价的:为部署程序定义自定义插件的应用程序的开发人员需要了解ZCA,或者他们需要开发自己类似的可扩展性系统。

最终,关于扩展性特性是否通过 Pyramid 好坏大多是毫无意义的。您不需要利用特定 Pyramid 应用程序,以影响对特定部署集的修改。您可以完全忽略应用程序的扩展性插件,而是使用版本控制分支和合并来管理应用程序部署修改,就像部署使用任何其他Web框架编写的应用程序一样。

Zope3默认执行“ttw”授权检查;Pyramid不执行

挑战

Pyramid 仅在以下位置执行自动授权检查 view 执行时间。Zope3使用安全代理包装上下文对象,这会导致Zope3在属性访问期间也进行安全检查。我喜欢这个,因为它意味着:

  1. 当我使用安全代理机制时,我可以有条件地显示某些HTML元素(如表单字段)或根据访问用户对上下文对象拥有的权限阻止修改某些属性。

  2. 我还想使用TwistedWeb通过RESTAPI公开我的资源。如果Pyramid通过Zope3的安全代理基于属性访问执行授权,我可以在 Pyramid 以同样的方式在扭曲的系统中。

防守

Pyramid 是由熟悉Zope2的人开发的,它有一个“通过Web”的安全模型。这个TTW安全模型是Zope3安全代理的前身。随着时间的推移, Pyramid 开发人员(在Zope2中工作)创建了这样的站点,我们发现在代码解释期间的授权检查在少数项目中非常有用。但大多数时候,TTW授权检查通常会减慢没有授权要求的项目的开发速度。特别是,如果我们不允许不受信任的用户编写由我们的应用程序执行的任意python代码,那么通过Web安全检查的负担就太昂贵了,不合理。多年来,我们(集体)还没有编写过一个应用程序,不受信任的开发人员可以在此基础上编写代码,因此在新的Web框架中默认地放弃这个模型似乎是有意义的。

而且,由于我们倾向于对所有Web应用程序使用相同的工具箱,所以在两个不同的Web框架下使用相同的一组受限执行代码从来就不是一个问题。

尽管如此,默认情况下禁用安全代理的理由是,考虑到Zope3安全代理本质上是病毒性的,使用安全代理的唯一要求是确保将单个对象包装在安全代理中,并确保在希望进行代理安全检查时正常访问该对象。可以覆盖 Pyramid 给定应用程序的遍历器(请参见 更改遍历器 )为了获得类似于zope3的行为,可以插入一个不同的遍历器,它为每个被遍历的对象(包括 context 以及 root )这将产生一个更像Zope3的环境,而不需要太多的努力。

Pyramid使用自己的HTTP异常类层次结构,而不是 webob.exc

在 1.1 版本加入.

中定义的HTTP异常类 pyramid.httpexceptions 非常像中定义的 webob.exc ,例如 HTTPNotFoundHTTPForbidden )它们具有相同的名称和基本上相同的行为,并且都有非常相似的实现,但不具有相同的身份。这就是为什么他们有一个独立的身份。

  • 将它们分开允许HTTP异常类子类 pyramid.response.Response . 由于Pyramid路由器的工作方式,这会稍微加快响应生成速度。同样的速度可以通过MonkeyPatching获得。 webob.response.Response 但通常情况下,蒙凯修补是邪恶和错误的。

  • 使它们分开,使它们能够提供备用的 __call__ 逻辑,也加速了响应生成。

  • 使它们分开允许异常类提供 RequestClass (pyramid.request.Request

  • 将它们分开让我们可以自由地思考存在于 webob.exc 与python 2.4相关,我们在python 1.1+中不再支持它。

  • 我们改变了两个班的行为 (HTTPNotFoundHTTPForbidden )在模块中,以便Pyramid内部使用它们 notfoundforbidden 例外情况。

  • 使它们分离允许我们影响异常类的docstring,以提供特定于Pyramid的文档。

  • 当响应对象用作异常(与 self.message

Pyramid比Zope有更简单的穿越机械

Zope的默认遍历器:

  • 允许开发人员在遍历时改变遍历名堆栈(他们可以添加和删除路径元素)。

  • 尝试使用自适应从当前遍历的对象获取路径中的下一个元素,然后返回到 __bobo_traverse____getitem__ 最终 __getattr__ .

Zope的默认遍历器允许开发人员在遍历期间通过改变遍历名堆栈 REQUEST['TraversalNameStack'] . Pyramid的默认遍历器 (pyramid.traversal.ResourceTreeTraverser )不提供这样做的方法。它不将堆栈维护为请求属性,即使这样,也不会在遍历时将请求传递给资源对象。虽然有时它很方便,但在Zope上构建的框架(如CMF和PLONE)中滥用了这个特性,这常常使得在遍历与视图不匹配时很难准确地判断到底发生了什么。我觉得对于那些想要这个特性来让他们替换遍历器的人来说,这比将特定的蜜罐构建到默认的遍历器要好。

Zope使用多种机制尝试基于名称获取资源树中的下一个元素。它首先尝试将当前资源调整为 ITraversable ,如果失败,则返回到尝试对资源使用许多魔术方法。 (__bobo_traverse____getitem____getattr__ )我在使用Zope和尝试在 repoze.zope2 让我相信:

  • 这个 违约 遍历器应该尽可能简单。Zope的发布服务器有点难以跟踪和复制,因为它在一个遍历方法失败时尝试了回退。它也很慢。

  • 这个 整个遍历器 应该是可更换的,而不仅仅是机械的部件。金字塔有一些大的组成部分,而不是过多的小部件。如果整个遍历器是可替换的,那么使默认遍历器的部分可替换是一种反模式。这样做是一种“旋钮对旋钮”的模式,不幸的是,这种模式在Zope中有点流行。在“knobs on knobs”模式中,一个较大组件的可替换子组件可以使用可用于替换较大组件的相同配置机制进行配置。例如,在Zope中,可以通过注册适配器来替换默认的遍历器。但是您也可以(或者交替地)通过注册一个或多个适配器来控制默认遍历器的遍历方式。由于能够完全替换较大的组件或打开较大组件的默认实现的旋钮,因此没有人知道何时(或是否)应该完全覆盖较大的组件。随着时间的推移,这会导致更大的“可替换”组件和框架本身生锈,因为人们开始依赖于默认组件的可用性,而只是为了转动它的旋钮。默认组件实际上成为框架的一部分,这完全颠覆了使其可替换的目标。在金字塔中,通常情况下,如果一个组件是可替换的,它本身就没有旋钮(它将是固态的)。如果要影响由该组件控制的行为,则将替换组件,而不是转动附加到组件的旋钮。

微框架有更小的hello world程序

存在自我描述的“微框架”。 BottleFlask 是两个正在流行的。 Bobo 它并没有把自己描述成一个微框架,但它的预期用户群却大同小异。还有很多人。我们甚至(只是作为教学工具,而不是任何形式的官方项目) created one using Pyramid . 视频使用的是bfg,这是金字塔的前身,但最终的代码是 available for Pyramid too )微框架是具有一个共同特征的小型框架:每个框架都允许其用户创建一个完全功能化的应用程序,该应用程序位于单个Python文件中。

一些开发人员和微框架作者指出,Pyramid的“你好世界”单文件程序比他们最喜欢的微框架中的等效程序长(大约5行)。被指控有罪。

这种损失不是因为缺乏尝试。Pyramid在微框架声称主导地位的相同情况下也很有用:单文件应用。但Pyramid并没有牺牲其可靠地支持更大应用程序的能力,以实现“hello world”行代码与当前的微框架产品的对等性。Pyramid的设计试图避免一些与幼稚的声明性配置方案相关的常见陷阱。下面的小节解释了其基本原理。

应用程序程序员不控制模块范围代码路径(导入时间副作用是邪恶的)

设想一个包含一组python文件的目录结构:

.
|-- app.py
|-- app2.py
`-- config.py

内容 app.py

 1from config import decorator
 2from config import L
 3import pprint
 4
 5@decorator
 6def foo():
 7    pass
 8
 9if __name__ == '__main__':
10    import app2
11    pprint.pprint(L)

内容 app2.py

1import app
2
3@app.decorator
4def bar():
5    pass

内容 config.py

1L = []
2
3def decorator(func):
4    L.append(func)
5    return func

如果我们 cd 到保存这些文件的目录,然后我们运行 python app.py ,考虑到上面的目录结构和代码,会发生什么?大概,我们 decorator 装饰器将被装饰功能使用两次,一次 foo 在里面 app.py 一次由装饰功能 bar 在里面 app2.py . 因为每次使用decorator时, L 在里面 config.py 附加到,我们希望打印一个包含两个元素的列表,对吗?遗憾的是,没有:

[chrism@thinko]$ python app.py
[<function foo at 0x7f4ea41ab1b8>,
 <function foo at 0x7f4ea41ab230>,
 <function bar at 0x7f4ea41ab2a8>]

通过目视检查,结果(列表中的三个不同功能)似乎是不可能的。我们只定义了两个函数,并且每个函数只修饰一次,因此我们相信 decorator decorator只运行两次。但是,我们认为事实上是错误的,因为我们的 app.py 模块是 执行两次 . 当脚本运行为 __main__ (通过 python app.py ,然后在 app2.py 导入与相同的文件 app .

这与我们与微框架的比较有什么关系?当前作物中的许多微框架(例如瓶子和Flask)鼓励您将配置装饰器附加到模块范围内定义的对象上。这些修饰符执行任意复杂的注册代码,该代码填充了一个单实例注册表,该注册表是一个全局注册表,而该注册表又在外部python模块中定义。这类似于上面的示例:上面示例中的“全局注册表”是列表 L .

让我们看看当我们使用相同的模式 Groundhog micro框架。替换的内容 app.py 上面有这个:

1from config import gh
2
3@gh.route('/foo/')
4def foo():
5    return 'foo'
6
7if __name__ == '__main__':
8    import app2
9    pprint.pprint(L)

替换的内容 app2.py 上面有这个:

1import app
2
3@app.gh.route('/bar/')
4def bar():
5    'return bar'

替换的内容 config.py 上面有这个:

1from groundhog import Groundhog
2gh = Groundhog('myapp', 'seekrit')

在“GH”土拨鼠应用程序的路由表中将注册多少个路由?如果你回答三,你是对的。一个临时读者(以及任何理智的开发者)希望注册多少人?如果你回答两个,你是对的。双重注册会有问题吗?我们的土拨鼠框架 route 方法支持这个应用程序,不是真的。它会使应用程序慢一点,因为当一条路径不匹配时,它需要错过两次。这会是另一个框架、另一个应用程序或另一个装饰器的问题吗?谁知道呢。您需要了解应用程序的整体性、框架的整体性和执行时间,以便能够预测意外代码双重执行的影响。

鼓励使用执行外部注册表填充的装饰器会产生意想不到的结果:应用程序开发人员现在必须声明对执行Python模块范围代码的每个代码路径的所有权。模块范围代码被当前基于装饰器的微框架假定为只执行一次。如果它执行了不止一次,就会开始发生奇怪的事情。由应用程序开发人员来维护这个不变量。不幸的是,实际上这是一个不可能的任务,因为Python程序员 不拥有模块范围代码路径,永远不会 . 任何人试图向你推销他们这样做的想法都是错误的。您可能希望用来运行代码测试的测试运行程序经常以奇怪的顺序执行任意代码的导入,这些顺序显示了类似上面演示的错误。API文档生成工具也有同样的作用。有些人甚至认为使用 Python 是安全的 reload 命令,或从中删除对象 sys.modules ,当用于具有导入时间副作用的代码时,每个都会产生滑稽的效果。

因此,全球注册处变异的微框架程序员在某个时候需要开始阅读关于什么的茶叶。 可以 如果模块范围代码执行了多次,就会发生这种情况,就像我们在上一段中所做的那样。当Python程序员假定他们可以使用模块范围的代码路径来运行任意代码(尤其是填充外部注册表的代码),而这个假设受到现实的挑战时,通常需要应用程序开发人员经历一个痛苦的、细致的调试过程,以找到一个不可避免的模糊症状的根本原因。解决方案通常是重新排列应用程序导入顺序,或者将导入语句从模块范围移动到函数体中。这样做的理由永远不能在修复附带的提交消息中充分表达,并且不能为开发团队的其他成员提供足够简洁的文档,这样问题就不会再次发生。这种情况还会再次发生,尤其是当您与其他尚未内化您在使用模块范围代码时所学到的经验的人一起工作时。 pdb . 这是一个非常糟糕的情况,在这种情况下,你会发现自己是一个应用程序开发人员:你可能甚至不知道你或你的团队注册了这项工作,因为基于装饰师的微框架提供的文档没有警告你。

那些对基于装饰器的、填充外部数据结构(如微框架作者)的热心配置有大量投资的人可能会争辩说,我上面概述的一组情况是反常的和人为的。他们会争辩说这是永远不会发生的。如果您从未打算让您的应用程序超过一个或两个或三个模块,那么这可能是正确的。但是,随着代码库的增长,并且扩展到更多的模块,多次执行模块范围代码的情况将越来越可能发生,并且越来越不可预测。它不负责声称模块范围代码的双重执行永远不会发生。会的,这只是运气、时间和应用程序复杂性的问题。

如果微框架作者承认这种情况不是人为造成的,那么他们可能会争辩说,真正的损害不会由于模块范围代码的双重执行(或三重执行等)而发生。你最好不要相信这个断言。多次执行的潜在结果太多,无法预测,因为它们涉及应用程序和框架代码之间的微妙关系以及代码执行的时间安排。框架作者根本不可能知道在任何情况下都会发生什么。但是,即使在某些有限的情况下给予了Omniscience的天赋,框架作者在编写新特性时几乎肯定不会想到双重执行异常。他们正在考虑添加一个特性,而不是针对可能由1%多个执行案例引起的问题进行保护。然而,任何1%的案例都可能导致50%的项目痛苦,所以如果它从未发生过,那就太好了。

负责任的微框架实际上提供了解决问题的后门方法。它们允许您完全废弃基于装饰器的配置。而不是要求您执行以下操作:

1gh = Groundhog('myapp', 'seekrit')
2
3@gh.route('/foo/')
4def foo():
5    return 'foo'
6
7if __name__ == '__main__':
8    gh.run()

它们可以让您不用修饰语法,几乎所有的命令都必须执行:

1def foo():
2    return 'foo'
3
4gh = Groundhog('myapp', 'seekrit')
5
6if __name__ == '__main__':
7    gh.add_route(foo, '/foo/')
8    gh.run()

这是Pyramid文档中鼓励的通用操作模式。一些现有的微框架(特别是Flask)也允许这样做。无(Pyramid除外) 鼓励 它。如果您从未期望您的应用程序增长超过两个或三个或四个或十个模块,那么您使用哪种模式可能无关紧要。但是,如果您的应用程序变大,那么命令式配置可以提供更好的可预测性。

备注

敏锐的读者可能会注意到Pyramid也有配置装饰器。啊哈!这些装饰工不是也有同样的问题吗?不。这些修饰符在执行时不填充外部python模块。它们只改变它们所附加到的函数(以及类和方法)。这些突变必须在具有可预测和结构化导入阶段的扫描过程中被发现。模块局部突变实际上是双重导入的最佳情况。如果一个模块在导入时只改变它自己和它的内容,如果它被导入两次,那就没问题了,因为每个修饰符调用都会改变它所连接的对象的独立副本,而不是像另一个模块中的注册表那样的共享资源。这样就不会进行双重注册。

路由需要相对排序

考虑以下几个简单的问题 Groundhog 应用:

 1from groundhog import Groundhog
 2app = Groundhog('myapp', 'seekrit')
 3
 4@app.route('/admin')
 5def admin():
 6    return '<html>admin page</html>'
 7
 8@app.route('/:action')
 9def do_action(action):
10    if action == 'add':
11       return '<html>add</html>'
12    if action == 'delete':
13       return '<html>delete</html>'
14    return app.abort(404)
15
16if __name__ == '__main__':
17    app.run()

如果运行此应用程序并访问URL /admin ,您将看到“管理”页面。这是预期的结果。但是,如果您重新排列文件中函数定义的顺序,会怎么样?

 1from groundhog import Groundhog
 2app = Groundhog('myapp', 'seekrit')
 3
 4@app.route('/:action')
 5def do_action(action):
 6    if action == 'add':
 7       return '<html>add</html>'
 8    if action == 'delete':
 9       return '<html>delete</html>'
10    return app.abort(404)
11
12@app.route('/admin')
13def admin():
14    return '<html>admin page</html>'
15
16if __name__ == '__main__':
17    app.run()

如果运行此应用程序并访问URL /admin ,您的应用程序现在将返回404错误。这可能不是你想要的。重新排列函数定义顺序时出现404错误的原因是,通过微框架的路由装饰器表达的路由声明具有 排序 以及订购事宜。

在第一种情况下,当我们达到预期的结果时,我们首先添加了一个具有该模式的路由 /admin ,然后我们用这个模式添加了一条路线。 /:action 通过模块范围内的装饰器添加路由模式。当一个请求 PATH_INFO 属于 /admin 进入我们的应用程序后,web框架按照模块中定义的顺序循环遍历每个应用程序的路由模式。因此,与 /admin 将调用路由模式,因为它首先匹配。世界上一切都好。

在第二种情况下,当我们没有达到预期的结果时,我们首先用这个模式添加了一条路线。 /:action ,然后我们用这个模式添加了一条路线。 /admin . 当一个请求 PATH_INFO 属于 /admin 进入我们的应用程序后,web框架按照模块中定义的顺序循环遍历每个应用程序的路由模式。因此,与 /:action 将调用路由模式,因为它首先匹配。出现404错误。这不是我们想要的;它只是由于我们定义视图函数的顺序而发生的。

这是因为土拨鼠路由是按导入顺序添加到路由图中的,并且在收到请求时按相同的顺序进行匹配。和土拨鼠一样,在本文中,瓶子按照它们在Python执行时定义的顺序匹配路径。另一方面,flask不根据进口订单订购工艺路线匹配。相反,它会根据添加到应用程序中的路由的“复杂性”对其重新排序。其他微框架有不同的路径排序策略。

您的应用程序可能足够小,在这种情况下,路由订购将不会导致问题。但是,如果您的应用程序变得足够大,那么在应用程序变大时指定或预测排序将是困难的。在某些时候,您可能需要更明确地开始控制路由顺序,特别是在需要可扩展性的应用程序中。

如果您的微框架基于复杂性订购路径匹配,那么您需要理解“复杂性”的含义,并且您需要尝试注入一个“不太复杂”的路径,使其在任何“更复杂”的路径之前得到匹配,以确保先尝试它。

如果您的微框架基于函数修饰器定义的相对导入/执行来订购其路径匹配,那么您需要确保以“正确”的顺序执行所有这些语句,并且您需要在扩展应用程序或尝试扩展应用程序时了解此导入/执行顺序。对于除最小的应用程序以外的所有应用程序来说,这是一个难以维护的不变量。

在这两种情况下,应用程序都必须导入包含配置装饰的非“主”模块,以便执行配置。这会让你有点不舒服吗?应该,因为 应用程序程序员不控制模块范围代码路径(导入时间副作用是邪恶的) .

Pyramid既不使用装饰器导入时间顺序,也不尝试将一条到另一条路线的相对复杂性作为定义路线匹配顺序的方法。在Pyramid中,您必须通过多个执行的时间顺序来强制维护相对的路由顺序。 pyramid.config.Configurator.add_route() 方法。重复呼叫的顺序 add_route 成为路由匹配的顺序。

如果需要维护这个命令命令,那么您可以使用 traversal 而不是路由匹配,这是一种将代码映射到URL的完全声明性(并且完全可预测)机制。虽然URL调度对于小的不可扩展应用程序更容易理解,但是遍历非常适合需要任意扩展的非常大的应用程序和应用程序。

“堆叠对象代理”太聪明/线程局部变量太麻烦

一些微框架使用 import 语句获取对象的句柄, 逻辑上不是全局的

 1from flask import request
 2
 3@app.route('/login', methods=['POST', 'GET'])
 4def login():
 5    error = None
 6    if request.method == 'POST':
 7        if valid_login(request.form['username'],
 8                       request.form['password']):
 9            return log_the_user_in(request.form['username'])
10        else:
11            error = 'Invalid username/password'
12    # this is executed if the request method was GET or the
13    # credentials were invalid

这个 Pylons 1.X Web框架使用类似的策略。它将这些东西称为“堆叠对象代理”,因此,在本讨论中,我也将这样做。

在python中导入语句 (import foofrom bar import baz )最常见的是获取对外部Python模块中全局定义的对象的引用。然而,在正常程序中,它们从未被用来获取对某个对象的引用,该对象的寿命是由函数体的范围来度量的。例如,尝试导入名为 i 表示在函数体中定义的循环计数器。例如,我们从未尝试导入 i 从下面的代码:

1def afunc():
2    for i in range(10):
3        print(i)

从本质上讲, 请求 由于wsgi服务器调用长寿命Web框架而创建的对象不能是全局的,因为单个请求的生存期将比运行框架的进程的生存期短得多。由Web框架创建的请求对象实际上与 i 在上面的示例中,循环计数器比它对在Python标准库或普通库代码中定义的任何类似的可导入对象都要重要。

但是,使用堆叠对象代理的系统会提升本地范围的对象,例如 request ,超出模块范围,以便为用户提供一个包含 import . 出于我认为可疑的原因,他们更愿意向他们的用户展示 request 作为 from framework import request 而不是理智的人 from myframework.threadlocals import get_request; request = get_request() 即使后者更明确。

它将会是 most 如果微框架根本不使用线程局部变量,则显式。向Pyramid视图函数传递请求对象。许多Pyramid的API都要求将显式请求对象传递给它们。它是 可能的 以线程局部变量的形式检索当前的Pyramid请求,但它是一种“在紧急情况下,打破玻璃”类型的活动。这种明确性使得Pyramid视图功能更容易进行单元测试,因为在测试设置期间,您不需要依赖框架来制造合适的“虚拟”请求(以及其他类似范围的)对象。它还使它们更可能在不需要MonkeyPatching的任意系统上工作,例如异步服务器。

显式WSGI

一些微框架提供 run() 应用程序对象的方法,该方法执行默认服务器配置以便于执行。

Pyramid目前并没有试图掩盖这样一个事实,即它的路由器是一个隐藏在便利背后的wsgi应用程序。 run() 应用程序编程接口。它只是告诉人们导入一个wsgi服务器,并根据该wsgi服务器的文档使用它来服务他们的Pyramid应用程序。

把发球台后的一步抽走,省去了多余的几行 run() 在一些微框架中,似乎已经做出了与API相关的可疑二阶决策。例如,瓶子包含 ServerAdapter 通过它支持的每种类型的wsgi服务器的子类 app.run() 机制。这意味着存在代码 bottle.py 这取决于以下模块: wsgireffluppastecherrypyfapwstornadogoogle.appenginetwisted.webdieselgeventgunicorneventletrocket . 通过将服务器名传递到 run 方法。从理论上讲,这听起来很好:我可以试试瓶子 gunicorn 只是传递一个名字!但是,要完全测试瓶子,必须安装所有这些第三方系统并使其正常工作。瓶子开发人员必须监视每个包的更改,并确保它们的代码仍然与它们正确地接口。这大大增加了测试所需的包数量;这是 lot 要求。由于需求冲突和构建问题,很可能很难完全自动化这些测试。

因此,对于单文件应用程序,我们目前不需要提供 run() 捷径。我们告诉人们导入他们选择的wsgi服务器并手动运行它。对于想要服务器抽象层的人,我们建议他们使用PasteDeploy。在基于PasteDeploy的系统中,确保服务器可以与wsgi应用程序接口的责任放在服务器开发人员身上,而不是Web框架开发人员身上,这使得它更有可能是及时和正确的。

包扎

下面是最简单的Pyramid应用程序的图表版本,其中内联注释考虑了我们在 微框架有更小的hello world程序 部分。

 1from wsgiref.simple_server import make_server  # explicitly WSGI
 2from pyramid.config import Configurator  # to configure app registry
 3from pyramid.response import Response  # explicit response, no threadlocal
 4
 5def hello_world(request):  # accept a request; no request threadlocal reqd
 6    # explicit response object means no response threadlocal
 7    return Response('Hello world!')
 8
 9if __name__ == '__main__':
10    with Configurator() as config:    # no global application object
11        config.add_view(hello_world)  # explicit non-decorator registration
12        app = config.make_wsgi_app()  # explicitly WSGI
13    server = make_server('0.0.0.0', 8080, app)
14    server.serve_forever()            # explicitly WSGI

Pyramid不提供可插入的应用程序

将多个外部源组成相同的配置是“Pyramid形”的,使用 include() . 任何数量的include都可以用来组成一个应用程序;include甚至可以从其他include中完成。任何指令都可以在一个include内使用,该include可以在一个include之外使用(例如 add_view()

Pyramid有一个冲突检测系统,如果两个包含的外部对象试图以冲突的方式添加相同的配置(例如两个外部对象都试图使用相同的名称添加路由,或者两个外部对象都试图添加具有相同谓词集的视图),该系统将抛出错误。将这组特性称为可以用“可插拔应用程序”组成系统的特性是非常诱人的。但事实上,声称这一点存在许多问题:

  • 这个术语有点生硬。Pyramid实际上没有多个“应用程序”的概念,只是一种从多个源组合配置以创建单个WSGi应用程序的方法。WSGi应用程序可以通过包括或不包括配置来获得行为,但是一旦所有这些都组合在一起,Pyramid就不能真正提供任何可以用来划分一个“应用程序”(从配置的意义上讲,它与添加路由、视图等的外部设备)与另一个“应用程序”的边界的机械。

  • Pyramid没有提供足够的“Rails”来让它能够真正地与上帝整合,从一个随机的地方下载一个应用程序,并插入一个插件来创建一个系统的“可插入”应用程序。因为Pyramid本身不固执己见(它不要求特定类型的数据库,它提供多种方式将URL映射到代码等),所以创建类似应用程序的人不太可能随意地将其重新分配给j.random pyramid用户,只需让他配置包含来自包。对于非常高级别的组件,如博客、维基、Twitter克隆、评论系统等,情况尤其如此。集成商(下载了广告称为“可插拔应用程序”的软件包的Pyramid开发人员)几乎肯定会做出不同的选择,例如,他使用的是哪种类型的持久性系统,以及对于集成商t为了满足“可插拔应用程序”的要求,他可能需要建立一个不同的数据库,对自己的代码进行更改,以防止他的应用程序隐藏可插拔应用程序(反之亦然),以及任何其他数量的任意更改。

因此,我们声称Pyramid具有“可扩展”的应用程序,而不是可插入的应用程序。任何Pyramid应用程序都可以在不复刻的情况下进行扩展,只要它的配置语句已经组合成可以通过 config.include .

对于单个开发人员或团队来说,创建一组可通过使用config.include启用或禁用的互操作组件也是完全合理的。该开发人员或团队将能够提供“Rails”(通过对用于创建项目的技术进行高层选择,因此将所有组件插在一起不会有任何问题)。只有当组件需要分配到 任意的 用户。请注意,Django在“可插拔的应用程序”方面也有类似的问题,这些应用程序需要为任意第三方工作,尽管它们提供的Rails比Pyramid多得多。即使它们提供的Rails也不足以让“可插拔应用程序”故事在不进行本地修改的情况下真正工作。

真正可插拔的应用程序需要在比Web框架更高的级别上创建,因为没有任何Web框架能够提供足够的约束来真正使它们开箱即用。相反,它们确实需要插入到应用程序中。用Pyramid构建一个提供这些约束的应用程序是一个崇高的目标,它确实提供了一种插入应用程序的方法(Joomla、Plone、Drupal想到了这一点)。

Pyramid里有Zope的东西,所以太复杂了

有时,有人会觉得不得不发布邮件列表消息,内容如下:

had a quick look at pyramid ... too complex to me and not really
understand for which benefits.. I feel should consider whether it's time
for me to step back to django .. I always hated zope (useless ?)
complexity and I love simple way of thinking

(实际上是从一封真实的电子邮件中转述的。)

让我们一点一点地看待这个批评。

太复杂

如果你能理解这个“你好世界”计划,你可以使用Pyramid:

 1from wsgiref.simple_server import make_server
 2from pyramid.config import Configurator
 3from pyramid.response import Response
 4
 5def hello_world(request):
 6    return Response('Hello World!')
 7
 8if __name__ == '__main__':
 9    with Configurator() as config:
10        config.add_route('hello', '/')
11        config.add_view(hello_world, route_name='hello')
12        app = config.make_wsgi_app()
13    server = make_server('0.0.0.0', 6543, app)
14    server.serve_forever()

金字塔有1200多页的文档(印刷),涵盖了从最基础到最高级的主题。 没什么 完全没有记录在案。它还有一个 太棒了 ,非常乐于助人的社区。访问 #pyramid IRC channel on Libera.Chat 然后你看。

憎恨的Zope

很抱歉你这么想。多年来,Zope品牌无疑占据了团块的份额,并以其与世隔绝和神秘著称。但是“zope”这个词在没有条件的情况下实际上毫无意义。什么 part 你讨厌Zope吗?”“Zope”是一个品牌,而不是技术。

如果它是Zope2的Web框架,那么Pyramid就不是。Pyramid的主要设计者和开发者,如果有的话,应该知道。我们写了Pyramid的前身 (repoze.bfg ,部分原因是 we 知道Zope2存在可用性问题和局限性。 repoze.bfg (现在) Pyramid )是为了解决这些问题而写的。

如果它是Zope3,Web框架,Pyramid是 一定地 不是那样。利用大量的Zope3技术是 Grok 项目。除了显而易见的事实,它们都是Web框架, Pyramid 是非常,非常不同于格罗克。Grok向最终用户公开了许多Zope技术。另一方面,如果你在使用Pyramid的时候需要理解一个纯Zope的概念,那么我们在一些非常基本的轴上失败了。

如果只是“佐普”这个词,这只能是联想的罪恶感。因为一个软件内部使用了一个名为 zope.foo 它不会将使用它的软件变成“zope”。有很多 伟大的 写的名字中有zope这个词的软件。Zope并不是一个整体的东西,它的很多软件都可以在外部使用。虽然这份文件并不是真正的职责来保护它,但Zope已经存在了10多年,拥有一个非常庞大、活跃的社区。如果您不相信,请访问https://pypi.org/search/?Q=Zope是一个令人大开眼界的现实检查。

爱的单纯

为了让开发人员尽可能简单地使用这个包及其文档,我们已经花了多年的时间来磨练它。当然,一切都是一种权衡,人们对什么是“简单”有自己的想法。如果你相信Pyramid是复杂的,你可能会有风格上的不同。它的开发者显然不同意。

其他挑战

鼓励将其他挑战发送至 Pylons-devel 邮件列表。我们将尝试通过考虑设计变更来解决问题,或者至少通过这里的说明来解决问题。