21

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):
    ...
Acccumulation
  • 3,491
  • 1
  • 8
  • 12
Lo L
  • 243
  • 2
  • 10
  • 1
    Default attributes are only evaluated once (when you define the function), so the random value will only be random the one time, and every time you call that function you will get the same initial value. – Ethan Furman Nov 17 '17 at 15:32
  • That's usual behavior which i want to change. – Lo L Nov 17 '17 at 15:33
  • That is a Python fundamental behavior and you cannot change it. – Ethan Furman Nov 17 '17 at 15:35
  • I was referring to the workaround. Of course i can't change the Python and i don't want to do it. – Lo L Nov 17 '17 at 15:36
  • What workaround? `def full_name(gender=Gender.RANDOM):` is not a workaround. – Ethan Furman Nov 17 '17 at 15:42
  • @EthanFurman I actually can do it using descriptor `classproperty` from [this](https://stackoverflow.com/questions/5189699/how-to-make-a-class-property) answer, but I was hoping for a simpler solution that would be much Pythonic. – Lo L Nov 17 '17 at 15:46
  • 1
    [Obligatory xkcd](https://xkcd.com/221/) – Pac0 Nov 21 '17 at 18:44

8 Answers8

14

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()
Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • You are right. It's much clearer, than using Gender.RANDOM as default value. Thanks! – Lo L Nov 17 '17 at 16:10
8

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
    ...
Abhijith Asokan
  • 1,865
  • 10
  • 14
  • While it does work, I'm down-voting because `EnumMeta` should only be subclassed when necessary and there are other ways to solve this problem. See [When should I subclass `EnumMeta`](https://stackoverflow.com/q/43730305/208880) for details on `EnumMeta` subclassing. However, since this was a successful effort I am offering a bounty on it. – Ethan Furman Jun 27 '19 at 15:22
5

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()
Reblochon Masque
  • 35,405
  • 10
  • 55
  • 80
  • We use this enum object as default value of some method, and it should be `Gender.RANDOM`, not `Gender.get_random()` or `Gender.RANDOM()`. It's for clear API. – Lo L Nov 17 '17 at 15:16
2

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'
developer_hatch
  • 15,898
  • 3
  • 42
  • 75
2

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()
    # ...
grovina
  • 2,999
  • 19
  • 25
0

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.

LeGBT
  • 104
  • 3
0

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])

7yl4r
  • 4,788
  • 4
  • 34
  • 46
0

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
Rafael C.
  • 2,245
  • 4
  • 29
  • 45