Using list comprehensions to make a funcion more pythonic - python

I'm doing some Google Python Class exercises and I'm trying to find a pythonic solution to the following problem.
D. Given a list of numbers, return a list where all adjacent ==
elements have been reduced to a single element, so [1, 2, 2, 3]
returns [1, 2, 3]. You may create a new list or modify the passed in
list.
My try, which is working perfectly is the following:
def remove_adjacent(nums):
result = []
for num in nums:
if len(result) == 0 or num != result[-1]:
result.append(num)
return result
For example, with remove_adjacent([2, 2, 3, 3, 3]) the output is [2, 3]. Everything's ok.
I'm trying to use list comprehensions in order to archieve this in a more pythonic way, so my try is the following:
def remove_adjacent(nums):
result = []
result = [num for num in nums if (len(result)==0 or num!=result[-1])]
return result
This, with the same input [2, 2, 3, 3, 3], the output is [2, 2, 3, 3, 3] (the same). Meeeh! Wrong.
What I'm doing wrong with the list comprehensions? Am I trying to do something which is impossible to do with list comprehensions? I know it's a bit weird to initialize the list (result = []), so maybe it's not posible to do it using list comprehensions in this case.

Am I trying to do something which is impossible to do with list comprehensions?
Yep. A list comprehension can't refer to itself by name, because the variable doesn't get bound at all until the comprehension is completely done evaluating. That's why you get a NameError if you don't have result = [] in your second code block.
If it's not cheating to use standard modules, consider using groupby to group together similar values in your list:
>>> import itertools
>>> seq = [1, 2, 2, 3]
>>> [k for k,v in itertools.groupby(seq)]
[1, 2, 3]
>>> seq = [2,2,3,3,3]
>>> [k for k,v in itertools.groupby(seq)]
[2, 3]

For the sake of learning, I'd suggest using core reduce function:
def remove_adjacent(lst):
return reduce(lambda x, y: x+[y] if not x or x[-1] != y else x, lst, [])

Related

How would I write a function that finds all occurrences of a specific value in a list -- continues on

and records--in a separate list--these occurrences in terms of the indices where they were found.
For example find_all([1, 2, 3, 4, 5, 2], 2) returns [1, 5]. find_all([1, 2, 3], 0) returns [ ].
I'm totally new to python and this question is stumping me on my first homework. I need to
take 2 arguments: a list of items, and a single element to search for in the list
returns 1 list: a list of indices into the input list that correspond to elements in the input list that match what we were looking for
not sure
Simple Method using list comprehension
def find_all(l, value):
return [i for i, v in enumerate(l) if v == value]
mylist = [1, 2, 3, 4, 5, 2]
print(find_all(mylist, 2)) # [1, 5]
Use this method since list.index only returns the first match
and you want them all
You can try something like this
def find_all(input_list, search_value):
result = []
for idx, num in enumerate(input_list):
if num == search_value:
result.append(idx)
return result
You should go through the tutorials available on the internet to learn the Python Basics.
https://treyhunner.com/2016/04/how-to-loop-with-indexes-in-python/
https://www.programiz.com/python-programming/function-argument
def find_all(list, num):
newlist = []
for i in range(len(list)):
if (list[i] == num):
newlist.append(i)
return newlist
list = [1, 2, 3, 6, 5, 2]
mylist = find_all(list, 2)
print mylist
Hope this helps.

Get complement (opposite) of list slice

Is there syntax to get the elements of a list not within a given slice?
Given the slice [1:4] it's easy to get those elements:
>>> l = [1,2,3,4,5]
>>> l[1:4]
[2, 3, 4]
If I want the rest of the list I can do:
>>> l[:1] + l[4:]
[1, 5]
Is there an even more succinct way to do this? I realize that I may be being too needy because this is already very concise.
EDIT: I do not think that this is a duplicate of Invert slice in python because I do not wish to modify my original list.
If you want to modify the list in-place, you can delete the slice:
>>> l = [1, 2, 3, 4, 5]
>>> del l[1:4]
>>> l
[1, 5]
Otherwise your originally suggestion would be the most succinct way. There isn't a way to get the opposite of a list slice using a single slice statement.
Clearly the best solution to create a class to encapsulate some magical behavior that occurs when you use 'c' as the step value. Clearly.
class SuperList(list):
def __getitem__(self, val):
if type(val) is slice and val.step == 'c':
copy = self[:]
copy[val.start:val.stop] = []
return copy
return super(SuperList, self).__getitem__(val)
l = SuperList([1,2,3,4,5])
print l[1:4:'c'] # [1, 5]
[x for i, x in enumerate(l) if i not in range(1, 4)]
Which is less concise. So the answer to your question is no, you can't do it more concisely.
I was looking for some solution for this problem that would allow for proper handling of the step parameter as well.
None of the proposed solution was really viable, so I ended up writing my own:
def complement_slice(items, slice_):
to_exclude = set(range(len(items))[slice_])
step = slice_.step if slice_.step else 1
result = [
item for i, item in enumerate(items) if i not in to_exclude]
if step > 0:
return result
else:
return result[::-1]
ll = [x + 1 for x in range(5)]
# [1, 2, 3, 4, 5]
sl = slice(1, 4)
ll[sl]
# [2, 3, 4]
complement_slice(ll, sl)
# [1, 5]
To the best of my knowledge, it does handle all the corner cases as well, including steps, both positive and negative, as well as repeating values.
I wanted to write it as a generator, but I got annoyed by checking all corner cases for positive/negative/None values for all parameters.
In principle, that is possible, of course.
You can use list comprehension with loop
l = [i for i in l if i not in l[1:4]]

Is there an equivalent to the C++ pre/postfix operators for use in Python list comprehensions?

I writing a function which populates a list of lists of two elements, where the first element is an element from a different list and the second element is a value which increments.
def list_of_pairs(seq, start):
""" Returns a list of pairs """
>>> list_of_pairs([3, 2, 1], 1)
[ [3, 1], [2, 2], [1, 3] ]
return [[i, start++] for i in seq]
Is there an equivalent to the C++ postfix operators which can be used? Thanks!
Bonus question: Is it more Pythonic to use the list constructor, than to construct the list using square brackets?
Edit: Here is my current (less beautiful) workaround -
def list_of_pairs(seq, start):
""" Returns a list of pairs """
>>> list_of_pairs([3, 2, 1], 1)
[ [3, 1], [2, 2], [1, 3] ]
return [[seq[i], start+i] for i in range(len(seq))]
You may use enumerate() to achieve this. Enumerate return the index along with value while iterating over the list of values. And as per your requirement, you need list of list as [val, index + count]. Below is the sample code to achieve that:
>>> seq = [2, 6, 9]
>>> count = 2
>>> [[val, count+i] for i, val in enumerate(seq)]
[[2, 2], [6, 3], [9, 4]]
You can achieve the same with itertools.count, calling next on the count object for each element of the sequence:
from itertools import count
c = count(start)
lst = [[x, next(c)] for x in seq]
construct the list using square brackets
That's a list comprehension. It's pretty standard and most certainly Pythonic to use list comprehensions for creating lists.
++ and -- have been deliberately excluded from Python, because using them in expressions tends to lead to confusing code and off-by-one errors.
You should use enumerate instead:
def list_of_pairs(seq, start):
return [[elem, i] for i, elem in enumerate(seq, start)]

reverse method mutating input

For an assignment we were asked to create a function that would reverse all the elements in an arbitrarily nested list. So inputs to the function should return something like this:
>>> seq = [1,[2,[3]]]
>>> print arb_reverse(seq)
[[[3],2],1]
>>> seq = [9,[[],[0,1,[[],[2,[[],3]]]],[],[[[4],5]]]]
>>> print arb_reverse(seq)
[[[[5,[4]]],[],[[[[3,[]],2],[]],1,0],[]],9]
I came up with a recursive solution which works well:
def arb_reverse(seq):
result = []
for element in reversed(seq):
if not is_list(element):
result.append(element)
else:
result.append(arb_reverse(element))
return result
But for a bit of a personal challenge I wanted to create a solution without the use of recursion. One version of this attempt resulted in some curious behavior which I am not understanding. For clarification, I was NOT expecting this version to work properly but the resulting input mutation does not make sense. Here is the iterative version in question:
def arb_reverse(seq):
elements = list(seq) #so input is not mutated, also tried seq[:] just to be thorough
result = []
while elements:
item = elements.pop()
if isinstance(item, list):
item.reverse() #this operation seems to be the culprit
elements += item
else:
result.append(item)
return result
This returns a flattened semi-reversed list (somewhat expected), but the interesting part is what it does to the input (not expected)...
>>> a = [1, [2, [3]]]
>>> arb_reverse(a)
[2, 3, 1]
>>> a
[1, [[3], 2]]
>>> p = [1, [2, 3, [4, [5, 6]]]]
>>> print arb_reverse(p)
[2, 3, 4, 5, 6, 1]
>>> print p
[1, [[[6, 5], 4], 3, 2]]
I was under the impression that by passing the values contained in the input to a variable using list() or input[:] as i did with elements, that I would avoid mutating the input. However, a few print statements later revealed that the reverse method had a hand in mutating the original list. Why is that?
The list() call is making a new list with shallow-copied lists from the original.
Try this (stolen from here):
from copy import deepcopy
listB = deepcopy(listA)
Try running the following code through this tool http://people.csail.mit.edu/pgbovine/python/tutor.html
o1 = [1, 2, 3]
o2 = [4, 5, 6]
l1 = [o1, o2]
l2 = list(l1)
l2[0].reverse()
print l2
print l1
Specifically look at what happens when l2[0].reverse() is called.
You'll see that when you call list() to create a copy of the list, the lists still reference the same objects.

A cleaner/shorter way to solve this problem?

This exercise is taken from Google's Python Class:
D. Given a list of numbers, return a list where
all adjacent == elements have been reduced to a single element,
so [1, 2, 2, 3] returns [1, 2, 3]. You may create a new list or
modify the passed in list.
Here's my solution so far:
def remove_adjacent(nums):
if not nums:
return nums
list = [nums[0]]
for num in nums[1:]:
if num != list[-1]:
list.append(num)
return list
But this looks more like a C program than a Python script, and I have a feeling this can be done much more elegant.
EDIT
So [1, 2, 2, 3] should give [1, 2, 3] and [1, 2, 3, 3, 2] should give [1, 2, 3, 2]
There is function in itertools that works here:
import itertools
[key for key,seq in itertools.groupby([1,1,1,2,2,3,4,4])]
You can also write a generator:
def remove_adjacent(items):
# iterate the items
it = iter(items)
# get the first one
last = next(it)
# yield it in any case
yield last
for current in it:
# if the next item is different yield it
if current != last:
yield current
last = current
# else: its a duplicate, do nothing with it
print list(remove_adjacent([1,1,1,2,2,3,4,4]))
itertools to the rescue.
import itertools
def remove_adjacent(lst):
i = iter(lst)
yield next(i)
for x, y in itertools.izip(lst, i):
if x != y:
yield y
L = [1, 2, 2, 3]
print list(remove_adjacent(L))
Solution using list comprehensions, zipping then iterating through a twice. Inefficient, but short and sweet. It also has the problem of extending a[1:] with something.
a = [ 1,2,2,2,3,4,4,5,3,3 ]
b = [ i for i,j in zip(a,a[1:] + [None]) if not i == j ]
This works, but I'm not quite happy with it yet because of the +[None] bit to ensure that the last element is also returned...
>>> mylist=[1,2,2,3,3,3,3,4,5,5,5]
>>> [x for x, y in zip(mylist, mylist[1:]+[None]) if x != y]
[1, 2, 3, 4, 5]
The most Pythonic way is probably to go the path of least resistance and use itertools.groupby() as suggested by THC4K and be done with it.
>>> def collapse( data ):
... return list(sorted(set(data)))
...
>>> collapse([1,2,2,3])
[1, 2, 3]
Second attempt after the additional requirment was added:
>>> def remove_adjacent( data ):
... last = None
... for datum in data:
... if datum != last:
... last = datum
... yield datum
...
>>> list( remove_adjacent( [1,2,2,3,2] ) )
[1, 2, 3, 2]
You may want to look at itertools. Also, here's a tutorial on Python iterators and generators (pdf).
This is also somewhat functional; it could be written as a one-liner using lambdas but that would just make it more confusing. In Python 3 you'd need to import reduce from functools.
def remove_adjacent(nums):
def maybe_append(l, x):
return l + ([] if len(l) and l[-1] == x else [x])
return reduce(maybe_append, nums, [])

Categories

Resources