Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pickling for strategies and transformed strategies #1092

Merged
merged 20 commits into from
Aug 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions axelrod/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,10 @@ def score(self, pair: Tuple[Action, Action]) -> Tuple[Score, Score]:
def __repr__(self) -> str:
return "Axelrod game: (R,P,S,T) = {}".format(self.RPST())

def __eq__(self, other):
if not isinstance(other, Game):
return False
return self.RPST() == other.RPST()


DefaultGame = Game()
12 changes: 12 additions & 0 deletions axelrod/strategies/sequence_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ def reset(self):
super().reset()
self.sequence_generator = self.generator_function(*self.generator_args)

def __getstate__(self):
return_dict = self.__dict__.copy()
del return_dict['sequence_generator']
return return_dict

def __setstate__(self, state):
self.__dict__.update(state)
self.__dict__['sequence_generator'] = self.generator_function(
*self.generator_args)
for turn in self.history:
next(self.sequence_generator)


class ThueMorse(SequencePlayer):
"""
Expand Down
162 changes: 146 additions & 16 deletions axelrod/strategy_transformers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@

import collections
import copy
from importlib import import_module
import inspect
import random
from typing import Any
from numpy.random import choice
from .action import Action
from .random_ import random_choice
from .player import defaultdict, Player


C, D = Action.C, Action.D
Expand Down Expand Up @@ -57,6 +60,15 @@ def __init__(self, *args, **kwargs):
else:
self.name_prefix = name_prefix

def __reduce__(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a docstring here? I get that we're rebuilding the class but if I didn't know what was going on I wouldn't know until I read through the file, and this is a fairly complicated part of the library so I'd like to err on the side of more documentation.

"""Gives instructions on how to pickle the Decorator object."""
factory_args = (strategy_wrapper, name_prefix, reclassifier)
return (
DecoratorReBuilder(),
(factory_args,
self.args, self.kwargs, self.name_prefix)
)

def __call__(self, PlayerClass):
"""
Parameters
Expand Down Expand Up @@ -84,24 +96,25 @@ def __call__(self, PlayerClass):
except KeyError:
pass

# Is the original strategy method a static method?
signature = inspect.signature(PlayerClass.strategy)
strategy_args = [p.name for p in signature.parameters.values()
if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD]
is_static = True
if len(strategy_args) > 1:
is_static = False

# Define the new strategy method, wrapping the existing method
# with `strategy_wrapper`
def strategy(self, opponent):

if is_static:
# static method
if strategy_wrapper == dual_wrapper:
# dual_wrapper figures out strategy as if the Player had
# played the opposite actions of its current history.
flip_play_attributes(self)

if is_strategy_static(PlayerClass):
proposed_action = PlayerClass.strategy(opponent)
else:
proposed_action = PlayerClass.strategy(self, opponent)

if strategy_wrapper == dual_wrapper:
# After dual_wrapper calls the strategy, it returns
# the Player to its original state.
flip_play_attributes(self)

# Apply the wrapper
return strategy_wrapper(self, opponent, proposed_action,
*args, **kwargs)
Expand Down Expand Up @@ -141,6 +154,29 @@ def __repr__(self):
prefix = ', '
return name

def reduce_for_decorated_class(self_):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring please.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while checking PEP8 about klass , i noticed about the importance of sticking with self and cls. even though it shadows a variable from outer scope, is it better to shadow the self variable than to use self_ ?

here's the quote

Function and method arguments
Always use self for the first argument to instance methods.
Always use cls for the first argument to class methods.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not terribly sure to be honest. I'm happy as is, I'm guessing that quote does not necessarily apply to nested def and class?

"""__reduce__ function for decorated class. Ensures that any
decorated class can be correctly pickled."""
class_module = import_module(self_.__module__)
import_name = self_.__class__.__name__

if player_can_be_pickled(self_):
return self_.__class__, (), self_.__dict__

decorators = []
for class_ in self_.__class__.mro():
import_name = class_.__name__
if hasattr(class_, 'decorator'):
decorators.insert(0, class_.decorator)
if hasattr(class_module, import_name):
break

return (
StrategyReBuilder(),
(decorators, import_name, self_.__module__),
self_.__dict__
)

# Define a new class and wrap the strategy method
# Dynamically create the new class
new_class = type(
Expand All @@ -149,15 +185,74 @@ def __repr__(self):
"name": name,
"original_class": PlayerClass,
"strategy": strategy,
"decorator": self,
"__repr__": __repr__,
"__module__": PlayerClass.__module__,
"classifier": classifier,
"__doc__": PlayerClass.__doc__,
"__reduce__": reduce_for_decorated_class,
})

return new_class
return Decorator


def player_can_be_pickled(player: Player) -> bool:
"""
Returns True if pickle.dump(player) does not raise pickle.PicklingError.
"""
class_module = import_module(player.__module__)
import_name = player.__class__.__name__
if not hasattr(class_module, import_name):
return False

to_test = getattr(class_module, import_name)
return to_test == player.__class__


def is_strategy_static(player_class) -> bool:
"""
Returns True if `player_class.strategy` is a `staticmethod`, else False.
"""
for class_ in player_class.mro():
method = inspect.getattr_static(class_, 'strategy', default=None)
if method is not None:
return isinstance(method, staticmethod)


class DecoratorReBuilder(object):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc string please.

"""
An object to build an anonymous Decorator obj from a set of pickle-able
parameters.
"""
def __call__(self, factory_args: tuple, args: tuple, kwargs: dict,
instance_name_prefix: str) -> Any:

decorator_class = StrategyTransformerFactory(*factory_args)
kwargs['name_prefix'] = instance_name_prefix
return decorator_class(*args, **kwargs)


class StrategyReBuilder(object):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you write a docstring for this.

"""
An object to build a new instance of a player from an old instance
that could not normally be pickled.
"""
def __call__(self, decorators: list, import_name: str,
module_name: str) -> Player:

module_ = import_module(module_name)
import_class = getattr(module_, import_name)

if hasattr(import_class, 'decorator'):
return import_class()
else:
generated_class = import_class
for decorator in decorators:
generated_class = decorator(generated_class)
return generated_class()


def compose_transformers(t1, t2):
"""Compose transformers without having to invoke the first on
a PlayerClass."""
Expand Down Expand Up @@ -208,7 +303,7 @@ def flip_wrapper(player, opponent, action):
flip_wrapper, name_prefix="Flipped")


def dual_wrapper(player, opponent, proposed_action):
def dual_wrapper(player, opponent: Player, proposed_action: Action) -> Action:
"""Wraps the players strategy function to produce the Dual.

The Dual of a strategy will return the exact opposite set of moves to the
Expand All @@ -228,12 +323,47 @@ def dual_wrapper(player, opponent, proposed_action):
-------
action: an axelrod.Action, C or D
"""
if not player.history:
player.original_player = player.original_class(**player.init_kwargs)

action = player.original_player.strategy(opponent)
player.original_player.history.append(action)
return action.flip()
# dual_wrapper is a special case. The work of flip_play_attributes(player)
# is done in the strategy of the new PlayerClass created by DualTransformer.
# The DualTransformer is dynamically created in StrategyTransformerFactory.

return proposed_action.flip()


def flip_play_attributes(player: Player) -> None:
"""
Flips all the attributes created by `player.play`:
- `player.history`,
- `player.cooperations`,
- `player.defections`,
- `player.state_distribution`,
"""
flip_history(player)
switch_cooperations_and_defections(player)
flip_state_distribution(player)


def flip_history(player: Player) -> None:
"""Flips all the actions in `player.history`."""
new_history = [action.flip() for action in player.history]
player.history = new_history


def switch_cooperations_and_defections(player: Player) -> None:
"""Exchanges `player.cooperations` and `player.defections`."""
temp = player.cooperations
player.cooperations = player.defections
player.defections = temp


def flip_state_distribution(player: Player) -> None:
"""Flips all the player's actions in `player.state_distribution`."""
new_distribution = defaultdict(int)
for key, val in player.state_distribution.items():
new_key = (key[0].flip(), key[1])
new_distribution[new_key] = val
player.state_distribution = new_distribution


DualTransformer = StrategyTransformerFactory(dual_wrapper, name_prefix="Dual")
Expand Down
Loading