简体   繁体   中英

Python: Assign a method from one class to an instance of another class

I want to use new.instancemethod to assign a function (aFunction) from one class (A) to an instance of another class (B). I'm not sure how I can get aFunction to allow itself to be applied to an instance of B - currently I am getting an error because aFunction is expecting to be executed on an instance of A.

[Note: I can't cast instance to A using the __class__ attribute as the class I'm using (B in my example) doesn't support typecasting]

import new

class A(object):
    def aFunction(self):
        pass

#Class which doesn't support typecasting
class B(object): 
    pass

b = B()
b.aFunction = new.instancemethod(A.aFunction, b, B.__class__)
b.aFunction()

This is the error:

TypeError: unbound method aFunction() must be called with A instance as first argument (got B instance instead)

new.instancemethod takes a function. A.aFunction is an unbound method. That's not the same thing. (You may want to try adding a bunch of print statements to display all of the things involved— A.aFunction() , A().aFunction , etc.—and their types, to help understanding.)

I'm assuming you don't how descriptors work. If you don't want to learn all of the gory details, what's actually going on is that your declaration of aFunction within a class definition creates a non-data descriptor out of the function. This is a magic thing that means that, depending on how you access it, you get an unbound method ( A.aFunction ) or a bound method ( A().aFunction ).

If you modify aFunction to be a @staticmethod , this will actually work, because for a static method both A.aFunction and A().aFunction are just functions. (I'm not sure that's guaranteed to be true by the standard, but it's hard to see how else anyone would ever implement it.) But if you want "aFunction to allow itself to be applied to an instance of B", a static method won't help you.

If you actually want to get the underlying function, there are a number of ways to do it; I think this is the clearest as far as helping you understand how descriptors works:

f = object.__getattribute__(A, 'aFunction')

On the other hand, the simplest is probably:

f = A.aFunction.im_func

Then, calling new.instancemethod is how you turn that function into a descriptor that can be called as a regular method for instances of class B:

b.aFunction = new.instancemethod(f, b, B)

Printing out a bunch of data makes things a lot clearer:

import new

class A(object):
  def aFunction(self):
    print self, type(self), type(A)

#Class which doesn't support typecasting
class B(object): 
    pass

print A.aFunction, type(A.aFunction)
print A().aFunction, type(A().aFunction)
print A.aFunction.im_func, type(A.aFunction.im_func)
print A().aFunction.im_func, type(A().aFunction.im_func)

A.aFunction(A())
A().aFunction()

f = object.__getattribute__(A, 'aFunction')
b = B()
b.aFunction = new.instancemethod(f, b, B)
b.aFunction()

You'll see something like this:

<unbound method A.aFunction> <type 'instancemethod'>
<bound method A.aFunction of <__main__.A object at 0x108b82d10>> <type 'instancemethod'>
<function aFunction at 0x108b62a28> <type 'function'>
<function aFunction at 0x108b62a28> <type 'function'>
<__main__.A object at 0x108b82d10> <class '__main__.A'> <type 'type'>
<__main__.A object at 0x108b82d10> <class '__main__.A'> <type 'type'>
<__main__.B object at 0x108b82d10> <class '__main__.B'> <type 'type'>

The only thing this doesn't show is the magic that creates the bound and unbound methods. For that, you need to look into A.aFunction.im_func.__get__ , and at that point you're better off reading the descriptor howto than trying to dig it apart yourself.

One last thing: As brandizzi pointed out, this is something you almost never want to do. Alternatives include: Write aFunction as a free function instead of a method, so you just call b.aFunction() ; factor out a function that does the real work, and have A.aFunction and B.aFunction just call it; have B aggregate an A and forward to it; etc.

I already posted an answer to the question you asked. But you shouldn't have had to get down to the level of having to understand new.instancemethod to solve your problem. The only reason that happened is because you asked the wrong question. Let's look at what you should have asked, and why.

In PySide.QtGui, I want the list widget items to have methods to set the font and colors, and they don't seem to.

This is what you really want. There may well be an easy way to do this. And if so, that's the answer you want. Also, by starting off with this, you avoid all the comments about "What do you actually want to do?" or "I doubt this is appropriate for whatever you're trying to do" (which often come with downvotes—or, more importantly, with potential answerers just ignoring your question).

Of course I could just write a function that takes a QListWidgetItem and call that function, instead of making it a method, but that won't work for me because _ _.

I assume there's a reason that won't work for you. But I can't really think of a good one. Whatever line of code said this:

item.setColor(color)

would instead say this:

setItemColor(item, color)

Very simple.

Even if you need to, eg, pass around a color-setting delegate with the item bound into it, that's almost as easy with a function as with a method. Instead of this:

delegate = item.setColor

it's:

delegate = lambda color: setItemColor(item, color)

So, if there is a good reason you need this to be a method, that's something you really should explain. And if you're lucky, it'll turn out you were wrong, and there's a much simpler way to do what you want than what you were trying.

The obvious way to do this would be to get PySide to let me specify a class or factory function, so I could write a QListWidgetItem subclass, and every list item I ever deal with would be an instance of that subclass. But I can't find any way to do that.

This seems like something that should be a feature of PySide. So maybe it is, in which case you'd want to know. And if it isn't, and neither you nor any of the commenters or answerers can think of a good reason it would be bad design or hard to implement, you should go file a feature request and it might be in the next version. (Not that this helps you if you need to release code next week against the current version, but it's still worth doing.)

Since I couldn't find a way to do that, I tried to find some way to add my setColor method to the QListWidgetItem class, but couldn't think of anything.

Monkey-patching classes is very simple:

QListWidgetItem.setColor = setItemColor

If you didn't know you could do this, that's fine. If people knew that's what you were trying to do, this would be the first thing they'd suggest. (OK, maybe not many Python programmers know much about monkey-patching, but it's still a lot more than the number who know about descriptors or new.instancemethod .) And again, besides being an answer you'd get faster and with less hassle, it's a better answer.

Even if you did know this, some extension modules won't let you do that. If you tried and it failed, explain what didn't work instead:

PySide wouldn't let me add the method to the class; when I try monkey-patching, _ _.

Maybe someone knows why it didn't work. If not, at least they know you tried.

So I have to add it to every instance.

Monkey-patching instances looks like this:

myListWidgetItem.setColor = setItemColor

So again, you'd get a quick answer, and a better one.

But maybe you knew that, and tried it, and it didn't work either:

PySide also wouldn't let me add the method to each instance; when I try, _ _.

So I tried patching out the __class__ of each instance, to make it point to a custom subclass, because that works in PyQt4, but in PySide it _ _.

This probably won't get you anything useful, because it's just the way PySide works. But it's worth mentioning that you tried.

So, I decided to create that custom subclass, then copy its methods over, like so, but _ _.

And all the way down here is where you'd put all the stuff you put in your original question. If it really were necessary to solving your problem, the information would be there. But if there were an easy solution, you wouldn't get a bunch of confused answers from people who were just guessing at how new.instancemethod works.

If possible you can make the aFunction unbound by using @staticmethod decorator: -

class A(object):
    @staticmethod
    def aFunction(B):    # Modified to take appropriate parameter..
        pass

class B(object): 
    pass

b = B()
b.aFunction = new.instancemethod(A.aFunction, b, B.__class__)
b.aFunction()

*NOTE: - You need to modify the method to take appropriate parameter..

I'm amazed that none of these answers gives what seems the simplest solution, specifically where you want an existing instance to have a method x replaced by a method x (same name) from another class (eg subclass) by being "grafted" on to it.

I had this issue in the very same context, ie with PyQt5. Specifically, using a QTreeView class, the problem is that I have subclassed QStandardItem (call it MyItem ) for all the tree items involved in the tree structure (first column) and, among other things, adding or removing such an item involves some extra stuff, specifically adding a key to a dictionary or removing the key from this dictionary.

Overriding appendRow (and removeRow , insertRow , takeRow , etc.) presents no problem:

class MyItem( QStandardItem ):
    ...

    def appendRow( self, arg ):
        # NB QStandarItem.appendRow can be called either with an individual item 
        # as "arg", or with a list of items 
        if isinstance( arg, list ):
            for element in arg:
                assert isinstance( element, MyItem )
                self._on_adding_new_item( element )
        elif isinstance( arg, MyItem ):
            self._on_adding_new_item( arg )
        else:
            raise Exception( f'arg {arg}, type {type(arg)}')
        super().appendRow( self, arg )

... where _on_adding_new_item is a method which populates the dictionary.

The problem arises when you want to want to add a "level-0" row, ie a row the parent of which is the "invisible root" of the QTreeView . Naturally you want the items in this new "level-0" row to cause a dictionary entry to be created for each, but how to get this invisible root item, class QStandardItem , to do this?

I tried overriding the model's method invisibleRootItem() to deliver not super().invisibleRootItem() , but instead a new MyItem . This didn't seem to work, probably because QStandardItemModel.invisibleRootItem() does things behind the scenes, eg setting the item's model() method to return the QStandardItemModel .

The solution was quite simple:

class MyModel( QStandardItemModel ):
    def __init__( self ):
        self.root_item = None
        ...

    ...

    def invisibleRootItem( self ):
        # if this method has already been called we don't need to do our modification
        if self.root_item != None:
            return self.root_item 

        self.root_item = super().invisibleRootItem()
        # now "graft" the method from MyItem class on to the root item instance
        def append_row( row ):
            MyItem.appendRow( self.root_item, row  )
        self.root_item.appendRow = append_row

... this is not quite enough, however: super().appendRow( self, arg ) in MyItem.appendRow will then raise an Exception when called on the root item, because a QStandardItem has no super() . Instead, MyItem.appendRow is changed to this:

def appendRow( self, arg ):
    if isinstance( arg, list ):
        for element in arg:
            assert isinstance( element, MyItem )
            self._on_adding_new_item( element )
    elif isinstance( arg, MyItem ):
        self._on_adding_new_item( arg )
    else:
        raise Exception( f'arg {arg}, type {type(arg)}')
    QStandardItem.appendRow( self, arg )

Thanks Rohit Jain - this is the answer:

import new

class A(object):
    @staticmethod
    def aFunction(self):
        pass

#Class which doesn't support typecasting
class B(object): 
    pass

b = B()
b.aFunction = new.instancemethod(A.aFunction, b, B.__class__)
b.aFunction()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM