Python复数与指数运算
- 笔记
- 21天前
- 7热度
- 0评论
最近在学 FFT,里头频繁出现复数和复指数项 e^{j\omega t},索性把 Python 里处理复数的几个工具梳理一下:内置的 complex 类型、标准库的 cmath 模块,以及 numpy 在数组场景下的覆盖。
前言:complex 类型与 cmath 模块
关于 complex 类型
complex 是 Python 自带的数值类型,从 Python 1.4(1996 年)就已经存在,和 int、float、bool 同属内置类型,不用 import 就能直接拿来用。每个复数对象内部由两个 float 组成,分别对应实部 real 和虚部 imag。它的字面量写法比较特别,要借助 j 来表示虚数单位,比如 3+4j。和 int、float 一样,复数对象也是不可变的(immutable)。
>>> isinstance(1+1j, complex)
True
关于 cmath 模块
cmath 也是 Python 标准库里的老成员,从 1.5 版本(1997 年)就提供了。它的定位是 math 模块的复数版本,里面所有函数都能接收复数参数并返回复数结果。包含的东西大致分几块:指数和对数(exp、log、log10、sqrt)、三角与反三角函数(sin、cos、tan、asin 等)、双曲函数(sinh、cosh 之类)、极坐标转换(polar、rect、phase),还有一些常量(pi、e、inf、nan、tau 等)。日常使用场景多见于信号处理、电气工程里的阻抗计算、量子力学公式以及傅里叶分析。
>>> 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
Python 里没有一个叫
j 的内置变量,单独写 j 会直接抛 NameError,必须带上系数写成 1j、2j 这样的形式。
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 是一个方法,要拿到共轭值就必须加括号去调用它;不加括号只是在引用方法对象本身而已。反过来,real 和 imag 属于属性,是不能加括号的。
复数支持的运算
>>> 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 所有对象的统一行为 - 不可变性:对象内部状态无法被修改,由类型决定(
int、float、tuple、complex都是) - 对象缓存:CPython 的实现层优化,只对小整数和短字符串生效

三、指数运算:math vs cmath vs numpy
math.exp(-1j) 会直接抛出 TypeError: must be real number, not complex。要算复数指数,就得改用 cmath 或 numpy。
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])
欧拉公式 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]
要是用 math 或 cmath,就只能自己写循环了。
三方对照表
| 功能 | 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(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. ])
三种调用方式对照表
| 调用方式 | 实数标量 | 复数标量 | 实数数组 | 复数数组 | 返回类型 |
|---|---|---|---|---|---|
abs(x) |
支持 | 支持 | 不支持 | 不支持 | int / float |
cmath.polar(z)[0] |
不支持 | 支持 | 不支持 | 不支持 | float |
np.abs(x) |
支持 | 支持 | 支持 | 支持 | np.float64 / ndarray |
abs();只要涉及数组,或者要和 np.exp、np.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)iPython 实现
>>> (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