在python中识别纯函数

Uri*_*ren 5 python abstract-syntax-tree purely-functional

我有一个装饰器@pure,将其注册为纯函数,例如:

@pure
def rectangle_area(a,b):
    return a*b


@pure
def triangle_area(a,b,c):
    return ((a+(b+c))(c-(a-b))(c+(a-b))(a+(b-c)))**0.5/4
Run Code Online (Sandbox Code Playgroud)

接下来,我要确定一个新定义的纯函数

def house_area(a,b,c):
    return rectangle_area(a,b) + triangle_area(a,b,c)
Run Code Online (Sandbox Code Playgroud)

显然house_area是纯函数,因为它仅调用纯函数。

如何自动发现所有纯函数(也许使用ast

Val*_*tin 5

假设运算符都是纯的,那么本质上您只需要检查所有函数调用。这确实可以通过 ast 模块来完成。

首先我将pure装饰器定义为:

def pure(f):
    f.pure = True
    return f
Run Code Online (Sandbox Code Playgroud)

添加一个属性来表明它是纯的,允许提前跳过或“强制”函数识别为纯的。如果您需要一个像math.sin纯函数这样的函数,这非常有用。此外,因为您无法向内置函数添加属性。

@pure
def sin(x):
    return math.sin(x)
Run Code Online (Sandbox Code Playgroud)

总而言之。使用该ast模块访问所有节点。然后对于每个Call节点检查所调用的函数是否是纯函数。

import ast

class PureVisitor(ast.NodeVisitor):
    def __init__(self, visited):
        super().__init__()
        self.pure = True
        self.visited = visited

    def visit_Name(self, node):
        return node.id

    def visit_Attribute(self, node):
        name = [node.attr]
        child = node.value
        while child is not None:
            if isinstance(child, ast.Attribute):
                name.append(child.attr)
                child = child.value
            else:
                name.append(child.id)
                break
        name = ".".join(reversed(name))
        return name

    def visit_Call(self, node):
        if not self.pure:
            return
        name = self.visit(node.func)
        if name not in self.visited:
            self.visited.append(name)
            try:
                callee = eval(name)
                if not is_pure(callee, self.visited):
                    self.pure = False
            except NameError:
                self.pure = False
Run Code Online (Sandbox Code Playgroud)

然后检查该函数是否具有该pure属性。如果不是,则获取代码并检查所有函数调用是否可以归类为纯函数。

import inspect, textwrap

def is_pure(f, _visited=None):
    try:
        return f.pure
    except AttributeError:
        pass

    try:
        code = inspect.getsource(f.__code__)
    except AttributeError:
        return False

    code = textwrap.dedent(code)
    node = compile(code, "<unknown>", "exec", ast.PyCF_ONLY_AST)

    if _visited is None:
        _visited = []

    visitor = PureVisitor(_visited)
    visitor.visit(node)
    return visitor.pure
Run Code Online (Sandbox Code Playgroud)

请注意,这print(is_pure(lambda x: math.sin(x)))不起作用,因为inspect.getsource(f.__code__)会逐行返回代码。因此返回的源getsource将包括printandis_pure调用,从而产生False. 除非这些功能被覆盖。


要验证它是否有效,请通过执行以下操作来测试它:

print(house_area) # Prints: True
Run Code Online (Sandbox Code Playgroud)

列出当前模块中的所有函数:

import sys, types

for k in dir(sys.modules[__name__]):
    v = globals()[k]
    if isinstance(v, types.FunctionType):
        print(k, is_pure(v))
Run Code Online (Sandbox Code Playgroud)

visited列表跟踪哪些函数已经被验证为纯函数。这有助于避免与递归相关的问题。由于代码未执行,因此评估将递归访问factorial.

@pure
def factorial(n):
    return 1 if n == 1 else n * factorial(n - 1)
Run Code Online (Sandbox Code Playgroud)

请注意,您可能需要修改以下代码。选择另一种方式从函数名称中获取函数。

try:
    callee = eval(name)
    if not is_pure(callee, self.visited):
        self.pure = False
except NameError:
    self.pure = False
Run Code Online (Sandbox Code Playgroud)