Updated __init__ and descriptors for inheritance.

Avoided dataclass overriding methods by customizing the __init__ and moving default methods to the descriptors.
This commit is contained in:
SimpleArt
2021-07-08 10:43:46 -04:00
parent f50e723981
commit e2970c0099

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from inspect import signature from inspect import signature
from typing import Any, Callable, Dict, Iterable, Iterator, Optional from typing import Any, Callable, Dict, Iterable, Iterator, Optional
from math import sqrt, ceil from math import sqrt, ceil
from dataclasses import dataclass, field from dataclasses import dataclass, field, _MISSING_TYPE
from types import MethodType from types import MethodType
import random import random
@ -117,12 +117,14 @@ def simple_linear(self: Attributes, weight: float) -> float:
class AttributesData: class AttributesData:
""" """
Attributes class which stores all attributes in a dataclass. Attributes class which stores all attributes in a dataclass.
This includes type-hints/annotations and default values. This includes type-hints/annotations and default values, except for methods.
Additionally gains dataclass features, including an __init__ and __repr__ to avoid boilerplate code. Additionally gains dataclass features, including an __init__ and __repr__ to avoid boilerplate code.
Developer Note: Developer Note:
See the Attributes class for default methods.
Override this class to set default attributes. See help(Attributes) for more information. Override this class to set default attributes. See help(Attributes) for more information.
""" """
@ -159,25 +161,30 @@ class AttributesData:
max_gene_mutation_rate: float = 0.15 max_gene_mutation_rate: float = 0.15
min_gene_mutation_rate: float = 0.01 min_gene_mutation_rate: float = 0.01
fitness_function_impl: Callable[["Attributes", Chromosome], float] = Fitness.is_it_5 #=================================#
make_gene: Callable[[Any], Gene] = Gene # Default methods are implemented #
make_chromosome: Callable[[Iterable[Any]], Chromosome] = Chromosome # in the Attributes descriptors: #
make_population: Callable[[Iterable[Iterable[Any]]], Population] = Population #=================================#
gene_impl: Callable[[], Any] = rand_1_to_10 fitness_function_impl: Callable[["Attributes", Chromosome], float] = None
chromosome_impl: Callable[[], Iterable[Any]] = use_genes make_gene: Callable[[Any], Gene] = None
population_impl: Callable[[], Iterable[Iterable[Any]]] = use_chromosomes make_chromosome: Callable[[Iterable[Any]], Chromosome] = None
make_population: Callable[[Iterable[Iterable[Any]]], Population] = None
weighted_random: Callable[[float], float] = simple_linear gene_impl: Callable[[], Any] = None
dist: Callable[["Attributes", Chromosome, Chromosome], None] = dist_fitness chromosome_impl: Callable[[], Iterable[Any]] = None
population_impl: Callable[[], Iterable[Iterable[Any]]] = None
parent_selection_impl: Callable[["Attributes"], None] = Parent.Rank.tournament weighted_random: Callable[[float], float] = None
crossover_individual_impl: Callable[["Attributes"], None] = Crossover.Individual.single_point dist: Callable[["Attributes", Chromosome, Chromosome], None] = None
crossover_population_impl: Callable[["Attributes", Chromosome, Chromosome], None] = Crossover.Population.sequential
survivor_selection_impl: Callable[["Attributes"], None] = Survivor.fill_in_best parent_selection_impl: Callable[["Attributes"], None] = None
mutation_individual_impl: Callable[["Attributes", Chromosome], None] = Mutation.Individual.individual_genes crossover_individual_impl: Callable[["Attributes"], None] = None
mutation_population_impl: Callable[["Attributes"], None] = Mutation.Population.random_avoid_best crossover_population_impl: Callable[["Attributes", Chromosome, Chromosome], None] = None
termination_impl: Callable[["Attributes"], bool] = Termination.fitness_generation_tolerance survivor_selection_impl: Callable[["Attributes"], None] = None
mutation_individual_impl: Callable[["Attributes", Chromosome], None] = None
mutation_population_impl: Callable[["Attributes"], None] = None
termination_impl: Callable[["Attributes"], bool] = None
database: Database = field(default_factory=sql_database.SQL_Database) database: Database = field(default_factory=sql_database.SQL_Database)
database_name: str = "database.db" database_name: str = "database.db"
@ -194,19 +201,69 @@ class AttributesData:
graph: Callable[[Database], Graph] = matplotlib_graph.Matplotlib_Graph graph: Callable[[Database], Graph] = matplotlib_graph.Matplotlib_Graph
def __init__(self: AttributesData, *args: Any, **kwargs: Any) -> None:
"""
Generated AttributesData.__init__, accepts dataclass fields as parameters.
Ignores parameters whose values are None if something is already set.
"""
# Extract the default value from a dataclass field.
def get_default(default_field):
if type(default_field.default) is not _MISSING_TYPE:
return default_field.default
else:
return default_field.default_factory()
# Extract the default values from all fields.
defaults = {name: get_default(default_field) for name, default_field in self.__dataclass_fields__.items() if default_field.init}
# Verify not too many args are passed in.
if len(args) > len(defaults):
raise TypeError(f"__init__ takes {len(defaults)} positional arguments but {len(args)} were given")
# Convert the args to kwargs.
args = dict(zip(defaults, args))
# Verify a parameter is not in both the args and kwargs.
for name in args.keys() & kwargs.keys():
raise TypeError(f"__init__() got multiple values for '{name}'")
# Verify all kwargs are in the fields.
for name in kwargs.keys() - self.__dataclass_fields__.keys():
raise TypeError(f"__init__ got an unexpected keyword argument '{name}'")
# Merge the defaults, args, and kwargs.
for name, value in {**defaults, **args, **kwargs}.items():
# Ignore None values if self already has that attribute.
if value is not None or name not in dir(self):
setattr(self, name, value)
# Run the __post_init__.
if hasattr(self, "__post_init__"):
self.__post_init__()
class AsMethod: class AsMethod:
"""A descriptor for converting function attributes into bound methods.""" """
A descriptor for converting function attributes into bound methods.
def __init__(self: AsMethod, name: str) -> None: To support both inheritance and dataclasses, if the method is None,
then nothing is set.
"""
def __init__(self: AsMethod, name: str, default: Callable) -> None:
if not callable(default):
raise TypeError(f"'default' must be a method i.e. callable.")
self.name = name self.name = name
self.default = default
def __get__(self: AsMethod, obj: "AttributesProperties", cls: type) -> MethodType: def __get__(self: AsMethod, obj: "AttributesProperties", cls: type) -> Callable:
return vars(obj)[self.name] # Already has the attribute on the object.
if self.name in vars(obj):
return vars(obj)[self.name]
# Otherwise use the default as a method.
if next(iter(signature(self.default).parameters), None) in ("self", "ga"):
return MethodType(self.default, obj)
# Otherwise use the default as a function.
return self.default
def __set__(self: AsMethod, obj: "AttributesProperties", method: Callable) -> None: def __set__(self: AsMethod, obj: "AttributesProperties", method: Optional[Callable]) -> None:
if not callable(method): if method is None:
raise TypeError(f"{self.name} must be a method i.e. callable.") return
elif not callable(method):
raise TypeError(f"'{self.name}' must be a method i.e. callable.")
elif next(iter(signature(method).parameters), None) in ("self", "ga"): elif next(iter(signature(method).parameters), None) in ("self", "ga"):
method = MethodType(method, obj) method = MethodType(method, obj)
vars(obj)[self.name] = method vars(obj)[self.name] = method
@ -264,19 +321,22 @@ class Attributes(AttributesData):
# functions into methods: # # functions into methods: #
#===========================# #===========================#
fitness_function_impl = AsMethod("fitness_function_impl") fitness_function_impl = AsMethod("fitness_function_impl", Fitness.is_it_5)
parent_selection_impl = AsMethod("parent_selection_impl") make_gene = AsMethod("make_gene", Gene)
crossover_individual_impl = AsMethod("crossover_individual_impl") make_chromosome = AsMethod("make_chromosome", Chromosome)
crossover_population_impl = AsMethod("crossover_population_impl") make_population = AsMethod("make_population", Population)
survivor_selection_impl = AsMethod("survivor_selection_impl") gene_impl = AsMethod("gene_impl", rand_1_to_10)
mutation_individual_impl = AsMethod("mutation_individual_impl") chromosome_impl = AsMethod("chromosome_impl", use_genes)
mutation_population_impl = AsMethod("mutation_population_impl") population_impl = AsMethod("population_impl", use_chromosomes)
termination_impl = AsMethod("termination_impl") dist = AsMethod("dist", dist_fitness)
dist = AsMethod("dist") weighted_random = AsMethod("weighted_random", simple_linear)
weighted_random = AsMethod("weighted_random") parent_selection_impl = AsMethod("parent_selection_impl", Parent.Rank.tournament)
gene_impl = AsMethod("gene_impl") crossover_individual_impl = AsMethod("crossover_individual_impl", Crossover.Individual.single_point)
chromosome_impl = AsMethod("chromosome_impl") crossover_population_impl = AsMethod("crossover_population_impl", Crossover.Population.sequential)
population_impl = AsMethod("population_impl") survivor_selection_impl = AsMethod("survivor_selection_impl", Survivor.fill_in_best)
mutation_individual_impl = AsMethod("mutation_individual_impl", Mutation.Individual.individual_genes)
mutation_population_impl = AsMethod("mutation_population_impl", Mutation.Population.random_avoid_best)
termination_impl = AsMethod("termination_impl", Termination.fitness_generation_tolerance)
#=============# #=============#
# Properties: # # Properties: #
@ -325,7 +385,7 @@ class Attributes(AttributesData):
@property @property
def max_chromosome_mutation_rate(self: AttributesProperties) -> float: def max_chromosome_mutation_rate(self: AttributesProperties) -> float:
# Default value. # Default value.
if vars(self)["max_chromosome_mutation_rate"] is None: if vars(self).get("max_chromosome_mutation_rate", None) is None:
return min(self.chromosome_mutation_rate * 2, (self.chromosome_mutation_rate + 1) / 2) return min(self.chromosome_mutation_rate * 2, (self.chromosome_mutation_rate + 1) / 2)
# Set value. # Set value.
return vars(self)["max_chromosome_mutation_rate"] return vars(self)["max_chromosome_mutation_rate"]
@ -341,7 +401,7 @@ class Attributes(AttributesData):
@property @property
def min_chromosome_mutation_rate(self: AttributesProperties) -> float: def min_chromosome_mutation_rate(self: AttributesProperties) -> float:
# Default value. # Default value.
if vars(self)["min_chromosome_mutation_rate"] is None: if vars(self).get("min_chromosome_mutation_rate", None) is None:
return max(self.chromosome_mutation_rate / 2, self.chromosome_mutation_rate * 2 - 1) return max(self.chromosome_mutation_rate / 2, self.chromosome_mutation_rate * 2 - 1)
# Set value. # Set value.
return vars(self)["min_chromosome_mutation_rate"] return vars(self)["min_chromosome_mutation_rate"]