Pythonの勉強のため、deapにおけるIndividualを例に取ってMutableSequenceな自作クラスを定義してみた。

import sys
import deap

print(sys.version)

3.9.13 | packaged by conda-forge | (main, May 27 2022, 17:01:00)
[Clang 13.0.1 ]
print(f"deap: {deap.__version__}")

deap: 1.3

deapにおける ‘Individual’

# https://deap.readthedocs.io/en/master/overview.html#types
from deap import base, creator

# FitnessMinの定義
creator.create("FitnessMin", base.Fitness, weights=(-1.0,))
# FitnessMinを使ってIndividualの定義
creator.create("Individual", list, fitness=creator.FitnessMin)

たとえば以下のような使い方が想定されている。

# リストのような振る舞いをする
individual = creator.Individual(range(3))
print(individual)

[0, 1, 2]
# 適応度を代入すると
individual.fitness.values = (1.23,)
# 重みをかけた値を取得できる
print(individual.fitness.wvalues)

(-1.23,)

要するに

  1. リストのように遺伝子を配列で保持できて、
  2. fitness (適合度) に関する情報をattributeとして持つことができて、さらに
  3. fitnessに重み付け (1もしくは-1をかけることで最小化問題に統一) することができる

ようなオブジェクトを生成したいだけ。

deapにより定義されたIndividualを代替するものをきちんと定義したいという思いをきっかけに、 上記の3条件を満たすオブジェクトを私の趣味を存分に取り入れながら定義してみようと思う。

(無駄に凝って型付けしがち)

自作の ‘Individual’

実装

from typing import (
    Literal,
    Optional,
    Union,
    TypeVar,
    overload,
    SupportsIndex,
)
from collections.abc import MutableSequence, Iterable, Iterator
from numbers import Number
import random

T = TypeVar("T")


class Individual(MutableSequence[T]):
    def __init__(
        self,
        __genes: Iterable[T],
        *,
        sign: Literal[1, -1] = 1,
        seed: Optional[int] = None,
        fitness: Optional[Number] = None,
    ) -> None:
        """遺伝的アルゴリズムにおける個体を表すクラス。

        Parameters
        ----------
        __genes : Iterable[T]
            遺伝子。個体の表現。
        sign : Literal[1, -1], optional
            符号。1または-1。
            最小化問題の場合は1、最大化問題の場合は-1。, by default 1
        seed : Optional[int], optional
            シード値。Noneの場合はランダムに割り当てられる, by default None
        fitness : Optional[Number], optional
            適応度。基本的にインスタンス生成時には代入しない, by default None
        """
        self.__genes = list(__genes)
        self.__fitness = fitness

        if seed is None:
            self.__seed = random.randint(0, 2**31 - 1)  # int32の範囲
        else:
            self.__seed = seed

        if sign not in {1, -1}:
            raise ValueError("sign must be 1 or -1")
        else:
            self.__sign = sign

    @property
    def seed(self) -> int:
        return self.__seed

    @property
    def sign(self) -> Literal[1, -1]:
        """符号。1または-1。
        最小化問題の場合は1、最大化問題の場合は-1。
        """
        return self.__sign

    @property
    def fitness(self) -> Optional[Number]:
        """適応度。Noneの場合は未評価。"""
        return self.__fitness

    @seed.setter
    def seed(self, __value) -> None:
        raise AttributeError("can't set attribute")

    @sign.setter
    def sign(self, __value) -> None:
        raise AttributeError("can't set attribute")

    @fitness.setter
    def fitness(self, __value: Number) -> None:
        self.__fitness = __value

    @overload
    def __setitem__(self, __index: SupportsIndex, __value: T) -> None:
        ...

    @overload
    def __setitem__(self, __index: slice, __value: Iterable[T]) -> None:
        ...

    # MutableSequenceのabstractmethod
    def __setitem__(
        self,
        __index: Union[SupportsIndex, slice],
        __value: Union[T, Iterable[T]],
    ) -> None:
        self.__genes.__setitem__(__index, __value)

    @overload
    def __getitem__(self, __index: SupportsIndex) -> T:
        ...

    @overload
    def __getitem__(self, __index: slice) -> "Individual[T]":
        ...

    # MutableSequenceのabstractmethod
    def __getitem__(
        self, __index: Union[SupportsIndex, slice]
    ) -> Union[T, "Individual[T]"]:
        # スライスの場合、Individualを返す
        if isinstance(__index, slice):
            return self.__class__(
                self.__genes.__getitem__(__index),
                sign=self.__sign,
                seed=self.__seed,
                fitness=self.__fitness,
            )
        else:
            return self.__genes.__getitem__(__index)

    # MutableSequenceのabstractmethod
    def __delitem__(self, __index: Union[SupportsIndex, slice]) -> None:
        self.__genes.__delitem__(__index)

    # MutableSequenceのabstractmethod
    def __len__(self) -> int:
        return self.__genes.__len__()

    # MutableSequenceのabstractmethod
    def insert(self, __index: SupportsIndex, __value: T) -> None:
        return self.__genes.insert(__index, __value)

    def __str__(self) -> str:
        # オブジェクトの中身がよくわかるように定義
        return "{}({}, sign={}, seed={}, fitness={})".format(
            self.__class__.__name__,
            self.__genes,
            self.__sign,
            self.__seed,
            self.__fitness,
        )

    def __repr__(self) -> str:
        # print()で表示される内容を定義
        return self.__str__()

    def __add__(self, other: Iterable[T]) -> "Individual[T]":
        # Individual同士の結合を定義
        # このとき、signやseed、fitnessは左側のIndividualのものを引き継ぐ
        self.__genes += list(other)
        return self

標準ライブラリのみで実装されている。なお趣味でたくさん型付けをしているため、Python3.9以上でないと正しく動作しない。

参考

Individualは、 ‘リストのようなもの’であり、これはMutableSequenceにあたるのでこれを継承して定義してみた。

参考

性質

# deapのindividualと同じように、listに似た形で定義できる
ind0 = Individual(range(2))
ind1 = Individual(range(2, 4))
ind0

Individual([0, 1], sign=1, seed=1585548440, fitness=None)

表示もいい感じ。

# 結合できる
ind = ind0 + ind1
ind

Individual([0, 1, 2, 3], sign=1, seed=1585548440, fitness=None)
# 個人の思想でシード値を個体に持たせている
ind.seed

1585548440
# 適応度
ind.fitness

# 符号
ind.sign

1
# シード値を変更しようとするとエラー
try:
    ind.seed = 1
except AttributeError as e:
    print(e)

can't set attribute
# 同様に符号を変更しようとするとエラー
try:
    ind.sign = 1
except AttributeError as e:
    print(e)

can't set attribute
# スライスでも正しくオブジェクトが取り出せる
ind[0:2]

Individual([0, 1], sign=1, seed=1585548440, fitness=None)
# for文では、個体のもつ遺伝子が取り出せる
for i in ind:
    print(i)

0
1
2
3

コメント

PythonのlistMutableSequenceを継承している ‘子’ のはずなのにlistを使わないと独自のMutableなSequenceを定義できないパラドックスに気持ち悪さを感じる。

isinstance(list(range(2)), MutableSequence)

True

有限長ならリストを使わずに定義できる (x0, x1, …をそれぞれOptional[T]で可能な限り定義しておいて、それぞれを0, 1, …のindexと対応づけて__getitem____setitem__をif文を使って定義すれば実現できる) が、無限長だとそうはいかない。

この部分がPythonのリストが「おかしい」と言われる所以なのだろうか。

リストの実体は「C言語の配列」として定義されているらしいけど、MutableSequenceの ‘子’ でもあるというのはどういう状況なのだろう…。