I am learning Python and while I was doing exercises with decorators, I found a case that I don't understand. The code bellow, inspired by the tutorial I use, is working:
>>> def maxExeTime(absMax,printMax=False):
... def decorFun(toDo):
... storeInfo = {"exeMax":0}
... def modified(*p,**pp):
... startTime = time.time()
... returnValue = toDo(*p,**pp)
... exeTime = time.time() - startTime
... if exeTime > storeInfo["exeMax"]:
... storeInfo["exeMax"] = exeTime
... if printMax:
... print("max execution time = {}".format(storeInfo["exeMax"]))
... if exeTime > absMax:
... raise Exception("Max exe time reached: {}".format(exeTime))
... return returnValue
... return modified
... return decorFun
...
>>> @maxExeTime(15,printMax=True)
... def mul(x,y):
... input("Press Enter...")
... return x*y
...
>>>
>>> mul(3,4)
Press Enter...
max execution time = 1.1439800262451172
12
>>> mul(3,5)
Press Enter...
max execution time = 2.1652064323425293
15
>>> mul(3,7)
Press Enter...
max execution time = 2.1652064323425293
21
>>> mul(3,10)
Press Enter...
max execution time = 21.074586629867554
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 13, in modified
Exception: Max exe time reached: 21.074586629867554
>>>
But I don't understand why the dictionary storeInfo
is persistent and can be re-used from one call to the other. If I try to store the information directly in a variable (ie exeMax=0
) and adapt the code accordingly, I get the behavior that I was expecting with an exception at run time: UnboundLocalError: local variable 'exeMax' referenced before assignment
when I try to compare the current execution time with the actual max value (if exeTime > exeMax:
)
Why does the dictionary persists out of the scope of the maxExeTime function execution?
Thanks to the links an the redirection to duplicate questions, I discovered closure which was a concept I never see before. Good. And I understand why it does not work in my test with the local variable exeMax
since I didn't declare it as nonlocal
. But I didn't do it either for the dictionary storeInfo
, and it is working. Why does this type of object have a different behavior?
Edit
@kindall what you say explains why it is necessary to use nonlocal when I use and integer directly since it seems that integers have a different behavior/rules than other objects, and I want to modify the value in the inner function.
But I still don't understand why if exeTime < exeMax:
fails when exeTime < storeInfo["exeMax"]
does not, and then why the next code is valid.
>>> a = 12
>>> def m(x):
... return a*x
...
>>> m(3)
36
>>> a = 15
>>> m(3)
45
For the moment, all these differences in behavior make the variable scope definition looking fuzzy for me. I need to find a clear definition of it, with, I hope, a small set of rules and no exception.