Подтвердить что ты не робот

Перечисления в python

Duplicate:
Что лучший способ реализовать & enquo; enum & rsquo; в Python?

Каков признанный способ выполнения перечислений в python?

Например, на данный момент я пишу игру и хочу иметь возможность перемещать "вверх", "вниз", "влево" и "вправо". Я использую строки, потому что я еще не понял, как перечисления работают на python, и поэтому моя логика усеяна такими вещами:

def move(self, direction):
    if direction == "up":
        # Do something

Я хочу заменить "up" на что-то вроде Directions.up

4b9b3361

Ответ 1

class Directions:
    up = 0
    down = 1
    left = 2
    right =3

Ответ 2

ОБНОВЛЕНИЕ 1: Python 3.4 будет иметь встроенную хорошо разработанную библиотеку enum. Значения всегда знают их имя и тип; существует целочисленный совместимый режим, но рекомендуемым по умолчанию для новых применений являются одиночные, не равные любому другому объекту.

ОБНОВЛЕНИЕ 2: после написания этого я понял, что критический тест для перечислений - это сериализация. Другие аспекты могут быть реорганизованы позже, но если ваше перечисление переходит в файлы/на провод, спросите себя, что должно произойти, если оно будет десериализовано более старой/более новой версией (которая может поддерживать другой набор значений)...


Если вы уверены, что вам нужно перечислить, другие ответили, как это сделать. Но посмотри, почему ты хочешь их? Понимание мотивации поможет выбрать решение.

  • Атомные значения - в C небольшие числа легко перемещаются, строки - нет. В Python строки, подобные "up", отлично подходят для многих применений. Более того, любое решение, которое заканчивается только числом, хуже для отладки!

  • Значимые значения - на C вам часто приходится иметь дело с существующей магией чисел, и просто для этого нужен какой-то синтаксический сахар. Это не так. Однако есть и другая значимая информация, которую вы можете связать с направления, например. вектор (dx, dy) - подробнее об этом ниже.

  • Проверка типов - в C, перечисления помогают ловить недопустимые значения во время компиляции. Но Python обычно предпочитает жертвовать компилятором, проверяя меньшее количество ввода.

  • Introspection (не существует в C перечислениях) - вы хотите знать все допустимые значения.

    • Завершение - редактор может показать вам возможные значения и помочь вам ввести их.

Выпущенные строки (ака Символы)

Итак, на светлой стороне решений Pythonic просто используйте строки и, возможно, список/набор всех допустимых значений:

DIRECTIONS = set(['up', 'down', 'left', 'right'])

def move(self, direction):
    # only if you feel like checking
    assert direction in DIRECTIONS
    # you can still just use the strings!
    if direction == 'up':
        # Do something

Обратите внимание, что отладчик скажет вам, что функция была вызвана с "вверх" в качестве аргумента. Любое решение, где direction на самом деле 0, намного хуже этого!

В семействе языков LISP это использование дублируется символы - атомные объекты, которые можно использовать так же легко, как цифры, но несут текстовое значение. (Чтобы быть точным, символы являются строковыми, но отдельными типами. Однако Python обычно использует регулярные строки, где LISP будет использовать символы.)

Строки с именами

Вы можете объединить идею о том, что 'up' лучше, чем 0 с другими решениями.

Если вы хотите поймать неверное поведение (во время выполнения):

UP = 'up'
...
RIGHT = 'right'

И если вы хотите настаивать на вводе префикса для завершения, поставьте вышеприведенное в классе:

class Directions:
    UP = "up"
    ...
    RIGHT = "right"

или просто в отдельном файле, что делает его модулем.

Модуль позволяет ленивым пользователям делать from directions import *, чтобы пропустить префикс - до вас, считаете ли вы это плюсом или минусом... (Я лично не хотел бы, чтобы меня заставляли вводить Directions.UP, если я использую это часто).

Объекты с функциональностью

Что делать, если есть полезная информация/функциональность, связанная с каждым значением? "right" - это не только одно из 4 произвольных значений, это положительное направление на оси X!

Если то, что вы делаете в этом if, выглядит примерно так:

def move(self, direction):
    if direction == 'up':
        self.y += STEP
    elif direction == 'down':
        self.y -= STEP
    elif direction == 'left':
        self.x -= STEP
    elif direction == 'right':
        self.x += STEP

чем вы хотели бы написать:

def move(self, direction):
    self.x += direction.dx * STEP
    self.y += direction.dy * STEP

и что это!

Итак, вы хотите использовать это в экземплярах:

# Written in full to give the idea.
# Consider using collections.namedtuple
class Direction(object):
    def __init__(self, dx, dy, name):
        self.dx = dx
        self.dy = dy
        self.name = name
    def __str__(self):
        return self.name

UP = Direction(0, 1, "up")
DOWN = Direction(0, -1, "down")
LEFT = Direction(-1, 0, "left")
RIGHT = Direction(1, 0, "right")

или только классы:

class Direction(object):
    pass

class Up(Direction):
    dx = 0
    dy = 1

...

class Right(Direction):
    dx = 1
    dy = 0

Помните, что в Python классы также являются объектами (отличными от любого другого объекта), и вы можете их сравнить: direction == Up и т.д.

В общем случае экземпляры, вероятно, более чистые, но если ваши перечисленные понятия имеют некоторые иерархические отношения, иногда их моделирование непосредственно с классами очень приятно.

Ответ 3

Я дал +1 к Kugel, но другой более компактный вариант

dirUp, dirDown, dirLeft, dirRight = range(4)
      • (Проходит некоторое время)

Итак, я думал... у нас есть очевидное нарушение DRY здесь, поскольку мы уже указываем четыре элемента на LHS, а затем снова указываем четыре на RHS. Что произойдет, если мы добавим элементы в будущем? Что происходит, когда кто-то их добавляет, и, может быть, они более неряшливы, чем мы сами? Одним из очевидных способов устранения нарушения DRY является использование списка самих перечислений для назначения их значений:

>>> enums = ['dirUp', 'dirDown']
>>> for v, k in enumerate(enums):
...     exec(k + '=' + str(v))
...     
>>> print dirDown
1
>>> print dirUp
0

Если вы можете использовать exec() для этого, то хорошо. Если нет, используйте другой подход. В любом случае, нынешняя дискуссия все академична. Однако проблема здесь еще не решена. Что делать, если перечисления используются во множестве исходного кода, а другой программист приходит и вставляет новое значение между dirUp и dirDown? Это вызовет несчастье, поскольку сопоставление между именами перечислений и самих перечислений будет неправильным. Имейте в виду, что это остается проблемой даже в оригинальном простом решении.

Здесь у нас есть новая идея использования встроенной функции hash() для определения нашего значения enum как int, и мы используем текстовое имя самого перечисления для определения хеша:

>>> for k in enums:
...     exec(k + '=' + str(hash(k)))
... 
>>> dirUp
-1147857581
>>> dirDown
453592598
>>> enums = ['dirUp', 'dirLeft', 'dirDown']
>>> for k in enums:
...     exec(k + '=' + str(hash(k)))
... 
>>> dirUp
-1147857581
>>> dirDown
453592598
>>> dirLeft
-300839747
>>> 

Обратите внимание, что мы ввели новое значение между dirUp и dirDown, т.е. dirLeft, и наши исходные значения отображения для первых двух не изменились.

Я могу использовать это в своем собственном коде. Благодаря OP для размещения вопроса.

      • (Проходит еще несколько часов)

Бени Чернявский-Паскин сделал очень хорошие комментарии:

  • Python по умолчанию hash() нестабилен на разных платформах (опасно для приложений с сохранением)
  • Возможность столкновения всегда присутствует.

Я согласен с обоими наблюдениями. Его предложение состоит в том, чтобы использовать сами строки (мне действительно нравится самодокументированное поведение использования значений) как хэш, и поэтому код становится следующим (обратите внимание, что мы используем набор вместо списка, чтобы обеспечить уникальность):

>>> items=('dirUp','dirDown','dirLeft','dirRight')
>>> for i in items:
        exec('{}="{}"'.format(i,i))
>>> dirDown
'dirDown'

Также тривиально помещать их в пространство имен, чтобы избежать столкновений с другим кодом:

>>> class Direction():
        for i in ('dirUp','dirDown','dirLeft','dirRight'):
            exec('{}="{}"'.format(i,i))

>>> Direction.dirUp
'dirUp'

Длина криптографического хеша, которую он упоминает, можно увидеть здесь:

>>> from hashlib import md5
>>> crypthash = md5('dirDown'.encode('utf8'))
>>> crypthash.hexdigest()
'6a65fd3cd318166a1cc30b3e5e666d8f'

Ответ 4

Объект collections.namedtuple может предоставить такое пространство имен:

>>> import collections
>>> dircoll=collections.namedtuple('directions', ('UP', 'DOWN', 'LEFT', 'RIGHT'))
>>> directions=dircoll(0,1,2,3)
>>> directions
directions(UP=0, DOWN=1, LEFT=2, RIGHT=3)
>>> directions.DOWN
1
>>> 

Ответ 5

Это просто и эффективно:

class Enum(object):
  def __init__(self, *keys):
    self.__dict__.update(zip(keys, range(len(keys))))

Использование:

>>> x = Enum('foo', 'bar', 'baz', 'bat')
>>> x.baz
2
>>> x.bat
3

Ответ 6

Если вы используете Python 2.6+, вы можете использовать namedtuple. У них есть преимущество с фиксированным числом свойств, и когда вам нужны все значения перечисления, вы можете использовать его как кортеж.

Для большего контроля над значениями перечисления вы можете создать свой собственный класс перечисления.

def enum(args, start=0):
    class Enum(object):
        __slots__ = args.split()

        def __init__(self):
            for i, key in enumerate(Enum.__slots__, start):
                setattr(self, key, i)

    return Enum()

>>> e_dir = enum('up down left right')
>>> e_dir.up
0
>>> e_dir = enum('up down left right', start=1)
>>> e_dir.up
1

Объявление __slots__ запечатывает ваш класс Enum, никакие атрибуты не могут быть установлены для объекта, который был создан из класса с свойством __slots__.

Ваш класс Enum также может быть namedtuple, в этом случае вы также получите функции кортежа. См. namedtuple docs в подклассе namedtuple