From 2ce0d4e90a9ba9352f3bfce2710fd9079e878bde Mon Sep 17 00:00:00 2001 From: Grygorii Iermolenko Date: Sat, 9 Mar 2019 16:26:50 +0200 Subject: [PATCH 1/3] Separate blackboard per python version --- README.md | 2 +- .../{blackboard.py => blackboard__py2.py} | 0 patterns/other/blackboard__py3.py | 122 ++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) rename patterns/other/{blackboard.py => blackboard__py2.py} (100%) create mode 100644 patterns/other/blackboard__py3.py diff --git a/README.md b/README.md index e23b56fb..06e543af 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ __Others__: | Pattern | Description | |:-------:| ----------- | -| [blackboard](patterns/other/blackboard.py) | architectural model, assemble different sub-system knowledge to build a solution, AI approach - non gang of four pattern | +| [blackboard](patterns/other/blackboard__py3.py) | architectural model, assemble different sub-system knowledge to build a solution, AI approach - non gang of four pattern | | [graph_search](patterns/other/graph_search.py) | graphing algorithms - non gang of four pattern | | [hsm](patterns/other/hsm/hsm.py) | hierarchical state machine - non gang of four pattern | diff --git a/patterns/other/blackboard.py b/patterns/other/blackboard__py2.py similarity index 100% rename from patterns/other/blackboard.py rename to patterns/other/blackboard__py2.py diff --git a/patterns/other/blackboard__py3.py b/patterns/other/blackboard__py3.py new file mode 100644 index 00000000..b9f8d9d0 --- /dev/null +++ b/patterns/other/blackboard__py3.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +@author: Eugene Duboviy | github.com/duboviy + +In Blackboard pattern several specialised sub-systems (knowledge sources) +assemble their knowledge to build a possibly partial or approximate solution. +In this way, the sub-systems work together to solve the problem, +where the solution is the sum of its parts. + +https://en.wikipedia.org/wiki/Blackboard_system +""" + +import abc +import random + + +class Blackboard(object): + def __init__(self): + self.experts = [] + self.common_state = { + 'problems': 0, + 'suggestions': 0, + 'contributions': [], + 'progress': 0, # percentage, if 100 -> task is finished + } + + def add_expert(self, expert): + self.experts.append(expert) + + +class Controller(object): + def __init__(self, blackboard): + self.blackboard = blackboard + + def run_loop(self): + while self.blackboard.common_state['progress'] < 100: + for expert in self.blackboard.experts: + if expert.is_eager_to_contribute: + expert.contribute() + return self.blackboard.common_state['contributions'] + + +class AbstractExpert(metaclass=abc.ABCMeta): + + def __init__(self, blackboard): + self.blackboard = blackboard + + @property + @abc.abstractmethod + def is_eager_to_contribute(self): + raise NotImplementedError('Must provide implementation in subclass.') + + @abc.abstractmethod + def contribute(self): + raise NotImplementedError('Must provide implementation in subclass.') + + +class Student(AbstractExpert): + @property + def is_eager_to_contribute(self): + return True + + def contribute(self): + self.blackboard.common_state['problems'] += random.randint(1, 10) + self.blackboard.common_state['suggestions'] += random.randint(1, 10) + self.blackboard.common_state['contributions'] += [self.__class__.__name__] + self.blackboard.common_state['progress'] += random.randint(1, 2) + + +class Scientist(AbstractExpert): + @property + def is_eager_to_contribute(self): + return random.randint(0, 1) + + def contribute(self): + self.blackboard.common_state['problems'] += random.randint(10, 20) + self.blackboard.common_state['suggestions'] += random.randint(10, 20) + self.blackboard.common_state['contributions'] += [self.__class__.__name__] + self.blackboard.common_state['progress'] += random.randint(10, 30) + + +class Professor(AbstractExpert): + @property + def is_eager_to_contribute(self): + return True if self.blackboard.common_state['problems'] > 100 else False + + def contribute(self): + self.blackboard.common_state['problems'] += random.randint(1, 2) + self.blackboard.common_state['suggestions'] += random.randint(10, 20) + self.blackboard.common_state['contributions'] += [self.__class__.__name__] + self.blackboard.common_state['progress'] += random.randint(10, 100) + + +if __name__ == '__main__': + blackboard = Blackboard() + + blackboard.add_expert(Student(blackboard)) + blackboard.add_expert(Scientist(blackboard)) + blackboard.add_expert(Professor(blackboard)) + + c = Controller(blackboard) + contributions = c.run_loop() + + from pprint import pprint + + pprint(contributions) + +### OUTPUT ### +# ['Student', +# 'Student', +# 'Scientist', +# 'Student', +# 'Scientist', +# 'Student', +# 'Scientist', +# 'Student', +# 'Scientist', +# 'Student', +# 'Scientist', +# 'Professor'] From eafccb0ad2be67c43fb075ed89eaca1c6739e601 Mon Sep 17 00:00:00 2001 From: Grygorii Iermolenko Date: Sat, 9 Mar 2019 16:56:33 +0200 Subject: [PATCH 2/3] Separate flyweight per python version --- README.md | 2 +- .../{flyweight.py => flyweight__py2.py} | 7 +- patterns/structural/flyweight__py3.py | 135 ++++++++++++++++++ 3 files changed, 138 insertions(+), 6 deletions(-) rename patterns/structural/{flyweight.py => flyweight__py2.py} (95%) create mode 100644 patterns/structural/flyweight__py3.py diff --git a/README.md b/README.md index 06e543af..1231d619 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ __Structural Patterns__: | [composite](patterns/structural/composite.py) | lets clients treat individual objects and compositions uniformly | | [decorator](patterns/structural/decorator.py) | wrap functionality with other functionality in order to affect outputs | | [facade](patterns/structural/facade.py) | use one class as an API to a number of others | -| [flyweight](patterns/structural/flyweight.py) | transparently reuse existing instances of objects with similar/identical state | +| [flyweight](patterns/structural/flyweight__py3.py) | transparently reuse existing instances of objects with similar/identical state | | [front_controller](patterns/structural/front_controller.py) | single handler requests coming to the application | | [mvc](patterns/structural/mvc.py) | model<->view<->controller (non-strict relationships) | | [proxy](patterns/structural/proxy.py) | an object funnels operations to something else | diff --git a/patterns/structural/flyweight.py b/patterns/structural/flyweight__py2.py similarity index 95% rename from patterns/structural/flyweight.py rename to patterns/structural/flyweight__py2.py index c8fba6a2..b3dae107 100644 --- a/patterns/structural/flyweight.py +++ b/patterns/structural/flyweight__py2.py @@ -87,12 +87,9 @@ def __repr__(self): return "" % (self.value, self.suit) -def with_metaclass(meta, *bases): - """ Provide python cross-version metaclass compatibility. """ - return meta("NewBase", bases, {}) +class Card2(object): + __metaclass__ = FlyweightMeta - -class Card2(with_metaclass(FlyweightMeta)): def __init__(self, *args, **kwargs): # print('Init {}: {}'.format(self.__class__, (args, kwargs))) pass diff --git a/patterns/structural/flyweight__py3.py b/patterns/structural/flyweight__py3.py new file mode 100644 index 00000000..af2e939c --- /dev/null +++ b/patterns/structural/flyweight__py3.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +*What is this pattern about? +This pattern aims to minimise the number of objects that are needed by +a program at run-time. A Flyweight is an object shared by multiple +contexts, and is indistinguishable from an object that is not shared. + +The state of a Flyweight should not be affected by it's context, this +is known as its intrinsic state. The decoupling of the objects state +from the object's context, allows the Flyweight to be shared. + +*What does this example do? +The example below sets-up an 'object pool' which stores initialised +objects. When a 'Card' is created it first checks to see if it already +exists instead of creating a new one. This aims to reduce the number of +objects initialised by the program. + +*References: +http://codesnipers.com/?q=python-flyweights + +*TL;DR80 +Minimizes memory usage by sharing data with other similar objects. +""" + +import weakref + + +class FlyweightMeta(type): + def __new__(mcs, name, parents, dct): + """ + Set up object pool + + :param name: class name + :param parents: class parents + :param dct: dict: includes class attributes, class methods, + static methods, etc + :return: new class + """ + dct['pool'] = weakref.WeakValueDictionary() + return super(FlyweightMeta, mcs).__new__(mcs, name, parents, dct) + + @staticmethod + def _serialize_params(cls, *args, **kwargs): + """ + Serialize input parameters to a key. + Simple implementation is just to serialize it as a string + """ + args_list = list(map(str, args)) + args_list.extend([str(kwargs), cls.__name__]) + key = ''.join(args_list) + return key + + def __call__(cls, *args, **kwargs): + key = FlyweightMeta._serialize_params(cls, *args, **kwargs) + pool = getattr(cls, 'pool', {}) + + instance = pool.get(key) + if instance is None: + instance = super(FlyweightMeta, cls).__call__(*args, **kwargs) + pool[key] = instance + return instance + + +class Card(object): + + """The object pool. Has builtin reference counting""" + + _CardPool = weakref.WeakValueDictionary() + + """Flyweight implementation. If the object exists in the + pool just return it (instead of creating a new one)""" + + def __new__(cls, value, suit): + obj = Card._CardPool.get(value + suit) + if not obj: + obj = object.__new__(cls) + Card._CardPool[value + suit] = obj + obj.value, obj.suit = value, suit + return obj + + # def __init__(self, value, suit): + # self.value, self.suit = value, suit + + def __repr__(self): + return "" % (self.value, self.suit) + + +class Card2(metaclass=FlyweightMeta): + def __init__(self, *args, **kwargs): + # print('Init {}: {}'.format(self.__class__, (args, kwargs))) + pass + + +if __name__ == '__main__': + # comment __new__ and uncomment __init__ to see the difference + c1 = Card('9', 'h') + c2 = Card('9', 'h') + print(c1, c2) + print(c1 == c2, c1 is c2) + print(id(c1), id(c2)) + + c1.temp = None + c3 = Card('9', 'h') + print(hasattr(c3, 'temp')) + c1 = c2 = c3 = None + c3 = Card('9', 'h') + print(hasattr(c3, 'temp')) + + # Tests with metaclass + instances_pool = getattr(Card2, 'pool') + cm1 = Card2('10', 'h', a=1) + cm2 = Card2('10', 'h', a=1) + cm3 = Card2('10', 'h', a=2) + + assert (cm1 == cm2) and (cm1 != cm3) + assert (cm1 is cm2) and (cm1 is not cm3) + assert len(instances_pool) == 2 + + del cm1 + assert len(instances_pool) == 2 + + del cm2 + assert len(instances_pool) == 1 + + del cm3 + assert len(instances_pool) == 0 + +### OUTPUT ### +# (, ) +# (True, True) +# (31903856, 31903856) +# True +# False From c1ab03c78b026e9f746736dc60266c279307ca2a Mon Sep 17 00:00:00 2001 From: Grygorii Iermolenko Date: Sat, 9 Mar 2019 17:17:57 +0200 Subject: [PATCH 3/3] Remove redundant test --- tests/structural/test_flyweight.py | 37 ------------------------------ 1 file changed, 37 deletions(-) delete mode 100644 tests/structural/test_flyweight.py diff --git a/tests/structural/test_flyweight.py b/tests/structural/test_flyweight.py deleted file mode 100644 index 82cdefea..00000000 --- a/tests/structural/test_flyweight.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import unittest -from patterns.structural.flyweight import Card - - -class TestCard(unittest.TestCase): - def test_instances_shall_reference_same_object(self): - c1 = Card('9', 'h') - c2 = Card('9', 'h') - self.assertEqual(c1, c2) - self.assertEqual(id(c1), id(c2)) - - def test_instances_with_different_suit(self): - """ - shall reference different objects - """ - c1 = Card('9', 'a') - c2 = Card('9', 'b') - self.assertNotEqual(id(c1), id(c2)) - - def test_instances_with_different_values(self): - """ - shall reference different objects - """ - c1 = Card('9', 'h') - c2 = Card('A', 'h') - self.assertNotEqual(id(c1), id(c2)) - - def test_instances_shall_share_additional_attributes(self): - expected_attribute_name = 'attr' - expected_attribute_value = 'value of attr' - c1 = Card('9', 'h') - c1.attr = expected_attribute_value - c2 = Card('9', 'h') - self.assertEqual(hasattr(c2, expected_attribute_name), True) - self.assertEqual(c2.attr, expected_attribute_value)