2
\$\begingroup\$

This function should return a string that is comprised from all the letters in the Alphabet except the letters in a pre-defined list in alphabetical order.

Assuming all letters in the pre-defined list are all lowercase;

I came up with two ways to do this and I would like to know which way is more efficient or more 'Pythonic'. Thanks in advance.

The first way:

import string

def get_remaining_letters(list_of_characters):
    # casting the Alphabet to a list so that it becomes mutable

    list_of_all_letters = list(string.ascii_lowercase)
# looping for each character in the pre-defined list
    for char in list_of_characters :
        if char in list_of_all_letters: # to avoid possible errors caused by the .remove in case of a duplicate charcter in the pre-defined list 
            list_of_all_letters.remove(char) 
    return ''.join(list_of_all_letters) # returns a string 

# ----------------------------------------------------------- #

pre_defined_list = ['a', 'b', 'b']

test = get_remaining_letters(pre_defined_list)
print(test)

output :

>>> cdefghijklmnopqrstuvwxyz

The second way:

import string

def get_remaining_letters(list_of_characters):

    if list_of_characters == []:
        return string.ascii_lowercase
    else:
        remaining_letters = ''
        for char in string.ascii_lowercase:
            if char not in list_of_characters:
                remaining_letters += char
        return remaining_letters



pre_defined_list = ['a', 'b', 'b']


test = get_remaining_letters(pre_defined_list)
print(test)

output :

>>> cdefghijklmnopqrstuvwxyz
\$\endgroup\$
  • \$\begingroup\$ Each line of code was written by me, both code sets are working. I edited the question, I didn't word myself appropriately. I apologize. \$\endgroup\$ – Elbasel Oct 11 at 21:11
  • \$\begingroup\$ it's okay, just making sure \$\endgroup\$ – bullseye Oct 11 at 21:19
  • \$\begingroup\$ Does the order of the returned result matter? \$\endgroup\$ – Reinderien Oct 11 at 22:00
  • \$\begingroup\$ Yes, it needs to be in alphabetical order. \$\endgroup\$ – Elbasel Oct 11 at 22:04
2
\$\begingroup\$

I messed around with performance of some alternate implementations:

#!/usr/bin/env python3

from collections import OrderedDict
from string import ascii_lowercase
from timeit import timeit


def method_1(exclude):
    list_of_all_letters = list(ascii_lowercase)
    for char in exclude:
        if char in list_of_all_letters:
            list_of_all_letters.remove(char) 
    return ''.join(list_of_all_letters)


def method_2(exclude):
    if exclude == []:
        return string.ascii_lowercase
    else:
        remaining_letters = ''
        for char in ascii_lowercase:
            if char not in exclude:
                remaining_letters += char
        return remaining_letters


def bullseye(exclude):
    return ''.join(letter for letter in ascii_lowercase if letter not in exclude)



ALL_LETS = set(ascii_lowercase)


def subtract_set(exclude):
    return ''.join(sorted(ALL_LETS - exclude))


LET_DICT = OrderedDict((l, l) for l in ascii_lowercase)


def update_dict(exclude):
    lets = LET_DICT.copy()
    lets.update((l, '') for l in exclude)
    return ''.join(lets.values())


methods = (method_1,
           method_2,
           bullseye,
           subtract_set,
           update_dict,
           )

def test():
    for method in methods:
        assert method(set()) == 'abcdefghijklmnopqrstuvwxyz'
        assert method(set('bcd')) == 'aefghijklmnopqrstuvwxyz'
        assert method(set('abcdefghijklmnopqrstuvwxyz')) == ''


def time_them():
    N = 100_000
    exclude = set('abcdefghi')
    for method in methods:
        def exec():
            method(exclude)    
        us = timeit(exec, number=N) * 1e6 / N
        print(f'{method.__name__:15} {us:.2f} us')

if __name__ == '__main__':
    test()
    time_them()

This outputs:

method_1        3.05 us
method_2        1.98 us
bullseye        2.40 us
subtract_set    1.76 us
update_dict     6.38 us

Of note:

  • You should always be passing the exclusion argument as a set, not a list, because set membership tests are O(1) and list membership tests are O(N)
  • @bullseye provided the typical list comprehension implementation, but it performs more poorly than your method 2, interestingly. This will vary based on Python version; I'm on 3.7.4.
  • subtract_set is so fast because set subtraction is itself fast, and if you hold onto a pre-constructed set for the whole alphabet, the added cost of a post-sort step is still worth it.
\$\endgroup\$
  • \$\begingroup\$ Thanks for the help!, can you please explain why method 1 way takes more time to evaluate than method 2 ? \$\endgroup\$ – Elbasel Oct 16 at 17:53
  • \$\begingroup\$ Probably the remove which does an O(n) search on every call \$\endgroup\$ – Reinderien Oct 16 at 17:58
  • \$\begingroup\$ Can you please explain the method used to measure execution time ? \$\endgroup\$ – Elbasel Oct 18 at 16:12
  • \$\begingroup\$ docs.python.org/3.7/library/timeit.html \$\endgroup\$ – Reinderien Oct 18 at 18:00
2
\$\begingroup\$

I suggest you check PEP0008 https://www.python.org/dev/peps/pep-0008/ the official Python style guide and Flake8 http://flake8.pycqa.org/en/latest/ as a tool for style enforcement.

  • Docstrings: Python documentation strings or docstrings are a way of documenting your classes/function/modules and are usually accessed through help(). You should include docstrings to your functions indicating what they do and what they return.
  • Type hints: Type Annotations are a new feature added in PEP 484 that allow for adding type hints to variables. They are used to inform someone reading the code what the type of a variable should be. As you're taking a list as input and will return a string, you should have some documentation indicating that. Use type hints.

    function should look like:

    def get_remaining_letters(letters: list) -> str:
        """Return lowercase alphabetic letters that are not present in letters."""
    
  • Blank lines: instead of this # ----------------------------------------------------------- # an empty line is sufficient and note that according to PEP0008 empty lines as well as comments should be used sparingly (when necessary) to separate logical sections as well as explain the not-so-obvious parts of the code.

  • main guard: use if __name__ == '__main__': guard at the end of the script to allow outside modules to import this module without running it as a whole. You will be calling your functions from there.

    if __name__ == '__main__':
        pre_defined_list = ['a', 'b', 'b']
        test = get_remaining_letters(pre_defined_list)
        print(test)
    
  • Evaluation to empty list: In Python empty sequences evaluate to False. instead of if list_of_characters == []: it is written: if not list_of_characters:

  • Adding to strings: Python strings are immutable which means that each time you're adding to a string like in remaining_letters += char a new string is created and assigned the new value which is inefficient and should be replaced with list comprehension syntax which is shorter and more efficient or Pythonic as you call it.

    An improved version of the code:

    import string
    
    
    def get_remaining_letters(letters: list) -> str:
        """Return lowercase alphabetic letters not present in letters."""
        return ''.join(letter for letter in string.ascii_lowercase if letter not in letters)
    
    
    if __name__ == '__main__':
        chosen = [letter for letter in 'sdfmmmsvvd']
        print(get_remaining_letters(chosen))
    
\$\endgroup\$
  • \$\begingroup\$ You should drop your inner list [] from the join. That can accept a generator directly. \$\endgroup\$ – Reinderien Oct 11 at 22:16
  • 1
    \$\begingroup\$ Okay, I'll edit, thanks \$\endgroup\$ – bullseye Oct 11 at 22:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.