If... else skip

So I’ve used a for loop to check for duplicates in the past but this time since I need to keep only the unique characters in the same order they were provided, I went with the dictionary route.

As you can see from the code below, however, it does not disregard case.

def duplicate_remover(stuff):
    new_list = list(dict.fromkeys(stuff))
    return new_list

duplicate_remover(['matt', 'bob', 'Matt', 'john', 'Matt',1,2,1,6,3,4,1,2])
  # note this does not weed out duplicates which have different cases but does work for both integers and strings

So I tried to add a modification which would convert the items to a string and then lower case if the input is a string and if not them to skip that step but it doesn’t work if use “continue” or “break” and I don’t know how to tell it to skip it… and it gives me an empty list now :frowning:

def duplicate_remover(stuff):
    stuff = [x.lower() for x in stuff if x == str]
    new_list = list(dict.fromkeys(stuff))
    return new_list

duplicate_remover(['matt', 'bob', 'Matt', 'john', 'Matt',1,2,1,6,3,4,1,2]) 

list comprehension

Think about what does the list comprehension do. Try to rewrite this command without using a list comprehension (using the ordinary for loop):

stuff = [x.lower() for x in stuff if x == str]

I think you will probably more easily see that it does not do what you described in words. …and I am not talking about the expression of the condition yet - see below.

testing of an object type

In Python everything is an object. So str is an object too. It is a different object than for example 'matt'. What about playing with the concept in the interactive Python?

>>> a = str
>>> b = 'matt'
>>> a
<class 'str'>
>>> b
'matt'
>>> a == b

What would be the result of the last expression (the comparison)?

What you are looking for: isinstance()

3 Likes

You can use isinstance or type(x) == str,

But then you will find that your list comprehension will drop all items that are not str.

type(x) == str is not a good choice as it does not take into account subclasses.

In this simple example the result would be the same but a robust implementation of the function cannot use type(x) == str.

I cannot recall encountering a subclass of str.
For other classes use of isinstance is better agreed.

E.g. Python 3.11 class enum.StrEnum, which notes against type(…) == str

Aha! I did not know that enum,StrEnum was a str subclass.
Thanks for the example.

A robust code should not be limited to what exists in the standard library or to what exists at the moment of writing it :wink:

The oppersite view from extreme programming is only write code for the problem you have now.

This is because you may never need to solve the problem for the more complex, but currently unknown requirements.

Research show that it is cheaper to refactor when the requirements change then guess at the code you other need, but usually never actually need.

2 Likes

Thank you!

I was able to get it to work… now I have to try and figure out how to put it back into a list comprehension if possible…

def duplicate_remover(stuff):
    new_stuff = []
    for x in stuff:
        if isinstance(x, str):
            new_stuff.append(x.lower())
        else:
            new_stuff.append(x)
      
    new_list = list(dict.fromkeys(new_stuff))
    return new_list

duplicate_remover(['matt', 'bob', 'Matt', 'john', 'Matt',1,2,1,6,3,4,1,2]) 

output: [‘matt’, ‘bob’, ‘john’, 1, 2, 6, 3, 4]

I know there is a different order if an else is involved… Is there another more simple way to accomplish this? I feel like the code I’m writing is caveman versions compared to the Pythonic way LOL

That is great!

List comprehension is possible! Inside it you cannot use statements, you can use only expressions. In this case the difference in the if-else branches is just in the expression (x vs x.lower()) so you can replace the whole if statement by sinlge expression containing a conditional expression. Then you can make it back to a list comprehension.

The new list comprehension will be more complex than the old one so maybe it would be useful to split it to two or three lines to make it more readable. …and I think the code will be Pythonic :slight_smile:

I (somewhat) understand that but I would never sacrifice best practices or improved robustness when the improved code has the same or just a slightly higher complexity.

def duplicate_remover(stuff):
    new_stuff = list(x.lower() if isinstance(x, str) else (x) for x in stuff)
    new_list = list(dict.fromkeys(new_stuff))
    return new_list
duplicate_remover(['matt', 'bob', 'Matt', 'john', 'Matt',1,2,1,6,3,4,1,2])

when I tried to just mash it all together like:

def duplicate_remover(stuff):
    new_stuff = [new_stuff.append(x.lower()) if isinstance(x, str) else new_stuff.append(x) for x in stuff]
    new_list = list(dict.fromkeys(new_stuff))
    return new_list

… it returned an error that new_stuff wasn’t defined so I reasoned that because we are including what is being appended into the list inside of the definition of the variable then we don’t need to reference the variable new_stuff at all and just removed it.

modified it to my quiz:

def unique(random):
    new_stuff = list(x.lower() if isinstance(x, str) else (x) for x in random)
    new_list = list(dict.fromkeys(new_stuff))
    return new_list

Plugged it into my quiz and:

Oops, your solution is incorrect.
unique() missing 1 required positional argument: ‘random’

this is what the quiz is asking for:

Write a function named unique. The function should accept one parameter, which is a list with any number of elements inside. The default value for the parameter should be an empty list ([ ]).

The function should return a new list with all duplicate elements removed. The function should preserve the original order of elements.

Example 1: for unique([1, 1, 4, 5, 1]), the output should be [1, 4, 5]
Example 2: for unique([‘Mark’, ‘Mark’, ‘John’, ‘Anne’]), the output should be [‘Mark’, ‘John’, ‘Anne’]

I can clearly see that I specified the parameter as random in the function so I’m not understanding why it says it’s missing.

Attention, you are creating a generator and then you create a list from it. Here I separated the two steps to make them more obvious:

new_stuff_generator = (x.lower() if isinstance(x, str) else (x) for x in stuff)
new_stuff = list(new_stuff_generator)

If you are comfortable with generators and generator expressions you can use it. In such a case creating the list new_stuff is redundant. You can feed the generator directly to dict.fromkeys().


You had it right in your code above. You just used a generator expression instead of list comprehension (() vs []). You do not need (and should not use!) the append() method inside a list comprehension. List comprehension builds the list for you from the elements you provide! Go back to your second piece of code in this topic (first post) to see how list comprehension looks like. You had this idea correct there, just the condition and the if clause was wrong.

Overview of brackets used for comprehensions and generator expressions:

  • [] - list comprehension
  • {} - set or dictionary comprehension
  • () - generator expression, the parentheses can be omitted in some contexts

The error message of the quiz checker is stripped of context which makes it really confusing. It is the testing code (calling your function) which fails with the message. It is not a failure in your code.

Knowing this read the assignment again carefully. I think you will notice what you missed before.

Thank you! I think it may be looking for list comprehension rather than a generator expression because this is a beginner Python video course and we haven’t covered generator expressions yet. I play around with it this weekend when I have some time.

As a hint for you here is the key term from the quiz assignment: “default value”

It said the Default parameter should be an empty list… but I don’t understand how to reference and modify an empty list that is not already assigned to a variable.

Is the function name itself the variable?

No, it said the default value of the parameter should be an empty list.

You have to learn something about function’s default argument values:

Hint: You have to create the empty list. See the “Important warning” part in the documentation.

By Barry Scott via Discussions on Python.org at 05Sep2022 23:08:

I cannot recall encountering a subclass of str.

Hmm, a quick glance around my own code:

 cs/lex.py
 1773:class FStr(FormatableMixin, str):
 cs/app/pilfer.py
 754:class FormatArgument(str):
 cs/chunkyString.py
 8:class ChunkyString(str):
 cs/timeseries.py
 162:class TypeCode(str):

I don’t really use ChunkyStr, it was more an exercise, but all the
others are pretty solid use cases IMO. Of course, that’s from 116534
lines of code, so it is hardly common.

For other classes use of isinstance is better agreed.

Best to always do it unless there is a special reason not to.

Cheers,
Cameron Simpson cs@cskk.id.au

1 Like

I totally forgot about that! They called it a keyword value. but that did it:

def unique(random = []):
    new_stuff = list(x.lower() if isinstance(x, str) else (x) for x in random)
    new_list = list(dict.fromkeys(new_stuff))
    return new_list

Here’s a sample solution to Coding Exercise 10:

def unique(input_list=[]):
  to_return = []
  for el in input_list:
    if el not in to_return:
      to_return.append(el)
  return to_return

This is what they offered as a possible solution SMH how !=Pythonic lol

By Brad Westermann via Discussions on Python.org at 10Sep2022 01:31:

Here’s a sample solution to Coding Exercise 10:

def unique(input_list=[]):
 to_return = []
 for el in input_list:
   if el not in to_return:
     to_return.append(el)
 return to_return

This is what they offered as a possible solution SMH how !=Pythonic lol

Indeed not. The most glaring issue, though, to my eye is the default
value input_list=[].

These values are computed at the time the function is defined, not when
it is called. That means that the default input_list is the same
list between calls.

If you function modifies input_list, that change will persist.

 >>> def f(input_list=[]):
 ...     input_list.append(1)
 ...     return input_list
 ...
 >>> print(f([1,2,3]))
 [1, 2, 3, 1]
 >>> print(f([1,2,3]))
 [1, 2, 3, 1]
 >>> print(f())
 [1]
 >>> print(f())
 [1, 1]
 >>> print(f())
 [1, 1, 1]

For this reason we almost always define default values like this:

 def f(input_list=None):
     if input_list is None:
         input_list = []
     ... rest of function ...

In this way the default value can’t leak modifications between calls.

The exception to this is basicly when the default is an immutable value:

 def f(x=()):

 def f(x="a string"):

 def f(x=3):

None of these can be modified, so they’re safe.

Cheers,
Cameron Simpson cs@cskk.id.au

2 Likes