22

I am wondering about the way Python (3.3.0) prints complex numbers. I am looking for an explanation, not a way to change the print.

Example:

>>> complex(1,1)-complex(1,1)
0j

Why doesn't it just print "0"? My guess is: to keep the output of type complex.

Next example:

>>> complex(0,1)*-1
(-0-1j)

Well, a simple "-1j" or "(-1j)" would have done. And why "-0"?? Isn't that the same as +0? It doesn't seem to be a rounding problem:

>>> (complex(0,1)*-1).real == 0.0
True

And when the imaginary part gets positive, the -0 vanishes:

>>> complex(0,1)
1j
>>> complex(0,1)*-1
(-0-1j)
>>> complex(0,1)*-1*-1
1j

Yet another example:

>>> complex(0,1)*complex(0,1)*-1
(1-0j)
>>> complex(0,1)*complex(0,1)*-1*-1
(-1+0j)
>>> (complex(0,1)*complex(0,1)*-1).imag
-0.0

Am I missing something here?

cxxl
  • 4,939
  • 3
  • 31
  • 52

4 Answers4

20

It prints 0j to indicate that it's still a complex value. You can also type it back in that way:

>>> 0j
0j

The rest is probably the result of the magic of IEEE 754 floating point representation, which makes a distinction between 0 and -0, the so-called signed zero. Basically, there's a single bit that says whether the number is positive or negative, regardless of whether the number happens to be zero. This explains why 1j * -1 gives something with a negative zero real part: the positive zero got multiplied by -1.

-0 is required by the standard to compare equal to +0, which explains why (1j * -1).real == 0.0 still holds.

The reason that Python still decides to print the -0, is that in the complex world these make a difference for branch cuts, for instance in the phase function:

>>> phase(complex(-1.0, 0.0))
3.141592653589793
>>> phase(complex(-1.0, -0.0))
-3.141592653589793

This is about the imaginary part, not the real part, but it's easy to imagine situations where the sign of the real part would make a similar difference.

Thomas
  • 174,939
  • 50
  • 355
  • 478
  • 3
    I just want to add to this answer, that all described cases are valid and python works as expected. You can find special test case with all representations relative to IEEE 754: `Lib/tests/test_complex.py`, `test_negative_zero_repr_str` – Alexey Kachayev Nov 14 '12 at 22:01
  • 1
    Just tested and it appears you're correct about `-0`: `>>> 0j.real.hex()` `'0x0.0p+0'` `>>> (0j*-1).real.hex()` `'-0x0.0p+0'` – Mark Ransom Nov 14 '12 at 22:02
  • I confirm the `-0` part: `print(0.0*-1)` prints "`-0.0`". And since `complex` type in Python is a pair of _float_ numbers, it doesn't matter whether you specified the fractional part or not. – ivan_pozdeev Nov 14 '12 at 23:41
  • The `repr` is wrong to use `'(-0-1j)'`. It needs to be `'-0.0-1j'` for `eval(repr(1j*-1))` to return the original value. `-0` is just `int(0)`. – Eryk Sun Nov 15 '12 at 11:19
  • Figured out the last part too, why we still want to print -0. – Thomas Nov 15 '12 at 19:54
  • Is there any way to test the imaginary part of a complex number and see if it's a negative or positive zero? – martineau Jan 27 '13 at 14:22
4

The answer lies in the Python source code itself.

I'll work with one of your examples. Let

a = complex(0,1)
b = complex(-1, 0)

When you doa*b you're calling this function:

real_part = a.real*b.real - a.imag*b.imag
imag_part = a.real*b.imag + a.imag*b.real

And if you do that in the python interpreter, you'll get

>>> real_part
-0.0
>>> imag_part
-1.0

From IEEE754, you're getting a negative zero, and since that's not +0, you get the parens and the real part when printing it.

if (v->cval.real == 0. && copysign(1.0, v->cval.real)==1.0) {
    /* Real part is +0: just output the imaginary part and do not
       include parens. */
...
else {
    /* Format imaginary part with sign, real part without. Include
       parens in the result. */
...

I guess (but I don't know for sure) that the rationale comes from the importance of that sign when calculating with elementary complex functions (there's a reference for this in the wikipedia article on signed zero).

jorgeca
  • 5,482
  • 3
  • 24
  • 36
3
  • 0j is an imaginary literal which indeed indicates a complex number rather than an integer or floating-point one.

  • The +-0 ("signed zero") is a result of Python's conformance to IEEE 754 floating point representation since in Python, complex is by definition a pair of floating point numbers. Due to the latter, there's no need to print or specify zero fraction parts for a complex too.

  • The -0 part is printed in order to accurately represent the contents as repr()'s documentation demands (repr() is implicitly called whenever an operation's result is output to the console).

  • Regarding the question why (-0+1j) = 1j but (1j*-1) = (-0+1j). Note that (-0+0j) or (-0.0+0j) aren't single complex numbers but expressions - an int/float added to a complex. To compute the result, first the first number is converted to a complex (-0-> (0.0,0.0) since integers don't have signed zeros, -0.0-> (-0.0,0.0)). Then its .real and .imag are added to the corresponding ones of 1j which are (+0.0,1.0). The result is (+0.0,1.0) :^) . To construct a complex directly, use complex(-0.0,1).

ivan_pozdeev
  • 33,874
  • 19
  • 107
  • 152
  • Thanks for the explanation about str/repr. But here a "print(0j*-1)" still returns "(-0+0j)", so it behaves the same. Furthermore, *if* python so strictly clings to the IEEE standard with its repr(), why does a "print(-0+0j)" return "0j"? – cxxl Nov 15 '12 at 07:06
  • 2cxxl: `(-0+0j)` isn't a single complex number but an expression - an integer added to a complex. When the result is computed, `-0` is converted to a complex and added to its `.real` which is `0`. The result is `+0` :^) – ivan_pozdeev Nov 15 '12 at 11:20
  • Since my assumption about `str()` proved false in this case, I wiped it. – ivan_pozdeev Nov 15 '12 at 11:51
1

As far as the first question is concerned: if it just printed 0 it would be mathematically correct, but you wouldn't know you were dealing with a complex object vs an int. As long as you don't specify .real you will always get a J component.

I'm not sure why you would ever get -0; it's not technically incorrect (-1 * 0 = 0) but it's syntactically odd.

As far as the rest goes, it's strange that it isn't consistent, however none are technically correct, just an artifact of the implementation.

Zero Piraeus
  • 56,143
  • 27
  • 150
  • 160
dbarnett
  • 85
  • 7