5
\$\begingroup\$

After programming in Haskell for a while, I've gotten attached to a functional style. This is clearly evident in the code for my genetic algorithm.

Could you provide me with some hints as to how I can make this code more pythonic? By that, I mean provide some method of organisation rather than throwing a bunch of functions around. Any other recommendations are also welcome.

import copy
import matplotlib.pyplot as pyplot
import random


def create_member(genes):
    return (sum(genes), genes)


def shuffle(pool):
    pool_shuffled = copy.deepcopy(pool)
    random.shuffle(pool_shuffled)
    return pool_shuffled


def calculate_pool_fitness(pool):
    return sum([member[0] for member in pool])


def calculate_member_fitness(member):
    return sum(member[1])


def recalculate_fitneses(pool):
    return [(calculate_member_fitness(member), member[1]) for member in pool]


def select_members_roulette(pool, count):
    selection = []
    while len(selection) < count:
        member = select_member_roulette(pool)
        selection.append(copy.deepcopy(pool[member]))
    return selection


def select_member_roulette(pool):
    drop = random.randint(0, calculate_pool_fitness(pool))
    total_fitness = 0
    for member in range(0, len(pool)):
        total_fitness += pool[member][0]
        if total_fitness >= drop:
            return member


def mutate_gene(gene, rate=1):
    return 1 - gene if random.random() <= rate else gene


def mutate_genes(genes, rate=1):
    return [mutate_gene(gene, rate) for gene in genes]


def mutate_member(member, rate=1):
    return member[0], mutate_genes(member[1], rate)


def mutate_pool(pool, rate=1):
    return [mutate_member(member, rate) for member in pool]


def create_random_gene():
    return random.choice([0, 1])


def create_random_genes(size):
    return [create_random_gene() for _ in range(size)]


def crossover_genes(mother, father, rate=1):
    if random.random() <= rate:
        split = random.randint(1, len(mother))
        daughter = mother[:split] + father[split:]
        son = father[:split] + mother[split:]
    else:
        daughter = copy.deepcopy(mother)
        son = copy.deepcopy(father)
    return daughter, son


def crossover_members(mother, father, rate=1):
    daughter_genes, son_genes = crossover_genes(mother[1], father[1])
    return [(mother[0], daughter_genes), (father[0], son_genes)]


def crossover_pool(pool, rate=1):
    children = []

    # select every two elements for crossover
    for mother, father in zip(pool[::2], pool[1::2]):
        children.extend(crossover_members(mother, father, rate))

    return children


def generate_pool(size, gene_size):
    pool = []
    for member in range(0, size):
        genes = create_random_genes(gene_size)
        pool.append(create_member(genes))
    return pool


def evolve(pool, rate_crossover=0.9, rate_mutation=0.01):
    successors = copy.deepcopy(pool)

    # perform roulette selection whilst keeping best member
    member_alpha = copy.deepcopy(max(successors, key=lambda member: member[0]))
    successors = select_members_roulette(pool, len(pool) - 1)
    successors.append(member_alpha)

    successors = shuffle(successors)
    successors = crossover_pool(successors, rate_crossover)
    successors = mutate_pool(successors, rate_mutation)
    successors = recalculate_fitneses(successors)
    return successors


def main():
    random.seed
    pyplot.figure(figsize=(14, 8), dpi=400)
    axgraph=pyplot.subplot(111)
    pool_size = 50
    gene_size = 50
    generations = 100
    pool = generate_pool(pool_size, gene_size)
    for generation in range(0, generations):
        pool = evolve(pool)
        axgraph.scatter(generation, sum([member[0] for member in pool]))
    pyplot.grid(True)
    pyplot.axis([0, generations, 0, pool_size * gene_size])
    pyplot.savefig('genetic_algorithms.png')

main()

if __name__ == '__main__':
    main()
\$\endgroup\$
4
\$\begingroup\$

I have been facing the same issue for a few months, even though I didn't write that much functional code before switching. Please take my comments with a grain of salt even if I say "do this" instead of "this might help but I'm not sure".

  • Use dictionaries instead of tuples:

    def create_member(genes):
        return {'sum': sum(genes), 'genes': genes}
    
  • Use generators instead of list-expressions:

    def mutate_pool(pool, rate=1):
        for member in pool:
            yield mutate_member(member, rate)
    

    Yay, lazy lists! (kind of...)

  • Write code for the next person which will be reading it. You shouldn't put comments only where you had some trouble ("[::2] selects one gene out of two") but try to make any function easy to understand and modify in isolation.

  • Prefer longer functions and document them using docstrings. A nice function name is usually not enough for documentation:

    def mutate_genes(genes, rate):
        """
        Given a list of genes, flip some of them according to rate.
        """
        for gene in genes:
            yield 1 - gene if random.random() <= rate else gene
    
    def mutate_pool(pool, rate=1):
        """
        Returns a new pool of member where every bit in every gene could have been
        flipped according to rate.
        """
        for member in pool:
            yield member[0], mutate_genes(member[1], rate)
    

    Of course those examples are a bit artificial but they do feel more pythonic. You don't have to document every function, just think about your readers. :)

  • Read nice Python code. I'd recommend Django and scikit-learn (Those are links to specific files that I chose randomly.)

I think it's worthwhile to try to force yourself to write pythonic code for some time so that you can tell what's nice in "the Python way" and what's actually better in a functional style.

\$\endgroup\$
5
\$\begingroup\$
  • random.seed is a bug: add parentheses to actually call the function.
  • Use collections.namedtuple so you can write member.fitness instead of the less readable member[0]
  • copy.deepcopy should not be necessary in a functional approach
  • Keeping the genes in a tuple instead of a list would make the member tuple fully immutable, in line with the functional approach. This eliminates the need to deep-copy objects as you can safely copy just references.

After these changes create_member becomes like this:

import collections
Member = collections.namedtuple("Member", "fitness genes")

def create_member(genes):
    genes = tuple(genes)
    return Member(sum(genes), genes)

In some places you create members without calling create_member. Be sure to change them, for example:

def mutate_member(member, rate=1):
    return create_member(mutate_genes(member.genes, rate))
  • With immutable members there is no need to recalculate fitnesses. You can delete such functions.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.