How to fake type with Python
Asked Answered
D

6

28

I recently developed a class named DocumentWrapper around some ORM document object in Python to transparently add some features to it without changing its interface in any way.

I just have one issue with this. Let's say I have some User object wrapped in it. Calling isinstance(some_var, User) will return False because some_var indeed is an instance of DocumentWrapper.

Is there any way to fake the type of an object in Python to have the same call return True?

Dimphia answered 23/7, 2011 at 22:4 Comment(2)
isinstance(some_var.user, User)? What are you actually trying to do?Safier
Just trying to have a transparent wrapper, which behaves exactly like the wrapped class. Including with isinstance. Multiple inheritance is not the solution, at least because User is just one of the many classes the DocumentWrapper wraps. (I don't have control over these classes, I'm not able to change their inheritance tree.)Dimphia
M
10

Testing the type of an object is usually an antipattern in python. In some cases it makes sense to test the "duck type" of the object, something like:

hasattr(some_var, "username")

But even that's undesirable, for instance there are reasons why that expression might return false, even though a wrapper uses some magic with __getattribute__ to correctly proxy the attribute.

It's usually preferred to allow variables only take a single abstract type, and possibly None. Different behaviours based on different inputs should be achieved by passing the optionally typed data in different variables. You want to do something like this:

def dosomething(some_user=None, some_otherthing=None):
    if some_user is not None:
        #do the "User" type action
    elif some_otherthing is not None:
        #etc...
    else:
         raise ValueError("not enough arguments")

Of course, this all assumes you have some level of control of the code that is doing the type checking. Suppose it isn't. for "isinstance()" to return true, the class must appear in the instance's bases, or the class must have an __instancecheck__. Since you don't control either of those things for the class, you have to resort to some shenanigans on the instance. Do something like this:

def wrap_user(instance):
    class wrapped_user(type(instance)):
        __metaclass__ = type
        def __init__(self):
            pass
        def __getattribute__(self, attr):
            self_dict = object.__getattribute__(type(self), '__dict__')
            if attr in self_dict:
                return self_dict[attr]
            return getattr(instance, attr)
        def extra_feature(self, foo):
            return instance.username + foo # or whatever
    return wrapped_user()

What we're doing is creating a new class dynamically at the time we need to wrap the instance, and actually inherit from the wrapped object's __class__. We also go to the extra trouble of overriding the __metaclass__, in case the original had some extra behaviors we don't actually want to encounter (like looking for a database table with a certain class name). A nice convenience of this style is that we never have to create any instance attributes on the wrapper class, there is no self.wrapped_object, since that value is present at class creation time.

Edit: As pointed out in comments, the above only works for some simple types, if you need to proxy more elaborate attributes on the target object, (say, methods), then see the following answer: Python - Faking Type Continued

Metamerism answered 23/7, 2011 at 23:29 Comment(7)
Thanks a lot for your valuable help =)Dimphia
And why is the (seemingly redundant) __metaclass__ = type necessary?Kurtiskurtosis
You have effectively neutered the descriptor protocol, e.g. wrap_user(obj).extra_feature() returns an unbound method. You need to, at the very least, check for __get__ methods on the objects you retrieve from self_dict. See Python - Faking Type Continued for a follow-up question someone asked.Anthocyanin
The call to wrapped_user() appears to return None, is that intended?Enamel
@oarfish: certainly not. the empty __new__ is erroneous, just deleting it improves things, but i'm not sure what i was trying to communicate with that example 7 years ago...Metamerism
Maybe you would know how to answer the following question: #60124200 @MetamerismEarthshaking
I wouldn't consider testing the type an anti-pattern.Overbuild
Q
21

Override __class__ in your wrapper class DocumentWrapper:

class DocumentWrapper(object):

  @property
  def __class__(self):
    return User

>>> isinstance(DocumentWrapper(), User)
True

This way no modifications to the wrapped class User are needed.

Python Mock does the same (see mock.py:612 in mock-2.0.0, couldn't find sources online to link to, sorry).

Quasimodo answered 22/3, 2017 at 16:52 Comment(0)
A
19

You can use the __instancecheck__ magic method to override the default isinstance behaviour:

@classmethod
def __instancecheck__(cls, instance):
    return isinstance(instance, User)

This is only if you want your object to be a transparent wrapper; that is, if you want a DocumentWrapper to behave like a User. Otherwise, just expose the wrapped class as an attribute.

This is a Python 3 addition; it came with abstract base classes. You can't do the same in Python 2.

Anemophilous answered 23/7, 2011 at 22:32 Comment(3)
this is in 2.6 docs.python.org/2/reference/…Cheesy
Big caveat, this method goes in the meta class of the wrapped class.Vestavestal
Maybe you would know how to answer the following question: #60124200 @AnemophilousEarthshaking
M
10

Testing the type of an object is usually an antipattern in python. In some cases it makes sense to test the "duck type" of the object, something like:

hasattr(some_var, "username")

But even that's undesirable, for instance there are reasons why that expression might return false, even though a wrapper uses some magic with __getattribute__ to correctly proxy the attribute.

It's usually preferred to allow variables only take a single abstract type, and possibly None. Different behaviours based on different inputs should be achieved by passing the optionally typed data in different variables. You want to do something like this:

def dosomething(some_user=None, some_otherthing=None):
    if some_user is not None:
        #do the "User" type action
    elif some_otherthing is not None:
        #etc...
    else:
         raise ValueError("not enough arguments")

Of course, this all assumes you have some level of control of the code that is doing the type checking. Suppose it isn't. for "isinstance()" to return true, the class must appear in the instance's bases, or the class must have an __instancecheck__. Since you don't control either of those things for the class, you have to resort to some shenanigans on the instance. Do something like this:

def wrap_user(instance):
    class wrapped_user(type(instance)):
        __metaclass__ = type
        def __init__(self):
            pass
        def __getattribute__(self, attr):
            self_dict = object.__getattribute__(type(self), '__dict__')
            if attr in self_dict:
                return self_dict[attr]
            return getattr(instance, attr)
        def extra_feature(self, foo):
            return instance.username + foo # or whatever
    return wrapped_user()

What we're doing is creating a new class dynamically at the time we need to wrap the instance, and actually inherit from the wrapped object's __class__. We also go to the extra trouble of overriding the __metaclass__, in case the original had some extra behaviors we don't actually want to encounter (like looking for a database table with a certain class name). A nice convenience of this style is that we never have to create any instance attributes on the wrapper class, there is no self.wrapped_object, since that value is present at class creation time.

Edit: As pointed out in comments, the above only works for some simple types, if you need to proxy more elaborate attributes on the target object, (say, methods), then see the following answer: Python - Faking Type Continued

Metamerism answered 23/7, 2011 at 23:29 Comment(7)
Thanks a lot for your valuable help =)Dimphia
And why is the (seemingly redundant) __metaclass__ = type necessary?Kurtiskurtosis
You have effectively neutered the descriptor protocol, e.g. wrap_user(obj).extra_feature() returns an unbound method. You need to, at the very least, check for __get__ methods on the objects you retrieve from self_dict. See Python - Faking Type Continued for a follow-up question someone asked.Anthocyanin
The call to wrapped_user() appears to return None, is that intended?Enamel
@oarfish: certainly not. the empty __new__ is erroneous, just deleting it improves things, but i'm not sure what i was trying to communicate with that example 7 years ago...Metamerism
Maybe you would know how to answer the following question: #60124200 @MetamerismEarthshaking
I wouldn't consider testing the type an anti-pattern.Overbuild
F
5

Here is a solution by using metaclass, but you need to modify the wrapped classes:

>>> class DocumentWrapper:
    def __init__(self, wrapped_obj):
        self.wrapped_obj = wrapped_obj

>>> class MetaWrapper(abc.ABCMeta):
    def __instancecheck__(self, instance):
        try:
            return isinstance(instance.wrapped_obj, self)
        except:
            return isinstance(instance, self)

>>> class User(metaclass=MetaWrapper):
    pass

>>> user=DocumentWrapper(User())
>>> isinstance(user,User)
True
>>> class User2:
    pass

>>> user2=DocumentWrapper(User2())
>>> isinstance(user2,User2)
False
Fer answered 23/7, 2011 at 22:50 Comment(1)
Big lightbulb, you need to modify the wrapped classes. So you can only "fake" your own types, e.g. not strings or ints.Vestavestal
B
0

It sounds like you want to test the type of the object your DocumentWrapper wraps, not the type of the DocumentWrapper itself. If that's right, then the interface to DocumentWrapper needs to expose that type. You might add a method to your DocumentWrapper class that returns the type of the wrapped object, for instance. But I don't think that making the call to isinstance ambiguous, by making it return True when it's not, is the right way to solve this.

Biotite answered 23/7, 2011 at 22:26 Comment(0)
J
0

The best way is to inherit DocumentWrapper from the User itself, or mix-in pattern and doing multiple inherintance from many classes

 class DocumentWrapper(User, object)

You can also fake isinstance() results by manipulating obj.__class__ but this is deep level magic and should not be done.

Jule answered 23/7, 2011 at 22:31 Comment(1)
Thanks. User is not the only document type wrapped, so this won't unfortunately work. But thanks, I didn't even know multiple inheritance was possible with Python =)Dimphia

© 2022 - 2024 — McMap. All rights reserved.