为什么 (inf + 0j)*1 的计算结果为 inf + nanj?

Why does (inf + 0j)*1 evaluate to inf + nanj?

>>> (float('inf')+0j)*1
(inf+nanj)

为什么?这在我的代码中造成了一个严重的错误。

为什么 1 不是乘法恒等式,给出 (inf + 0j)

首先将1转换为复数,1 + 0j,然后进行inf * 0乘法,得到nan.

(inf + 0j) * 1
(inf + 0j) * (1 + 0j)
inf * 1  + inf * 0j  + 0j * 1 + 0j * 0j
#          ^ this is where it comes from
inf  + nan j  + 0j - 0
inf  + nan j

从机制上讲,接受的答案当然是正确的,但我认为可以给出更深入的答案。

首先,澄清问题是有用的 @PeterCordes 在评论中做了:"Is there a multiplicative identity for 对 inf + 0j 有效的复数?" 或者换句话说就是 OP 发现计算机执行复数乘法的弱点,或者是 inf+0j

在概念上有些不合理

简答:

使用极坐标,我们可以将复数乘法视为缩放和旋转。将无限“手臂”旋转 0 度,就像乘以 1 的情况一样,我们不能期望以有限的精度放置其尖端。 所以确实,inf+0j 有一些根本不对的地方,即, 一旦我们处于无穷大,有限的偏移量就变得毫无意义。

长答案:

背景:这个问题所围绕的“大事”就是事情 扩展数字系统(想想实数或复数)。一个理由 人们可能想要这样做是添加一些无穷大的概念,或者 如果碰巧是数学家,则“紧凑化”。还有其他 也有原因 (https://en.wikipedia.org/wiki/Galois_theory, https://en.wikipedia.org/wiki/Non-standard_analysis),但我们对这里的那些不感兴趣。

一点压缩

这种扩展的棘手之处当然是我们需要这些新的 数字以适应现有的算法。最简单的方法是添加一个 无穷远处的单个元素 (https://en.wikipedia.org/wiki/Alexandroff_extension) 并使其等于除零以外的任何值除以零。这适用于 实数 (https://en.wikipedia.org/wiki/Projectively_extended_real_line) and the complex numbers (https://en.wikipedia.org/wiki/Riemann_sphere).

其他扩展...

虽然一点紧化很简单并且在数学上是合理的,但已经寻求包含多个无穷大的“更丰富”的扩展。用于实浮点数的 IEEE 754 标准有 +inf 和 -inf (https://en.wikipedia.org/wiki/Extended_real_number_line)。长相 自然而直接,但已经迫使我们跳过篮球和 发明像 -0 https://en.wikipedia.org/wiki/Signed_zero

这样的东西

... 复平面

复平面的多于一个 inf 扩展如何?

在计算机中,复数通常通过将两个 fp 实数粘在一起来实现,一个用于实部,一个用于虚部。只要一切都是有限的,那很好。然而,一旦无穷大被认为是事情变得棘手。

复平面具有自然的旋转对称性,这与复杂的算术很好地结合在一起,因为将整个平面乘以 e^phij 与绕 0.

的 phi 弧度旋转相同

那个附件G的东西

现在,为了简单起见,复数 fp 仅使用底层实数实现的扩展(+/-inf、nan 等)。这种选择可能看起来很自然,甚至不被视为一种选择,但让我们仔细看看它意味着什么。这个复平面扩展的简单可视化看起来像 (I = infinite, f = finite, 0 = 0)

I IIIIIIIII I
             
I fffffffff I
I fffffffff I
I fffffffff I
I fffffffff I
I ffff0ffff I
I fffffffff I
I fffffffff I
I fffffffff I
I fffffffff I
             
I IIIIIIIII I

但由于真正的复平面是复数乘法平面,因此信息量更大的投影是

     III    
 I         I  
    fffff    
   fffffff   
  fffffffff  
I fffffffff I
I ffff0ffff I
I fffffffff I
  fffffffff  
   fffffff   
    fffff    
 I         I 
     III    

在这个投影中,我们看到了无穷大的“不均匀分布”,这不仅丑陋,而且是 OP 遇到的问题的根源:大多数无穷大(形式为 (+/-inf,有限)和(有限,+/-inf)在四个主要方向上集中在一起,所有其他方向仅由四个无穷大(+/-inf,+-inf)表示。将复数乘法扩展到这种几何形状是一场噩梦。

C99 规范的附件 G 尽最大努力使其发挥作用,包括改变 infnan 交互方式的规则(本质上 inf 胜过 nan ). OP 的问题通过不将实数和拟议的纯虚类型提升为复数来回避,但是让实数 1 的行为与复数 1 不同并没有让我觉得这是一个解决方案。引人注目的是,附件 G 没有完全指定两个无穷大的乘积应该是什么。

我们可以做得更好吗?

通过选择更好的无穷大几何来尝试解决这些问题是很诱人的。类似于延长的实线,我们可以为每个方向添加一个无穷大。这种结构类似于射影平面,但不会将相反的方向混为一谈。 无穷大将以极坐标 inf x e^{2 omega pi i} 表示, 定义产品会很简单。尤其是OP的问题,自然就解决了

但这就是好消息结束的地方。在某种程度上,我们可能会被——并非不合理地——要求我们的新式无穷大支持提取实部或虚部的函数而回到原点。添加是另一个问题;添加两个非对映无穷大我们必须将角度设置为未定义,即 nan =28=]

黎曼救场

鉴于这一切,也许最好的旧一点紧凑化是最安全的做法。也许附件 G 的作者在强制执行将所有无穷大集中在一起的函数 cproj 时也有同感。


这里是 a related question 比我更能干的人的回答。

这是关于如何在 CPython 中实现复数乘法的实现细节。与其他语言(例如 C 或 C++)不同,CPython 采用了一种稍微简单化的方法:

  1. ints/floats 在乘法中被提升为复数
  2. 简单的 school-formula is used,一旦涉及无穷大,它就不会提供 desired/expected 结果:
Py_complex
_Py_c_prod(Py_complex a, Py_complex b)
{
    Py_complex r;
    r.real = a.real*b.real - a.imag*b.imag;
    r.imag = a.real*b.imag + a.imag*b.real;
    return r;
}

上述代码的一个问题案例是:

(0.0+1.0*j)*(inf+inf*j) = (0.0*inf-1*inf)+(0.0*inf+1.0*inf)j
                        =  nan + nan*j

但是,结果是 -inf + inf*j

在这方面其他语言并没有遥遥领先:复数乘法很长一段时间都不是 C 标准的一部分,仅作为附录 G 包含在 C99 中,它描述了应该如何执行复数乘法 - 而且它并不像上面的学校公式那么简单! C++ 标准没有指定复杂的乘法应该如何工作,因此大多数编译器实现都回落到 C 实现,这可能是 C99 符合(gcc、clang)或不符合(MSVC)。

对于上述 "problematic" 示例,符合 C99 的实现(more complicated than the school formula) would give (see live)预期结果:

(0.0+1.0*j)*(inf+inf*j) = -inf + inf*j 

即使使用 C99 标准,也不会为所有输入定义明确的结果,即使对于符合 C99 的版本,结果也可能不同。

在 C99 中,float 未被提升为 complex 的另一个副作用是,将 inf+0.0j1.01.0+0.0j 相乘会导致不同的结果(现场直播):

  • (inf+0.0j)*1.0 = inf+0.0j
  • (inf+0.0j)*(1.0+0.0j) = inf-nanj,虚部是 -nan 而不是 nan(对于 CPython)在这里不起作用,因为所有安静的 nan 都是等价的(见 this), even some of them have sign-bit set (and thus printed as "-", see this) 有些不是。

这至少是违反直觉的。


我的主要收获是:"simple" 复数乘法(或除法)并不简单,当在语言甚至编译器之间切换时,必须为微妙的 bugs/differences 做好准备.

来自 Python 的有趣定义。如果我们用笔和纸解决这个问题,我会说预期结果将是 expected: (inf + 0j) 正如您所指出的,因为我们知道我们指的是 1 的范数,所以 (float('inf')+0j)*1 =should= ('inf'+0j):

但正如您所见,情况并非如此...当我们 运行 时,我们得到:

>>> Complex( float('inf') , 0j ) * 1
result: (inf + nanj)

Python 将此 *1 理解为复数而不是 1 的范数,因此它解释为 *(1+0j) 并且当我们尝试执行 inf * 0j = nanj 因为 inf*0 无法解析。

你真正想做的(假设1是1的范数):

回想一下,如果z = x + iy是实部x虚部y的复数,z的复共轭定义为z* = x − iy,其绝对值,也称为 norm of z 定义为:

假设 11 的标准,我们应该这样做:

>>> c_num = complex(float('inf'),0)
>>> value = 1
>>> realPart=(c_num.real)*value
>>> imagPart=(c_num.imag)*value
>>> complex(realPart,imagPart)
result: (inf+0j)

我知道不是很直观...但有时编码语言的定义方式与我们日常使用的方式不同。