避免 Python 中 GDAL/OGR 绑定导致的崩溃:一个所有权问题

在使用 OGR 和 GDAL 的 Python 绑定时,有时你可能会遇到一个奇怪的现象:同样的代码结构,在某些情况下正常运行,而在其他情况下却导致 Python 闪退崩溃。本文将解释这种现象的原因,并演示如何避免这种问题。

Understanding GDAL: A Comprehensive Guide - Geographic Book

问题重现

假设我们正在使用一个 shapefile 文件,并尝试从中提取几何图形信息:

from osgeo import ogr

shp_ds = ogr.Open(r'D:\gadm\France\gadm41_FRA_0.shp')
lyr = shp_ds.GetLayer(0)
lyr.GetFeature(0).GetGeometryRef().GetX()  # 这一行可能导致崩溃

结果会发生闪退。

image-20241015112641614

上面的代码在执行时可能导致 Python 闪退。通过修改代码,问题似乎得到解决:

from osgeo import ogr

shp_ds = ogr.Open(r'D:\gadm\France\gadm41_FRA_0.shp')
lyr = shp_ds.GetLayer(0)
feature = lyr.GetFeature(0)
feature.GetGeometryRef().GetX()  # 这次不会崩溃

image-20241015112743523

这次的结果不会闪退。

问题分析

这种崩溃背后的原因与 GDAL 和 OGR 的对象生命周期管理有关。在 GDAL/OGR 的 Python 绑定中,当 Python 删除一个对象时,背后的 C++ 对象会随之被销毁。如果该 C++ 对象还拥有其他子对象的所有权(例如,dataset 对象包含 bandfeature 对象包含 geometry),那么当父对象被删除后,子对象也会被销毁。如果 Python 中还保留着子对象的引用,访问该对象时就会导致崩溃。

这是因为 OGR/GDAL 绑定对这些 C++ 对象的引用关系没有进行足够的保护,可能会导致访问已被销毁的对象。

举个类似的例子:

from osgeo import gdal

dataset = gdal.Open('C:\\RandomData.img')
band = dataset.GetRasterBand(1)
print(band.Checksum())  # 正常工作,输出 31212

在上面的代码中,band 对象依赖于 dataset 对象。如果我们删除 dataset 后再尝试使用 band,将会出现错误:

from osgeo import gdal

dataset = gdal.Open('C:\\RandomData.img')
band = dataset.GetRasterBand(1)
del dataset
band.Checksum()  # 抛出 TypeError

在 GDAL 3.7 及更早的版本中,删除 dataset 后再使用 band 甚至可能导致 Python 崩溃,而不是抛出异常。

如何避免

为了避免这种问题,确保在使用子对象时,父对象的引用仍然存在。比如,在上面的 OGR 示例中,通过将 lyr.GetFeature(0) 的返回值赋给一个变量,可以确保 lyrshp_dsfeature 使用时不会被销毁。

正确的方式:

shp_ds = ogr.Open(r'D:\gadm\France\gadm41_FRA_0.shp')
lyr = shp_ds.GetLayer(0)
feature = lyr.GetFeature(0)  # 确保 feature 对应的 lyr 仍然存在
feature.GetGeometryRef().GetX()

总结

GDAL 和 OGR 的 Python 绑定中的对象生命周期管理可能会导致 Python 崩溃,尤其是当父对象在子对象仍在使用时被销毁。通过小心管理对象引用,可以避免此类崩溃问题。