0

I am trying to implement a hierarchical tree structure in which every node has a slightly changed version of a dictionary. My problem is that, unlike similar structures in R, python dictionaries are just labels for external variables rather than true 'value containers'. Therefore, any changes made at one of the nodes affect the dictionary of all other nodes as well.

Given this behaviour of dict, what would be the proper way to implement this in python? It seems like a common approach so I feel like I must be missing something but have been banging my head against the wall for hours now.

Background: I am trying to implement a Minimax approach for a turn-based perfect information adversarial board game in python using a dictionary for the board state. I create a hierarchical tree structure of nodes and children based on all possible moves, and so far I have been trying to modify the dictionary for each node. The true nature of dictionaries in python was unclear to me so I was fighting with strange results from my approach because the changes from every single node were applied to all other nodes as well.

Example

#Create some original state (dict of dicts)
state_original = {'field1' : {'player':1, 'count':2}, 'field2' : {'player':2, 'state': 4}}
print(state_original)

#Define object for the tree nodes
class Node(object):
  def __init__(self, depth, state, field=None):
    self.depth = depth
    self.state = state
    self.field = field
    self.subnodes = []
    if self.depth > 0:
      self.SpawnSubnodes()
  def SpawnSubnodes(self):
    for field in self.state:
      depth_new = self.depth -1
      state_new = self.state
      state_new[field]['count'] += 1
      self.subnodes.append(Node(depth_new, state_new, field))

#Build tree
nodes = Node(3, state_original)
nodes.subnodes

#But: This is a mess now
print(state_original)

#This is a mess, too. Results are not meaningful :( 
print(nodes.subnodes[1].state)

It works with deepcopy, but is too slow for my (larger) tree

from copy import deepcopy

#Define object for the tree nodes
class Node(object):
  def __init__(self, depth, state, field=None):
    self.depth = depth
    self.state = state
    self.field = field
    self.subnodes = []
    if self.depth > 0:
      self.SpawnSubnodes()
  def SpawnSubnodes(self):
    for field in self.state:
      depth_new = self.depth -1
      state_new = deepcopy(self.state)
      state_new[field]['count'] += 1
      self.subnodes.append(Node(depth_new, state_new, field))

Edit: I realised that copy does not work for me because my board state is a dictionary of dictionaries rather than a simple dictionary. I have updated my example code to accurately reflect this. While a potential workaround would be do try and come up with a simpler representation of the board (probably splitting it up in a "board shape" dict and a "board state" dict), I feel like there should be a more pythonic way to solve this problem?

JanLauGe
  • 2,297
  • 2
  • 16
  • 40
  • Thank you for editing the comment markers. Sorry, it's the first question I ever posted and I got confused about how to comment out lines in here. – JanLauGe Jan 02 '16 at 23:37
  • A good principle to consider in Python is if you have a class with `__init__` and one method you probably don't need a class. Jack Diederich has a [good presentation on the subject](https://www.youtube.com/watch?v=o9pEzgHorH0) on YouTube. – msw Jan 03 '16 at 09:57

1 Answers1

0

Instead of copy.deepcopy, use copy.copy (shallow copy) because you don't really need deep copy.

import copy

#Define object for the tree nodes
class Node(object):
  def __init__(self, depth, state, field=None):
    self.depth = depth
    self.state = state
    self.field = field
    self.subnodes = []
    if self.depth > 0:
      self.SpawnSubnodes()
  def SpawnSubnodes(self):
    for field in self.state:
      depth_new = self.depth -1
      state_new = copy.copy(self.state)
      state_new[field] += 1
      self.subnodes.append(Node(depth_new, state_new, field))

A shallow copy is much faster than deep copy. Here's a simple timing test:

In [5]: %timeit copy.deepcopy(state_original)
The slowest run took 6.96 times longer than the fastest. This could mean that an intermediate result is being cached 
100000 loops, best of 3: 4.97 µs per loop

In [6]: %timeit copy.copy(state_original)
The slowest run took 8.84 times longer than the fastest. This could mean that an intermediate result is being cached 
1000000 loops, best of 3: 709 ns per loop

Note: the above solution works only when the dict in question is simple, i.e., it does not contain other dicts.

In case the dict to begin with contains other simple-dicts, performing a shallow copy of its contents iteratively can be faster than a deepcopy operation.

def mycopy(d):
    return {k: copy.copy(v) for k, v in d.items()}

A preliminary performance analysis of mycopy gives me roughly an order-of-magnitude improvement over copy.deepcopy.

Community
  • 1
  • 1
piggs_boson
  • 987
  • 2
  • 12
  • 25
  • Thank you for your answer! I tested it with both the example code I provided, as well as with my actual Minimax function. It works fine for the former, but results in illegal moves for the latter, a problem not occurring when using deepcopy. I will look into the details and try to find out what's causing this behaviour. – JanLauGe Jan 03 '16 at 00:13
  • So I found out why copy.copy does not work for my actual project: My board state is actually a dict of dicts. Sorry for not including that in my original example. I wanted to present a minimum working example and aimed for simplicity, not knowing that it would make such a difference. I have updated the question accordingly. – JanLauGe Jan 03 '16 at 09:39
  • @JanLauGe -- if you are sure that your board state does not contain 3-level deep nested dicts, you can try the `mycopy` function as above. – piggs_boson Jan 03 '16 at 13:46
  • The 'mycopy' function works like a charm! It's even faster than my original approach that only used the one dictionary (and hence produced errors). Thank you very much! – JanLauGe Jan 04 '16 at 12:31