Codementor Events

Using Python Generators to avoid extra service calls

Published Nov 06, 2020Last updated May 04, 2021

I've been using Python Generators for a while now, having to deal with large Django Querysets, reading large Excel files, and being able to save memory by using generators became part of my day to day work. However, these days I faced a different problem where I wanted to avoid making extra calls to services and solved it with generators. This is what this post is about.

Suppose you want to check if a given user_email is registered in any of the following social media: Facebook, Github, or Twitter.

def has_facebook_account(user_email):
    print('calling Facebook service')
    return False

def has_github_account(user_email):
    print('calling Github service')
    return True

def has_twitter_account(user_email):
    print('calling Twitter service')
    return True

def has_social_media_account(user_email):
    print('Checking social media apps...')
    # Python's `any` receives an Iterable
    # and returns True when the first truthy clause is found.
    response = any([
        has_facebook_account(user_email),  # This is False
        has_github_account(user_email),  # This is True
        has_twitter_account(user_email),  # This is True
    ])
    print('Done!')
    return response

What is wrong with the approach above? Well, if you run it locally you will see the following output:

>>> has_social_media_account('fake@email.com')
Checking social media apps...
calling Facebook service  # This is False, keep going...
calling Github service  # This is True! I can stop now...
calling Twitter service  # Oh no!
Done!

The problem is that a list is evaluated as soon as it is created (opposite to lazy evaluation). Thus, if it's a list of method calls, all the calls will be performed.

At the moment, I thought the problem was that I was actually calling the methods when initializing the list. So maybe keeping only the methods references and using a list comprehension would do the trick:

def has_social_account(user_email):
    calls = [has_facebook_account, has_github_account, has_twitter_account]  # Refs
    return any([call(user_email) for call in calls])

Well, it did not work as I wanted either. As I said before, a list is fully evaluated upon its creation. In the example above, it will first make sure to call all methods so the list is fully built, having all of its elements evaluated - which in turn means having all methods being called. The list comprehension is not aware of its context either, i.e., it does not know that it is being used as an argument to any(...).

How did I solve it? By using generator.

def has_social_account(user_email):
    calls = [has_facebook_account, has_github_account, has_twitter_account]
    return any((call(user_email) for call in calls))  # Note the ( ) instead of [ ]

>>> has_social_account('fake@email.com')
calling Facebook service  # This is False, keep going...
calling Github service  # Aw yeah!

(call(user_email) for call in calls) evaluates to a generator (not a fully built list), which is then used by any. any(...) will iterate over the generator elements evaluating one at a time. This way, no extra calls are being made for once any evaluates the second element (has_github_account(user_email) -> True), the any function is evaluated itself, returning True and not calling the third service method (has_twitter_account).


Here is the Gist if you're interested.

Please let me know in the comments below if something is not clear and/or if you'd like to have a more in-depth article on Python Generators.

Discover and read more posts from Gabriel Saldanha
get started
post comments6Replies
burbigo deen
4 years ago

I find it quite practical, thanks for the info and for sharing

GL Holman
4 years ago

Yes, would love to hear more about generators from you. Thx.

Andrew
4 years ago

Gabriel, thank you for sharing this easy-to-use practice, but
maybe it’s little faster to use asyncio.as_completed…

What do you think about it?

Gabriel Saldanha
4 years ago

Thanks, Andrew! You’re correct! I’ve been working on a draft that is an iteration over the presented approach, using the async library. If your services have no co-dependency, you can definitely make your requests async and consume the first response.

However, there is a tradeoff where you would end up always submitting a request to all services. This way, you’d trade performance in cost of more resources :)

Gabriel Saldanha
4 years ago

Hey @Andrew! Just an update: based on your feedback I’ve just finished writing a follow up post using concurrent.futures (not asyncio).

I intend to write another one using asyncio :) I’m still learning it

Show more replies