强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Julia 教程 / Python 互操作(PyCall.jl)

Python 互操作(PyCall.jl)

Python 拥有最丰富的科学计算和机器学习生态。PyCall.jl 让 Julia 无缝调用 Python 代码,反之亦然,实现了两个生态系统的优势互补。


1. PyCall.jl 安装与配置

1.1 安装

using Pkg
Pkg.add("PyCall")

# 或在 Pkg 模式下
# ]add PyCall

1.2 配置 Python 环境

# 查看当前使用的 Python
using PyCall
println(pyversion)  # 如 v"3.11.0"

# 指定 Python 路径(修改前需要重启 Julia)
# ENV["PYTHON"] = "/usr/bin/python3"
# Pkg.build("PyCall")

1.3 使用 Conda.jl 管理 Python 环境

using Conda

# 列出已安装的包
Conda.list()

# 安装 Python 包
Conda.add("numpy")
Conda.add("matplotlib")
Conda.add("scikit-learn")

# 指定 channel
Conda.add("opencv", channel="conda-forge")

# 创建独立环境
Conda.create("myenv")
Conda.add("numpy", "myenv")
配置方式Python 来源适用场景
默认Conda 内置 Python简单使用,无需系统 Python
ENV["PYTHON"]系统指定路径需要使用特定 Python 环境
Conda.jlConda 管理的环境精确控制 Python 包版本

2. 基本使用:导入 Python 模块

2.1 pyimport

using PyCall

# 导入 Python 模块
math = pyimport("math")
os = pyimport("os")
json = pyimport("json")

# 调用函数
println(math.sin(3.14159))  # ≈ 0
println(math.sqrt(2))       # 1.4142135623730951
println(os.getcwd())

2.2 导入 Python 对象

# 导入特定函数
@pyimport numpy as np
@pyimport matplotlib.pyplot as plt

# 使用
arr = np.array([1, 2, 3, 4, 5])
println(np.mean(arr))

💡 提示@pyimport 是宏形式,等价于 pyimport 但更简洁。Julia 1.0+ 推荐使用 pyimport 配合 const

2.3 嵌套模块导入

# 导入子模块
sklearn = pyimport("sklearn")
ensemble = sklearn.ensemble
rf = ensemble.RandomForestClassifier(n_estimators=100)

# 或一步到位
RandomForestClassifier = pyimport("sklearn.ensemble").RandomForestClassifier

3. 类型自动转换

3.1 标量类型转换

Python 类型Julia 类型方向
intInt64双向
floatFloat64双向
strString双向
boolBool双向
bytesVector{UInt8}双向
Nonenothing双向
complexComplex{Float64}双向
# Julia → Python
py_val = PyCall.pyint(42)        # Python int
py_float = PyCall.pyfloat(3.14)  # Python float

# Python → Julia
math = pyimport("math")
jl_val = convert(Int, math.factorial(10))  # 3628800

3.2 容器类型转换

# Python list → Julia Vector
py_list = py"[1, 2, 3, 'hello']"  # Python 字面量
jl_vec = Vector(py_list)           # [1, 2, 3, "hello"]

# Julia Vector → Python list
jl_arr = [1, 2, 3]
py_list2 = PyCall.pycall(py"list", PyObject, jl_arr)

# Python dict → Julia Dict
py_dict = py"{'a': 1, 'b': 2}"
jl_dict = Dict(py_dict)  # Dict("a" => 1, "b" => 2)

# Julia Dict → Python dict
jl_data = Dict("x" => 10, "y" => 20)
py_obj = PyObject(jl_data)

3.3 禁用自动转换

# 保持 Python 对象不自动转换
np = pyimport("numpy")
arr = np.array([1, 2, 3])

typeof(arr)  # PyObject(而不是 Julia Array)

# 手动转换
jl_arr = Array(arr)  # 转为 Julia 数组

4. 调用 Python 函数

4.1 基本函数调用

# 使用 pycall(推荐)
builtins = pyimport("builtins")
result = PyCall.pycall(builtins.len, Int, [1, 2, 3, 4, 5])  # 5

# 直接调用(语法糖)
str_mod = pyimport("str")
py_str = str_mod.upper("hello")  # "HELLO"

4.2 使用关键字参数

json = pyimport("json")

# Python: json.dumps(data, indent=2, ensure_ascii=False)
data = Dict("name" => "张三", "age" => 30)
result = json.dumps(data, indent=2, ensure_ascii=false)
println(result)

4.3 处理 Python 异常

try
    pyimport("nonexistent_module")
catch e
    if e isa PyCall.PyError
        println("Python 错误: ", e.val)
        # e.val 是 Python 异常对象
    end
end

5. NumPy 数组互操作

5.1 零拷贝转换

using PyCall

np = pyimport("numpy")

# Julia → NumPy(零拷贝)
jl_arr = rand(1000, 1000)
np_arr = np.array(jl_arr)  # 共享内存,无复制!

# NumPy → Julia(零拷贝)
np_data = np.random.randn(500, 500)
jl_data = PyCall.PyArray(np_data)
# jl_data 与 np_data 共享同一块内存

5.2 PyArray 接口

np = pyimport("numpy")

# 创建 NumPy 数组
np_arr = np.zeros((3, 4), dtype=np.float64)

# 作为 Julia 数组操作(零拷贝)
jl_view = PyCall.PyArray(np_arr)
jl_view[1, 1] = 42.0  # 直接修改,np_arr 也会变

# 验证
println(np_arr)  # 第一个元素变为 42.0

5.3 类型对应表

NumPy dtypeJulia 类型
float64Float64
float32Float32
int64Int64
int32Int32
complex128ComplexF64
boolBool
uint8UInt8

⚠️ 注意:NumPy 默认行优先(C order),Julia 默认列优先(Fortran order)。传递矩阵时注意内存布局差异。


6. Python 类实例化

6.1 创建 Python 对象

# 实例化 Python 类
collections = pyimport("collections")
Counter = collections.Counter

# 创建实例
counter = Counter("abracadabra")
println(counter)  # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})

# 访问属性
println(counter["a"])  # 5
println(counter.most_common(3))  # [('a', 5), ('b', 2), ('r', 2)]

6.2 自定义 Python 类

# 在 Julia 中定义 Python 类(使用 pytype)
# 更常见的是直接使用 Python 已有的类

datetime = pyimport("datetime")
dt = datetime.datetime(2024, 1, 15, 10, 30, 0)
println(dt.year)   # 2024
println(dt.month)  # 1

# 调用方法
formatted = dt.strftime("%Y-%m-%d %H:%M")
println(formatted)  # "2024-01-15 10:30"

6.3 继承 Python 类

# 使用 @pydef 宏(PyCall 提供,但较旧)
# 推荐直接调用 Python 方法

# 更实用的方式:创建包装器
struct PythonWrapper
    obj::PyObject
end

function Base.show(io::IO, w::PythonWrapper)
    print(io, py"repr($(w.obj))")
end

7. PythonPlot.jl — Matplotlib 绑定

7.1 基本使用

using PythonPlot

# 创建图形
fig, ax = subplots()
x = range(0, 2π, length=100)
ax.plot(x, sin.(x), label="sin(x)")
ax.plot(x, cos.(x), label="cos(x)")
ax.legend()
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_title("三角函数")
fig.savefig("trig.png", dpi=150)

7.2 子图布局

using PythonPlot

fig, axes = subplots(2, 2, figsize=(10, 8))

# 左上
axes[0, 0].plot(rand(50))
axes[0, 0].set_title("随机线图")

# 右上
axes[0, 1].hist(randn(1000), bins=30)
axes[0, 1].set_title("正态分布直方图")

# 左下
axes[1, 0].scatter(rand(50), rand(50), c=rand(50), cmap="viridis")
axes[1, 0].set_title("散点图")

# 右下
data = [randn(100) for _ in 1:4]
axes[1, 1].boxplot(data)
axes[1, 1].set_title("箱线图")

tight_layout()
fig.savefig("subplots.png")

7.3 3D 绘图

using PythonPlot

fig = figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection="3d")

x = range(-3, 3, length=50)
y = range(-3, 3, length=50)
X, Y = meshgrid(x, y)
Z = sin.(sqrt.(X.^2 + Y.^2))

ax.plot_surface(X, Y, Z, cmap="coolwarm", alpha=0.8)
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
fig.savefig("3d_surface.png")

8. 实用场景

8.1 调用 Python 的 scikit-learn

using PyCall

sklearn = pyimport("sklearn")
datasets = sklearn.datasets
model_selection = sklearn.model_selection

# 加载数据集
iris = datasets.load_iris()
X = Array(iris["data"])    # Julia 数组
y = Array(iris["target"])  # Julia 数组

# 训练模型
RandomForestClassifier = pyimport("sklearn.ensemble").RandomForestClassifier
clf = RandomForestClassifier(n_estimators=100, random_state=42)

# 使用 Python 的 train_test_split
X_train, X_test, y_train, y_test = model_selection.train_test_split(
    X, y, test_size=0.3, random_state=42
)

# 训练
clf.fit(X_train, y_train)

# 预测
accuracy = clf.score(X_test, y_test)
println("准确率: $accuracy")

8.2 调用 Python 的 pandas

using PyCall

pd = pyimport("pandas")

# 创建 DataFrame
df = pd.DataFrame(Dict(
    "name" => ["Alice", "Bob", "Charlie"],
    "age" => [25, 30, 35],
    "city" => ["Beijing", "Shanghai", "Guangzhou"]
))

# 数据操作
println(df.describe())
println(df[df["age"] .> 28])

# 读取 CSV
# df = pd.read_csv("data.csv")

# 转为 Julia DataFrame
using DataFrames
jl_df = DataFrame(df)

9. JuliaCall — Python 调用 Julia

9.1 安装

pip install juliacall

9.2 在 Python 中使用 Julia

# Python 代码
from juliacall import Main as jl

# 调用 Julia 函数
result = jl.sqrt(2.0)
print(result)  # 1.4142135623730951

# 加载 Julia 包
jl.seval("using LinearAlgebra")
A = jl.rand(3, 3)
eigenvalues = jl.eigvals(A)
print(eigenvalues)

# 传递 NumPy 数组
import numpy as np
arr = np.array([1.0, 2.0, 3.0])
result = jl.sum(arr)

9.3 在 Julia 中准备接口

# mymodule.jl
module MyModule

export process_data, compute_statistics

function process_data(data::Vector{Float64})
    return data .- mean(data)
end

function compute_statistics(data::Vector{Float64})
    m = mean(data)
    s = std(data)
    return Dict("mean" => m, "std" => s, "min" => minimum(data), "max" => maximum(data))
end

end
# 在 Python 中使用
from juliacall import Main as jl
jl.include("mymodule.jl")

import numpy as np
data = np.random.randn(1000)
result = jl.compute_statistics(data)
print(result)

10. 性能对比与选择指南

10.1 调用开销对比

操作PyCall 开销原生 Julia说明
标量函数调用~1 μs~1 nsPyCall 有 Python 解释器开销
数组传递(零拷贝)~0.1 μs0PyArray 零拷贝
数组传递(复制)~10 μs/MB-取决于数组大小
模块加载~100 ms~10 ms首次 pyimport 较慢

10.2 何时使用 PyCall

场景推荐方案
Python 已有成熟库(如 scikit-learn, TensorFlow)✅ 使用 PyCall
Julia 已有等效库❌ 使用 Julia 原生库
频繁调用小函数❌ 尽量用 Julia 重写
批量数据处理✅ 使用 PyArray 零拷贝
需要 Python 可视化(matplotlib)✅ 使用 PythonPlot.jl
生产环境部署⚠️ 考虑依赖复杂度

10.3 性能优化建议

# ❌ 不推荐:在循环中频繁调用 Python
for i in 1:10000
    pyfunc(data[i])  # 每次调用都有 Python 解释器开销
end

# ✅ 推荐:批量调用
pyfunc(data)  # 一次调用处理整个数组

# ✅ 推荐:预编译 Python 模块
const np = pyimport("numpy")  # const 避免重复查找
const pd = pyimport("pandas")

常见问题与陷阱

问题原因解决方案
PyError: No module named 'xxx'Python 包未安装Conda.add("xxx")
数组维度顺序错误C/Fortran 顺序差异使用 np.asfortranarraypermutedims
GIL 竞争Python 全局解释器锁单线程调用 Python
内存泄漏Python 对象未释放使用 pyfinalize() 或手动 del
字符串编码问题UTF-8/Latin-1 混淆确保 Python 端使用 UTF-8
# 检查 Python 环境
using PyCall
pyimport("sys").path  # Python 模块搜索路径
pyimport("numpy").__version__  # 检查版本

业务场景

场景一:机器学习模型推理

训练阶段使用 Python 的 TensorFlow,推理阶段使用 Julia 通过 PyCall 加载模型。Julia 的高性能数据预处理配合 Python 的模型,实现了低延迟推理服务。

场景二:数据科学工作流

使用 Julia 进行高性能数值计算和数据处理,通过 PyCall 调用 pandas 进行数据透视和可视化。两种语言各取所长。

场景三:渐进式迁移

团队计划从 Python 迁移到 Julia。使用 PyCall 允许逐模块迁移,Julia 代码可以调用尚未迁移的 Python 模块,实现平滑过渡。


总结

主题关键要点
安装PyCall.jl + Conda.jl 或系统 Python
导入pyimport("module")@pyimport module as name
类型转换标量自动转换,数组使用 PyArray 零拷贝
NumPynp.array(jl_arr) 零拷贝,注意行/列优先
PythonPlotmatplotlib 的 Julia 封装
JuliaCallpip install juliacall,Python 调用 Julia
性能批量调用优于逐元素调用

扩展阅读