如何在不达到递归限制的情况下腌制 class() 具有子对象或邻居关系的对象并在加载时保留对象

How to pickle class() objects with children or neighbour relationships without hitting recursion limits and retain objects when loading

底部有一个 TLDR。我有一个 python class 如下,它用于为 D* 寻路创建节点(不包括寻路过程):

import numpy as np
from tqdm import tqdm 
import sys
class Node():
    
    """A node class for D* Pathfinding"""

    def __init__(self,parent, position):
        self.parent = parent
        self.position = position
        self.tag = "New"
        self.state = "Empty"
        self.h = 0
        self.k = 0
        self.neighbours = []

    def __eq__(self, other):
        if type(other) != Node : return False
        else: return self.position == other.position
    def __lt__(self, other):
        return self.k < other.k

def makenodes(grid):
    
    """Given all the enpoints, make nodes for them"""
    endpoints = getendpoints(grid) 
    nodes = [Node(None,pos) for pos in endpoints]
    t = tqdm(nodes, desc='Making Nodes') #Just creates a progress bar
    for node in t:
        t.refresh()
        node.neighbours = getneighbours(node,grid,nodes)
    return nodes
def getneighbours(current_node,grid,spots):
'''Given the current node, link it to it's neighbours''' 
    neighbours = []
    for new_position in [(0, -1), (0, 1), (-1, 0), (1, 0)]: # Adjacent nodes

            # Get node position
            node_position = (int(current_node.position[0] + new_position[0]), int(current_node.position[1] + new_position[1]))

            # Make sure within range
            if node_position[0] > (len(maze) - 1) or node_position[0] < 0 or node_position[1] > (len(maze[len(maze)-1]) -1) or node_position[1] < 0:
                continue

            # Create new node
            new_node = spots[spots.index(Node(None, node_position))]
            neighbours.append(new_node)
    return neighbours

def getendpoints(grid):
    
    """returns all locations on the grid"""
    
    x,y = np.where(grid == 0)
    endpoints = [(x,y) for (x,y) in zip(x,y)]
    return endpoints

"""Actually Making The Nodes Goes as Follows"""
grid = np.zeros((100,100))
spots = makenodes(grid)
sys.setrecursionlimit(40000)
grid = np.zeros((100,100))
spots = makenodes(grid)

with open('100x100 Nodes Init 4 Connected', 'wb') as f:
    pickle.dump(spots, f)
print('Done 100x100')

该程序 运行 很好,但是这个节点制作过程在 100x100 上需要 3 分钟,但在 1000x1000 上需要一个多星期。为了解决这个问题,我使用 pickle.dump() 来保存所有节点,然后当我 运行 我的程序时,我可以使用 spots = pickle.load(...)。在 grid=np.zeros((40,50)) 上,我必须将递归限制设置为大约 16000,在 100x100 上,我将递归限制设置为 40000,但内核死机了

为了帮助您直观地了解节点是什么以及它们与邻居的连接方式,请参阅我绘制的图像,其中黑色:节点和白色:连接。

如果我将 getneighbours() 更改为 return (x,y) 坐标元组(而不是节点对象),那么我可以腌制节点,因为它们不再递归。这种方法会减慢程序的其余部分,因为每次我想引用一个节点时,我都必须重建它并在列表 spots 中搜索它。 (因为问题已经很长了,所以我简化了这部分的解释)。

如何保存这些节点对象,同时将它们的邻居保持为更大网格的节点?[​​=37=]

TLDR: 我有一个节点 class 是递归的,因为每个节点都包含对其邻居的引用,使其高度递归。我可以使用 pickle for grid = np.zeros((40,50)) 保存节点并设置相当高的递归限制 16000。对于较大的网格,我在调用 pickle.dump

时达到了最大系统递归

再次包含 TLDR(这样您就可以检查这是否适合您)。我找到了一种解决方法,它保留了 pickling 的省时优势,但允许节点即使在更大的尺寸下也能被 pickle。包括用于证明这一点的测试:

我更改了 makenodes() 中每个 Node() 的结构,通过剥离它的邻居来进行腌制,从而减少递归。使用问题中提到的方法,grid = np.zeros((40,50)) 将需要 sys.setrecursionlimt(16000) 左右。使用这种方法,永远不会达到 Python 的内置递归限制 1000,并且可以在加载 pickle 文件时重建预期的结构。

基本上酸洗的时候,都是按照下面的流程进行的。

  1. 列出所有节点及其相应的索引,以便:
In [1]: nodes
Out [1]: [[0, <__main__.Node at 0x7fbfbebf60f0>],...]
  1. 对邻居做同样的事情:
In [2]: neighbours
Out [2]: [[0, [<__main__.Node at 0x7fbfbebf6f60>, <__main__.Node at 0x7fbfbec068d0>]],...]

以上是任何大小的迷宫的节点和邻居的外观示例。 '...' 表示列表继续包含它包含的元素,另外注意 len(nodes) 应该等于 len(neighbours。该索引不是必需的,但我已将其作为附加检查包括在内。

变化如下:

def makenodes(grid):
    
    """Given all the enpoints, make nodes for them"""
    
    endpoints = getendpoints(grid)
    spots = [Node(None,pos) for pos in endpoints]
    neighbours = []
    nodes = []
    t = tqdm(spots, desc='Making Nodes')
    i = 0
    for node in t:
        t.refresh()
        neighbour = getneighbours(node,grid,spots)
        nodes.append([i,node])
        neighbours.append([i,neighbour])
        i+=1
            
    return nodes, neighbours

"""Actually Making the Nodes"""
grid = np.zeros((100,100))
nodes, neighbours = makenodes(grid)
arr = np.array([nodes,neighbours])
with open('100x100 Nodes Init 4 Connected.pickle', 'wb') as f:
    pickle.dump(arr,f)
print('Done 100x100')
def reconstruct(filename):

'''Reconstruct the relationships by re-assigning neighbours'''
    with open(filename, 'rb') as file:
        f = pickle.load(file)
    nodes = f[0]
    neighbours = f[1]
    reconstructed = []
    for node in nodes:
        i = node[0]
        for neighbour in neighbours[i][1]:
            node[1].neighbours.append(neighbour)
            reconstructed.append(node[1])
    return reconstructed

nodes_in = reconstruct('100x100 Nodes Init 4 Connected.pickle')

这实现了预期的输出,并且可以在重新加载(重建)时重新建立 neighbour/children 关系。此外,每个节点的邻居列表中的对象直接指向它们对应的节点,这样如果你 运行 只在选择的数组上这样做而不重建它:

In [3]: nodes2[1][1] is neighbours2[0][1][0]
Out[3]: True

时差 创建和保存 50x50 网格的平均时间是。这样做 60 次的平均时间:

  1. 原路:9.613s
  2. 适配方式(重构):9.822s

平均加载时间:

  1. 原路:0.0582s
  2. 适应方式:0.04071s

两个文件大小:410KB
结论

适应的方式可以处理更大的尺寸,从时代来看,具有相同的性能。这两种方法都会创建相同大小的文件,因为 pickle 只存储每个对象一次,请阅读相关文档。 请注意,选择使用 50x50 网格是为了防止原始方法达到递归限制。我能够用适应的方式腌制的最大尺寸是 500x500。我没有做得更大,仅仅是因为节点制作过程在我的机器上花费了将近 2 天的时间。然而加载 500x500 只需要不到 1 秒,所以好处更加明显。

**TLDR:**我设法将节点及其相应的邻居保存为 np.array([nodes.neighbours]) 的数组,以便在 pickling 时,重新生成必要的数据当可以使用 un-pickling 时建立关系。本质上,必要的数据以不需要提高递归限制的方式计算和保存。然后您可以在加载时快速重建数据。