1

Specifically, I would want MyClass.my_method to be used for lookup of a value in the class dictionary, but MyClass.my_method() to be a method that accepts arguments and performs a computation to update an attribute in MyClass and then returns MyClass with all its attributes (including the updated one).

I am thinking that this might be doable with Python's descriptors (maybe overriding __get__ or __call__), but I can't figure out how this would look. I understand that the behavior might be confusing, but I am interested if it is possible (and if there are any other major caveats).

I have seen that you can do something similar for classes and functions by overriding __repr__, but I can't find a similar way for a method within a class. My returned value will also not always be a string, which seems to prohibit the __repr__-based approaches mentioned in these two questions:

joelostblom
  • 43,590
  • 17
  • 150
  • 159
  • 1
    `MyClass.my_method()` works by looking up `MyClass.my_method`, and then attempting to call that object. So the result of `MyClass.my_method` *cannot* be a plain string, int, or other common data type, because none of those types are callable. – jasonharper Apr 04 '22 at 04:13
  • could you provide feedback on my answer? did it help you? – Guilherme Correa Apr 07 '22 at 14:05
  • Sorry for the late reply (hectic week) and thank you for taking the time to reply to my question @GuilhermeCorrea ! I have added an answer with a minimal implementation of your suggestion and to better reflect that I am trying to update the value set on the method itself, rather than a separate modifiable param (but I will of course award the bounty to you). One thing I do not quite understand is why passing a list or dictionary returns an empty list/dict. I want to assign these data types as well, do you know how I can go about this (see my answer below for details). – joelostblom Apr 07 '22 at 18:52

3 Answers3

1

Thank you Joel for the minimal implementation. I found that the remaining problem is the lack of initialization of the parent, since I did not find a generic way of initializing it, I need to check for attributes in the case of list/dict, and add the initialization values to the parent accordingly.

This addition to the code should make it work for lists/dicts:

def classFactory(parent, init_val, target):
    class modifierClass(parent):
        def __init__(self, init_val):
            super().__init__()
            dict_attr = getattr(parent, "update", None)
            list_attr = getattr(parent, "extend", None)
            if callable(dict_attr):  # parent is dict
                self.update(init_val)
            elif callable(list_attr):  # parent is list
                self.extend(init_val)
            self.target = target

        def __call__(self, *args):
            self.target.__init__(*args)

    return modifierClass(init_val)


class myClass:
    def __init__(self, init_val=''):
        self.method = classFactory(init_val.__class__, init_val, self)

Unfortunately, we need to add case by case, but this works as intended.

A slightly less verbose way to write the above is the following:

def classFactory(parent, init_val, target):
    class modifierClass(parent):
        def __init__(self, init_val):
            if isinstance(init_val, list):
                self.extend(init_val)
            elif isinstance(init_val, dict):
                self.update(init_val)
            self.target = target

        def __call__(self, *args):
            self.target.__init__(*args)

    return modifierClass(init_val)


class myClass:
    def __init__(self, init_val=''):
        self.method = classFactory(init_val.__class__, init_val, self)
joelostblom
  • 43,590
  • 17
  • 150
  • 159
  • Thanks again! Is there are reason to check the `callable` as you do and to and to use `super().__init__()` or do you think the less verbose version that I added to your answer is equivalent? The seem to work the same for me. – joelostblom Apr 10 '22 at 03:35
  • 1
    yeah, its more succinct to manually check if it's an instance, and ```super().__init__``` is just to initialize the base class, in this case I believe all of those inherit from python's ```object```. I saw this being done in "Mastering object oriented Python" from Steve F. Lott, and I think it's a good practice. – Guilherme Correa Apr 11 '22 at 19:11
1

As jasonharper commented,

MyClass.my_method() works by looking up MyClass.my_method, and then attempting to call that object. So the result of MyClass.my_method cannot be a plain string, int, or other common data type [...]

The trouble comes specifically from reusing the same name for this two properties, which is very confusing just as you said. So, don't do it.

But for the sole interest of it you could try to proxy the value of the property with an object that would return the original MyClass instance when called, use an actual setter to perform any computation you wanted, and also forward arbitrary attributes to the proxied value.

class MyClass:

    _my_method = whatever

    @property
    def my_method(self):

        my_class = self

        class Proxy:
            def __init__(self, value):
                self.__proxied = value

            def __call__(self, value):
                my_class.my_method = value
                return my_class

            def __getattr__(self, name):
                return getattr(self.__proxied, name)

            def __str__(self):
                return str(self.__proxied)

            def __repr__(self):
                return repr(self.__proxied)

        return Proxy(self._my_method)

    @my_method.setter
    def my_method(self, value):
        # your computations
        self._my_method = value

a = MyClass()
b = a.my_method('do not do this at home')

a is b
# True
a.my_method.split(' ')
# ['do', 'not', 'do', 'this', 'at', 'home']

And today, duck typing will abuse you, forcing you to delegate all kinds of magic methods to the proxied value in the proxy class, until the poor codebase where you want to inject this is satisfied with how those values quack.

N1ngu
  • 2,862
  • 17
  • 35
0

This is a minimal implementation of Guillherme's answer that updates the method instead of a separate modifiable parameter:

def classFactory(parent, init_val, target):
    class modifierClass(parent):
        def __init__(self, init_val):
            self.target = target

        def __call__(self, *args):
            self.target.__init__(*args)

    return modifierClass(init_val)


class myClass:
    def __init__(self, init_val=''):
        self.method = classFactory(init_val.__class__, init_val, self)

This and the original answer both work well for single values, but it seems like lists and dictionaries are returned as empty instead of with the expected values and I am not sure why so help is appreciated here:

enter image description here

joelostblom
  • 43,590
  • 17
  • 150
  • 159