V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
xuegy
V2EX  ›  Python

大量操作 dict 内元素时有什么能省略 dict 名字的语法糖?

  •  
  •   xuegy · 2023-09-21 10:11:23 +08:00 · 2519 次点击
    这是一个创建于 427 天前的主题,其中的信息可能已经有所发展或是发生改变。

    标题可能描述的不够具体。大致操作是从 json 读入数据,经过一通计算,用 python-docx-replace 替换模版快速生成文档。代码举例(省略数字格式化字符串部分):

    dict = json.load(xxx)
    dict[A]=dict[B]+dict[C]
    dict[D]=dict[E]-dict[F]
    dict[G]=dict[H]*dict[I]
    dict[J]=dict[K]/dict[L]
    ...
    docx_replace(doc, **dict)
    

    因为实际代码是很长的数学公式,个人觉得写这么多dict[]可读性实在太差,于是采用了如下写法(我知道不妥,但是想不出更好的),被 LD 批评很不 Pythonic 。

    dict = json.load(xxx)
    locals().update(dict)
    A=B+C
    D=E-F
    G=H*I
    J=K/L
    ...
    dict.update({key: value for (key, value) in locals().items() if type(value) == int or type(value) == float})
    docx_replace(doc, **dict)
    

    求助各位高人有没有更合理的写法?

    34 条回复    2023-09-21 18:44:28 +08:00
    thinkershare
        1
    thinkershare  
       2023-09-21 10:40:35 +08:00   ❤️ 2
    没有办法,python 不支持这种类似 js 的 with 临时作用域附加(js 的 with 大多数时候也被认为是一个不好的东西),因为 python 字典的 key 很可能不是一个合法的变量名称,而且它的类型也不一定是一个字符串。
    不管使用 local()或者 global 都是一个糟糕的实现方式。
    xuegy
        2
    xuegy  
    OP
       2023-09-21 10:47:35 +08:00
    @thinkershare 有个前提是,我的 json 文件可以保证所有的 key 都是合法的变量名称
    hitmanx
        3
    hitmanx  
       2023-09-21 10:51:18 +08:00
    想到一个更 hack 的方法,把计算放到一个 function 里,定义一个类似 C/C++里面"preprocess"的 decorator 加在函数上。

    在这个 decorator 的实现:通过 inspect.getsource(func)去拿到 source 。然后每一行里把 dict 里的存在的 token 替换成 dict[token],最后调用 exec()去执行替换完的字符串。

    相当于你自己实现了一个 preprocessor
    Ricardoo
        4
    Ricardoo  
       2023-09-21 10:52:31 +08:00   ❤️ 1
    这种我一般会转成类
    my_json = type('MyJson', (object,), my_json)
    my_json.A = my_json.B + my_json.C

    只比 dict 方式强一丢丢
    thinkershare
        5
    thinkershare  
       2023-09-21 10:53:59 +08:00   ❤️ 1
    @xuegy 没有办法,因为变量在现代编程语言中都是很特殊的存在。任何尝试动态解构变量的做法都会导致性能的下降,因为编译器会尝试在今日函数前对函数做优化。确定要给整个函数分配的堆栈空间大小,如果搞动态变量注入到局部,那么函数将无法优化,函数的机器码也无法被缓存重用,这些都是实实在在的性能问题。javascript 当初对 with 的支持就是一个错误。python 已经有很多语法都是因为容易写但性能差而被滥用了,你看看现在 python 写的很多程序的性能为什么如此差就明白了,python 现在支持的语法已经非常容易让人写出性能极差,时间复杂度极高的代码了。可读性和性能有时候就是相互矛盾的。
    我们目前一般是这么做的 d=EasyDict(json.load(txt), d.a+d.b 这样。
    stein42
        6
    stein42  
       2023-09-21 10:54:01 +08:00
    exec 函数了解一下
    ```
    env = {'a': 1, 'b': 2}
    exec('c = a + b', None, env)
    print(env)
    ```
    FYFX
        7
    FYFX  
       2023-09-21 11:04:19 +08:00
    你字符串转换成运行时的变量名, 不管怎么实现都是一种不安全的操作了
    xuegy
        8
    xuegy  
    OP
       2023-09-21 11:06:36 +08:00
    @thinkershare 这个 easydict 看起来是一个可以接受的折中方案
    wuwukai007
        9
    wuwukai007  
       2023-09-21 11:11:36 +08:00
    BingoXuan
        10
    BingoXuan  
       2023-09-21 11:14:05 +08:00
    def some_cal(a=None,b=None,c=None):
    # return anything serializable, like namedtuple

    data = json.loads(json_str)
    try:
    res=some_cal(**data)
    except:
    ...# handle exception
    else:
    json.dumps(res)
    Tanix2
        11
    Tanix2  
       2023-09-21 11:14:10 +08:00
    如果计算都是示例那样两元素的加减乘除,那么可以使用如下代码

    import re

    d = {
    'B': 2,
    'C': 3,
    }


    def dict_calc(d: dict, text: str):
    for line in text.splitlines(keepends=False):
    sp = re.split(r'([=+\-*/])', line)
    if len(sp) == 5:
    sp = map(str.strip, sp)
    a, eq, b, op, c = sp
    if eq == '=' and op in '+-*/':
    d[a] = eval(f'{d[b]}{op}{d[c]}')


    dict_calc(d, '''
    A = B + C
    D = A - C
    E = A * D
    F = E / D
    ''')

    print(d)
    # Output:
    # {'B': 2, 'C': 3, 'A': 5, 'D': 2, 'E': 10, 'F': 5.0}
    xuegy
        12
    xuegy  
    OP
       2023-09-21 11:15:21 +08:00
    @Tanix2 是很长很长的数学公式,什么都有,甚至还要用一下 numpy
    Leviathann
        13
    Leviathann  
       2023-09-21 11:16:40 +08:00
    你这个本质上是在造 dsl
    mylifcc
        14
    mylifcc  
       2023-09-21 11:20:00 +08:00
    我想说 如果是我会写成
    dict[A]=dict.get("B", 0) + dict.get("C", 0)
    ......
    liuhai233
        15
    liuhai233  
       2023-09-21 11:26:22 +08:00
    看起来你这个 json 结构是固定的,为什么不考虑 parse 成 pydantic 对象呢,感觉很多人写 python 就不考虑创建 model 类了
    Tanix2
        16
    Tanix2  
       2023-09-21 11:29:08 +08:00
    @xuegy 如果你能找到 A 、B 、C 这样的名称的规律,可以用正则表达式把它们都找出来(只找等号右侧),然后再 eval ,不过这样写是没有代码提示的,也存在安全性问题。
    iOCZ
        17
    iOCZ  
       2023-09-21 11:31:44 +08:00
    没必要
    LandCruiser
        18
    LandCruiser  
       2023-09-21 11:32:55 +08:00
    没有人说第二种可读性太差吗
    xuegy
        19
    xuegy  
    OP
       2023-09-21 11:43:34 +08:00
    @LandCruiser 你告诉我,像这种东西:
    PL = PA * C3 * FR * k/(k-1.0) * N * C4 * (np.power(P0 / float(PA), (k-1.0) / (k*N)) - 1.0) / (EA * EM)
    全写成 dict 还能看吗?
    yesterdaysun
        20
    yesterdaysun  
       2023-09-21 11:57:39 +08:00
    要不用解构的方式导出本地变量计算, 算完再导回 dict

    A, B, C, D, E, F, G, H, I, J, K, L = dict.values()

    A = B + C
    D = E - F
    G = H * I
    J = K / L

    dict = {"A": A, "B": B, "C": C, "D": D, "E": E, "F": F, "G": G, "H": H, "I": I, "J": J, "K": K, "L": L}
    aloxaf
        21
    aloxaf  
       2023-09-21 12:00:44 +08:00
    我感觉只能用 locals 了,如果你们 leader 觉得不够 pythonic ,你可以间接使用,就像这样

    def calc():
    return A + B

    env = {"A": 1, "B": 2}
    print(eval(calc.__code__, env))
    hitmanx
        22
    hitmanx  
       2023-09-21 12:07:51 +08:00
    >> PL = PA * C3 * FR * k/(k-1.0) * N * C4 * (np.power(P0 / float(PA), (k-1.0) / (k*N)) - 1.0) / (EA * EM)

    @xuegy 可能可以把表达式通过 Python AST( https://docs.python.org/3/library/ast.html)转成抽象语法树(AST),然后在 iterate 这个 AST 的时候把 node 替换成对应的 dict value
    hitmanx
        23
    hitmanx  
       2023-09-21 12:11:09 +08:00
    这是我让 chatgpt4 根据这个 idea 写的代码:
    ```
    import ast
    import json

    data = json.load(xxx) # some sample data source


    class TransformVarToDict(ast.NodeTransformer):
    def visit_Name(self, node):
    # Replace variable reference with dictionary access
    if isinstance(node.ctx, (ast.Load, ast.Store)):
    return ast.Subscript(
    value=ast.Name(id='data', ctx=ast.Load()),
    slice=ast.Index(value=ast.Str(s=node.id)),
    ctx=node.ctx
    )
    return node


    def process_formula(formula_str):
    # Parse the formula to an AST
    parsed = ast.parse(formula_str)

    # Transform the AST
    transformed = TransformVarToDict().visit(parsed)
    ast.fix_missing_locations(transformed) # Fix line numbers

    # Compile and execute the modified AST
    code = compile(transformed, '<string>', 'exec')
    exec(code, globals())


    # Sample formulas
    formulas = [
    "A = B + C",
    "D = E - F",
    "G = H * I",
    "J = K / L"
    ]


    for formula in formulas:
    process_formula(formula)


    print(data)
    ```
    xuegy
        24
    xuegy  
    OP
       2023-09-21 12:12:08 +08:00
    @hitmanx 不现实,我的任务是写一个类似于范文的东西,然后让一群学机械工程的人照着我这个范本写大量类似的东西出来。所以原则上来说,语法越像 MATLAB 那样简单直接越好。
    hitmanx
        25
    hitmanx  
       2023-09-21 12:16:19 +08:00
    @xuegy 你可能没明白这个意思。这个不需要你自己定义 DSL ,还是用 python 的 syntax 。

    所以只要你的表达式本来就是 python 的 syntax ,直接一行不改应该就能用 python ast 把它 parse 成 AST 。然后只是遍历的时候把它替换成对应的 dict form 而已。

    当然,所以依赖于 exec(string)的方法都会有 security 的问题,需要你的输入是 sanitized
    MoYi123
        26
    MoYi123  
       2023-09-21 12:29:22 +08:00
    json parse 到 class 里面, 然后加几个 method 去算不就好了? 用 ast 和 eval 真的有点搞了吧.
    Maboroshii
        27
    Maboroshii  
       2023-09-21 13:53:51 +08:00 via Android
    加减乘除 括号,自己写一个解析字符串的计算器吧。
    NoOneNoBody
        28
    NoOneNoBody  
       2023-09-21 14:14:09 +08:00   ❤️ 1
    @LandCruiser
    @MoYi123
    起初我也不明白,虽然繁琐,但无法避免逐条算式写,那先写简单的,然后在编辑器用正则替换就可以了,直到#24

    对于不懂 python 的人来说,反而第二种可读性是高的
    看 OP #24 所说,看样子恰好就是这样,要给不懂 python 的人写这堆算式,跟 dict 什么的无关,就是指代某个对象某个 item

    最简便就是 SimpleNamaspace
    In [18]: from types import SimpleNamespace

    In [19]: d=SimpleNamespace(**{'A':1,'B':2,'C':3})

    In [20]: d.A=d.B+d.C

    In [21]: d
    Out[21]: namespace(A=5, B=2, C=3)
    victorc
        29
    victorc  
       2023-09-21 14:30:41 +08:00
    “写这么多 dict[]可读性实在太差”--- 这样写反而清楚明了

    写程序要朴素,别玩花了
    ipwx
        30
    ipwx  
       2023-09-21 14:36:51 +08:00
    为什么我觉得楼主这个场景里面,需要执行的代码是可信的(自己写的),只不过变量值是外部读入的?

    那直接构造 locals() 用 exec/eval 不就行了。。。
    pursuer
        31
    pursuer  
       2023-09-21 14:40:40 +08:00
    直接修改 locals 真的可以吗? 我记得不行啊,只能改 globals 或者 exec ,我刚刚还特意去试了下 3.8 和 3.10 不行啊,是哪个版本支持了吗?
    xuegy
        32
    xuegy  
    OP
       2023-09-21 15:43:13 +08:00 via iPhone
    @NoOneNoBody 这个和 easydict 的简化效果差不多。不过 easydict 更好一点,可以直接送进 python-docx-replace 替换
    xuegy
        33
    xuegy  
    OP
       2023-09-21 15:48:54 +08:00 via iPhone
    @ipwx 原理上是这样的,主要是这个东西的可维护性和普通 python 代码的可维护性不是一回事。
    我希望它是一种把前后都写好,中间就能像 MATLAB 一样的,这样机械工程的人可以直接往中间填公式。
    ZX576
        34
    ZX576  
       2023-09-21 18:44:28 +08:00
    用 Pydantic 序列化,然后 __init__ 里修改,最后再 dict 导出,既清晰,扩展性又强,还能随便帮你检查 dict 中的值
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   956 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 21:25 · PVG 05:25 · LAX 13:25 · JFK 16:25
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.