Looking back at what Python 3.4 did for enum

Plus explore some of the underutilized but still useful Python features.
51 readers like this.

This is the fifth in a series of articles about features that first appeared in a version of Python 3.x. Python 3.4 was first released in 2014, and even though it has been out for a long time, many of the features it introduced are underused and pretty cool. Here are three of them.

enum

One of my favorite logic puzzles is the self-descriptive Hardest Logic Puzzle Ever. Among other things, it talks about three gods who are called A, B, and C. Their identities are True, False, and Random, in some order. You can ask them questions, but they only answer in the god language, where "da" and "ja" mean "yes" and "no," but you do not know which is which.

If you decide to use Python to solve the puzzle, how would you represent the gods' names and identities and the words in the god language? The traditional answer has been to use strings. However, strings can be misspelled with disastrous consequences.

If, in a critical part of your solution, you compare to the string jaa instead of ja, you will have an incorrect solution. While the puzzle does not specify what the stakes are, that's probably best avoided.

The enum module gives you the ability to define these things in a debuggable yet safe manner:

import enum

@enum.unique
class Name(enum.Enum):
    A = enum.auto()
    B = enum.auto()
    C = enum.auto()
    
@enum.unique
class Identity(enum.Enum):
    RANDOM = enum.auto()
    TRUE = enum.auto()
    FALSE = enum.auto()

        
@enum.unique
class Language(enum.Enum):
    ja = enum.auto()
    da = enum.auto()

One advantage of enums is that in debugging logs or exceptions, the enum is rendered helpfully:

name = Name.A
identity = Identity.RANDOM
answer = Language.da
print("I suspect", name, "is", identity, "because they answered", answer)
    I suspect Name.A is Identity.RANDOM because they answered Language.da

functools.singledispatch

While developing the "infrastructure" layer of a game, you want to deal with various game objects generically but still allow the objects to customize actions. To make the example easier to explain, assume it's a text-based game. When you use an object, most of the time, it will just print You are using <x>. But using a special sword might require a random roll, and it will fail otherwise.

When you acquire an object, it is usually added to the inventory. However, a particularly heavy rock will smash a random object; if that happens, the inventory will lose that object.

One way to approach this is to have methods use and acquire on objects. More and more of these methods will be added as the game's complexity increases, making game objects unwieldy to write.

Instead, functools.singledispatch allows you to add methods retroactively—in a safe and namespace-respecting manner.

You can define classes with no behavior:

class Torch:
    name="torch"

class Sword:
    name="sword"

class Rock:
    name="rock"
import functools

@functools.singledispatch
def use(x):
    print("You use", x.name)

@functools.singledispatch
def acquire(x, inventory):
    inventory.add(x)

For the torch, those generic implementations are enough:

inventory = set()

def deploy(thing):
    acquire(thing, inventory)
    use(thing)
    print("You have", [item.name for item in inventory])

deploy(Torch())
    You use torch
    You have ['torch']

However, the sword and the rock need some specialized functionality:

import random

@use.register(Sword)
def use_sword(sword):
    print("You try to use", sword.name)
    if random.random() < 0.9:
        print("You succeed")
    else:
        print("You fail")

deploy(sword)
    You try to use sword
    You succeed
    You have ['sword', 'torch']
import random

@acquire.register(Rock)
def acquire_rock(rock, inventory):
    to_remove = random.choice(list(inventory))
    inventory.remove(to_remove)
    inventory.add(rock)

deploy(Rock())
    You use rock
    You have ['sword', 'rock']

The rock might have crushed the torch, but your code is much easier to read.

pathlib

The interface to file paths in Python has been "smart-string manipulation" since the beginning of time. Now, with pathlib, Python has an object-oriented way to manipulate paths:

import pathlib
gitconfig = pathlib.Path.home() / ".gitconfig"
text = gitconfig.read_text().splitlines()

Admittedly, using / as an operator to generate path names is a little cutesy, but it ends up being nice in practice. Methods like .read_text() allow you to get text out of small files without needing to open and close file handles manually.

This lets you concentrate on the important stuff:

for line in text:
    if not line.strip().startswith("name"):
        continue
    print(line.split("=")[1])
     Moshe Zadka

Welcome to 2014

Python 3.4 was released about seven years ago, but some of the features that first showed up in this release are cool—and underused. Add them to your toolkit if you haven't already.

What to read next
Tags
Moshe sitting down, head slightly to the side. His t-shirt has Guardians of the Galaxy silhoutes against a background of sound visualization bars.
Moshe has been involved in the Linux community since 1998, helping in Linux "installation parties". He has been programming Python since 1999, and has contributed to the core Python interpreter. Moshe has been a DevOps/SRE since before those terms existed, caring deeply about software reliability, build reproducibility and other such things.

Comments are closed.

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.