How to get random value of attribute of Enum on each iteration?

Viewed 3880

I have created such Enum object:

class Gender(Enum):
    FEMALE = 'female'
    MALE = 'male'
    RANDOM = random.choice([FEMALE, MALE])

and i want to get really random value each time, but it does not work:

>>> class Gender(Enum):
...    MALE = 'male'
...    FEMALE = 'female'
...    RANDOM = choice([MALE, FEMALE])
... 
>>> Gender.RANDOM
<Gender.MALE: 'male'>
>>> Gender.RANDOM
<Gender.MALE: 'male'>
>>> Gender.RANDOM
<Gender.MALE: 'male'>
>>> Gender.RANDOM
<Gender.MALE: 'male'>

I have also tried use lambda, but it's looks not so good, although it works:

Gender.RANDOM()

Are there other way to get random values each time, without using lambda expressions?

We use this enum object as default value of the argument of the some method that's why it should be an attribute, not a function, because when we use Gender.FEMALE it is not a function, it's an attribute and Gender.RANDOM should be an attribute too:

def full_name(gender=Gender.FEMALE):
    ...


def full_name(gender=Gender.RANDOM):
    ...
8 Answers

As others have said, the best way is to just make random() be a method on your enum class to make it clear that RANDOM is not a member.

However, since I like puzzles:

from enum import Enum
import random

class enumproperty(object):
    "like property, but on an enum class"

    def __init__(self, fget):
        self.fget = fget

    def __get__(self, instance, ownerclass=None):
        if ownerclass is None:
            ownerclass = instance.__class__
        return self.fget(ownerclass)

    def __set__(self, instance, value):
        raise AttributeError("can't set pseudo-member %r" % self.name)

    def __delete__(self, instance):
        raise AttributeError("can't delete pseudo-member %r" % self.name)

class Gender(Enum):
    FEMALE = 'female'
    MALE = 'male'
    @enumproperty
    def RANDOM(cls):
        return random.choice(list(cls.__members__.values()))

In your full_name definition, using Gender.RANDOM as a default value will not get you what you want. The standard for such is:

def full_name(gender=None):
    if gender is None:
        gender = Gender.RANDOM   # we get `MALE` or `FEMALE`, not `RANDOM`

Which is going to be confusing to the reader. This is much better using a normal method:

def full_name(gender=None):
    if gender is None:
        gender = Gender.random()

I tried a way with metaclasses. And it works!

import random
import enum
class RANDOM_ATTR(enum.EnumMeta):
    @property
    def RANDOM(self):
        return random.choice([Gender.MALE, Gender.FEMALE])


class Gender(enum.Enum,metaclass=RANDOM_ATTR): #this syntax works for python3 only
    FEMALE = 'female'
    MALE = 'male'


print(Gender.RANDOM)   #prints male or female randomly

Here by making RANDOM_ATTR the metaclass of Gender, Gender is like an object of class RANDOM_ATTR, so Gender has the property RANDOM.

However,the below code you described in your question doesn't work the way you expect.

def full_name(gender=Gender.RANDOM):
    ...

The RANDOM property will be called only once. To know why, please read this answer. Default arguments are like attributes to function, which will be initialised only once.

For that i would suggest you do something like this:

def full_name(gender=None):
    gender = gender or Gender.RANDOM
    ...

You probably should create a method in your Enum to obtain a random gender:

import random
import enum

class Gender(enum.Enum):
    FEMALE = 'female'
    MALE = 'male'

    @classmethod
    def get_gender(cls):
        return random.choice([Gender.FEMALE, Gender.MALE])

Gender.get_gender()

I think you want a method random, instead to save the value directly in the variable, because if you do that, the value will never change:

If you DON'T WANT A FUNCTION

import random import enum

class Gender(enum.Enum):

  MALE = 'male'
  FEMALE = 'female'
  RANDOM = random.choice([MALE, FEMALE])


  def __getattribute__(self,item):
    if item == "RANDOM":
      Gender.RANDOM = random.choice([self.MALE, self.FEMALE])
      return Gender.RANDOM
    else:
      return object.__getattribute__(self, item)

gender = Gender()

look:

   gender.RANDOM
=> 'female'
   gender.RANDOM
=> 'male'
   gender.RANDOM
=> 'male'
   gender.RANDOM
=> 'male'
   gender.RANDOM
=> 'female'
   gender.RANDOM
=> 'male'
   gender.RANDOM
=> 'male'
   gender.RANDOM
=> 'female'

As RANDOM is not really an item in your enumeration, I think a more coherent approach would be to keep it precisely as a method and not an attribute (it isn't after all!).

import random
import enum


class Gender(enum.Enum):
    MALE = 'male'
    FEMALE = 'female'

    @staticmethod
    def random():
        return random.choice(list(Gender))

Then, you could transfer the "I'm not choosing" behaviour to the function where it actually makes more sense.

def full_name(gender=None):
    if gender is None:
        gender = Gender.random()
    # ...

From the documentation : "An enumeration is a set of symbolic names (members) bound to unique, constant values." You need to switch to another representation like a class.

Although modifying the class may be clearer and DRY-er in many cases, if you're only doing this once or don't want to modify the enum you could do:

random.choice([enm.value for enm in Gender])

Another option that works for your goal is:

from enum import Enum
import random

class Gender(Enum):
    MALE = object()
    FEMALE = object()

    @classmethod
    def get_random_gender(cls):
        gender = random.choice(cls._member_names_)
        return gender.lower()


print(Gender.get_random_gender())
>> female
Related