具有嵌套和属性查找的字典?



我正在寻找一个类似命名空间的对象,它在获取和设置项目时的行为类似于 Python 评估上下文,大致相当于以下不安全示例:

from collections import UserDict
class UnsafeNamespaceDict(UserDict):
def __getitem__(self, var):
return eval(var, {}, self.data)
def __setitem__(self, var, val):
return exec(f'{var} = val', {'val': val}, self.data)

您可以通过访问嵌套数据或属性的查找查看预期行为:

>>> nd = UnsafeNamespaceDict()
>>> nd['x'] = 'abc'
>>> nd['x[1]']
'b'
>>> nd['x.count']
<built-in method count of str object>
>>> nd['x[1].count']
<built-in method count of str object>

设置不存在的项目会以自然方式失败:

>>> nd['y.z'] = 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in __setitem__
File "<string>", line 1, in <module>
NameError: name 'y' is not defined

元组拼接工作:

>>> nd['a, b, c'] = 'ABC'
>>> nd['c']
'C'

如何使示例类安全(删除evalexec),同时仍然允许至少嵌套查找和属性访问?

您正在寻找一个object类,默认情况下它允许使用.操作符更改class.__dict__。如果您仍然想要类似eval的功能,您可以随时编写自己的实现。 请注意,这适用于其他内置和库,但您需要检查所做的更改是否与默认值冲突。

class Name_Space(object):
def __getattribute__(self, name):
if '_even' in name:
ret = object.__getattribute__(
self, name.replace('_even', '')
)
if ret % 2:
return False
else:
return True
if '_len' in name:
return len(object.__getattribute__(
self, name.replace('_len', '')
))
return object.__getattribute__(self, name)

if __name__ == '__main__':
ns = Name_Space()
ns.num = 3
print(ns.num_even)
ns.a, ns.b, ns.c = 'ABC'
print(ns.a)
ns.text = 'ZXC'
print(ns.text_len)
print(ns.text.lower())
ns.nested = Name_Space()
ns.nested.arr = [1, 2, 3]
print(ns.nested.arr_len)

研究使evalexec安全使我ast.literal_eval,这反过来又使我更普遍地使用ast模块。

事实证明,使用字符串查找创建安全命名空间就像在eval模式下解析密钥的 AST 一样简单,然后在仅允许某些节点的情况下遍历它。我在下面为满足原始问题要求的最小 AST 节点集提供了解决方案。有一个用于更完整实现的存储库。

首先举几个例子:

>>> # create a new namespace
>>> ns = NamespaceDict()
>>> 
>>> # basic setting and getting
>>> ns['x'] = 'abc'
>>> ns['x']
'abc'
>>> ns['x[1]']
'b'
>>> ns['x.upper']()
'ABC'
>>> 
>>> # tuple setting and getting
>>> ns['x, y, z'] = [0], [0], [0]
>>> ns['x[0], y[0], z[0]'] = 1, 1, 1
>>> ns['x, y, z']
([1], [1], [1])

你明白了。这是实现:

import ast

class NamespaceDict:
def __init__(self, data=None):
self.data = data if data is not None else {}
def __getitem__(self, key):
if not isinstance(key, str):
raise TypeError('key is not a string')
expr = ast.parse(key, filename='<key>', mode='eval')
return self._get(key, expr.body)
def __setitem__(self, key, value):
if not isinstance(key, str):
raise TypeError('key is not a string')
expr = ast.parse(key, filename='<key>', mode='eval')
return self._set(key, expr.body, value)
@staticmethod
def _bad_node(key, node):
details = ('<key>', node.lineno, node.col_offset+1, key)
raise SyntaxError('invalid syntax', details)
def _get(self, key, node):
meth = getattr(self, f'_get_{node.__class__.__name__}', None)
if meth is None:
self._bad_node(key, node)
return meth(key, node)
def _set(self, key, node, value):
meth = getattr(self, f'_set_{node.__class__.__name__}', None)
if meth is None:
self._bad_node(key, node)
return meth(key, node, value)
def _get_Name(self, key, node):
return self.data[node.id]
def _set_Name(self, key, node, value):
self.data[node.id] = value
def _get_Num(self, key, node):
return node.n
def _get_Str(self, key, node):
return node.s
def _get_Constant(self, key, node):
return node.value
def _get_Subscript(self, key, node):
return self._get(key, node.value)[self._get(key, node.slice)]
def _set_Subscript(self, key, node, value):
self._get(key, node.value)[self._get(key, node.slice)] = value
def _get_Slice(self, key, node):
lower = node.lower and self._get(key, node.lower)
upper = node.upper and self._get(key, node.upper)
step = node.step and self._get(key, node.step)
return slice(lower, upper, step)
def _get_Index(self, key, node):
return self._get(key, node.value)
def _get_Attribute(self, key, node):
return getattr(self._get(key, node.value), node.attr)
def _set_Attribute(self, key, node, value):
setattr(self._get(key, node.value), node.attr, value)
def _get_Tuple(self, key, node):
return tuple(self._get(key, e) for e in node.elts)
def _set_Tuple(self, key, node, value):
i = iter(value)
n = 0
for e in node.elts:
try:
v = next(i)
except StopIteration:
raise ValueError(f'not enough values to unpack '
f'(expected {len(node.elts)}, got {n})')
self._set(key, e, v)
try:
next(i)
except StopIteration:
pass
else:
raise ValueError(f'too many values to unpack '
f'(expected {len(node.elts)})')

最新更新