Sunday 29 May 2022

Some Python Tricks

I've been writing a lot of python code these last months and there are some simple tricks that were not so obvious to me the first weeks and that now have become quite essential in my day to day. Let's see:

Dictionary access and default Initially, when getting a value from a dictionary and assigning a default value if the key was missing, I was using the "sort of" ternary operator, like this:


my_value = (my_dict["key"] if key in my_dict
	else "default")

Well, there's a much cleaner way to do this, the get method:


my_value = my_dict.get("key", "default")

First item matching condition I'm pretty used to the .Net First(predicate) and FirstOrDefault(predicate) Linq Extension Methods. It seems odd to me that python lacks a "first(predicate)" function (either builtin or in itertools), but well, I've managed to interiorize the pythonic way to do this. Use a generator expression to apply the filter (rather than a list comprehension, so that you get an iterator that lazily advances as you call it) and then call next on it.


def cities_generator():
    cities = ["Toulouse", "Paris", "Porto", "Prague", "Xixon"]
    for city in cities:
        print(f"reading: {city}")
        yield city

first_p = next(city 
    for city in cities_generator()
    if city.startswith("P")
)

That's equivalent to First(). As for FirstOrDefault(), the next() function happens to accept a default value to return if the iterator is finished. Nice:


first_o = next((city 
    for city in cities_generator() 
    if city.startswith("O")
    ), None
)

itertools.takewhile and itertools.dropwhile. These are (particularly the former) pretty useful functions. They are the equivalents to .Net TakeWhile and SkipWhile, nothing more to say.

I guess almost everyone agrees that slicing is one of the most beautiful python features. Slicing does not work with iterators, so the obvious solution is to convert to iterator to a list and apply the slicing. That's fine with small iterables, but if the iterator is going to return many items and you just need an intermediate slice, processing all of them with the list conversion is a real waste. itertools.islice comes to the rescue. Obviously, it will have to process all the previous items to those in the slice that you are requesting, but it won't process the ones that come after the slice, and furthermore it won't store in memory all the previous (not needed) elements, so you are also saving memory space, not just CPU cycles. An important notice, islice does not support negative indexes.

To get the last item in an iterator that matches a predicate I think the best that we can do is to convert the iterator to a list, create a reverse iterator with reversed, and then call next. I use the reversed() function rather than reversing the list with list.reverse() cause the second one would do extra work by traversing the whole list to create the (in place) reverse list.


last_p = next(city 
    for city in reversed(list(cities_generator()))
    if city.startswith("P")
)

No comments:

Post a Comment