在大型Python项目中使用严格的模块:Instagram的经验。 第二部分

我们向您介绍第二部分资料的翻译,该资料致力于Instagram Python项目中使用模块的功能。 翻译的第一部分对情况进行了概述,并显示了两个问题。 其中之一涉及服务器启动缓慢,其次涉及不安全导入命令的副作用。 今天,这场对话将继续。 我们将考虑另一个麻烦,并讨论解决所有提出的问题的方法。



问题3:可变的全球地位


看看另一类常见错误。

def myview(request):     SomeClass.id = request.GET.get("id") 

在这里,我们处于表示功能中,并根据从请求中接收的数据将属性附加到某个类。 您可能已经了解了问题的实质。 事实是类是全局单调。 在这里,我们根据请求将状态放入一个长期存在的对象中。 在需要很长时间才能完成的Web服务器过程中,这可能导致对该过程中将来提出的每个请求的污染。

在测试中很容易发生相同的事情。 特别是在程序员尝试使用猴子补丁而不使用上下文管理器的情况下 ,例如mock.patchmock.patch 。 这可能不会导致请求污染,但会导致将在同一过程中执行的所有测试受到污染。 这是我们测试系统行为不可靠的严重原因。 这是一个重要的问题,很难防止这种情况。 结果,我们放弃了统一的测试系统,转而使用一种测试隔离方案,可以将其描述为“每个过程一个测试”。

实际上,这是我们的第三个问题。 可变的全局状态不是Python独有的现象。 您可以在任何地方找到它。 我们谈论的是有关模块或类的类,模块,列表或字典,有关在模块级别创建的单例对象。 在这样的环境中工作需要纪律。 为了防止程序运行时对全局状态的污染,您需要非常了解Python。

引入严格的模块


我们的问题的根本原因之一可能是我们使用Python解决了该语言并非旨在解决的问题。 在小型团队和小型项目中,如果您在使用Python时遵循规则,那么这种语言就可以正常工作。 我们应该使用更严格的语言。

但是我们的代码库已经超出了它的大小,使我们至少可以谈论如何用另一种语言重写它。 而且,更重要的是,尽管我们面临所有问题,但Python与它有很大关系。 他给我们带来的好处多于坏的事情。 我们的开发人员非常喜欢这种语言。 结果,这仅取决于我们如何使Python能够按我们的规模工作,以及如何确保随着项目的发展我们可以继续从事该项目。

寻找解决问题的方法使我们有了一个主意。 它包括使用严格的模块。

严格模块是一种新类型的Python模块,其开头是__strict__ = True构造。 它们是使用Python已经拥有的许多低级可扩展性机制实现的。 特殊的模块加载器使用ast模块解析代码,对加载的代码执行抽象解释以对其进行分析,对AST进行各种转换,然后使用内置的compile函数将AST编译回Python字节码。

无进口副作用


严格的模块对模块级别可能发生的情况施加了一些限制。 因此,所有模块级代码(包括在模块级调用的装饰器和函数/初始化器)必须是干净的,即,没有副作用且不使用I / O机制的代码。 这些条件由抽象解释器在编译时使用静态代码分析的方法进行检查。

这意味着使用严格的模块在导入它们时不会引起副作用。 在模块导入期间执行的代码不再会导致意外问题。 由于我们使用了解大量Python子集的工具在抽象解释级别进行了检查,因此消除了过度限制Python表达能力的需要。 可以在模块级别安全地使用许多类型的没有副作用的动态代码。 这包括各种修饰符,以及使用列表或字典生成器的模块级别常量的定义。

让我们更清楚地说明一个例子。 这是正确编写的严格模块:

 """Module docstring.""" __strict__ = True from utils import log_to_network MY_LIST = [1, 2, 3] MY_DICT = {x: x+1 for x in MY_LIST} def log_calls(func):    def _wrapped(*args, **kwargs):        log_to_network(f"{func.__name__} called!")        return func(*args, **kwargs)    return _wrapped @log_calls def hello_world():    log_to_network("Hello World!") 

在此模块中,我们可以使用常规的Python构造,包括动态代码,一种用于创建字典的代码和一种描述模块级装饰器的代码。 同时,在_wrappedhello_world函数中访问网络资源是完全正常的。 事实是,它们不是在模块级别调用的。

但是,如果将log_to_network调用移至外部log_calls函数,或者如果尝试使用引起副作用的装饰器(如上例中的@route ),或者如果我们在模块级别使用hello_world()调用,那么它将不再严格严格-模块。

如何找出在模块级别调用log_to_networkroute函数并不安全? 我们从这样的假设出发:从不是严格模块的模块导入的所有内容都是不安全的,但标准库中的某些已知安全的功能除外。 如果utils模块是一个严格的模块,那么我们可以依靠对模块的分析来让我们知道log_to_network函数是否log_to_network

除了提高代码的可靠性外,没有副作用的导入消除了确保增量代码下载的严重障碍。 这为探索加速进口团队的方式开辟了其他可能性。 如果模块级代码没有副作用,那么这意味着我们可以在访问模块属性时根据请求以“惰性”模式安全地执行各个模块指令。 这比遵循“贪婪”算法要好得多,在该算法中,所有模块代码都预先执行。 而且,考虑到严格模块中所有类的形式在编译时是完全已知的,将来我们甚至可以尝试组织由代码执行生成的模块元数据(类,函数,常量)的永久存储。 这将使我们能够组织快速导入未更改的模块,而无需重复执行模块级别的字节码。

免疫和__slots__属性


创建的严格模块和类在创建后是不可变的。 借助于模块主体的内部转换,使模块变得不可变,该函数通过闭包变量来组织对所有全局变量的访问。 尽管决定通过模块级别的可变容器使用可变全局状态,仍然可以计算出可变状态,但这些更改已严重减少了全局状态随机更改的可能性。

在严格模块中声明的类的成员也必须在__init__声明。 在模块加载程序执行AST转换期间,它们会自动写入__slots__属性。 因此,以后您将无法再将其他属性附加到类实例。 这是一个类似的类:

 class Person:    def __init__(self, name, age):        self.name = name        self.age = age 

在AST转换期间(在严格模块的处理期间执行),将检测在__init__执行的为nameage属性分配值的操作,并将__slots__ = ('name', 'age')形式的属性附加到该类。 这将防止将任何其他属性添加到类实例。 (如果使用类型注释,那么我们将考虑有关类级别上可用类型的信息,例如name: str ,并将它们添加到插槽列表中)。

所描述的限制不仅使代码更可靠。 它们有助于加快代码执行速度。 添加__slots__属性后,类的自动转换会提高使用这些类时的内存使用效率。 这样,您可以在处理类的各个实例时摆脱字典搜索,从而加快了对属性的访问。 另外,我们可以在执行Python代码期间继续优化这些模式,这将使我们能够进一步改善我们的系统。

总结


严格的模块仍然是实验技术。 我们有一个有效的原型,我们处于生产中部署这些功能的早期阶段。 我们希望在获得使用严格模块的足够经验之后,我们将能够更多地讨论它们。

亲爱的读者们! 您认为严格的模块提供的功能在您的Python项目中派上用场吗?


Source: https://habr.com/ru/post/zh-CN475242/


All Articles