1

EDIT: I've heavily edited this question from the original, to focus on the advantages of Enum in this case (the question had been put on hold for being primarily opinion-based)


I understand the sense of using an Enum when it's converting a human-readable string into an underlying (e.g. numeric) value, with the FederalHoliday class in this answer being a good example of that.

But the use-case I'm considering is just where a function parameter is restricted to a set of possible values, which are currently passed as "magic strings". So implementing an Enum here wouldn't really improve code readability (if anything, it would make the code more cluttered). And turning a string into an Enum, only to compare on what is effectively the same string (i.e. the name of the Enum) feels like overkill.

This answer has a fantastic list of general advantages of enums, but I'm not sure how many of them apply in this case.

To clarify the case I'm meaning, there is an example of it here, with a function that prints a string with certain capitalization as specified by mode:

def print_my_string(my_string, mode):
    if mode == 'l':
        print(my_string.lower())
    elif mode == 'u':
        print(my_string.upper())
    elif mode == 'c':
        print(my_string.capitalize())
    else:
        raise ValueError("Unrecognised mode")

To see this in action, running this:

for mode in ['l', 'u', 'c']:
    print_my_string("RaNdoM CAse StRING", mode)

gives:

random case string
RANDOM CASE STRING
Random case string

So my question is:

What advantage does an Enum bring when the strings don't represent another value underneath? Is there any way that it makes the code more robust?


Other things I've read:

Mainly about Enums in general, especially when the strings represent another value underneath:

This seems to have an example similar to mine (under Enums are interoperable), but I struggled to understand the technical discussion around it, and it only shows setting the Enum, not using it:


EDIT: In this case it'd be possible to map the strings directly to the function they correspond to, but this isn't possible in my actual use case as the code is more complicated. (But it's a good tip from those who suggested it in the comments.)

Tim
  • 1,839
  • 10
  • 18
  • 4
    This may get closed as opinion-based. My _opinion_: no, it's not necessarily unpythonic. It's fine to use strings like that as long as they are clearly documented. – wim May 04 '18 at 22:53
  • 4
    The standard library uses magic strings all the time. – Aran-Fey May 04 '18 at 22:57
  • Maybe not generalizable to your actual usecase, but why not have the caller simply pass in a function that they want applied to the string? You could then do `if user_function in whitelist: return user_function(my_string) else : return my_string` – Patrick Haugh May 04 '18 at 22:57
  • 1
    It's less a question of pythonic or non-pythonic, and more a question of coding style imo. Second, this is probably a better question for the code review stack exchange site. – pushkin May 04 '18 at 22:57
  • @PatrickHaugh, that option occurred to me too for this case, but as you guessed that wouldn't work in my actual usecase - the code is more complicated than simply choosing the appropriate function to call. But a good tip otherwise :) – Tim May 04 '18 at 22:59
  • Using strings like this can lead to problems later; for example, making sure you've checked for every possible value. Or no longer being able to use your editor/IDE to jump to the string's definition. I'd suggest considering making your strings keys in a dictionary and the values could be your print function. – Metropolis May 04 '18 at 22:59
  • "*that seems more appropriate when the strings represent another value underneath."* - if the strings don't represent another value, what do they represent? Is it clear from the problem domain? e.g. opening a file, it's common for the choices to be `r`, `w`, `rb` because those represent system wide and mostly language independent things to do with file opening - I think it would be clearer with `read`, `write`, but there are few choices and they are common. But `s` being string-swapped-case is uncommon and not obvious (IMO) and therefore a poor choice. – TessellatingHeckler May 04 '18 at 23:02
  • @TessellatingHeckler So would your suggestion be to use more descriptive strings, but still use strings, rather than defining an enum? – Tim May 04 '18 at 23:04
  • Besides enums, you can just define a class or module and define your "constant" strings in there, then refer to them by qualifying the name for readabilty like `case.UPPER`, `case.SWAP` – progmatico May 04 '18 at 23:10
  • 3
    An enum is _often_ "just representing itself". Think of, for example, the `RegexFlag` enum in the `re` module. `VERBOSE` may have a `64` buried under the covers, but it doesn't represent that 64 as far as anyone but the internal implementation of the C module is concerned; to the user, it just represents `VERBOSE`. And this has the added advantage that `X` and `VERBOSE` are the same value, so you can just do `if mode == VERBOSE` instead of `if mode = 'X' or mode == 'VERBOSE':` – abarnert May 04 '18 at 23:12
  • Meanwhile, if you're actually using the string values _as string values_, like taking `ord(mode)` or passing them to some C function that expects to get a `'c'` character, then you definitely want strings (although even then, you could use an `Enum` where the underlying values are strings, like `class Mode(Enum):` `t = 't'`, etc.). But if you're not, I can't see any way they're a better choice, except for saving a few lines of typing to declare the values. – abarnert May 04 '18 at 23:14
  • I like the way `re.IGNORECASE` and `re.I` both map to the same value, and aren't strings, that's part of the `re.RegexFlag` enum. Then `re.MULTILINE + re.IGNORECASE` is possible. Whether that's applicable to your real problem or not, only you can say. – TessellatingHeckler May 04 '18 at 23:25
  • Using the string instead of defining `SOME_VARIABLE_NAME` to represent the value (regardless of what type it is) are pretty much equivalent in this context (except that defining a `ALL_CAPS` name to represent the constant tells others what it is (arguably) a little more explicitly. – martineau May 04 '18 at 23:49
  • YMMV, but it seems to me unlikely that your edit is true ("I can't map the label to the function directly"), at least not if there's not some other serious code smell there. – Adam Smith May 05 '18 at 01:07
  • Using an `enum` here is perfectly acceptable. I cannot make sense of your objections to using an `enum` – juanpa.arrivillaga May 05 '18 at 01:40

1 Answers1

1

I don't know whether my way is pythonic. But I may use this way:

string_mode = {
    'l': lambda s: s.lower(),
    'u': lambda s: s.upper(),
    'c': lambda s: s.capitalize(),
    't': lambda s: s.title(),
    's': lambda s: s.swapcase()
}

for mode in string_mode:
    print(string_mode.get(mode, lambda s: s)("RaNdoM CAse StRING"))

According to the comment of @Yakym Pirozhenko, we can use a more pythonic way to do this:

string_mode = {
    'l': str.lower,
    'u': str.upper,
    'c': str.capitalize,
    't': str.title,
    's': str.swapcase
}

for mode in string_mode:
    print(string_mode.get(mode, lambda s: s)("RaNdoM CAse StRING"))

Moreover, we can use a function, too:

def string_mode(string, mode='default'):
    return {
        'l': str.lower,
        'u': str.upper,
        'c': str.capitalize,
        't': str.title,
        's': str.swapcase
    }.get(mode, lambda s:s)(string)
Heaven
  • 491
  • 3
  • 11