Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I return self and another variable in a python class method while method chaining?

Tags:

python

I understand what I am asking here is probably not the best code design, but the reason for me asking is strictly academic. I am trying to understand how to make this concept work.

Typically, I will return self from a class method so that the following methods can be chained together. My understanding is by returning self, I am simply returning an instance of the class, for the following methods to work on.

But in this case, I am trying to figure out how to return both self and another value from the method. The idea is if I do not want to chain, or I do not call any class attributes, I want to retrieve the data from the method being called.

Consider this example:

class Test(object):
    def __init__(self):
        self.hold = None

    def methoda(self):
        self.hold = 'lol'
        return self, 'lol'

    def newmethod(self):
        self.hold = self.hold * 2
        return self, 2

t = Test()
t.methoda().newmethod()
print(t.hold)

In this case, I will get an AttributeError: 'tuple' object has no attribute 'newmethod' which is to be expected because the methoda method is returning a tuple which does not have any methods or attributes called newmethod.

My question is not about unpacking multiple returns, but more about how can I continue to chain methods when the preceding methods are returning multiple values. I also understand that I can control the methods return with an argument to it, but that is not what I am trying to do.

As mentioned previously, I do realize this is probably a bad question, and I am happy to delete the post if the question doesnt make any sense.

like image 640
securisec Avatar asked Dec 10 '19 01:12

securisec


People also ask

Can you return self in Python?

If a method can fail, it should return success or failure or raise an exception to be handled. One caveat is that if you use the return self idiom, Python will allow you to assign all your methods to variables and you might think you are getting a data result or a list when you are actually getting the object.

Can we have two methods in a class with the same name Python?

Python does not support function overloading. When we define multiple functions with the same name, the later one always overrides the prior and thus, in the namespace, there will always be a single entry against each function name.


1 Answers

Following the suggestion by @JohnColeman, you can return a special tuple with attribute lookup delegated to your object if it is not a normal tuple attribute. That way it acts like a normal tuple except when you are chaining methods.

You can implement this as follows:

class ChainResult(tuple):
    def __new__(cls, *args):
        return super(ChainResult, cls).__new__(cls, args)
    def __getattribute__(self, name):
        try:
            return getattr(super(), name)
        except AttributeError:
            return getattr(super().__getitem__(0), name)


class Test(object):
    def __init__(self):
        self.hold = None

    def methoda(self):
        self.hold = 'lol'
        return ChainResult(self, 'lol')

    def newmethod(self):
        self.hold = self.hold * 2
        return ChainResult(self, 2)

Testing:

>>> t = Test()
>>> t.methoda().newmethod()
>>> print(t.hold)
lollol

The returned result does indeed act as a tuple:

>>> t, res = t.methoda().newmethod()
>>> print(res)
2
>>> print(isinstance(t.methoda().newmethod(), tuple))
True

You could imagine all sorts of semantics with this, such as forwarding the returned values to the next method in the chain using closure:

class ChainResult(tuple):
    def __new__(cls, *args):
        return super(ChainResult, cls).__new__(cls, args)
    def __getattribute__(self, name):
        try:
            return getattr(super(), name)
        except AttributeError:
            attr = getattr(super().__getitem__(0), name)
            if callable(attr):
                chain_results = super().__getitem__(slice(1, None))
                return lambda *args, **kw: attr(*(chain_results+args), **kw)
            else:
                return attr

For example,

class Test:
    ...
    def methodb(self, *args):
        print(*args)

would produce

>>> t = Test()
>>> t.methoda().methodb('catz')
lol catz

It would be nice if you could make ChainResults invisible. You can almost do it by initializing the tuple base class with the normal results and saving your object in a separate attribute used only for chaining. Then use a class decorator that wraps every method with ChainResults(self, self.method(*args, **kw)). It will work okay for methods that return a tuple but a single value return will act like a length 1 tuple, so you will need something like obj.method()[0] or result, = obj.method() to work with it. I played a bit with delegating to tuple for a multiple return or to the value itself for a single return; maybe it could be made to work but it introduces so many ambiguities that I doubt it could work well.

like image 175
Neapolitan Avatar answered Nov 01 '22 16:11

Neapolitan