Difference between lists direct assignment and slice assignment - python-3.x

I have:
def reverseString(self, s: List[str]) -> None:
s[:] = s[::-1] # Works
... and
def reverseString(self, s: List[str]) -> None:
s = s[::-1] # Doesn't work
Where s is a list of characters lets say s = ["k","a","k","a","s","h","i"]
While doing a question on leetcode it rejected when I used s = ... but accepted when I used s[:] = ... and also it was written that DO NOT RETURN ANYTHING but return s.reverse also worked.

This is actually a bit complex and requires two explanations.
First, a python function argument act as label on the passed object. For example, in the following code, arg is the local label (/name) attached to the initial list. When the label arg is re-used, for example by attaching it to a new object (17), the original list is not reachable anymore within function f.
On the other hand, outside of f, the list labeled L is still here, untouched:
def f(arg):
arg = 17
print(arg) # 17
L = ['a', 'list']
f(L)
print(L) # ['a', 'list']
That explains why the following function doesn't reverse your list in place:
def reverse_list(arg):
arg = arg[::-1]
print(arg) # ['list', 'a']
L = ['a', 'list']
reverse_list(L)
print(L) # ['a', 'list']
This function simply attach the label arg to a new list (that is indeed equal to the reversed list).
Secondly, the difference between arg[:] = ... and arg = ... is that the first will modify the content of the list (instead of attaching the label arg to a new object). This is the reason why the following works as expected:
def alt_reverse_list(arg):
arg[:] = arg[::-1]
L = ['a', 'list']
alt_reverse_list(L)
print(L) # ['list', 'a']
In this second example we say that the list has been mutated (modified in place). Here is a detailed explanation on slice assignments
For the same reason, calling arg.reverse() would have worked.
Identifying objects
Using the id() function can help figure out what is going on with the argument in the first example (where we don't mutate the list but affect a new value):
def reverse_list(arg):
print("List ID before: ", id(arg))
arg = arg[::-1]
print("List ID after: ", id(arg))
L = ['a', 'list']
print("Original list ID: ", id(L))
reverse_list(L)
print("Final list ID: ", id(L))
Which will print something like:
Original list ID: 140395368281088
List ID before: 140395368281088
List ID after: 140395368280447 <--- intruder spotted
Final list ID: 140395368281088
Here we can clearly see that after calling arg = arg[::-1] the object we are manipulating under the name arg is not the same. This shows why the function doesn't have any (side) effect.

Related

Python : Create a function that takes a list of integers and strings and returns a new list with the strings filtered out

I am new to coding in Python and I am struggling with a very simple problem. There is the same question but for javascript on the forum but it does not help me.
My code is :
def filter_list(l):
for i in l:
if i != str():
l.append(i)
i = i + 1
return(l)
print(filter_list([1,2,'a','b']))
If you can help!
thanks
Before I present solution here are some problems you need to understand.
str()
str() creates a new instance of the string class. Comparing it to an object with == will only be true if that object is the same string.
print(1 == str())
>>> False
print("some str" == str())
>>> False
print('' == str())
>>> True
iterators (no +1)
You have i = i + 1 in your loop. This doesn't make any sense. i comes from for i in l meaning i looping over the members of list l. There's no guarantee you can add 1 to it. On the next loop i will have a new value
l = [1,2,'a']
for i in l:
print(i)
>>> 1
>>> 2
>>> 'a'
To filter you need a new list
You are appending to l when you find a string. This means that when your loop finds an integer it will append it to the end of the list. And later it will find that integer on another loop interation. And append it to the end AGAIN. And find it in the next iteration.... Forever.
Try it out! See the infinite loop for yourself.
def filter_list(l):
for i in l:
print(i)
if type(i) != str:
l.append(i)
return(l)
filter_list([1,2,'a','b'])
Fix 1: Fix the type check
def filter_list(l):
for i in l:
if type(i) != str:
l.append(i)
return(l)
print(filter_list([1,2,'a','b']))
This infinite loops as discussed above
Fix 2: Create a new output array to push to
def filter_list(l):
output = []
for i in l:
if type(i) != str:
output.append(i)
return output
print(filter_list([1,2,'a','b']))
>>> [1,2]
There we go.
Fix 3: Do it in idiomatic python
Let's use a list comprehension
l = [1,2,'a','b']
output = [x for x in l if type(x) != str]
print(output)
>>> [1, 2]
A list comprehension returns the left most expression x for every element in list l provided the expression on the right (type(x) != str) is true.

return unique python lists of chars ignoring order

Problem:
Consider a python list of lists that contains a sequence of chars:
[['A', 'B'],['A','B','C'],['B','A'],['C','A','B'],['D'],['D'],['Ao','B']]
The goal is to return the unique lists, regardless of order:
[['A','B'],['A','B','C'],['D'],['Ao','B']]
Attempt:
I'm able to achieve my goal using many if/else statements with try/exceptions. What would be the most pythonic (faster) way to approach this problem? Thanks!
def check_duplicates(x,list_):
for li in list_:
if compare(x,li):
return True
def compare(s, t):
t = list(t) # make a mutable copy
try:
for elem in s:
t.remove(elem)
except ValueError:
return False
return not t
vars_list = [['A', 'B'],['A','B','C'],['B','A'],['C','A','B'],['D'],['D'],['Ao','B']]
second_list = []
for i in vars_list:
if check_duplicates(i,second_list):
continue
else:
second_list.append(i)
print(i)
Assuming that the elements of the nested lists are hashable, you can isolate the unique collections by constructing a set of frozensets from the nested list:
unique_sets = {frozenset(l) for l in vars_list}
# {frozenset({'D'}),
# frozenset({'A', 'B'}),
# frozenset({'A', 'B', 'C'}),
# frozenset({'Ao', 'B'})}
If you need a list-of-lists as the output, you can obtain one trivially with [list(s) for s in unique_sets].

A simple Python program to study classes

For the sake of studying the concept of classes in Python, I have written a program which is meant to calculate the average of a tuple of numbers. However, the program returns an error message which is quoted.
#!/usr/bin/python3
"""
Python program to calculate the average value of
a set of integers or float numbers.
Input format: a tuple, e.g. (1,2,3)
When run, the program generates an error message in line 27
"""
class Mean_value():
def __init__(self, operand):
self.operand = operand
def calculate_average(self, operand):
self.operand = operand
all_in_all = sum(operand)
nmbr = len(operand)
average = all_in_all/nmbr
self.average = average
return self.average
operand = input("Key in numbers as a tuple: ")
print(operand) #temp, the operand is taken in by the program
x = Mean_value.calculate_average(operand) #line 27
print(x)
The error message:
Traceback (most recent call last):
File "D:\Python\Exercise76a.py", line 27, in <module>
x = Mean_value.calculate_average(operand)
TypeError: calculate_average() missing 1 required positional argument: 'operand'
I would highly appreciate any hints from members more experienced than myself.
Any method in your class with self as the first parameter is an instance method, meaning it's supposed to be called on an instance of the class and not on the class itself.
In other words, the self parameter isn't just for show. When you do this:
x = Mean_value(operand)
x.calculate_average(operand)
the python interpreter actually takes x and passes it through to the function as the first parameter (i.e. self). Hence, when you try to call calculate_average() on the class Mean_value instead of on an object of that type, it only passes one of the two required parameters (there's no instance to pass automatically as self, so it just passes the one argument you've given it, leaving the second argument unspecified).
If you want to have a method be static (called on the class instead of on an instance of the class), you should use the #staticmethod decorator on the method in question, and omit the self parameter.
Another way to fix this error is to make your calculate_average method static. Like this:
#staticmethod
def calculate_average(operand):
# but be careful here as you can't access properties with self here.
all_in_all = sum(operand)
nmbr = len(operand)
average = all_in_all/nmbr
return average
The program contains comments
#!/usr/bin/python3
"""
The program computes the average value of a sequence of positive integers,
keyed in as a tuple.
After entering the tuple, the input function returns a string,
e.g.(1,2,3) (tuple) --> (1,2,3) (string).
On screen the two objects look the same.
The major code block deals with reversing the type of the input,
i.e. string --> tuple,
e.g. (1,2,3) (string) --> (1,2,3) (tuple).
The maths is dealt with by the class Average.
"""
class Average:
def __init__(self, tup):
self.tup = tup
def calculate(self):
return sum(self.tup)/len(self.tup)
"""Major code block begins ----------"""
#create containers
L_orig_lst = []
S_str = ""
print("The program computes the average value of")
print("a sequence of positive integers, input as a tuple.\n")
#in orig_str store the string-type of the tuple keyed in
orig_str = input("Key in the numbers as a tuple:\n")
lnth = len(orig_str)
#copy each character from orig_str into the list L_orig_lst (the original list)
for i in range(lnth):
if orig_str[i] in ['(', '.', ',' , ')', '1', '2', '3','4', '5', '6', '7', '8', '9', '0']:
#if one of the characters left parenthesis, period, comma, right parenthesis or a digit
#is found in orig_str, then S_str is extended with that character
S_str = S_str + orig_str[i]
L_orig_lst.append(S_str)
#set S_str to be empty
S_str = ""
elif orig_str[i] == " ":
pass
else:
print("Error in input")
break
#at this stage the following transformation has taken place,
#tuple (string) --> tuple (list)
#e.g. (1,2,3) (string) --> ['(' , '1' , ',' , '2' , ',' , '3' , ')'], L_orig_lst
#create new container
#and set S_str to be empty
L_rev_lst = []
S_str = ""
lnth = len(L_orig_lst)
#from the original list, L_orig_lst, copy those elements which are digits (type string)
#and append them to the revised list, L_rev_lst.
for i in range(lnth):
if L_orig_lst[i] in ['1', '2', '3','4', '5', '6', '7', '8', '9', '0']:
#if the element in the original list is a digit (type string),
#then extend S_str with this element
S_str = S_str + L_orig_lst[i]
elif L_orig_lst[i] == ',':
#if the element in the original list is a comma, then finalize the reading of the number,
#convert the current number (type string) into an integer
S_int = int(S_str)
#and append the integer to the revised list
L_rev_lst.append(S_int)
#set S_str to be empty
S_str = ""
else:
pass
#also convert the last number (type string) to an integer,
S_int = int(S_str)
#and also append the last integer to the revised list
L_rev_lst.append(S_int)
#now convert the revised list into a tuple
T_tpl = tuple(L_rev_lst)
"""Major code block ends --------"""
#calculate the average for the tuple T_tpl
#call the class
x = Average(T_tpl)
#instantiate the class
y = x.calculate()
print("Average value: ", y)

Is there a way to extract function parameters from a dictionary?

I am trying to create some more functional code in Python and I want to know if it is possible to transform dictionary (key,values) to pass as a function parameter.
I am currently doing this in a more imperative way, where I filter and then manually extract each key depending on the result of the filter. My current code:
def a(i: int, config: dict):
function_array = [function1, function2, function3]
selected = function_array[i]
if (i == "0"):
result = selected(x = config['x'])
elif (i == "1"):
result = selected(y = config['y'])
elif (i == "2"):
result = selected(z = config['z'])
return result
The current result is correct, but when I have many cases, I need to hardcode each parameter for the specified function. So, that is why I want to know if it is possible to pass the config object as I want (with an x when i is 0, for example) and then just do something like this:
def a(i: int, config: dict):
function_array = [function1, function2, function3]
result = function_array[i](config)
return result
The syntax for passing items from a dictionary as function parameters is simply selected(**config)
So for your example, it would look something like this:
def function1(x=0):
return x + 1
def function2(y=42):
return y * 2
def function3(z=100):
return z
def a(i, config):
function_array = [function1, function2, function3]
selected = function_array[i]
return selected(**config)
config = {x: 10}
a(0, config) # calls function1(x=10)
config = {y: 20}
a(1, config) # calls function2(y=20)
config = {}
a(2, config) # calls function3()
Every python function can be instructed to take a dictionary of keywords. See e.g. https://www.pythoncheatsheet.org/blog/python-easy-args-kwargs . (Official source at https://docs.python.org/3/reference/compound_stmts.html#function-definitions, but it's harder to read.)
You could do:
def a(i: int, keyword: str, **kwargs: dict):
if keyword in kwargs:
result = kwargs[keyword](i)
and you would run it with something like:
a(5, "func3", func1=print, func2=sum, func3=all)
Or, you could just pass a dictionary itself into the function:
def a(i: int, keyword: str, config: dict)
if keyword in config:
result = config[keyword](i)
This would be run with something like:
a(5, "func3", {"func1": print, "func2": sum, "func3": all})
The only difference is that the ** in the function declaration tells python to automatically make a dictionary out of explicit keywords. In the second example, you make the dictionary by yourself.
There's an important thing happening behind the scenes here. Functions are being passed around just like anything else. In python, functions are still objects. You can pass a function just as easily as you can pass an int. So if you wanted to have a list of lists, where each inner list is a function with some arguments, you easily could:
things_to_do = [[sum, 5, 7, 9], [any, 1, 0], [all, 1, 0]]
for thing_list in things_to_do:
function = thing_list[0]
args = thing_list[1:]
print(function(args))
And you'll get the following results:
21
True
False
(Note also that all of those functions take an iterable, such as a list. If you want to pass each argument separately, you would use *args instead of args.)
You can do it with defined functions, too. If you have
def foo1(arg1):
pass
def foo2(arg1, arg2):
pass
you can just as easily have
things_to_do = [[sum, 5, 7, 9], [foo1, 'a'], [foo2, 0, None]]

Most "pythonic" way of populating a nested indexed list from a flat list

I have a situation where I am generating a number of template nested lists with n organised elements where each number in the template corresponds to the index from a flat list of n values:
S =[[[2,4],[0,3]], [[1,5],[6,7]],[[10,9],[8,11],[13,12]]]
For each of these templates, the values inside them correspond to the index value from a flat list like so:
A = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n"]
to get;
B = [[["c","e"],["a","d"]], [["b","f"],["g","h"]],[["k","j"],["i","l"],["n","m"]]]
How can I populate the structure S with the values from list A to get B, considering that:
- the values of list A can change in value but not in a number
- the template can have any depth of nested structure of but will only use an index from A once as the example shown above.
I did this with the very ugly append unflatten function below that works if the depth of the template is not more then 3 levels. Is there a better way of accomplishing it using generators, yield so it works for any arbitrary depth of template.
Another solution I thought but couldn't implement is to set the template as a string with generated variables and then assigning the variables with new values using eval()
def unflatten(item, template):
# works up to 3 levels of nested lists
tree = []
for el in template:
if isinstance(el, collections.Iterable) and not isinstance(el, str):
tree.append([])
for j, el2 in enumerate(el):
if isinstance(el2, collections.Iterable) and not isinstance(el2, str):
tree[-1].append([])
for k, el3 in enumerate(el2):
if isinstance(el3, collections.Iterable) and not isinstance(el3, str):
tree[-1][-1].append([])
else:
tree[-1][-1].append(item[el3])
else:
tree[-1].append(item[el2])
else:
tree.append(item[el])
return tree
I need a better solution that can be employed to accomplish this when doing the above recursively and for n = 100's of organised elements.
UPDATE 1
The timing function I am using is this one:
def timethis(func):
'''
Decorator that reports the execution time.
'''
#wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, end-start)
return result
return wrapper
and I am wrapping the function suggested by #DocDrivin inside another to call it with a one-liner. Below it is my ugly append function.
#timethis
def unflatten(A, S):
for i in range(100000):
# making sure that you don't modify S
rebuilt_list = copy.deepcopy(S)
# create the mapping dict
adict = {key: val for key, val in enumerate(A)}
# the recursive worker function
def worker(alist):
for idx, entry in enumerate(alist):
if isinstance(entry, list):
worker(entry)
else:
# might be a good idea to catch key errors here
alist[idx] = adict[entry]
#build list
worker(rebuilt_list)
return rebuilt_list
#timethis
def unflatten2(A, S):
for i in range (100000):
#up to level 3
temp_tree = []
for i, el in enumerate(S):
if isinstance(el, collections.Iterable) and not isinstance(el, str):
temp_tree.append([])
for j, el2 in enumerate(el):
if isinstance(el2, collections.Iterable) and not isinstance(el2, str):
temp_tree[-1].append([])
for k, el3 in enumerate(el2):
if isinstance(el3, collections.Iterable) and not isinstance(el3, str):
temp_tree[-1][-1].append([])
else:
temp_tree[-1][-1].append(A[el3])
else:
temp_tree[-1].append(A[el2])
else:
temp_tree.append(A[el])
return temp_tree
The recursive method is much better syntax, however, it is considerably slower then using the append method.
You can do this by using recursion:
import copy
S =[[[2,4],[0,3]], [[1,5],[6,7]],[[10,9],[8,11],[13,12]]]
A = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n"]
# making sure that you don't modify S
B = copy.deepcopy(S)
# create the mapping dict
adict = {key: val for key, val in enumerate(A)}
# the recursive worker function
def worker(alist):
for idx, entry in enumerate(alist):
if isinstance(entry, list):
worker(entry)
else:
# might be a good idea to catch key errors here
alist[idx] = adict[entry]
worker(B)
print(B)
This yields the following output for B:
[[['c', 'e'], ['a', 'd']], [['b', 'f'], ['g', 'h']], [['k', 'j'], ['i', 'l'], ['n', 'm']]]
I did not check if the list entry can actually be mapped with the dict, so you might want to add a check (marked the spot in the code).
Small edit: just saw that your desired output (probably) has a typo. Index 3 maps to "d", not to "c". You might want to edit that.
Big edit: To prove that my proposal is not as catastrophic as it seems at a first glance, I decided to include some code to test its runtime. Check this out:
import timeit
setup1 = '''
import copy
S =[[[2,4],[0,3]], [[1,5],[6,7]],[[10,9],[8,11],[13,12]]]
A = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n"]
adict = {key: val for key, val in enumerate(A)}
# the recursive worker function
def worker(olist):
alist = copy.deepcopy(olist)
for idx, entry in enumerate(alist):
if isinstance(entry, list):
worker(entry)
else:
alist[idx] = adict[entry]
return alist
'''
code1 = '''
worker(S)
'''
setup2 = '''
import collections
S =[[[2,4],[0,3]], [[1,5],[6,7]],[[10,9],[8,11],[13,12]]]
A = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n"]
def unflatten2(A, S):
#up to level 3
temp_tree = []
for i, el in enumerate(S):
if isinstance(el, collections.Iterable) and not isinstance(el, str):
temp_tree.append([])
for j, el2 in enumerate(el):
if isinstance(el2, collections.Iterable) and not isinstance(el2, str):
temp_tree[-1].append([])
for k, el3 in enumerate(el2):
if isinstance(el3, collections.Iterable) and not isinstance(el3, str):
temp_tree[-1][-1].append([])
else:
temp_tree[-1][-1].append(A[el3])
else:
temp_tree[-1].append(A[el2])
else:
temp_tree.append(A[el])
return temp_tree
'''
code2 = '''
unflatten2(A, S)
'''
print(f'Recursive func: { [i/10000 for i in timeit.repeat(setup = setup1, stmt = code1, repeat = 3, number = 10000)] }')
print(f'Original func: { [i/10000 for i in timeit.repeat(setup = setup2, stmt = code2, repeat = 3, number = 10000)] }')
I am using the timeit module to do my tests. When running this snippet, you will get an output similar to this:
Recursive func: [8.74395573977381e-05, 7.868373290111777e-05, 7.9051584698027e-05]
Original func: [3.548609419958666e-05, 3.537480780214537e-05, 3.501355930056888e-05]
These are the average times of 10000 iterations, and I decided to run it 3 times to show the fluctuation. As you can see, my function in this particular case is 2.22 to 2.50 times slower than the original, but still acceptable. The slowdown is probably due to using deepcopy.
Your test has some flaws, e.g. you redefine the mapping dict at every iteration. You wouldn't do that normally, instead you would give it as a param to the function after defining it once.
You can use generators with recursion
A = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n"]
S = [[[2,4],[0,3]], [[1,5],[6,7]],[[10,9],[8,11],[13,12]]]
A = {k: v for k, v in enumerate(A)}
def worker(alist):
for e in alist:
if isinstance(e, list):
yield list(worker(e))
else:
yield A[e]
def do(alist):
return list(worker(alist))
This is also a recursive approach, just avoiding individual item assignment and letting list do the work by reading the values "hot off the CPU" from your generator. If you want, you can Try it online!-- setup1 and setup2 copied from #DocDriven 's answer (but I recommend you don't exaggerate with the numbers, do it locally if you want to play around).
Here are example time numbers:
My result: [0.11194685893133283, 0.11086182110011578, 0.11299032904207706]
result1: [1.0810202199500054, 1.046933784848079, 0.9381260159425437]
result2: [0.23467918601818383, 0.236218704842031, 0.22498539905063808]

Resources