如何确保Python函数仅基于其输入生成输出?

7
为了生成输出,函数通常只使用其参数的值。然而,有时候函数需要从文件系统、数据库或者网络中读取内容来生成输出。我希望有一种简单可靠的方式来确保不会发生这种情况。
一个解决方法是创建一个白名单,列出可以用于读取文件系统、数据库或网络的Python库。但如果采用这种方法,从哪里获取这个(可能很大)列表呢?此外,我不想禁用整个库,只因为它可以用于读取文件系统。例如,我希望用户能够使用pandas库(存储和操作表格数据),但不希望他们能够使用该库从文件系统中读取数据。
这个问题是否有解决方案?

1
退一步,你为什么要防止某人从外部源读取信息? - chepner
有很多原因。首先,我想确保将来该函数生成的输出与今天相同。其次,一般来说,当一个函数在某个地方读取某些东西时,我认为这是一种“丑陋”的解决方案。它应该只看到它明确接收的输入。如果应该从文件或数据库中读取某些内容,则应该在函数外部进行读取,并将其作为其输入之一传递给函数。 - Roman
你想要使用你不信任的代码吗? - Steven Rumbalski
1
你试图保护这个函数免受谁的攻击?那些将要更改函数以从磁盘读取的人也可以更改你的白名单,这样做有什么意义呢? - dano
3
听起来你可能需要一个被隔离的 Python 版本。已经有一些失败的尝试去实现这个功能(比如 pysandbox)。PyPy 支持此功能,但我不知道还有其他确实安全的选择。 - dano
显示剩余3条评论
3个回答

8

答案是否定的。您需要寻找一个测试 函数纯度 的函数。但是,正如此代码所示,无法保证不会实际调用任何副作用。

class Foo(object):
    def __init__(self, x):
        self.x = x
    def __add__(self, y):
        print("HAHAHA evil side effects here...")
        # proceed to read a file and do stuff
        return self

# this looks pure...
def f(x): return x + 1

# but really...
>>> f(Foo(1))
HAHAHA evil side effects here...

由于对象可以全面重新定义它们的行为(字段访问、调用、操作符重载等),因此您始终可以传递一个使纯函数变成不纯的输入。因此,唯一的纯函数是那些字面上不对其参数进行任何操作的函数...这类函数通常不太有用。

当然,如果您可以指定其他限制,这将变得更加容易。


在你的例子中,“恶劣效果”发生是因为函数的用户做了一些“坏事”(用户使用了一个“好”的函数,但使用了一个“坏”的参数)。而在我的情况下,我是函数的用户。所以,我不会以“不良”的方式调用函数。我只需要确保我使用的函数是“好”的即可。 - Roman
2
@Roman:你的要求原本更强烈。你写道:“我希望用户能够……但我不希望他们能够使用这个库来读取文件系统中的数据。”而现在你却写道:“我不会以错误的方式调用函数。”这似乎很不寻常。你相信用户,但不相信已安装的软件? - hynekcer

4

即使你删除所有模块和所有函数,也可以打破您所需的限制。如果代码可以使用任意简单对象的属性,例如数字零的属性,则可以访问文件。

(0).__class__.__base__.__subclasses__()[40]('/etc/pas'+'swd')

索引40是Python 2.7的特点之一,但是子类<type 'file'>的索引可以很容易地找到:

[x for x in (1).__class__.__base__.__subclasses__()if'fi'+'le'in'%s'%x][0](
 '/etc/pas'+'swd')

白名单和黑名单的任何组合都不安全和/或过于限制性。 pypy沙箱 的原则是坚不可摧的:

... 此子进程可以运行任意不受信任的Python代码,但其所有输入/输出都序列化为stdin / stdout管道,而不是直接执行。外部进程读取管道并决定允许或不允许哪些命令(沙盒化),甚至以不同方式重新解释它们...

此外,基于seccomp内核功能的解决方案足够安全。(博客


我想确保在未来该函数将生成与今天相同的输出。

编写具有固定可重现结果且不易被阻止的函数非常容易:

class A(object):
    "This can be any very simple class"
    def __init__(self, x):
        self.x = x
    def __repr__(self):
        return repr(self.x)

def strange_function():
    # You get a different result probably everytimes.
    return list(set(A(i) for i in range(20)))

>>> strange_function()
[1, 18, 12, 5, 16, 15, 8, 2, 14, 0, 6, 19, 13, 11, 10, 9, 17, 3, 7, 4]
>>> strange_function()
[0, 9, 14, 3, 17, 5, 6, 11, 8, 1, 15, 7, 12, 13, 2, 10, 16, 4, 19, 18]

即使您删除了所有依赖于时间、随机数生成器、哈希函数等的内容,也很容易编写一个函数,有时会超出可用内存或超时限制,有时会给出结果。
< p > 编辑:
Roman,您最近写道您确信可以相信用户。那么存在一种现实的解决方案。它是通过将输入和输出记录到文件并在运行远程IPython笔记本的虚拟机上进行验证(漂亮的短教程视频,支持远程计算,通过Web文档菜单从浏览器重新启动后端服务只需一秒钟,因为它是由我们的活动逐步动态创建的调用远程后端的javascript而不会丢失笔记本(HTML文档)中的数据(输入/输出))。

您无需关心内部调用,只需全局输入和输出,直到找到差异为止。虚拟机应能够独立且可重复地验证结果。配置防火墙,使该机器接受您的连接,但不能发起出站连接。配置文件系统,使当前用户无法保存数据,因此除软件组件外,它们不存在。禁用数据库服务。以随机顺序验证结果输入/输出,或在不同端口上启动两个IPython笔记本服务,并为笔记本上的每个命令行选择随机后端,或在重要事项之前频繁重新启动后端进程。如果发现差异,请调试您的代码并修复它。

如果您不需要交互性,则可以最终使用IPython远程计算自动化它而不使用“笔记本”。


1
需要注意的是,您还可以从对象的随机化内存地址获取随机数。class A:pass;str(A) - PythonNut

4

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接