1

I wrote a simple Proxy class in python3, but I have a problem with "was_called" function

class Proxy:
    last_invoked = ""
    calls = {}

    def __init__(self, obj):
        self._obj = obj

    def __getattr__(self, item):
        attrs = dir(self._obj)

        if item in attrs:
            Proxy.last_invoked = item
            if item in Proxy.calls.keys():
                Proxy.calls[item] += 1
            else:
                Proxy.calls[item] = 1
            if item in Proxy.calls.keys():
                Proxy.calls[item] += 1
            else:
                Proxy.calls[item] = 1
            return getattr(self._obj, item)
        else:
            raise Exception('No Such Method')

    def last_invoked_method(self):
        if Proxy.last_invoked == "":
            raise Exception('No Method Is Invoked')
        else:
            return Proxy.last_invoked

    def count_of_calls(self, method_name):
        if method_name in Proxy.calls.keys():
            return Proxy.calls[method_name]
        return 0

    def was_called(self, method_name):
        if method_name in Proxy.calls.keys():
            if Proxy.calls[method_name] > 0: return True
        return False

class Radio():
    def __init__(self):
        self._channel = None
        self.is_on = False
        self.volume = 0

    def get_channel(self):
        return self._channel

    def set_channel(self, value):
        self._channel = value

    def power(self):
        self.is_on = not self.is_on

radio = Radio()
radio_proxy = Proxy(radio)
radio.number = 3
radio_proxy.number = 3
radio_proxy.power()
print(radio_proxy.was_called("number"))
print(radio_proxy.was_called("power"))

"was_called" function is work for functions and attributes that is in radio at first such as "power", but it's not work for new attributes that we add such as "number".

I expect for both print "True", because both of "power" and "number" is called. but first print return False!

What do you suggest?

7
  • I don't know what to suggest. What exactly is your question? Commented Apr 20, 2020 at 18:24
  • I expect for both print "True", because both of "power" and "number" is called. but first print return False! @mkrieger1 Commented Apr 20, 2020 at 18:29
  • 3
    You should add an implementation of class Radio. Commented Apr 20, 2020 at 18:35
  • 1
    __getattr__ is only called if item doesn't already exist. Further, methods do not exist in the instance; they are class attributes. Commented Apr 20, 2020 at 18:38
  • 1
    Also, __getattr__ isn't called when setting an attribute. Commented Apr 20, 2020 at 18:49

1 Answer 1

1
def Proxy(class_type):
    class ProxyClass(class_type):

        def __init__(self, *args, **kwargs):

            # Set your _calls and _last_invoked here, so that they are not class attributes (and are instead instance attributes).
            self._calls = {}
            self._last_invoked = ""

            # Pass the arguments back to the class_type (in our case Radio) to initialize the class.
            super().__init__(*args, **kwargs)

        def __getattribute__(self, item):

            # We must do this prelimary check before continuing on to the elif statement.
            # This is since _calls and _last_invoked is grabbed when self._last_invoked/self._calls is called below.
            if item in ("_calls", "_last_invoked"):
                return super(ProxyClass, self).__getattribute__(item)
            elif not item.startswith("_"):
                self._last_invoked = item
                self._calls[item] = 1 if item not in self._calls.keys() else self._calls[item] + 1

            return super(ProxyClass, self).__getattribute__(item)

        def __setattr__(self, item, val):

            # Wait until _calls is initialized before trying to set anything.
            # Only set items that do not start with _
            if not item == "_calls" and not item.startswith("_"):
                self._calls[item] = 0

            super(ProxyClass, self).__setattr__(item, val)

        def last_invoked_method(self):    
            if self._last_invoked == "":
                raise Exception('No Method Is Invoked')
            else:
                return self._last_invoked

        def count_of_calls(self, method_name):
            return self._calls[method_name] if method_name in self._calls.keys() else 0

        def was_called(self, method_name):
            return True if method_name in self._calls.keys() and self._calls[method_name] > 0 else False

    return ProxyClass

@Proxy
class Radio():
    def __init__(self):
        self._channel = None
        self.is_on = False
        self.volume = 0

    def get_channel(self):
        return self._channel

    def set_channel(self, value):
        self._channel = value

    def power(self):
        self.is_on = not self.is_on
radio = Proxy(Radio)()
radio.number = 3       # Notice that we are only setting the digit here.
radio.power()
print(radio._calls)
print(radio.number)    # Notice that this when we are actually calling it.
print(radio._calls)

outputs:

{'is_on': 0, 'volume': 0, 'number': 0, 'power': 1}
3
{'is_on': 0, 'volume': 0, 'number': 1, 'power': 1}

A few modifications here and there, but you should be able to see the bigger idea by reading through the code. From here you should be able to modify the code to your liking. Also note that any variable that starts with _ is automatically removed from the _calls dictionary.

If you rather not use the decorator @Proxy, you may initialize your Radio class (as a proxy) like so:

# Second parentheses is where your Radio args go in.
# Since Radio does not take any args, we leave it empty.
radio_proxy = Proxy(Radio)()

Also, make sure to understand the difference between class attributes, and instance attributes.


Edit:

class Test:
    def __init__(self, var):
        self.var = var
        self.dictionary = {}

    def __getattribute__(self, item):
        print("we are GETTING the following item:", item)

        # If we don't do this, you end up in an infinite loop in which Python is
        # trying to get the `dictionary` class to do `self.dictionary['dictionary'] = ...`

        if item == "dictionary":
            super(Test, self).__getattribute__(item)
        else:
            self.dictionary[item] = "Now we can use this!"

        return super(Test, self).__getattribute__(item)

    def __setattr__(self, item, key):
        print("we are SETTING the following item:", item)
        super(Test, self).__setattr__(item, key)

Notice:

test = Test(4)

outputs:

we are SETTING the following item: var
we are SETTING the following item: dictionary

then following it:

test.var

outputs:

we are GETTING the following item: var
we are GETTING the following item: dictionary
Sign up to request clarification or add additional context in comments.

5 Comments

Firstable thank you, it was really useful. But is there any way to do it it with out Proxy function, just with one class? and in constructor you get class not instance? I tried and tried but all I get it's maximum recursion depth exceeded error :(
I'll separate the answers between comments. "But is there any way to do it it without Proxy function, just with one class?" Technically yes, but that will require you to keep track of the Radio variables and functions via the Radio class. In essence, you will have to implement the __getattribute__ and __setattr__ in every class you want to have the Proxy-like functionality you are seeking. The implementation of Proxy above is to mitigate that issue, and allow you to utilize Proxy in whatever class you chose to use it at -- not just Radio.
"and in constructor you get class not instance?" Initiating variables inside the __init__ creates instance variables. Class variables are initiated outside the __init__ and are shared between all classes. i.e. class Test: z = 12; a = Test; b = Test; a.z = 10. Since a.z = 10 was performed, and a z is a class variable (not instance variable), when you do b.z you will get 10, since z is shared between all classes Test.
"I tried and tried but all I get it's maximum recursion depth exceeded error :(" This is because when you do self.var inside __getattribute__, you are essentially going back into __getattribute__ to get that variable. Notice in the code above how I organized the if block inside __getattribute__. This is so when I do self.var, I made sure that the variable was initialized to begin with. I encourage you to create a normal class and overwrite the __getattribute__ method to perform its normal functionality, but also print what value is being "get".
Made an edit to the post above to make the last point more clearer. :) Also, if you are using Jupyter Notebook, make sure to always reset the kernel. Since it saves the states of classes and the sort, it glitches out a lot of times, and ends you up in a recursive loop.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.