@@ -49,116 +49,40 @@ def factorial(n):
49
49
factorial(x)
50
50
```
51
51
52
- ### How it works
53
-
54
- When a function (in this case ` factorial ` ) is decorated by ` @tail_recursive ` , it returns
55
- an object implementing the ` tail_call ` method. This object also overrides the ` __call__ `
56
- method, meaning that it can be called just like the original function (e.g. ` factorial(x) ` ).
57
-
58
- Decorated functions test whether they return a call to ` tail_call(...) ` . If this is the case
59
- then the return value is pushed on a call stack implemented as a list. ` tail_call ` returns
60
- an object storing the function it was called on (e.g. ` factorial ` ) and the (keyword)
61
- arguments (e.g. ` n - 1 ` ) it was called with. If the arguments contain a nested call to ` tail_call ` then this
62
- call is also pushed onto the call stack. On the other hand if ` tail_call ` is passed no nested
63
- ` tail_call ` s then the function that it stores is called with the stored (keyword) arguments. The
64
- return value of this lazy call then (a) replaces the argument it was passed as or (b)
65
- returns another ` tail_call ` which is pushed to the stack or (c) is the final return value of
66
- the call to the decorated function (e.g. ` factorial(x) ` ).
67
-
68
- But how can ` factorial.tail_call(n - 1) ` be multiplied by ` n ` ? Well, the object returned by
69
- ` tail_call ` overrides most dunder methods, such as ` __mul__ ` and ` __rmul__ ` , pushing the
70
- equivalent of ` tail_recursive(int.__rmul__).tail_call(n, factorial.tail_call(n - 1) ` to the
71
- call stack.
72
-
73
- The call stack for ` factorial(3) ` would looks something like this.
74
-
75
- 1 . Because ` factorial(3) ` is called, ` <lazy_call_obj>(func=factorial, args=(3,), kwargs={}) `
76
- is ** pushed** on the stack.
77
-
78
- ``` python
79
- [
80
- < lazy_call_obj> (func=factorial, args=(3 ,), kwargs={}),
81
- ]
82
- ```
83
-
84
- 2 . Because ` <lazy_call_obj>(func=factorial, args=(3,), kwargs={}) ` contains no nested arguments,
85
- it is ** popped** off the stack. It is then lazily evaluated, returning another ` <lazy_call_obj> ` , which is ** pushed** to the stack.
86
-
87
- ``` python
88
- [
89
- < lazy_call_obj> (func=int .__rmul__ , args(< lazy_call_obj> (func=factorial, args=(2 ,), kwargs={}), 3 ), kwargs={}),
90
- ]
91
- ```
92
-
93
- 3 . The lazy call to ` __rmul__ ` has a nested call as an argument. Consequentially, this
94
- argument is ** pushed** on the call stack.
95
-
96
- ``` python
97
- [
98
- < lazy_call_obj> (func=int .__rmul__ , args=(< lazy_call_obj> (func=factorial, args=(2 ,), kwargs={}), 3 ), kwargs={}),
99
- < lazy_call_obj> (func=factorial, args=(2 ,), kwargs={}),
100
- ]
101
- ```
102
-
103
- 4 . As in step _ 2_ the lazy call to ` factorial(2) ` is ** pop** off the stack and its return
104
- value is ** pushed** on.
105
-
106
- ``` python
107
- [
108
- < lazy_call_obj> (func=int .__rmul__ , args=(< lazy_call_obj> (func=factorial, args=(2 ,), kwargs={}), 3 ), kwargs={}),
109
- < lazy_call_obj> (func=int .__rmul__ , args=(< lazy_call_obj> (func=factorial, args=(1 ,), kwargs={}), 2 ), kwargs={}),
110
- ]
111
- ```
52
+ ## Features
112
53
113
- 5 . Similarly to step _ 3_ , because the lazy call to ` __rmul__ ` has a nested call as an
114
- argument, this argument is ** pushed** on the stack.
54
+ ### Feature Sets
115
55
56
+ Three feature sets are currently supported:
57
+ ` FeatureSet.BASE ` , ` FeatureSet.NESTED_CALLS ` and ` FeatureSet.FULL ` .
58
+ Where the "base" feature set provides a subset of the functionality of the "nested_calls" feature set which provides
59
+ a subset of the "full" functionality.
60
+ As a result the following python code is valid:
116
61
``` python
117
- [
118
- < lazy_call_obj> (func=int .__rmul__ , args=(< lazy_call_obj> (func=factorial, args=(2 ,), kwargs={}), 3 ), kwargs={}),
119
- < lazy_call_obj> (func=int .__rmul__ , args=(< lazy_call_obj> (func=factorial, args=(1 ,), kwargs={}), 2 ), kwargs={}),
120
- < lazy_call_obj> (func=factorial, args=(1 ,), kwargs={}),
121
- ]
122
- ```
123
-
124
- 6 . ` <lazy_call_obj>(func=int.__rmul__, args=(1,), kwargs={}) ` has no nested lazy calls
125
- as arguments, so it is ** popped** off the stack and its return value replaces
126
- the argument of ` __rmul__ ` that it was originally passed as.
62
+ from tail_recursive import FeatureSet
127
63
128
- ``` python
129
- [
130
- < lazy_call_obj> (func=int .__rmul__ , args=(< lazy_call_obj> (func=factorial, args=(2 ,), kwargs={}), 3 ), kwargs={}),
131
- < lazy_call_obj> (func=int .__rmul__ , args=(1 , 2 ), kwargs={}),
132
- ]
64
+ assert FeatureSet.FULL == FeatureSet.FULL | FeatureSet.NESTED_CALLS == FeatureSet.FULL | FeatureSet.NESTED_CALLS | FeatureSet.BASE
133
65
```
134
66
135
- 7 . The same process as in _ 6_ is repeated, where
136
- ` <lazy_call_obj>(func=int.__rmul__, args=(2, 1), kwargs={}) ` is ** popped** off the
137
- stack and its return value replaces the second argument of the lazy call to
138
- ` int.__rmul__(3, ...) ` .
67
+ The default feature set is "full".
68
+ Choosing a feature set with less functionality will provide some performance increase.
139
69
70
+ The desired feature set can be set be passing a value to the ` feature_set ` parameter, e.g.:
140
71
``` python
141
- [
142
- < lazy_call_obj > ( func= int . __rmul__ , args=( 2 , 3 ), kwargs={}),
143
- ]
72
+ @tail_recursive ( feature_set = FeatureSet. NESTED_CALLS )
73
+ def func (...):
74
+ ...
144
75
```
145
-
146
- 8 . Finally, because the lazy call to ` __rmul__ ` no longer has any nested calls as
147
- arguments, it can be ** popped** off the stack. Furthermore, it was not passed
148
- as an argument of a previous call on the stack and, for that reason, is returned
149
- from our decorated function (i.e. ` factorial(3) = int.__rmul__(2, 3) = 6 ` ).
150
-
76
+ You can also pass a lowercase string as a shorthand, e.g.:
151
77
``` python
152
- []
78
+ @tail_recursive (feature_set = " nested_calls" )
79
+ def func (...):
80
+ ...
153
81
```
154
82
155
- ## Features
156
-
157
- ### Nested Tail Calls
83
+ ### Nested Calls (` feature_set="nested_calls"/FeatureSet.NESTED_CALLS | "full"/FeatureSet.FULL ` )
158
84
159
- (only works for ` feature_set="full"|FeatureSet.FULL ` )
160
-
161
- As mentioned above nested tail calls are sequentially evaluated by creating a call stack.
85
+ This feature will resolve tail calls passed as parameters to other tail calls.
162
86
163
87
``` python
164
88
...
@@ -176,38 +100,24 @@ def factorial(n):
176
100
...
177
101
```
178
102
179
- Nested calls, however, comes a performance cost and can be disabled as follows .
103
+ As mentioned this comes at a performance cost and can be disabled by using the "base" feature set .
180
104
181
- ``` python
182
- @tail_recursive (feature_set = " base" )
183
- def factorial (n , accumulator = 1 ):
184
- if n == 1 :
185
- return accumulator
186
- return factorial.tail_call(n - 1 , n * accumulator)
187
- ```
188
-
189
- or
105
+ ### Method Calls (` feature_set="full"/FeatureSet.Full ` )
190
106
107
+ Method calls on tail calls are supported, e.g.:
191
108
``` python
192
- from tail_recursive import tail_recursive, FeatureSet
193
-
194
- ...
195
-
196
- @tail_recursive (nested_call_mode = FeatureSet.BASE )
197
- def factorial (n , accumulator = 1 ):
198
- ...
109
+ @tail_recursive (feature_set = feature_set)
110
+ def func (n ):
111
+ if n == 0 :
112
+ return set ()
113
+ return func.tail_call(n - 1 ).union({n})
199
114
```
200
115
201
- Similarly, use ` feature_set="full" ` or ` feature_set=FeatureSet.FULL `
202
- to explicitly enable this feature.
203
-
204
- ### Dunder Method Overrides
205
-
206
- (only works for ` feature_set="full"|FeatureSet.FULL ` )
116
+ ### Operator Overloading and Dunder Method Overriding (` feature_set="full"/FeatureSet.Full ` )
207
117
208
118
` n * factorial.tail_call(n - 1) ` shows that numeric operations
209
- can be done on tail calls and so long as the result of the expression
210
- is returned by the function. These expression will ultimately
119
+ can be done on tail calls, so long as the result of the expression
120
+ is returned by the function. These expressions will ultimately
211
121
evaluate to the same value that they would have if ` tail_call ` had been omitted.
212
122
This is also true for comparison and bitwise
213
123
operations, attribute and index access (i.e. ` <func>.tail_call(...)[...] ` )
@@ -275,7 +185,7 @@ For properties place the `@property` decorator before `@tail_recursive`.
275
185
276
186
### Return Values
277
187
278
- Currently tail calls that are returned as item/member in a tuple or other
188
+ Currently, tail calls that are returned as item/member in a tuple or other
279
189
data structures are not evaluated.
280
190
281
191
The following will not evaluate the tail call.
@@ -337,6 +247,118 @@ class MathStuff:
337
247
^^^^ ^^^^
338
248
```
339
249
250
+
251
+ ### How it works
252
+
253
+ ``` python
254
+ @tail_recursive
255
+ def factorial (n ):
256
+ if n <= 1 :
257
+ return n
258
+ return n * factorial.tail_call(n - 1 )
259
+ ```
260
+
261
+ When a function (in this case ` factorial ` ) is decorated by ` @tail_recursive ` , it returns
262
+ an object implementing the ` tail_call ` method. This object also overrides the ` __call__ `
263
+ method, meaning that it can be called just like the original function (e.g. ` factorial(x) ` ).
264
+
265
+ Decorated functions test whether they return a call to ` tail_call(...) ` . If this is the case
266
+ then the return value is pushed on a call stack implemented as a list. ` tail_call ` returns
267
+ an object storing the function it was called on (e.g. ` factorial ` ) and the (keyword)
268
+ arguments (e.g. ` n - 1 ` ) it was called with. If the arguments contain a nested call to ` tail_call ` then this
269
+ call is also pushed onto the call stack. On the other hand if ` tail_call ` is passed no nested
270
+ ` tail_call ` s then the function that it stores is called with the stored (keyword) arguments. The
271
+ return value of this lazy call then (a) replaces the argument it was passed as or (b)
272
+ returns another ` tail_call ` which is pushed to the stack or (c) is the final return value of
273
+ the call to the decorated function (e.g. ` factorial(x) ` ).
274
+
275
+ But how can ` factorial.tail_call(n - 1) ` be multiplied by ` n ` ? Well, the object returned by
276
+ ` tail_call ` overrides most dunder methods, such as ` __mul__ ` and ` __rmul__ ` , pushing the
277
+ equivalent of ` tail_recursive(int.__rmul__).tail_call(n, factorial.tail_call(n - 1) ` to the
278
+ call stack.
279
+
280
+ The call stack for ` factorial(3) ` would looks something like this.
281
+
282
+ 1 . Because ` factorial(3) ` is called, ` <lazy_call_obj>(func=factorial, args=(3,), kwargs={}) `
283
+ is ** pushed** on the stack.
284
+
285
+ ``` python
286
+ [
287
+ < lazy_call_obj> (func=factorial, args=(3 ,), kwargs={}),
288
+ ]
289
+ ```
290
+
291
+ 2 . Because ` <lazy_call_obj>(func=factorial, args=(3,), kwargs={}) ` contains no nested arguments,
292
+ it is ** popped** off the stack. It is then lazily evaluated, returning another ` <lazy_call_obj> ` , which is ** pushed** to the stack.
293
+
294
+ ``` python
295
+ [
296
+ < lazy_call_obj> (func=int .__rmul__ , args(< lazy_call_obj> (func=factorial, args=(2 ,), kwargs={}), 3 ), kwargs={}),
297
+ ]
298
+ ```
299
+
300
+ 3 . The lazy call to ` __rmul__ ` has a nested call as an argument. Consequentially, this
301
+ argument is ** pushed** on the call stack.
302
+
303
+ ``` python
304
+ [
305
+ < lazy_call_obj> (func=int .__rmul__ , args=(< lazy_call_obj> (func=factorial, args=(2 ,), kwargs={}), 3 ), kwargs={}),
306
+ < lazy_call_obj> (func=factorial, args=(2 ,), kwargs={}),
307
+ ]
308
+ ```
309
+
310
+ 4 . As in step _ 2_ the lazy call to ` factorial(2) ` is ** pop** off the stack and its return
311
+ value is ** pushed** on.
312
+
313
+ ``` python
314
+ [
315
+ < lazy_call_obj> (func=int .__rmul__ , args=(< lazy_call_obj> (func=factorial, args=(2 ,), kwargs={}), 3 ), kwargs={}),
316
+ < lazy_call_obj> (func=int .__rmul__ , args=(< lazy_call_obj> (func=factorial, args=(1 ,), kwargs={}), 2 ), kwargs={}),
317
+ ]
318
+ ```
319
+
320
+ 5 . Similarly to step _ 3_ , because the lazy call to ` __rmul__ ` has a nested call as an
321
+ argument, this argument is ** pushed** on the stack.
322
+
323
+ ``` python
324
+ [
325
+ < lazy_call_obj> (func=int .__rmul__ , args=(< lazy_call_obj> (func=factorial, args=(2 ,), kwargs={}), 3 ), kwargs={}),
326
+ < lazy_call_obj> (func=int .__rmul__ , args=(< lazy_call_obj> (func=factorial, args=(1 ,), kwargs={}), 2 ), kwargs={}),
327
+ < lazy_call_obj> (func=factorial, args=(1 ,), kwargs={}),
328
+ ]
329
+ ```
330
+
331
+ 6 . ` <lazy_call_obj>(func=int.__rmul__, args=(1,), kwargs={}) ` has no nested lazy calls
332
+ as arguments, so it is ** popped** off the stack and its return value replaces
333
+ the argument of ` __rmul__ ` that it was originally passed as.
334
+
335
+ ``` python
336
+ [
337
+ < lazy_call_obj> (func=int .__rmul__ , args=(< lazy_call_obj> (func=factorial, args=(2 ,), kwargs={}), 3 ), kwargs={}),
338
+ < lazy_call_obj> (func=int .__rmul__ , args=(1 , 2 ), kwargs={}),
339
+ ]
340
+ ```
341
+
342
+ 7 . The same process as in _ 6_ is repeated, where
343
+ ` <lazy_call_obj>(func=int.__rmul__, args=(2, 1), kwargs={}) ` is ** popped** off the
344
+ stack and its return value replaces the second argument of the lazy call to
345
+ ` int.__rmul__(3, ...) ` .
346
+
347
+ ``` python
348
+ [
349
+ < lazy_call_obj> (func=int .__rmul__ , args=(2 , 3 ), kwargs={}),
350
+ ]
351
+ ```
352
+
353
+ 8 . Finally, because the lazy call to ` __rmul__ ` no longer has any nested calls as
354
+ arguments, it can be ** popped** off the stack. Furthermore, it was not passed
355
+ as an argument of a previous call on the stack and, for that reason, is returned
356
+ from our decorated function (i.e. ` factorial(3) = int.__rmul__(2, 3) = 6 ` ).
357
+
358
+ ``` python
359
+ []
360
+ ```
361
+
340
362
## Other Packages
341
363
342
364
Check out [ tco] ( https://github.com/baruchel/tco ) for an alternative api with extra functionality.
0 commit comments