将 Python 复杂字符串输出如 (-0-0j) 转换为等效的复杂字符串

Converting Python complex string output like (-0-0j) into an equivalent complex string

在 Python 中,我想要一种将其复数字符串输出转换为等效字符串表示形式的好方法,当由 Python 解释时,它给出相同的值。

基本上我想要函数 complexStr2str(s: str): str 具有 属性,eval(complexStr2str(str(c)))c 无法区分,对于任何 c 其值为类型复杂。但是 complexStr2str() 只需要处理 str()repr() 输出的复杂值的字符串模式。请注意,对于复数 str()repr() 做同样的事情。

我所说的“无法区分”并不是指 Python 意义上的 ==;您可以定义(或重新定义)任何您想要的意思; “无法区分”意味着如果你在程序中有字符串 a 代表某个值,并在程序中用字符串 b 替换它(可能正好是 a),那么就有没有办法区分 Python 程序的 运行 和替换程序,缺少程序的自省。

请注意 (-0-0j)-0j 不同,尽管前者是 Python 将为 str(-0j)repr(-0j) 输出的内容。如下图所示,-0j 有实部和虚部 float -0.0 而 -0-0j 有实部和虚部 float positive 0.0.

如果存在 naninf 这样的值,问题会变得更加困难。虽然在 Python 3.5+ ish 中你可以 import 来自 math 的这些值,出于各种原因,我想避免这样做。但是使用 float("nan") 没问题。

考虑这个 Python 会话:

>>> -0j
(-0-0j)
>>> -0j.imag
-0.0
>>> -0j.real
-0.0
>>> (-0-0j).imag
0.0  # this is not -0.0
>>> (-0-0j).real
0.0  # this is also not -0.0
>>> eval("-0-0j")
0j # and so this is -0j
>>> atan2(-0.0, -1.0)
-3.141592653589793
>>> atan2((-0-0j).imag, -1.0)
3.141592653589793
>>> -1e500j
(-0-infj)
>>> (-0-infj)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'infj' is not defined

附录:

这个问题引起了一些轰动(例如,这个问题及其可接受的解决方案有很多反对票)。并且对该问题进行了大量编辑,因此某些评论可能已过时。

批评的主旨是人们不应该想要这样做。从现有程序的文本中解析数据是经常发生的事情,有时您无法控制生成数据的程序。

一个可以控制输出程序但需要让它出现在文本中的相关问题是编写一个更好的 repr() 函数,该函数更适合浮点数和复数,并遵循描述于结束。这样做很简单,即使它有点难看,因为要完全做到这一点,您还需要处理 float/complex 复合类型,如列表、元组、集合和字典。

最后,我要说的是,Python 的 str()repr() 复值输出似乎没有帮助,这就是为什么这个问题更具体到 Python 比其他支持复数作为原始数据类型或通过库支持的语言要好。

这是一个显示此内容的会话:

>>> complex(-0.0, -0.0)
(-0-0j)  # confusing and can lead to problems if eval'd
>>> repr(complex(-0.0, -0.0))
'(-0-0j)' # 'complex(-0.0, -0.0)' would be the simplest, clearest, and most useful

请注意,在通过 print() 进行输出时会调用 str()repr() 是这种使用的首选方法,但这里它与 str() 相同,并且都存在 infnan.

等问题

对于任何 built-in type (eval(repr(c)) 应该与 c.

没有区别

这道题是基于错误的前提。要在使用复数时正确保留带符号的零、nan 和无穷大,您应该使用函数调用而不是 binops:

complex(real, imag)

应该用两个浮点数调用它:

>>> complex(-0., -0.)  # correct usage
(-0-0j)
>>> complex(-0, -0j)  # incorrect usage
-0j

您尝试使用 eval 文字的问题是 -0-0j 实际上不是复杂文字。它是一个二元运算,整数 0 与复数 0j 的减法。整数首先应用了 unary sub,但那是整数零的空操作。

解析器将揭示这一点:

>>> ast.dump(ast.parse("-0-0j"))
'Module(body=[Expr(value=BinOp(left=UnaryOp(op=USub(), operand=Constant(value=0, kind=None)), op=Sub(), right=Constant(value=0j, kind=None)))], type_ignores=[])'

Python 在这里的选择如果你理解 tokenizer 的工作原理会更有意义,它不想回溯:

$ echo "-0-0j" > wtf.py
$ python -m tokenize wtf.py
0,0-0,0:            ENCODING       'utf-8'        
1,0-1,1:            OP             '-'            
1,1-1,2:            NUMBER         '0'            
1,2-1,3:            OP             '-'            
1,3-1,5:            NUMBER         '0j'           
1,5-1,6:            NEWLINE        '\n'           
2,0-2,0:            ENDMARKER      ''

但是你自己也可以很容易地从数据模型挂钩和运算符优先级中推断出来:

>>> -0-0j  # this result seems weird at first
0j
>>> -(0) - (0j)  # but it's parsed like this
0j
>>> (0) - (0j)  # unary op (0).__neg__() applies first, does nothing
0j
>>> (0).__sub__(0j)  # left-hand side asked to handle first, but opts out
NotImplemented
>>> (0j).__rsub__(0)  # right-hand side gets second shot, reflected op works
0j

同样的推理适用于-0j,它实际上是一个否定,实部也被隐式否定:

>>> -0j  # where did the negative zero real part come from?
(-0-0j)
>>> -(0j)  # actually parsed like this
(-0-0j)
>>> (0j).__neg__()  # so *both* real and imag parts are negated
(-0-0j)

说到这一部分,把矛头指向了错误的方向:

Python's str() representation for complex numbers with negative real and imaginary parts is unhelpful

不,这里 __str__ 的实现没有任何错误,而且你对 complex(-0,-0j) 的使用让我怀疑你一开始就没有完全理解发生了什么。首先,没有理由写 -0 因为整数没有带符号的零,只有浮点数。正如我上面解释的那样,虚部 -0j 仍然被解析为复数上的 USub 。通常你不会在这里传递一个虚数本身作为虚部,调用 complex 的正确方法就是用两个浮点数:complex(-0., -0.)。这里没有惊喜。

虽然我同意复杂表达式的 parsing/eval 是违反直觉的,但我不同意它们的字符串表示有任何问题。 "improve" 对表达式求值的建议可能是可行的,目标是使 eval(repr(c)) 完全往返 - 但这意味着你不能使用 Python 的左 -向右咀嚼解析器了。该解析器快速、简单且易于解释。 为了使涉及复数零的表达式表现得不那么奇怪而使解析树变得非常复杂是不公平的权衡,当没有人需要关心这些细节时应该选择repr(c) 作为他们的序列化格式。

请注意,ast.literal_eval 只是为了方便才允许这样做。尽管 not 是文字,但 ast.literal_eval("0+0j") 仍然有效,反之亦然:

>>> ast.literal_eval("0+0j")
0j
>>> ast.literal_eval("0j+0")
ValueError: malformed node or string: <_ast.BinOp object at 0xcafeb4be>

总之,复数的字符串表示是没问题的。这是您创建重要数字的方式。 str(c) 用于人类可读的输出,如果您关心保留带符号的零、nan 和无穷大,请使用机器友好的序列化格式。

因为eval(repr(c))方法不适用于complex类型,使用pickle是序列化数据最可靠的方法:

import pickle


numbers = [
    complex(0.0, 0.0),
    complex(-0.0, 0.0),
    complex(0.0, -0.0),
    complex(-0.0, -0.0),
]
serialized = [pickle.dumps(n) for n in numbers]

for n, s in zip(numbers, serialized):
    print(n, pickle.loads(s))

输出:

0j 0j
(-0+0j) (-0+0j)
-0j -0j
(-0-0j) (-0-0j)

正如@wim 在评论中指出的那样,这可能不是真正问题的正确解决方案;最好不要首先通过 str 将这些复数转换为字符串。关心正零和负零之间的差异也很不寻常。但是我可以想象在极少数情况下您确实关心这种差异,并且在得到 str() 之前访问复数不是一种选择;所以这是一个直接的答案。

我们可以用正则表达式匹配零件; [+-]?(?:(?:[0-9.]|[eE][+-]?)+|nan|inf) 对于匹配浮点数来说有点松散,但它可以。我们需要在匹配的部分使用 str(float(...)) 以确保它们作为浮点字符串是安全的;所以例如'-0' 被映射到 '-0.0'。我们还需要无穷大和 NaN 的特殊情况,因此它们被映射到可执行的 Python 代码 "float('...')",它将产生正确的值。

import re

FLOAT_REGEX = r'[+-]?(?:(?:[0-9.]|[eE][+-]?)+|nan|inf)'
COMPLEX_PATTERN = re.compile(r'^\(?(' + FLOAT_REGEX + r'\b)?(?:(' + FLOAT_REGEX + r')j)?\)?$')

def complexStr2str(s):
    m = COMPLEX_PATTERN.match(s)
    if not m:
        raise ValueError('Invalid complex literal: ' + s)

    def safe_float(t):
        t = str(float(0 if t is None else t))
        if t in ('inf', '-inf', 'nan'):
            t = "float('" + t + "')"
        return t

    real, imag = m.group(1), m.group(2)
    return 'complex({0}, {1})'.format(safe_float(real), safe_float(imag))

示例:

>>> complexStr2str(str(complex(0.0, 0.0)))
'complex(0.0, 0.0)'
>>> complexStr2str(str(complex(-0.0, 0.0)))
'complex(-0.0, 0.0)'
>>> complexStr2str(str(complex(0.0, -0.0)))
'complex(0.0, -0.0)'
>>> complexStr2str(str(complex(-0.0, -0.0)))
'complex(-0.0, -0.0)'
>>> complexStr2str(str(complex(float('inf'), float('-inf'))))
"complex(float('inf'), float('-inf'))"
>>> complexStr2str(str(complex(float('nan'), float('nan'))))
"complex(float('nan'), float('nan'))"
>>> complexStr2str(str(complex(1e100, 1e-200)))
'complex(1e+100, 1e-200)'
>>> complexStr2str(str(complex(1e-100, 1e200)))
'complex(1e-100, 1e+200)'

字符串输入示例:

>>> complexStr2str('100')
'complex(100.0, 0.0)'
>>> complexStr2str('100j')
'complex(0.0, 100.0)'
>>> complexStr2str('-0')
'complex(-0.0, 0.0)'
>>> complexStr2str('-0j')
'complex(0.0, -0.0)'