0

I've been playing around reversing single-linked lists and all works fine but then I decided to add a decorator to time execution of functions. And now I get errors like this when I try to run the code:

Traceback (most recent call last):
  File "C:/Users/cherp2/AppData/Roaming/JetBrains/PyCharmCE2020.3/scratches/scratch_7.py", line 86, in <module>
    main()
  File "C:/Users/cherp2/AppData/Roaming/JetBrains/PyCharmCE2020.3/scratches/scratch_7.py", line 78, in main
    print_ll(ll)
  File "C:/Users/cherp2/AppData/Roaming/JetBrains/PyCharmCE2020.3/scratches/scratch_7.py", line 10, in wrapped
    f(*args, **kwargs)
  File "C:/Users/cherp2/AppData/Roaming/JetBrains/PyCharmCE2020.3/scratches/scratch_7.py", line 37, in print_ll
    vals.append(str(node.val))
AttributeError: 'NoneType' object has no attribute 'val'

My decorator code is below. I checked and its pretty much the same as in the answer here:

def func_time(f):
    def wrapped(*args, **kwargs):
        t = dt.datetime.now()
        f(*args, **kwargs)
        print(f'{f.__name__} -- elapsed: {dt.datetime.now() - t}')
    return wrapped

Could the issues come because I'm using recursion in most of the functions? This is the linked list node definition and one of the erring functions:

from dataclasses import dataclass, field
import datetime as dt
from typing import Optional
import random


@dataclass(order=True)
class Node:
    val: int
    next: 'Node' = field()


def make_ll(n, head: Optional[Node] = None, node: Optional[Node] = None):
    if not head:
        head = Node(random.randint(0, 20), None)
        node = head
    node.next = Node(random.randint(0, 20), None)
    if n == 0:
        return head
    else:
        return make_ll(n-1, head, node.next)


@func_time
def print_ll(node: Node, vals: Optional[list] = None):
    if not vals:
        vals = []
    vals.append(str(node.val))
    if node.next:
        return print_ll(node.next, vals)
    else:
        vals.append('None')
        print(' -> '.join(vals))


def main():
    ll = make_ll(10, None)
    print('Original list: ')
    print_ll(ll)


if __name__ == '__main__':
   main()

EDIT: Looks like it's definitely an issue with recursion. I tested the decorator on non-recursive functions and it works as expected:

@func_time
def add(l):
    running_sum = 0
    for a in l:
        running_sum += a
    return running_sum

>>> l = [i for i in range(10**6)]
>>> x = add(l)
Out: add -- elapsed: 0:00:00.052296

EDIT2: Testing code in the question:

def main():
    # ll = make_ll(10, None)
    ll = Node(1, None)
    ll.next = Node(2, None)
    ll.next.next = Node(3, None)
    ll.next.next.next = Node(4, None)
    ll.next.next.next.next = Node(5, None)
    print('Original list: ')
    print_ll(ll)

    rev = reverse_ll(ll)
    print('Reversed list: ')
    print_ll(rev)

>>> main()

Out: 
Original list: 
1 -> 2 -> 3 -> 4 -> 5 -> None

Reversed list: 
5 -> 4 -> 3 -> 2 -> 1 -> None
NotAName
  • 3,821
  • 2
  • 29
  • 44
  • I think the problem is your decorator ***isn't*** written properly (not recursion). However I can't test that theory because you haven't provided a [mre]. Please edit your question and add one. – martineau Apr 12 '21 at 02:02
  • I've appended the code in the question to make it an MRE. I've also added an example in the EDIT section in the bottom that the decorator works fine with non-recursive functions. – NotAName Apr 12 '21 at 02:08
  • The code you added doesn't produce the `AttributeError` shown in your question — although it doesn't do what the non-recursive does (nor what you want). – martineau Apr 12 '21 at 05:58
  • The second part of your comment is quite cryptic. What do you mean by: *it doesn't do what the non-recursive does (nor what you want)* – NotAName Apr 12 '21 at 06:05
  • What part was unclear? The code in the version now in your question doesn't cause the error, but doesn't work properly and do what you want either. (There's a different fix for it — the decorator issue doesn't affect it.) – martineau Apr 12 '21 at 06:11
  • I'm not sure I follow. The code in the question works fine without the decorator and produces correct results. Ans same goes for the parts not included in the question dealing with reversing the list - it all works fine. What exactly is not working properly in your opinion? – NotAName Apr 12 '21 at 06:29
  • @martineau, I've added example that code works fine if I remove the decorator and prints expected result including the result of reversing (not included for brevity). – NotAName Apr 12 '21 at 06:40
  • I meant the code did not time the function correctly. Will look at this again tomorrow morning. – martineau Apr 12 '21 at 06:57

1 Answers1

1

Ok, this is quite embarrassing, but what happened was that I forgot to add a return statement to the decorator and so when it's used recursively, on the very first recursion it returns None and then recursion fails because None is not an acceptable input argument.

Simply put, this fixes all issues:

def func_time(f):
    def wrapped(*args, **kwargs):
        t = dt.datetime.now()
        foo = f(*args, **kwargs)
        print(f'{f.__name__} -- elapsed: {dt.datetime.now() - t}')
        return foo
    return wrapped
NotAName
  • 3,821
  • 2
  • 29
  • 44
  • Yes, that was the decorator problem I noticed when I made my initial comment under your question and will get rid of the `AttributeError`. However the decorator will time each recursive call to the the `print_ll()` function separately — which I doubt is what you would want to happen. _That's_ what my "cryptic" remarks were all about. – martineau Apr 12 '21 at 15:54
  • Oh yeah, but no, that actually was what I wanted - to measure the execution of each of recursive steps. – NotAName Apr 12 '21 at 23:08