{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Modifying Operators with the Interceptor Pattern" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The interceptor design pattern does what?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this framework, an interceptor adds to the behaviour of an operator. This example implements an interceptor that changes any selector to an elitist selector." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To begin, consider a simple example. Declare a class NumberBox with attribute `.value` and method `.increment`. For now, calling `.increment` increments `.value` by 1." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "from typing import Self\n", "from typing import override" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "class NumberBox:\n", " def __init__(self: Self, value: int)-> None:\n", " self.value: int = value\n", "\n", " def increment(self: Self)-> None:\n", " self.value = self.value + 1\n", "\n", "def check_increment(nb: NumberBox):\n", " old_value = nb.value\n", " print(f\"Initial value is {old_value}\")\n", " new_value = (nb.increment(), nb.value)[-1]\n", " print(f\"Increased by {new_value - old_value}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Check that `NumberBox` behaves as expected:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Initial value is 1\n", "Increased by 1\n" ] } ], "source": [ "nb = NumberBox(1)\n", "check_increment(nb)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There are many ways to change the number `.increment` increases `.value` by. For example, one can define a new class, NumberBoxBy2, which extends NumberBox and overrides `.increment`." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "class NumberBoxBy2(NumberBox):\n", " @override\n", " def increment(self: Self)-> None:\n", " self.value = self.value + 2" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Initial value is 1\n", "Increased by 2\n" ] } ], "source": [ "nb_new = NumberBoxBy2(1)\n", "check_increment(nb_new)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This approach is inflexible, however. For each class with `.increment` that increases its `.value`, one must extend it separately. ~~It would be helpful~~ to have a way to change the behaviour of `.increment` for _any_ class that has that method." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Suppose there is a function that takes a `NumberBox` and changes its behaviour, so that its `.increment` now increases `.value` by 2. The signature of that function is as follows:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "def by2(sel: NumberBox):\n", " pass" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "from typing import Any, Callable\n", "from functools import wraps\n", "from types import MethodType" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "def by2(numbox: NumberBox):\n", " pass" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this example, `by2` creates a wrapper of the `.increment` method of its argument `numbox`, then replaces the original `.increment` with that wrapper." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "def by2(numbox: NumberBox):\n", " def wrap_function(original_increment:\n", " Callable[[NumberBox], None]) -> Callable:\n", "\n", " @wraps(original_increment)\n", " def wrapper(self: NumberBox) -> None:\n", " original_increment(self)\n", " original_increment(self)\n", " return wrapper\n", "\n", " setattr(numbox, 'increment',\n", " MethodType(\n", " wrap_function(numbox.increment.__func__), # type:ignore\n", " numbox))" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Initial value is 1\n", "Increased by 1\n" ] } ], "source": [ "new_nb = NumberBox(1)\n", "check_increment(new_nb)" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Initial value is 1\n", "Increased by 2\n" ] } ], "source": [ "modified_nb = NumberBox(1)\n", "by2(modified_nb)\n", "check_increment(modified_nb)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This use case is common in the framework. For example, all selectors share methods named `.select` and `.select_to_many`. It would be good to be able to change the behaviour of an instance of a selector without modifying or extending its class." ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [], "source": [ "from evokit.core import Selector, Population, Individual\n", "\n", "from typing import Callable, Tuple, TypeVar, Any\n", "\n", "D = TypeVar(\"D\", bound=Individual)\n", "\n", "def Elitist(sel: Selector[D]) -> Selector:\n", "\n", " def wrap_function(original_select_to_many:\n", " Callable[[Selector[D], Population[D]],\n", " Tuple[D, ...]]) -> Callable:\n", "\n", " @wraps(original_select_to_many)\n", " def wrapper(self: Selector[D],\n", " population: Population[D],\n", " *args: Any, **kwargs: Any) -> Tuple[D, ...]:\n", "\n", " population_best: D = population.best()\n", " my_best: D\n", " \n", " # Monkey-patch an attribute onto the selector. If the \n", " # Current name is taken from a randomly generated SSH pubkey.\n", " # Nobody else will use a name *this* absurd.\n", " UBER_SECRET_BEST_INDIVIDUAL_NAME = \"___g1AfoA2NMh8ZZCmRJbwFcne4jS1f3Y2TRPIvBmVXQP\"\n", " if not hasattr(self, UBER_SECRET_BEST_INDIVIDUAL_NAME):\n", " setattr(self, UBER_SECRET_BEST_INDIVIDUAL_NAME, population_best.copy())\n", "\n", " hof_individual: D\n", " my_best = getattr(self, UBER_SECRET_BEST_INDIVIDUAL_NAME)\n", "\n", " if my_best.fitness > population_best.fitness:\n", " hof_individual = my_best\n", " #print(\"use my best\", end=\"\")\n", " else:\n", " hof_individual = population_best\n", " setattr(self, UBER_SECRET_BEST_INDIVIDUAL_NAME, population_best.copy())\n", " #print(\"use population best\", end=\"\")\n", " #print(f\", {str(hof_individual)}score is m{my_best.fitness} > p{population_best.fitness}\")\n", "\n", " # Acquire results of the original selector\n", " results: Tuple[D, ...] = \\\n", " original_select_to_many(self, population, *args, **kwargs)\n", "\n", " # Append the best individual to results\n", " return (*results, hof_individual.copy())\n", " return wrapper\n", "\n", " setattr(sel, 'select_to_many',\n", " MethodType(\n", " wrap_function(sel.select_to_many.__func__), # type:ignore\n", " sel))\n", " return sel" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.0" } }, "nbformat": 4, "nbformat_minor": 2 }