+ 5

empty dict/list as argument: best practice?

I know that you should do this in this form: fun(a = None): if a is None: a = {} (or a = []) Otherwise each function calls uses the same list/dictionary. I have two examples: first one really needs an empty dictionary: https://code.sololearn.com/cA1cVq8J87qw/?ref=app Because each stored key depends on the sum and array. Another function call with different nums produces a different dictionary. But the fibonacci function would not need it. https://code.sololearn.com/cAke55R5P81K/?ref=app Because Fibonnaci is unique. For example I can call F(10), my dictionary gets filled with some values. Then I can call F(5) and here I could use the already calculated values from calling F(10). But which is best practice: a) it depends? b) use always None and check against None to create an emtpy dict/list? Thanks!

7th May 2022, 9:38 AM
Denise Roßberg
Denise Roßberg - avatar
17 Réponses
+ 4
Even if you want this effect, it is best to avoid this antipattern. If you happen to work on a bigger codebase, you might want to write tests to verify if your function consistently works fine. But as I demonstrated, if you use list as default parameter, the individual function calls are not isolated, they interact with each other. So the code becomes untestable. I think that following the functional programming patterns, ultimately makes everyone a better developer who can write more stable, predictable, bugless code.
7th May 2022, 4:41 PM
Tibor Santa
Tibor Santa - avatar
+ 7
First an article where it's explained in detail, why it's a bad idea to use a list (or other mutable container) as default parameter. https://realpython.com/null-in-JUMP_LINK__&&__python__&&__JUMP_LINK/#using-none-as-a-default-parameter The essence is that it breaks the referential transparency of your code. It is a principle of functional programming, that a function with the same input has to produce the same output. However, in Python, default parameters are only initialized ONCE in the code. So if you use an empty list as default, it will be created once and whatever modification you do on it, will persist for the next time you call the same function again. So it is foolish to rely on such a behavior, because it can become a nightmare to debug, why your code doesn't do what you expect. Same way why we try to avoid global variables. Memoization can be achieved easily with decorators (functools.lru_cache) or it could even work to use a nested (inner) function which handles the memoized list.
7th May 2022, 2:45 PM
Tibor Santa
Tibor Santa - avatar
+ 6
To show my point, regard this code and see the difference between using an empty list Vs None as default parameter https://code.sololearn.com/cT5c46uYq29B/?ref=app Memoization with decorator: https://code.sololearn.com/c578o3uoLhym/?ref=app
7th May 2022, 3:02 PM
Tibor Santa
Tibor Santa - avatar
+ 4
My preferred solution would be this: @lru_cache def can_sum_memo(total, nums): if total == 0: return True if total < 0: return False return any(can_sum_memo(total-n, nums) for n in nums if n != 0) print(can_sum_memo(20, (5, 3, 6, 9))) Explanation: - using 'sum' as variable name is bad, because you override the built-in sum function - the base case of the recursion is when total = 0. It makes no sense to verify if the total is included in the list of numbers, that would be thinking one step ahead of the recursion. - rather than writing a for loop, I used the any() function with a generator expression. Infinite recursion is avoided by filtering out 0. - I cannot use a list with lru_cache decorator, because the list is not hashable. So I pass a tuple of numbers instead. This makes caching trivial. https://code.sololearn.com/cYgYe3wAgp8N/?ref=app
7th May 2022, 5:10 PM
Tibor Santa
Tibor Santa - avatar
+ 2
ravilnicki Thank you very much.
7th May 2022, 10:25 AM
Denise Roßberg
Denise Roßberg - avatar
+ 2
Ashkan Sh 🇺🇦🇺🇦🇺🇦 The memoization is part of the exercises. So, yes. ;) From my understanding you do this, when you know some function calls happens more than once. Edit: The exercises are from this tutorial: https://youtu.be/oBt53YbR9Kk I need to think about can_sum() where you really can see that memoization makes sense. fibonacci is much easier: f(6) / \ f(5) f(4) / \ / \ f(4) f(3) f(3) f(2) / \ / \ / \ f(3) f(2) f(2) f(1) f(2) f(1) / \ f(2) f(1) These are all function calls for f(6) . And you can see that you call f(3) 3 times. If you already know the value for f(3) you don't need to calc it again and again. Maybe you need also to watch the freecodecamp tutorial about recursion.
7th May 2022, 11:23 AM
Denise Roßberg
Denise Roßberg - avatar
+ 2
Ashkan Sh 🇺🇦🇺🇦🇺🇦 Here is a much better example. I just printed memo so you can see that it gets filled. https://code.sololearn.com/coQQ0OhyofPQ/?ref=app
7th May 2022, 1:35 PM
Denise Roßberg
Denise Roßberg - avatar
+ 1
Not a direct answer to your main question but: I think "if memo is None" might be safer than "if memo == None"?
7th May 2022, 10:20 AM
Lisa
Lisa - avatar
+ 1
Denise Roßberg thank you. Now I get I better.
7th May 2022, 2:00 PM
Ashkan Shakibi
Ashkan Shakibi - avatar
+ 1
Tibor Santa I need to read the article but a short question: You mean, even if I want this effect (yes, I know it is really hard to debug lol) I should avoid using mutables as parameter? Btw: ravilnicki also showed me the cache thingy ... I guess I really should learn more about it. 😉 Thanks!
7th May 2022, 3:26 PM
Denise Roßberg
Denise Roßberg - avatar
+ 1
Tibor Santa Antipattern: Now it is clear to me. can_sum(): Thank you very much. Your code really helps. And it looks much cleaner/pythonic. I guess this part: if 1 in nums: return True is also thinking one step ahead of recursion, right? And good to know that variable name sum overrides the function sum. 🤪
7th May 2022, 6:31 PM
Denise Roßberg
Denise Roßberg - avatar
+ 1
Denise Roßberg if 1 in nums - yes technically this is also a shortcut, which should be unnecessary. But in this case it is actually an efficiency hack. Because if you imagine you want to determine if a large number can be summed from only the list of [1] this is true for any whole number, and actually Python would sooner run out of memory or reach maximum allowed recursion depth, before reaching the result through repeated recursive calls. In some other functional languages where recursion is more supported it would be cleaner to omit this case. I would argue in Python this helps to solve a known edge case with a shortcut.
7th May 2022, 7:15 PM
Tibor Santa
Tibor Santa - avatar
+ 1
Thinking further, another such shortcut could be the generalization of this 1-condition. There can be several numbers in the list, but if the target number is a multiple of any of them, then it is also guaranteed that the sum can be created, by repeatedly using the same number. Translating this to code: if any(total % x == 0 for x in nums): return True when this does not work, we still fall back to recursion of adding at least two different numbers to reach the result.
7th May 2022, 7:35 PM
Tibor Santa
Tibor Santa - avatar
0
Lisa Yes, your right. I changed it.
7th May 2022, 10:23 AM
Denise Roßberg
Denise Roßberg - avatar
7th May 2022, 10:58 AM
Ashkan Shakibi
Ashkan Shakibi - avatar
0
I think If we have nums=[9,3,7,2] and sum=16, then 16-9=7 and there will be nums in new sum. Also memo never got value (true) as you can see in can_sum1, and there is no difference between can_sum1 and can_sum2 which has no memo. Is there any condition that these 2 code have different results? It's really interesting that we can use a function inside the function itself, but I don't fully get it. I probably need more practice in this.
7th May 2022, 12:26 PM
Ashkan Shakibi
Ashkan Shakibi - avatar
0
Ashkan Sh 🇺🇦🇺🇦🇺🇦 🤦‍♀️My fault. 16-9 is 7 and not 5. I will edit my other post 🤪
7th May 2022, 12:55 PM
Denise Roßberg
Denise Roßberg - avatar