Rewriting nested if-statements in a more Pythonic fashion - python

I'm working on a function that, given a sequence, tries to find said sequence within a list and should then return the list item immediately after that sequence terminates.
Currently this code does return the list item immediately after the end of the sequence, however I'm not to happy with having this many nested if-statements and would love to rewrite it but I can't figure out how to go about it as it is quite unlike anything I've ever written in the past and feel a bit out of practice.
def sequence_in_list(seq, lst):
m, n = len(lst), len(seq)
for i in xrange(m):
for j in xrange(n):
if lst[i] == seq[j]:
if lst[i+1] == seq[j+1]:
if lst[i+2] == seq[j+2]:
return lst[i+3]
(My intention is to then extend this function so that if that sequence occurs more than once throughout the list it should return the subsequent item that has happened the most often after the sequence)

I would do this with a generator and slicing:
sequence = [1, 2, 3, 5, 1, 2, 3, 6, 1, 2, 3]
pattern = [1, 2, 3]
def find_item_after_pattern(sequence, pattern):
n = len(pattern)
for index in range(0, len(sequence) - n):
if pattern == sequence[index:index + n]:
yield sequence[index + n]
for item in find_item_after_pattern(sequence, pattern):
print(item)
And you'll get:
5
6
The function isn't too efficient and won't work for infinite sequences, but it's short and generic.

Since you are comparing consecutive indexes, and assuming lst and seq are of the same type, you can use slicing:
def sequence_in_list(seq, lst):
m, n = len(lst), len(seq)
for i in xrange(m):
for j in xrange(n):
if lst[i:i+3] == seq[j:j+3]:
return lst[i+3]
If the sequences are of different kind you should convert to a common type before doing the comparison(e.g. lst[i:i+3] == list(seq[j:j+3]) would work if seq is a string and lst is a list).
Alternatively, if the sequences do not support slicing, you can use the built-in all to check for more conditions:
def sequence_in_list(seq, lst):
m, n = len(lst), len(seq)
for i in xrange(m):
for j in xrange(n):
if all(lst[i+k] == seq[j+k] for k in range(3)):
return lst[i+3]
If you want to extend the check over 10 indices instead of 3, simply change range(3) to range(10).
Side note: your original code would raise an IndexError at some point, since you access list[i+1] where i may be len(list) - 1. The above code doesn't produce any errors, since slicing may produce a slice shorter than the difference of the indices, meainig that seq[j:j+3] can have less than 3 elements. If this is a problem you should adjust the indexes on which you are iterating over.
Last remark: don't use the name list since it shadows a built-in name.

You can combine list comprehension with slicing to make comparing more readable:
n, m = len(lst), len(seq)
[lst[j+3] for i in range(m-2) for j in range(n-2) if seq[i:i+3] == lst[j:j+3]]
Of course there are more efficient ways to do it, but this is simple, short and python styled.

Related

Merge Sorted array

I'm trying to solve the problem which is defined as:
we are given two integer arrays nums1 and nums2, sorted in non-decreasing order, and two integers m and n, representing the number of elements in nums1 and nums2 respectively.
Merge nums1 and nums2 into a single array sorted in non-decreasing order.
The function should not return the final sorted array, but instead, be stored inside the array nums1. To accommodate this, nums1 has a length of m + n, where the first m elements denote the elements that should be merged, and the last n elements are set to 0 and should be ignored. nums2 has a length of n.
I have written the following code in python :
def merge(nums1, m, nums2, n):
"""
Do not return anything, modify nums1 in-place instead.
"""
nums1=nums1[:m]
# print("before",nums1)
for i in nums1[m:n]:
i=0
# print("after1",nums1)
nums1[m:n]=nums2[:n]
nums1.sort()
# print(nums1)
merge([0],0,[1],1)
I have tried to submit the solution but showing up as an error. Can anyone find the solution to the given problem? Please do something with the above code, not anything from outside.
Input: nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
Output: [1,2,2,3,5,6]
Explanation: The arrays we are merging are [1,2,3] and [2,5,6].
The result of the merge is [1,2,2,3,5,6] with the underlined elements coming from nums1.
You would like us to start from your code attempt, but none of the statements in your attempt are salvageable in an efficient solution:
nums1=nums1[:m]. This statement creates a new list, storing its reference in the variable that had a reference to the original input list. Thereby you lose the reference to the input list, and make it impossible to change anything in that list -- which was the purpose of this function.
for i in nums1[m:n]: In an efficient solution, you would not create a new list (like with using slice notation).
i=0: there is no benefit in repeating this in a loop. This is an assignment to a variable, not to a member of a list. It seems you thought this loop would clear part of the original list, but you cannot clear a list by assigning 0 to a variable. Moreover, there is no need to clear anything in any list for this code challenge.
nums1[m:n]=nums2[:n] although this copies the second list into nums1, this is copying it in a local list -- a list that the caller has no access to. Secondly, you would need to use the free space in the left list to efficiently merge the data. Now by having copied all values from the second list in it, you don't have any space anymore for such merge.
nums1.sort(). Calling sort defeats the purpose of the code challenge. sort doesn't use the knowledge that we're dealing with 2 sorted lists, and can only offer a time complexity of O(nlogn), while you can do it with a complexity of O(n).
So... nothing in your code can stay. It goes wrong from the first statement onwards, and takes the wrong approach.
The algorithm that should be implemented will have an index going from end to start in the nums1 and store the greatest value there from the values that have not yet been stored that way. As the input lists are sorted, there are only 2 values candidate for this operation.
Here is an implementation:
def merge(nums1, m, nums2, n):
# Let m and n refer to the last used index in given lists
m -= 1
n -= 1
for i in range(m + n + 1, -1, -1):
if n < 0 or m >= 0 and nums1[m] > nums2[n]:
nums1[i] = nums1[m]
m -= 1
else:
nums1[i] = nums2[n]
n -= 1
What you are asking for is just the classic function used within the merge sort algorithm. You can find many solutions online.
Here is a possible solution:
def merge(nums1, m, nums2, n):
i = m - 1
j = n - 1
for last in range(m + n - 1, -1, -1):
if i < 0:
nums1[last] = nums2[j]
j -= 1
elif j < 0:
nums1[last] = nums1[i]
i -= 1
elif nums1[i] < nums2[j]:
nums1[last] = nums2[j]
j -= 1
else:
nums1[last] = nums1[i]
i -= 1
Example:
>>> nums1 = [1, 3, 5, 7, 0, 0, 0]
>>> nums2 = [4, 6, 8]
>>> m = 4
>>> n = 3
>>> merge(nums1, m, nums2, n)
>>> nums1
[1, 3, 4, 5, 6, 7, 8]
Another example:
>>> nums1 = [1, 2, 3, 0, 0, 0]
>>> nums2 = [2, 5, 6]
>>> m = 3
>>> n = 3
>>> merge(nums1, m, nums2, n)
>>> nums1
[1, 2, 2, 3, 5, 6]
If you want to keep your original code and fix it then you can do this:
def merge(nums1, m, nums2, n):
nums1[m:] = nums2
nums1.sort()
Beware that this is much slower than my solution, and it's not the standard way to solve this well known problem.

Use list comprehensions to make a list of count of elements smaller than the element in an array

I was solving this leetcode problem - https://leetcode.com/problems/how-many-numbers-are-smaller-than-the-current-number/
I solved it easily by using nested for loops but list comprehensions have always intrigued me. Ive spent a lot of time to make that one liner work but I always get some syntax error.
here's the solution:
count = 0
ans = []
for i in nums:
for j in nums:
if i > j:
count = count + 1
ans.append(count)
count = 0
return ans
these were the ones so far I think shouldve worked:
return [count = count + 1 for i in nums for j in nums if i > j]
return [count for i in nums for j in nums if i > j count = count + 1]
return [count:= count + 1 for i in nums for j in nums if i > j]
Ill also be happy if there's some resource or similar to put it together, Ive been searching the python docs but didnt find something that'll help me
I will transform the code step by step in order to show the thought process.
First: we don't care what the value of count is afterward, but we need it to be 0 at the start of each inner loop. So it is simpler logically to set it there, rather than outside and then also at the end of the inner loop:
ans = []
for i in nums:
count = 0
for j in nums:
if i > j:
count = count + 1
ans.append(count)
return ans
Next, we focus on the contents of the loop:
count = 0
for j in nums:
if i > j:
count = count + 1
ans.append(count)
A list comprehension is not good at math; it is good at producing a sequence of values from a source sequence. The transformation we need to do here is to put the actual elements into our "counter" variable1, and then figure out how many there are (in order to append to ans). Thus:
smaller = []
for j in nums:
if i > j:
smaller.append(j)
ans.append(len(smaller))
Now that the creation of smaller has the right form, we can replace it with a list comprehension, in a mechanical, rule-based way. It becomes:
smaller = [j for j in nums if i > j]
# ^ ^^^^^^^^^^^^^ ^^^^^^^^
# | \- the rest of the parts are in the same order
# \- this moves from last to first
# and then we use it the same as before
ans.append(len(smaller))
We notice that we can just fold that into one line; and because we are passing a single comprehension argument to len we can drop the brackets2:
ans.append(len(j for j in nums if i > j))
Good. Now, let's put that back in the original context:
ans = []
for i in nums:
ans.append(len(j for j in nums if i > j))
return ans
We notice that the same technique applies: we have the desired form already. So we repeat the procedure:
ans = [len(j for j in nums if i > j) for i in nums]
return ans
And of course:
return [len(j for j in nums if i > j) for i in nums]
Another popular trick is to put a 1 in the output for each original element, and then sum them. It's about the same either way; last I checked the performance is about the same and I don't think either is clearer than the other.
Technically, this produces a generator expression instead. Normally, these would be surrounded with () instead of [], but a special syntax rule lets you drop the extra pair of () when calling a function with a single argument that is a generator expression. This is especially convenient for the built-in functions len and sum - as well as for any, all, max, min and (if you don't need a custom sort order) sorted.
Hmm, three people write sum solutions but every single one does sum(1 for ...). I prefer this:
[sum(j < i for j in nums) for i in nums]
Instead of trying to advance an external counter, try adding ones to your list and then sum it:
for example:
nums = [1,2,3,4,5]
target = 3
print(sum(1 for n in nums if n < target))
Using counter inside the list comprehension creates the challenge of resetting it's value, each iteration of the first loop.
This can be avoided by filtering, and summing, in the second loop:
You use the first loop to iterate over the values of nums array.
return [SECOND_LOOP for i in nums]
You use the second loop, iterating over all elements of nums array. You filter in the elements that are smaller than i, the current element in the first loop, with if i < j, and evaluating 1 for each of them. Finally, you sum all the 1s generated:
sum(1 for j in nums if i > j)
You get the number of values that meet the requirements, by the list comprehension of the first loop:
return [sum(1 for j in nums if i > j) for i in nums]
This solution has been checked & validated in LeetCode.
You need a slightly different approach for the inner loop than a list comprehension. Instead of repeatedly appending a value to a list you need to repeatedly add a value to a variable.
This can be done in a functional way by using sum and a generator expression:
count = 0
# ...
for j in nums:
if i > j:
count = count + 1
can be replaced by
count = sum(1 for j in nums if i > j)
So that we now have this:
ans = []
for i in nums:
count = sum(1 for j in nums if i > j)
ans.append(count)
return ans
This pattern can in fact be replaced by a list comprehension:
return [sum(1 for j in nums if i > j) for i in nums]
Alternative Solution
We can also use the Counter from collections:
class Solution:
def smallerNumbersThanCurrent(self, nums):
count_map = collections.Counter(nums)
smallers = []
for index in range(len(nums)):
count = 0
for key, value in count_map.items():
if key < nums[index]:
count += value
smallers.append(count)
return smallers

Finding two integers that multiply to 20. Can I make this code more "pythonic"?

I did this code that finds two integers in a said list (in this case [2,4,5,1,6,40,-1]) that multiply to twenty. I got a little stuck in the beginning, but adding a function to it solved my problems. I showed this code to a friend of mine who's a programmer and he said I could make this code more "pythonic", but I have no clue how.
Here's the code:
num_list = [2,4,5,1,6,40,-1]
def get_mult_num(given_list):
for i in given_list:
for j in range(i+1, len(given_list)): #for j not to be == i and to be in the list
mult_two_numbers = i * j
if mult_two_numbers == 20:
return i,j
print(get_mult_num(num_list))
I don't necessarily think it is 'unpythonic', you are using standard Python idioms to loop over your data and produce a single result or None. The term Pythonic is nebulous, a subject marred in "I know it when I see it" parameters.
Not that you produced a correct implementation. While i loops over given_numbers, j loops over an integer from i + 2 through to len(given_numbers), mixing values from given_list with indices? For your sample input, you are taking j from the half-open ranges [4, 7), [6, 7), [7, 7) (empty), [3, 7), [8, 7) (empty), [42, 7) (empty) and [1, 7), respectively. That it produces the correct answer at all is luck, not due to correctness; if you give your function the list [2, 10], it'll not find a solution! You want to loop over given_numbers again, limited with slicing, or generate indices starting at the current index of i, but then your outer loop needs to add a enumerate() call too:
for ii, i in enumerate(given_numbers):
for j in given_numbers[ii + 1:]:
# ...
or
for ii, i in enumerate(given_numbers):
for jj in range(ii + 1, len(given_numbers)):
j = given_numbers[jj]
# ...
All this is not nearly as efficient as it can be; the Python standard library offers you the tools to generate your i, j pairs without a nested for loop or slicing or other forms of filtering.
Your double loop should generate combinations of the integer inputs, so use the itertools.combinations() object to generate unique i, j pairs:
from itertools import combinations
def get_mult_num(given_list):
return [(i, j) for i, j in combinations(given_list, 2) if i * j == 20]
This assumes there can be zero or more such solutions, not just a single solution.
If you only ever need the first result or None, you can use the next() function:
def get_mult_num(given_list):
multiplies_to_20 = (
(i, j) for i, j in combinations(given_list, 2)
if i * j == 20)
return next(multiplies_to_20, None)
Next, rather than produce all possible combinations, you may want to invert the problem. If you turn given_list into a set, you can trivially check if the target number 20 can be divided cleanly without remainder by any of your given numbers and where the result of the division is larger and is also an integer in the set of numbers. That gives you an answer in linear time.
You can further limit the search by dividing with numbers smaller than the square root of the target value, because you won't find a larger value to match in your input numbers (given a number n and it's square root s, by definition s * (s + 1) is going to be larger than n).
If we add an argument for the target number to the function and make it a generator function, then you get:
def gen_factors_for(target, numbers):
possible_j = set(numbers)
limit = abs(target) ** 0.5
for i in numbers:
if abs(i) < limit and target % i == 0:
j = target // i
if j in possible_j and abs(j) > abs(i):
yield i, j
This approach is a lot faster than testing all permutations, especially if you need to find all possible factors. Note that I made both functions generators here to even out the comparisons:
>>> import random, operator
>>> from timeit import Timer
>>> def gen_factors_for_division(target, numbers):
... possible_j = set(numbers)
... limit = abs(target) ** 0.5
... for i in numbers:
... if abs(i) < limit and target % i == 0:
... j = target // i
... if j in possible_j and abs(j) > abs(i):
... yield i, j
...
>>> def gen_factors_for_combinations(target, given_list):
... return ((i, j) for i, j in combinations(given_list, 2) if i * j == target)
...
>>> numbers = [random.randint(-10000, 10000) for _ in range(100)]
>>> targets = [operator.mul(*random.sample(set(numbers), 2)) for _ in range(5)]
>>> targets += [t + random.randint(1, 100) for t in targets] # add likely-to-be-unsolvable numbers
>>> for (label, t) in (('first match:', 'next({}, None)'), ('all matches:', 'list({})')):
... print(label)
... for f in (gen_factors_for_division, gen_factors_for_combinations):
... test = t.format('f(t, n)')
... timer = Timer(
... f"[{test} for t in ts]",
... 'from __main__ import targets as ts, numbers as n, f')
... count, total = timer.autorange()
... print(f"{f.__name__:>30}: {total / count * 1000:8.3f}ms")
...
first match:
gen_factors_for_division: 0.219ms
gen_factors_for_combinations: 4.664ms
all matches:
gen_factors_for_division: 0.259ms
gen_factors_for_combinations: 3.326ms
Note that I generate 10 different random targets, to try to avoid a lucky best-case-scenario hit for either approach.
[(i,j) for i in num_list for j in num_list if i<j and i*j==20]
This is my take on it, which uses enumerate:
def get_mult_num(given_list):
return [
item1, item2
for i, item1 in enumerate(given_list)
for item2 in given_list[:i]
if item1*item2 == 20
]
I think your friend may be hinting towards using comprehensions when it makes the code cleaner (sometimes it doesn't).
I can think of using list-comprehension. This also helps to find multiple such-pairs if they exist in the given list.
num_list = [2,4,5,1,6,40,-1]
mult_num = [(num_list[i],num_list[j]) for i in range(len(num_list)) for j in range(i+1, len(num_list)) if num_list[i]*num_list[j] == 20]
print mult_num
Output:
[(4, 5)]
I came up with this. It reverses the approach a little bit, in that it searches in num_list for the required pair partner that the iteration value val would multiply to 20 with. This makes the code easier and needs no imports, even if it's not the most efficient way.
for val in num_list:
if 20 / val in num_list:
print(val, int(20/val))
You could make it more pythonic by using itertools.combinations, instead of nested loops, to find all pairs of numbers. Not always, but often iterating over indices as in for i in range(len(L)): is less pythonic than directly iterating over values as in for v in L:.
Python also allows you to make your function into a generator via the yield keyword so that instead of just returning the first pair that multiplies to 20, you get every pair that does by iterating over the function call.
import itertools
def factors(x, numbers):
""" Generate all pairs in list of numbers that multiply to x.
"""
for a, b in itertools.combinations(numbers, 2):
if a * b == x:
yield (a, b)
numbers = [2, 4, 5, 1, 6, 40, -1]
for pair in factors(20, numbers):
print(pair)

Invalid syntax when trying to map(lambda..)

I'm writing a function that loops through a list of numbers, and returns a list of the sums of each number's divisors. I'm trying to trim my function down a little, and learn map(lambda..) at the same time.
def DoWork(nums):
sums = []
for n in nums:
divisors = []
map(lambda i: divisors.append(i) if not (n%i), range(1,n+1))
sums.append(sum(divisors))
return sums
I'm getting a syntax error though, regarding the first , in the map() line.
Typically, we use map() in situations where we have a sequence of things [1] and want to transform it into another sequence -- of the same length -- by applying a function to each element.
You can still use it to find the sum of divisors, but I would argue that it's not the most suitable tool for that.
Instead, let me show you how we can apply map and lambda to your problem by looking at it from a slightly different angle.
First of all, let's rewrite your function to use a list comprehension:
def DoWork(nums):
sums = []
for n in nums:
divisors = [i for i in range(1, n+1) if not n % i]
sums.append(sum(divisors))
return sums
Now observe that the divisors list is unnecessary and can eliminated:
def DoWork(nums):
sums = []
for n in nums:
sums.append(sum(i for i in range(1, n+1) if not n % i))
return sums
Now we can easily transform the entire function into a single call to map():
def DoWork(nums):
return map(lambda n: sum(i for i in range(1, n+1) if not n % i), nums)
[1] map() can also be used to transform multiple sequences -- generally of the same length -- into a single sequence by applying the same function repeatedly across the input sequences.
A python lambda does not work that way. It is should be a simple substitute for a function that takes a value and returns another value. You can not have if conditions floating in it.
updated_range = filter(lambda x: not n%x, range(1,n+1))
map(lambda i: divisors.appens(i), updated_range)
#The above code is a bad style. See below!
Note: Typically, lambdas do not modify an external state. It is considered a bad style to call divisors.append(..)
Improved version #1:
updated_range = filter(lambda x: not n%x, range(1,n+1))
map(lambda i: divisors.append(i), updated_range)
Improved version #2 (fixing the lambda that doesn't modify external state):
updated_range = filter(lambda x: not n%x, range(1,n+1))
divisors = map(lambda i: i, updated_range)
#(lambda i: i) is an identity function: returns whatever we give it.
#Sort of useless, though.
Improved version #3 (More pythonic!):
divisors = [x for x in range(1, n+1) if not n%x]
The syntax error is rising from incomplete 'if not' logic, where an 'else' clause is required.
def DoWork(nums):
sums = []
for n in nums:
divisors = []
map(lambda i: divisors.append(i) if not (n%i) else 1, range(1,n+1))
sums.append(sum(divisors))
return sums
error free execution...
DoWork(range(0,10))
[0, 1, 3, 4, 7, 6, 12, 8, 15, 13]

Looping through a list not in order in Python

I am very new to programming, so please bear with me...I have been learning Python and I just did an assessment that involved looping through a list using your current value as the next index value to go to while looping. This is roughly what the question was:
You have a zero-indexed array length N of positive and negative integers. Write a function that loops through the list, creates a new list, and returns the length of the new list. While looping through the list, you use your current value as the next index value to go to. It stops looping when A[i] = -1
For example:
A[0] = 1
A[1] = 4
A[2] = -1
A[3] = 3
A[4] = 2
This would create:
newlist = [1, 4, 2, -1]
len(newlist) = 4
It was timed and I was not able to finish, but this is what I came up with. Any criticism is appreciated. Like I said I am new and trying to learn. In the meantime, I will keep looking. Thanks in advance!
def sol(A):
i = 0
newlist = []
for A[i] in range(len(A)):
e = A[i]
newlist.append(e)
i == e
if A[i] == -1:
return len(newlist)
This might be the easiest way to do it if your looking for the least lines of code to write.
A = [1,4,-1,3,2]
B = []
n = 0
while A[n] != -1:
B.append(A[n])
n = A[n]
B.append(-1)
print(len(B))
First of all, note that for A[i] in range(len(A)) is a pattern you certainly want to avoid, as it is an obscure construct that will modify the list A by storing increasing integers into A[i]. To loop over elements of A, use for val in A. To loop over indices into A, use for ind in xrange(len(A)).
The for loop, normally the preferred Python looping construct, is not the right tool for this problem because the problem requires iterating over the sequence in an unpredictable order mandated by the contents of the sequence. For this, you need to use the more general while loop and manage the list index yourself. Here is an example:
def extract(l):
newlist = []
ind = 0
while l[ind] != -1:
newlist.append(l[ind])
ind = l[ind]
newlist.append(-1) # the problem requires the trailing -1
print newlist # for debugging
return len(newlist)
>>> extract([1, 4, -1, 3, 2])
[1, 4, 2, -1]
4
Note that collecting the values into the new list doesn't really make sense in any kind of real-world scenario because the list is not visible outside the function in any way. A more sensible implementation would simply increment a counter in each loop pass and return the value of the counter. But since the problem explicitly requests maintaining the list, code like the above will have to do.
It's simpler to just use a while loop:
data = [1,4,-1,3,2]
ls = []
i = 0
steps = 0
while data[i] != -1:
ls.append(data[i])
i = data[i]
steps += 1
assert steps < len(data), "Infinite loop detected"
ls.append(-1)
print ls, len(ls)

Categories

Resources