[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Should nested classes in an Enum be Enum members?

On Thu, Jun 28, 2018 at 10:06 PM Ben Finney <ben+python at benfinney.id.au> wrote:
> Ethan Furman <ethan at stoneleaf.us> writes:
> > On 06/28/2018 05:58 PM, Ben Finney wrote:
> >
> > > So I remain dumbfounded as to why anyone would want a class to *both* be
> > > an enumerated type, *and* have callable attributes in its API.
> >
> > Perhaps I am using Enum incorrectly, but here is my FederalHoliday
> > Enum. [?]
> Thanks for the example. Yes, my personal impression is that class
> is not a good use of enum.Enum (nor enum.AutoEnum).
> To inherit from enum.Enum (or enum.AutoEnum) signals, to my mind, that
> the class is not really intended as a typical Python class, but instead
> is intended to be that special beast known as an ?enumerated type? which
> has little behaviour other than being a namespace for constant values.

That sounds like a C attitude talking. Python enums are heavily
inspired by Java enums, which also permit enum members to have
attributes and methods. In fact, that planets example that Steven
mentioned is ripped straight out of the Java documentation. You may
find it weird, but I actually find it really, really useful to be able
to attach methods to enums. For example:

class Color(Enum):
    RED = 'red'
    GREEN = 'green'
    BLUE = 'blue'

    def __str__(self):
        return 'the color %s' % self.value

How else is one to override the str() of an enum? Another example:

class ChessPiece(Enum):
    PAWN = 1, 'P'
    KNIGHT = 2, 'N'
    BISHOP = 3, 'B'
    ROOK = 4, 'R'
    # ...

    def label(self):
        return self.value[1]

    def __lt__(self, other):
        assert isinstance(other, ChessPiece)
        return self.value < other.value

This enum type defines its own ordering based on the relative values
of the chess pieces.

To be fair, Java does this a bit better than Python does. For example,
Java allows enum methods to be overridden by individual enum members.
This makes it easy to, e.g., implement a Strategy pattern where the
Strategy of choice depends upon an enum value. If you want to do that
in Python, you're pretty much stuck with passing either lambdas or
functions defined elsewhere as part of the enum member's value. But we
can do it, which leads us to our next example:

def pawn_moves(board, from_square):
    "Generate all pawn moves possible from the given square."
    # ...

def knight_moves(board, from_square):
    "Generate all knight moves possible from the given square."
    # ...

def pawn_attacks(board, from_square):
    "Generate all squares that would be attacked by a pawn a the given square."
    # ...

# ...

class ChessPiece(Enum):
    PAWN = 'P', pawn_moves, pawn_attacks
    KNIGHT = 'N', knight_moves, knight_attacks
    BISHOP = 'B', bishop_moves, bishop_attacks
    ROOK = 'R', rook_moves, rook_attacks
    # ...

    def moves(self, board, from_square):
        return self.value[1](board, from_square)

    def attacks(self, board, from_square):
        return self.value[2](board, from_square)

Now, elsewhere we can easily do something like:

    all_moves = []
    for square in board:
        piece = board[square]
        if piece:
            all_moves.extend(piece.moves(board, square))

Et voila. ChessPiece is still an enum as it should be, and it also has
a useful API on top of that.