More easily readable Python code with Named slices

2021.03.10

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

When you want to obtain a slice in Python, first thing that comes to your mind is probably something like this: name[:3]

For simple situations it is usually enough, you can easily tell that it is taking first 3 characters of a string. When there is a lot of slicing involved it can get messy quickly. Today we are going to discover a way to write slicing in slightly more concise notation.

Scenario - nickname generator

Users insert their first name and our application generates them a nickname.

class User():
  def __init__(self, name):
    if len(name) < 5:
      raise Exception("Name too short")
    self.name = name
  
  def __repr__(self):
    return self.nickname

  @property
  def nickname(self):
    return "{}_{}_{}_{}".format(
      self.name[:3],
      self.name[::2],
      self.name[1::2],
      self.name[-3:]
    )

users = [
  User("Daisuke"),
  User("Ryota"),
  User("Shinya"),
  User("Hirokazu"),
]

print(users)

Can you tell what the nickname property is doing? This is still a simple example, but in real world, when reading some code you will have your head occupied by other parts of the project and things that you want to introduce to it. In such case I don't want to spend time interpreting what the code is doing, I want to know it at first sight. One way to mitigate it is to add comments and other is to write self-explaining code. We are going to try the latter.

But first let's look at the output of the print

[Dai_Diue_ask_uke, Ryo_Roa_yt_ota, Shi_Siy_hna_nya, Hir_Hrkz_ioau_azu]

The nickname is made of first three letters of the name, followed by letters at odd indexes of the name, followed by letters at even indexes of the name, with last three letters, all joined with underscores.

Slice object

Slice object works the same as the notation above but it is reusable.

>>> name = "Michal"
>>> FIRST_THREE = slice(None, 3, None)
>>> name[:3]
'Mic'
>>> name[FIRST_THREE]
'Mic'
>>> "Developers"[FIRST_THREE]
'Dev'

Let's try to put it in use in our application

  @property
  def nickname(self):
    FIRST_THREE = slice(None, 3)
    ODD_INDEXES = slice(None, None, 2)
    EVEN_INDEXES = slice(1, None, 2)
    LAST_THREE = slice(-3, None)

    return "{}_{}_{}_{}".format(
      self.name[FIRST_THREE],
      self.name[ODD_INDEXES],
      self.name[EVEN_INDEXES],
      self.name[LAST_THREE]
    )

Isn't that just nice? Now, if you see a slice object being created and assigned to a variable with a meaningful name, every time it you'll see it in the code, you'll know right away what is happening.

More examples

Get status from HTTP response

We know that first line of HTTP response starts with "HTTP/1.1 ", that is 9 characters followed by 3 characters long status code. Let me rephrase the requirement: get 3 characters after first 9 characters. Let's put it into a slice object.

>>> first_line = "HTTP/1.1 200 OK"
>>> HTTP_STATUS = slice(9, 9 + 3, None)
>>> first_line[HTTP_STATUS]
'200'

CSV file row

We want to obtain name, team and body attributes of players.

# HEADER
# "Name", "Team", "Position", "Height(inches)", "Weight(lbs)", "Age"
rows = [
  ["Adam Donachie", "BAL", "Catcher", 74, 180, 22.99]
  ["Paul Bako", "BAL", "Catcher", 74, 215, 34.69]
]
NAME = slice(0, 1)
TEAM = slice(1, 2)
BODY_ATTRIBUTES = slice(3, 3 + 3)

Conclusion

We discovered how to use slice object to create named slices. Named slices are reusable and improve readability of our code.

  • Any similarity to names is entirely coincidental.