Python复数与指数运算

最近在学 FFT,里头频繁出现复数和复指数项 e^{j\omega t},索性把 Python 里处理复数的几个工具梳理一下:内置的 complex 类型、标准库的 cmath 模块,以及 numpy 在数组场景下的覆盖。


前言:complex 类型与 cmath 模块

关于 complex 类型

complex 是 Python 自带的数值类型,从 Python 1.4(1996 年)就已经存在,和 intfloatbool 同属内置类型,不用 import 就能直接拿来用。每个复数对象内部由两个 float 组成,分别对应实部 real 和虚部 imag。它的字面量写法比较特别,要借助 j 来表示虚数单位,比如 3+4j。和 intfloat 一样,复数对象也是不可变的(immutable)。

>>> isinstance(1+1j, complex)
True

关于 cmath 模块

cmath 也是 Python 标准库里的老成员,从 1.5 版本(1997 年)就提供了。它的定位是 math 模块的复数版本,里面所有函数都能接收复数参数并返回复数结果。包含的东西大致分几块:指数和对数(exploglog10sqrt)、三角与反三角函数(sincostanasin 等)、双曲函数(sinhcosh 之类)、极坐标转换(polarrectphase),还有一些常量(pieinfnantau 等)。日常使用场景多见于信号处理、电气工程里的阻抗计算、量子力学公式以及傅里叶分析。

>>> import cmath
>>> cmath.sqrt(-1)         # math.sqrt(-1) 会报错
1j
💡 选用建议

处理实数选 math,处理单个复数选 cmath,处理数组(不论实数还是复数)就交给 numpy

一、复数的创建

复数对象有两种创建方式。第一种是字面量写法,直接写 1+1j,比较推荐这种;第二种是借助构造函数 complex(1, 1)

>>> z1 = 1 + 1j          # 字面量方式(推荐)
>>> type(z1)
<class 'complex'>

>>> z2 = complex(1, 1)   # 构造函数方式
>>> z1 == z2
True
⚠️ 虚数单位必须写成 1j

Python 里没有一个叫 j 的内置变量,单独写 j 会直接抛 NameError,必须带上系数写成 1j2j 这样的形式。

math.exp(-j) 错 → NameError: name 'j' is not defined

cmath.exp(-1j) 对 → 正确写法

值相等并不等于同一对象

>>> z1 == z2
True
>>> id(z1), id(z2)
(2009311009456, 2009311009264)   # 不同的内存地址

复数对象虽然是不可变的,但 CPython 并没有像处理小整数那样去缓存复数实例,因而两个值相同的复数仍然位于不同的内存地址。


二、复数的属性与方法

dir(z1) 可以把复数对象身上的所有方法都列出来。把魔法方法剔除掉以后,公开的属性和方法其实只有三个。

名称 类别 含义 示例
.real 属性 实部(float z1.real → 1.0
.imag 属性 虚部(float z1.imag → 1.0
.conjugate() 方法 共轭复数 z1.conjugate() → (1-1j)
>>> z1.real
1.0
>>> z1.imag
1.0
>>> z1.conjugate          # 不加括号 → 返回方法对象
<built-in method conjugate of complex object at 0x...>
>>> z1.conjugate()        # 加括号 → 调用方法
(1-1j)
💡 conjugate 与 conjugate() 的区别

conjugate 是一个方法,要拿到共轭值就必须加括号去调用它;不加括号只是在引用方法对象本身而已。反过来,realimag 属于属性,是不能加括号的。

复数支持的运算

>>> z1 + z2
(2+2j)

complex 实现了 __add____sub____mul____truediv____pow____abs__ 等魔法方法,因而可以直接使用 +-*/** 以及 abs()

引用语义与不可变性

复数虽然是不可变对象,但很多人会被 id() 的变化搞糊涂。下面三个现象放在一起看,才算完整。

现象 1:变量赋值是引用复制

>>> z3 = 1 - 2j
>>> z1 = z3
>>> z1 == z3
True
>>> id(z1), id(z3)
(2389253579408, 2389253579408)   # 同一对象

现象 2:重新赋值会创建新对象

>>> z3 = 1 - 2j              # 看似"赋同样的值",实则创建新对象
>>> id(z1), id(z3)
(2389253579408, 2389253580400)  # z3 换了,z1 不动

z1 和 z3 指向的复数值相等,但 id 已经分开。这也印证了 复数不会被 CPython 缓存,不像 -5 ~ 256 的小整数池那样复用。

现象 3:真正的不可变性证明

>>> z = 1 + 2j
>>> z.real = 5
AttributeError: readonly attribute
>>> z.imag = 3
AttributeError: readonly attribute

属性只读,无法原地修改——这才是 immutable 的硬证据。所以日常使用中,对复数的"修改"只能通过创建新对象来实现。

💡 引用、不可变、缓存——三件事别混为一谈

  • 引用语义a = b 共享对象,这是 Python 所有对象的统一行为
  • 不可变性:对象内部状态无法被修改,由类型决定(intfloattuplecomplex 都是)
  • 对象缓存:CPython 的实现层优化,只对小整数和短字符串生效

运行截图
运行截图


三、指数运算:math vs cmath vs numpy

🚫 math.exp() 不接受复数

math.exp(-1j) 会直接抛出 TypeError: must be real number, not complex。要算复数指数,就得改用 cmathnumpy

math 模块(仅实数标量)

>>> import math
>>> math.exp(1)
2.718281828459045        # 即数学常数 e
>>> math.exp([1, 2])     # 不支持序列/数组
TypeError: must be real number, not list

cmath 模块(复数标量)

>>> import cmath
>>> cmath.exp(-1j)
(0.5403023058681398-0.8414709848078965j)
>>> cmath.pi
3.141592653589793
>>> cmath.exp([1, 2])    # 同样不支持数组
TypeError

numpy.exp(实数、复数、数组都通吃)

>>> import numpy as np

# 1. 实数(等价 math.exp)
>>> np.exp(1)
np.float64(2.718281828459045)

# 2. 复数(等价 cmath.exp)
>>> np.exp(-1j)
np.complex128(0.5403023058681398-0.8414709848078965j)

# 3. 数组(math/cmath 做不到)
>>> np.exp([0, 1, 2])
array([1.        , 2.71828183, 7.3890561 ])

# 4. 复数数组(numpy 真正能派上用场的地方)
>>> np.exp([1j, -1j, 2j])
array([0.5403+0.8415j, 0.5403-0.8415j, -0.4161+0.9093j])
💬 借助 numpy 一次性验证欧拉公式

欧拉公式 e^{i\theta} = \cos\theta + i\sin\theta\theta = 0, \pi/4, \pi/2, \pi 时,对应单位圆上的四个点。

theta = np.array([0, np.pi/4, np.pi/2, np.pi])

np.exp(1j * theta) 一行就能得到 [1+0j, 0.707+0.707j, 0+1j, -1+0j]

要是用 mathcmath,就只能自己写循环了。

三方对照表

功能 math cmath numpy 说明
自然指数 math.exp(x) cmath.exp(z) np.exp(...) numpy 三类全支持
圆周率 math.pi cmath.pi np.pi 都是 float
自然常数 e math.e cmath.e np.e 都是 float
对数 math.log cmath.log np.log cmath / numpy 接受负数和复数
三角函数 math.sin/cos cmath.sin/cos np.sin/cos cmath / numpy 接受复数
输入支持 实数标量 复数标量 标量 + 数组(实/复) numpy 最通用
返回类型 float complex np.float64 / np.complex128 / ndarray numpy 类型可向量化
💡 三种场景的选择建议

  • 单个实数的运算:math.exp 最快,也最轻量
  • 单个复数的运算:cmath.exp
  • 数组、批量值,或者要和其它 numpy 运算混在一起用:首选 np.exp

四、复数的模(绝对值)

方式 1:内置 abs()(只能处理标量)

>>> abs(1 + 1j)
1.4142135623730951
>>> type(abs(1 + 1j))
<class 'float'>
>>> abs([1+1j, 2+2j])         # 不支持列表/数组
TypeError: bad operand type for abs(): 'list'

方式 2:cmath 没有 abs

cmath 模块里并没有提供 abs 函数,求模直接借助内置 abs() 就好。

>>> import cmath
>>> abs(cmath.exp(-1j))       # 配合 cmath 的结果使用内置 abs
0.9999999999999999             # ≈ 1(单位圆上)
ℹ️ cmath.polar(z) 会返回一个 (模, 辐角) 形式的元组,第一个元素就是模值。

cmath.polar(1 + 1j)(1.4142135623730951, 0.7853981633974483)

方式 3:numpy.abs()(标量和数组都能处理)

>>> import numpy as np

# 标量
>>> np.abs(1 + 1j)
np.float64(1.4142135623730951)

# 实数数组
>>> np.abs([-3, -2, 5])
array([3, 2, 5])

# 复数数组(numpy 在这种场景下最能派上用场)
>>> np.abs([1+1j, 3+4j, -1j])
array([1.41421356, 5.        , 1.        ])
ℹ️ 数学定义:复数 z = a + bi 的模为 |z| = \sqrt{a^2 + b^2}

三种调用方式对照表

调用方式 实数标量 复数标量 实数数组 复数数组 返回类型
abs(x) 支持 支持 不支持 不支持 int / float
cmath.polar(z)[0] 不支持 支持 不支持 不支持 float
np.abs(x) 支持 支持 支持 支持 np.float64 / ndarray
💡 经验法则:处理单个值就用内置 abs();只要涉及数组,或者要和 np.expnp.fft 之类的 numpy 运算链式配合,统一改用 np.abs

实际应用:判断"近似纯虚数"

工程计算里经常会碰到极小的实部误差,借助模可以判断出信号真正的幅值大小。

>>> a = -1.0014211682118912e-13 - 750.0000000000011j
>>> np.abs(a)
np.float64(750.0000000000011)     # 实部接近 0,模值等于虚部的绝对值

>>> b = 1.127098414599459e-12 - 1499.9999999999993j
>>> np.abs(b)
np.float64(1499.9999999999993)
⚠️ 浮点数误差

1e-13 这种量级的实部,基本可以认定就是浮点运算误差,按 0 处理就行。要是觉得有必要,可以用 round() 或者自己写个阈值函数来清洗:

```python
def clean(z, eps=1e-10):
re = 0 if abs(z.real) < eps else z.real
im = 0 if abs(z.imag) < eps else z.imag
return complex(re, im)
```


五、复数的乘法

数学定义

(a + bi)(c + di) = (ac - bd) + (ad + bc)i

Python 实现

>>> (1 + 2j) * (3 + 4j)
(-5+10j)
# 推导:(1·3 - 2·4) + (1·4 + 2·3)i = -5 + 10i

几何意义

复数的乘法运算在复平面上对应的是模相乘、辐角相加

z_1 \cdot z_2 = |z_1||z_2| \cdot e^{i(\theta_1 + \theta_2)}
>>> import cmath
>>> z = 1 + 1j
>>> cmath.polar(z)            # (模, 辐角)
(1.4142135623730951, 0.7853981633974483)   # (√2, π/4)
>>> cmath.rect(1.4142, 0.7853)  # 极坐标 → 直角坐标
(1.0000...+0.9999...j)

六、常见错误速查

错误代码 报错 缘由 修正
math.exp(-1j) TypeError math 不支持复数 改用 cmath.exp(-1j)
cmath.exp(-j) NameError: 'j' j 不是合法变量 写成 -1j
z1.conjugate 返回方法对象 漏写括号 z1.conjugate()
np.abs(z1) NameError: 'np' 未导入 numpy import numpy as np

七、模块导入总结

import math      # 实数指数 / 三角 / 对数
import cmath     # 复数指数 / 三角 / 对数
import numpy as np  # 向量化复数运算(处理数组时使用)
📋 选用速记

  • 单个实数运算 → math
  • 单个复数运算 → cmath
  • 数组、矩阵运算 → numpy