reset password
Author Message
asalin14
Posts: 12
Posted 17:12 Mar 20, 2019 |

After today's lecture, I built a Wrapper class that lets you specify your own custom operator with a few conditions. (See operatorWrapper.py)

One of the conditions is that you must have a special variable defined between each operator character:

a <|> b must be called using a <X|X> b

 

Here is how is my thought process for building it (You can skip to 'How it works' if you only want to know what the final code does):

Since 3 < 4 is the same as calling (3).__lt__(4), we can take advantage of this. 

I originally tried overwriting the __lt__ function to not require any arguments:

class myInt(int):

    def __lt__(self):

        return self

 

num = myInt(3)

print(num) # 3

print(num.__lt__()) # 3

print(num < ) # SyntaxError: invalid syntax

 

After trying that, Python returned a parsing error. For Python to parse an operator that is supposed to take 2 values (such as <, >, ==), there must be a value after the operator (even if it is not used in the __lt__ method)

(NOTE: running print(num < 3) would have resulted in a runtime error because my __lt__ does not take in a parameter)

I wanted to write an operator <+> that takes two arguments, adds them together then doubles the result. (x <+> y  -> (x+y)*2)

Since the < operator requires a second value (that is not also an operator), I compromised by inserting ().

The new operator was x <()+()> y where the () symbols are placeholder values (and can be replaced with any other value or variable name) until the function evaluates.

When I tested the operator, it resulted in an error because > has a higher precedence than + so the interpreter would try to evaluate () > y.

I was able to fix the issue using parentheses to specify precedence but that looked bad (even though it worked correctly):

 

print(((myInt(3) <())+())> 4) # 14

You can try this code in OperatorTesting.py.

When this didnt work, I wanted to try replacing () with a class that knows how to handle precedence. As a result, I built the operatorWrapper.py.

 

How it works:

My version of operatorWrapper.py only supports any the <, | , > operators where you can use the | symbol as many times as necessary but only use < and > once (I will explain why soon). Using my code, you can easily implement more operators such as +, /, *, ^, %. I only used a few operators for a proof of concept. 

The operatorWrapper.py logic starts with the MyWrapper class. This class should wrap any other classes that want to have special operator functions. When MyWrapper wraps a class, it will check for that classes types attribute which should hold a dictionary with the special operators and its associated function.

The Wrapper's lt, gt, and or functions check if second value of the operator is an X. If so, the wrapper creates an OperatorStreak instance that stores a stack of all operators found. 

Since order of operations does not necessarily go from left to right with operators, OperatorStreak instances and the X class have their own cases for what happens if operators are called on them. If an Operator Streak instance or the X class runs into the second argument of your custom operator (i.e. 3 <X| 5 => second value is 5), then the OperatorStreak will store the second value. Eventually, when most operator functions have been executed, the original Wrapper class will find either the second value or an operator that is storing the second value. At this point, the Wrapper knows that it has evaluated every operator and will check if the OperatorStreak stack matches with one of the defined custom operators. If so, it will execute that operator function. If the operator is not found, a runtime error will be triggered.

 

How to use:

Any class that is defined by the Wrapper (MyWrapper(class()) and has custom operators defined in the types variable will be able to utilize the operators. The catch is that when calling the operator, you must insert the X class between every operator.

To call a ||| b you must call a |X|X| b.

You can rename the X class to whatever you want as shown on line 185 of OperatorWrapper.py

 

Interesting Finding:

The only issue I have found with my method is the way that python parses multiple < or > signs.

When python evaluates a < b > c, it does not evaluate a < b => RESULT then RESULT > 0 (Note that is a possible implementation that Python could have used since True and False have an __int__ value)

Instead, Python evaluates a < b > c by starting with a < b . If the result of a < b is falsy, then it will return the result of a < b. If the result of a < b is truthy, then it will return b > c. (NOT (result of a < b) > c)

You can see this from the follow python checks:

3 < 2 > -1 # false

2 < 3 > 2 # true

 

While this is fine when lt and gt is always returning boolean values, this makes it impossible for MyWrapper class to evaluate an operator of Wrapper <X> b since the wrapper will be lost after Wrapper < X evaluates to an object (truthy). Calling Wrapper <X> b will only return X>b which evaluates to an OperatorStreak in my code. 

Even though the issue of < and > parsing arises, my code still works if you do not combine < and > preventing the parsing issue.

 

Final Issue

I wanted MyWrapper class to be a decorator class but had trouble figuring out how to implement a decorator class. When I added @MyWrapper to the top of the MyInt class, i got TypeError: 'MyWrapper' object is not callable. If anyone has any suggestions on how to turn the class into a real decorator, please let me know.

 

Conclusion

I know my explanation was confusing but hopefully the code in OperatorWrapper.py can better help explain what I did (and the print statements starting n line 187 help explain how to use the operators). 

On top, I created a SILENT_Trace variable. If you enable that to False, my objects will print whenever an operator is used to help illustrate order of operations.

Thank you to the presenters today. Your presentations helped enable this wrapper class :)

 

 

Last edited by asalin14 at 17:19 Mar 20, 2019.
avarg116
Posts: 8
Posted 17:29 Mar 20, 2019 |

This is Awesome Abel!

thanks for sharing,

Austin Vargason

asalin14
Posts: 12
Posted 17:38 Mar 20, 2019 |
avarg116 wrote:

This is Awesome Abel!

thanks for sharing,

Austin Vargason

Thanks for presenting your Infix version Austin. I feel like my code follows a similar concept to your Infix version except instead of using functions, I use classes (to allow chaining). I assumed that a class was necessary for chaining operators since you could apply multiple data model functions like __lt__ and __gt__. Do you think your Infix function can be modified to allow chaining too or do you think classes are required like in my implementation?

Last edited by asalin14 at 17:39 Mar 20, 2019.
avarg116
Posts: 8
Posted 17:45 Mar 20, 2019 |

I think the way you did  classes looks a bit more robust. It might be possible with functions but I’d have to play around some more with it.

rabbott
Posts: 1649
Posted 20:08 Mar 20, 2019 |

Amazing amount of work. I'll have to look at it more carefully.

WRT why the system didn't allow you to use MyWrapper as a decorator:  it's missing a __call__ method. Recall that the point of decorators is to decorate functions. So if you use a class as a decorator you will get an instance of that class to replace the decorated function. Since it replaces a function, it must be callable. But class objects are generally not callable. They are just objects. It's possible to make them callable by including a __call__ method. A class with a __call__ method may be used as if it were a function. For example, this will run.

class CallableClass:

    def __call__(self):
        print('Hi, there')


c = CallableClass() # Instantiate the class
c()                 # Call the object
CallableClass()()   # Instantiate the class and call the object immediately

 

The output will be:

Hi, there
Hi, there 

Check out p. 6 of the Amuse-Bouche homework. When used as part of a decorator, the __call__ method is called when the function would be called.

Last edited by rabbott at 20:12 Mar 20, 2019.
asalin14
Posts: 12
Posted 22:10 Mar 20, 2019 |
rabbott wrote:

WRT why the system didn't allow you to use MyWrapper as a decorator:  it's missing a __call__ method. Recall that the point of decorators is to decorate functions. So if you use a class as a decorator you will get an instance of that class to replace the decorated function. Since it replaces a function, it must be callable. But class objects are generally not callable. They are just objects. It's possible to make them callable by including a __call__ method. A class with a __call__ method may be used as if it were a function. 

Thank you for that explanation. Here is an updated version that uses a decorator. I detached the example from the Wrapper code to make it its own module.

(I also added a few more operators and fixed a bug with some cases of operator chaining) 

rabbott
Posts: 1649
Posted 22:43 Mar 20, 2019 |

Thanks, Abel, Good work!

For anyone who is interested, I've modified the previous simple example minimally so that it can be used as a decorator.

class CallableClass:

    def __init__(self, fn=None):
        self.fn = fn

    def __call__(self, arg=None):
        print('Hi, there')
        if self.fn is not None:
            self.fn(arg)


c = CallableClass() # Instantiate the class
c()                 # Run the object

CallableClass()()   # Instantiate the class and call the object immediately

@CallableClass
def testFn(name: str):
    print(name)

testFn("Mr. Python")  # Call the decorated function

The output is:

Hi, there
Hi, there
Hi, there
Mr. Python

Last edited by rabbott at 22:48 Mar 20, 2019.