3

I would like to know if there is an easy way to do some identical edits on several methods of a class. An example :

class Dog():
    def __init__(self):
        self.name = 'abc'
        self.age = 1

    def setName(self, newValue):
        self.name = newValue

    def setAge(self, newValue):
        self.age = newValue

class TalkingDog(Dog):

    def __init__(self):
        super().__init__()
        # The end is in pseudo code : 
        for method in TalkingDog.allMethods :
            method = method + 'print('I have been edited !')'

I know that I can also overwrite each method but in a situation with tens of methods, that will be a little boring...

So I tried this :

class TalkingDog(Dog):

    def __init__(self):
        super().__init__()
        for method in self.__dir__():
            if method.startswith('set'):
                oldMethod = getattr(self, method)
                def _newMethod(newValue):
                    oldMethod(newValue)
                    print('I have been edited !')
                setattr(self, method, _newMethod)


a = TalkingDog()
print(a.setName) >>> <function TalkingDog.__init__.<locals>._newMethod at 0x0000000002C350D0>

That almost works but setName is not anymore a method. It's an attribute which contains a function. I completely understand why but I'm trying to get a cleaner result. With that result, I risk of having problems later. For example I can't use the library pickle with that object (got the error _pickle.PicklingError: Can't pickle <function TalkingDog.__init__.<locals>._newMethod at 0x00000000003DCBF8>: attribute lookup _newMethod on __main__ failed).

Morgan
  • 589
  • 9
  • 20
  • 2
    Use properties, not `set` and `get` methods: http://stackoverflow.com/q/1554546/3001761 – jonrsharpe May 30 '16 at 10:17
  • 1
    Also, what's the actual use case for this? Are you really making a talking dog? If so, a dog shouldn't talk whenever his age is changed. if you want the dog to talk, make him talk separately from changing the age. – Markus Meskanen May 30 '16 at 10:22
  • @jonrsharpe I don't understand why and how using properties will help me in that case. – Morgan May 30 '16 at 10:27
  • @Morgan They don't solve your question (that's why it's a comment and not an answer), it's just a general improvement. You should *always* use properties and **never** `get` and `set` in Python. – Markus Meskanen May 30 '16 at 10:28
  • @Morgan I meant in general. But you could use the descriptor protocol, which is what properties use, to do this. – jonrsharpe May 30 '16 at 10:29
  • @MarkusMeskanen No, I'm not trying to create talking dog. I have got a class which has got an attribute 'haveBeenEdited'. I want to set that attribute to True each time a set function of the class is used. – Morgan May 30 '16 at 10:32
  • Ok for properties. I used get and set to create a shorter example. – Morgan May 30 '16 at 10:36
  • 1
    @Morgan note that your [mcve] should *actually illustrate your problem*. Otherwise it's very hard to make useful suggestions as to what the approach should be. – jonrsharpe May 30 '16 at 10:43

2 Answers2

2

The Pythonic way to do this is probably to use the descriptor protocol, which is also what properties use:

class VocalAttribute:

    def __init__(self, name, feedback):
        """Called when you first create the descriptor."""
        self.name = name  # the name of the attribute 'behind' the property
        self.feedback = feedback  # the feedback to show when the value changes

    def __get__(self, obj):
        """Called when you get the descriptor value."""
        return getattr(obj, self.name)

    def __set__(self, obj, value):
        """Called when you set the descriptor value."""
        prev = getattr(obj, self.name, None)
        if value != prev:
            setattr(obj, self.name, value)
            print(self.feedback)

    def __delete__(self, obj):
        """Called when you delete the descriptor value."""
        delattr(obj, self.name)


class Foo:

    bar = VocalAttribute('_bar', 'I have been edited!')


foo = Foo()

print('1.')

foo.bar = 'hello'

print('2.')

foo.bar = 'hello'

print('3.')

foo.bar = 'world'

Output:

1.
I have been edited!
2.
3.
I have been edited!

Note that this only gives feedback when the new value is different to the old one - you can tweak the behaviour as needed in __set__. It also means you can directly read from and assign to foo.bar, rather than needing to call getters and setters (what is this, Java?)

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
  • I'm not familiar with the descriptor protocal. I will read a little more about it and try it on my code. Thanks for the help ! :) – Morgan May 30 '16 at 11:04
-1

since decorator could explicit called here a way to use it:

def updater(obj, call_back, call_back_args=(), call_back_kw=None, replace=False):
    # ability to be called on the fly with different args and kw for the callback
    # now it returns the updated obj (instance or class) 
    # but could a be factory returning a new obj in this case make a copy of obj, update this coy and return it

    def update_function(fn, *args, **kw):
        def wrapper(*args, **kw):
            if replace:
                # call only the callback
                res = call_back(*call_back_args, **call_back_kw)
            else:
                res = fn(*args, **kw)
                call_back(*call_back_args, **call_back_kw)
            return res
        return wrapper

    # get all methods of the obj
    # and apply update_function (a decorator) to all methods
    for name, m in inspect.getmembers(
            obj, predicate=lambda x: inspect.isfunction(x) or inspect.ismethod(x)):
        # make the selection here
        # could be made on the name for instance
        if not name.startswith('_'):
            new_m = update_function(m)
            setattr(obj, name, new_m)

    return obj

# declare a callback 
def call_back(*args, **kw):
    # simple callback
    print("I have been edited and called with %r args and %r kw " % (args, kw))

a = Dog()

# could be called on instance or class
# apply the callback on all "public" methods 
updater(
    a,
    call_back,
    call_back_args=(2, 3, 4),
    call_back_kw={"kw1": "v_1"}
)
Ali SAID OMAR
  • 6,404
  • 8
  • 39
  • 56