Another Few Ideas
An exchange with Bill triggers some ideas for the flood. It’s 0330 hours. Let’s see what we can do.
I woke up and can’t get back to sleep. Since I have a couple of ideas for the flood, I decided to get up and work on them. I have a few things in mind, one of them slightly larger than the others.
- Rename the
frontierandexaminedvariables for clarity, and any others that seem to follow; - Break out chunks of the code into separate methods, again for naming clarity of those chunks;
- Create a “Method Object” to do the actual work. Give it a fluent interface.
Obviously that last one is the larger. I envision a Flood object, into which we’ll move all the code. I’m not sure whether we’ll keep a flood method on cell or not, but I’m leaning toward replacing it with the Flood facility.
Despite it being the hardest bit and despite the obscene hour, I think we’ll start with the Flood object. Rather than write new tests for it, we’ll try to refactor the existing flood to create and use the new class.
To that end, here’s Cell.flood:
class Cell:
def flood(self, *,
select=lambda cell: True,
next_value=lambda cell, value: 0,
initial_value=0,
randomness=0.0):
frontier = {self: initial_value}
examined = {self}
while frontier:
current_cell = next(iter(frontier))
current_value = frontier.pop(current_cell)
yield current_cell, current_value
examined.add(current_cell)
neighbors = self._get_neighbors(current_cell, randomness)
for neighbor in neighbors:
if (neighbor not in examined and
select(neighbor) and
neighbor not in frontier):
frontier[neighbor] = next_value(current_cell, current_value)
@staticmethod
def _get_neighbors(next_cell, randomness):
if random.random() < randomness:
return next_cell.random_neighbors
return next_cell.neighbors
I’ll demand the object into being by just creating it at the top of flood:
class Cell:
def flood(self, *,
select=lambda cell: True,
next_value=lambda cell, value: 0,
initial_value=0,
randomness=0.0):
flooder = Flood(self)
...
class Flood:
def __init__(self, origin):
self.origin = origin
That passes. Commit: working toward Flood object.
Now what? I think we’ll call our flooder, passing the four parameters from flood in via methods:
def flood(self, *,
select=lambda cell: True,
next_value=lambda cell, value: 0,
initial_value=0,
randomness=0.0):
flooder = Flood(self)
flooder.select(select)
flooder.next_value(next_value)
flooder.initial_value(initial_value)
flooder.randomness(randomness)
class Flood:
def __init__(self, origin):
self._origin = origin
self._select = lambda cell: True,
self._next_value = lambda cell, value: 0
self._initial_value = 0
self._randomness = 0.0
def select(self, function):
self._select = function
def next_value(self, function):
self._next_value = function
def initial_value(self, result):
self._initial_value = result
def randomness(self, fraction):
self._randomness = fraction
Now Flood knows everything that Cell.flood knows. Commit a save point.
Let’s posit a method flood and move our operational code over. It’ll need a bit of tweaking but with luck not much.
After I got the first bit right, i.e. as you see it above, the rest was pretty straightforward. Here, with renaming the class to Flooder, because it seemed better:
class Flooder:
def __init__(self, origin):
self._origin = origin
self._select = lambda cell: True
self._next_value = lambda cell, value: 0
self._initial_value = 0
self._randomness = 0.0
@staticmethod
def default_select(cell):
return True
def select(self, function):
self._select = function
return self
def next_value(self, function):
self._next_value = function
return self
def initial_value(self, result):
self._initial_value = result
return self
def randomness(self, fraction):
self._randomness = fraction
return self
def flood(self):
frontier = {self._origin: self._initial_value}
examined = {self._origin}
while frontier:
current_cell = next(iter(frontier))
current_value = frontier.pop(current_cell)
yield current_cell, current_value
examined.add(current_cell)
neighbors = self._get_neighbors(current_cell, self._randomness)
for neighbor in neighbors:
if (neighbor not in examined and
self._select(neighbor) and
neighbor not in frontier):
frontier[neighbor] = self._next_value(current_cell, current_value)
@staticmethod
def _get_neighbors(next_cell, randomness):
if random.random() < randomness:
return next_cell.random_neighbors
return next_cell.neighbors
And in Cell:
class Cell:
def flood(self, *,
select=lambda cell: True,
next_value=lambda cell, value: 0,
initial_value=0,
randomness=0.0):
for c, v in Flooder(self)\
.select(select)\
.next_value(next_value)\
.initial_value(initial_value)\
.randomness(randomness)\
.flood():
yield c, v
This is all green, all 50-odd tests. Commit: Cell.flood uses Flooder object.
Now I did have some improvements to the flood itself in mind as well:
def flood(self):
to_be_delivered = {self._origin: self._initial_value}
delivered = set()
while to_be_delivered:
current_cell = next(iter(to_be_delivered))
current_value = to_be_delivered.pop(current_cell)
yield current_cell, current_value
delivered.add(current_cell)
neighbors = self._get_neighbors(current_cell, self._randomness)
for neighbor in neighbors:
if (neighbor not in delivered and
self._select(neighbor) and
neighbor not in to_be_delivered):
to_be_delivered[neighbor] = self._next_value(current_cell, current_value)
Renamed frontier to to_be_delivered and examined to delivered. Noticed that delivered could be initialized to empty. Green commit: refactoring
Move the delivered.add line up, extract method:
def flood(self):
to_be_delivered = {self._origin: self._initial_value}
delivered = set()
while to_be_delivered:
current_cell, current_value = self.next_to_deliver(to_be_delivered, delivered)
yield current_cell, current_value
neighbors = self._get_neighbors(current_cell, self._randomness)
for neighbor in neighbors:
if (neighbor not in delivered and
self._select(neighbor) and
neighbor not in to_be_delivered):
to_be_delivered[neighbor] = self._next_value(current_cell, current_value)
def next_to_deliver(self, to_be_delivered, delivered):
current_cell = next(iter(to_be_delivered))
current_value = to_be_delivered.pop(current_cell)
delivered.add(current_cell)
return current_cell, current_value
Make to_be_delivered and delivered instance variables:
def flood(self):
self.to_be_delivered = {self._origin: self._initial_value}
self.delivered = set()
while self.to_be_delivered:
current_cell, current_value = self.next_to_deliver(self.to_be_delivered, self.delivered)
yield current_cell, current_value
neighbors = self._get_neighbors(current_cell, self._randomness)
for neighbor in neighbors:
if (neighbor not in self.delivered and
self._select(neighbor) and
neighbor not in self.to_be_delivered):
self.to_be_delivered[neighbor] = self._next_value(current_cell, current_value)
Remove the parameters from next_to_deliver, reference the members.
def flood(self):
self.to_be_delivered = {self._origin: self._initial_value}
self.delivered = set()
while self.to_be_delivered:
current_cell, current_value = self.next_to_deliver()
yield current_cell, current_value
neighbors = self._get_neighbors(current_cell, self._randomness)
for neighbor in neighbors:
if (neighbor not in self.delivered and
self._select(neighbor) and
neighbor not in self.to_be_delivered):
self.to_be_delivered[neighbor] = self._next_value(current_cell, current_value)
def next_to_deliver(self):
current_cell = next(iter(self.to_be_delivered))
current_value = self.to_be_delivered.pop(current_cell)
self.delivered.add(current_cell)
return current_cell, current_value
Change _get_neighbors to use instance variable:
def flood(self):
self.to_be_delivered = {self._origin: self._initial_value}
self.delivered = set()
while self.to_be_delivered:
current_cell, current_value = self.next_to_deliver()
yield current_cell, current_value
neighbors = self._get_neighbors(current_cell)
for neighbor in neighbors:
if (neighbor not in self.delivered and
self._select(neighbor) and
neighbor not in self.to_be_delivered):
self.to_be_delivered[neighbor] = self._next_value(current_cell, current_value)
def _get_neighbors(self, next_cell):
if random.random() < self._randomness:
return next_cell.random_neighbors
return next_cell.neighbors
Green, commit. Forgot to commit the _get_neighbors. Slow down! (I get on a roll, seeing next moves and making them. Better to commit each one.)
Inline the get neighbors … I think that’ll be good …
def flood(self):
self.to_be_delivered = {self._origin: self._initial_value}
self.delivered = set()
while self.to_be_delivered:
current_cell, current_value = self.next_to_deliver()
yield current_cell, current_value
for neighbor in self._get_neighbors(current_cell):
if (neighbor not in self.delivered and
self._select(neighbor) and
neighbor not in self.to_be_delivered):
self.to_be_delivered[neighbor] = self._next_value(current_cell, current_value)
Commit. Extract that whole for loop thing to new method enqueue_relevant_neighbors:
def flood(self):
self.to_be_delivered = {self._origin: self._initial_value}
self.delivered = set()
while self.to_be_delivered:
current_cell, current_value = self.next_to_deliver()
yield current_cell, current_value
self.enqueue_relevant_neighbors(current_cell, current_value)
def enqueue_relevant_neighbors(self, current_cell, current_value):
for neighbor in self._get_neighbors(current_cell):
if (neighbor not in self.delivered and
self._select(neighbor) and
neighbor not in self.to_be_delivered):
self.to_be_delivered[neighbor] = self._next_value(current_cell, current_value)
Green, commit. The flood method looks rather nice now. Let’s see about fixing up the enqueue one.
def enqueue_relevant_neighbors(self, current_cell, current_value):
for neighbor in self._get_neighbors(current_cell):
if (self.is_relevant(neighbor)):
self.to_be_delivered[neighbor] = self._next_value(current_cell, current_value)
def is_relevant(self, neighbor):
return (neighbor not in self.delivered and
self._select(neighbor) and
neighbor not in self.to_be_delivered)
Better but what I really want is this, which I’ll just type in as Wishful Thinking:
def enqueue_relevant_neighbors(self, current_cell, current_value):
for neighbor in self._relevant_neighbors(current_cell):
self.to_be_delivered[neighbor] = self._next_value(current_cell, current_value)
def _relevant_neighbors(self, cell):
return (neighbor for neighbor in self._get_neighbors(cell) if self.is_relevant(neighbor))
Green. Commit then look at the whole thing and assess.
class Flooder:
def __init__(self, origin):
self._delivered = None
self._to_be_delivered = None
self._origin = origin
self._select = lambda cell: True
self._next_value = lambda cell, value: 0
self._initial_value = 0
self._randomness = 0.0
def select(self, function):
self._select = function
return self
def next_value(self, function):
self._next_value = function
return self
def initial_value(self, result):
self._initial_value = result
return self
def randomness(self, fraction):
self._randomness = fraction
return self
def flood(self):
self._to_be_delivered = {self._origin: self._initial_value}
self._delivered = set()
while self._to_be_delivered:
current_cell, current_value = self.next_to_deliver()
yield current_cell, current_value
self.enqueue_relevant_neighbors(current_cell, current_value)
def next_to_deliver(self):
current_cell = next(iter(self._to_be_delivered))
current_value = self._to_be_delivered.pop(current_cell)
self._delivered.add(current_cell)
return current_cell, current_value
def enqueue_relevant_neighbors(self, current_cell, current_value):
for neighbor in self._relevant_neighbors(current_cell):
self._to_be_delivered[neighbor] = self._next_value(current_cell, current_value)
def _relevant_neighbors(self, cell):
return (neighbor
for neighbor in self._get_neighbors(cell)
if self.is_relevant(neighbor))
def is_relevant(self, neighbor):
return (neighbor not in self._delivered and
neighbor not in self._to_be_delivered and
self._select(neighbor) )
def _get_neighbors(self, next_cell):
if random.random() < self._randomness:
return next_cell.random_neighbors
return next_cell.neighbors
Summary
Just shy of 60 lines with all those broken out methods. It was 25 in Cell class, and we have added a few blank lines and 25 lines just in the fluent interface. We’re going to be glad we have that if and when we remove the convenience method from Cell and ask users to use Flooder to get their fills done.
What I like most about this is the code for flood itself:
def flood(self):
self._to_be_delivered = {self._origin: self._initial_value}
self._delivered = set()
while self._to_be_delivered:
current_cell, current_value = self.next_to_deliver()
yield current_cell, current_value
self.enqueue_relevant_neighbors(current_cell, current_value)
I do think that is rather nice. We’ll see what we think in a few days. But here at 0530, I like it. Now I’m gonna try to get back to sleep.
See you next time!