Can we make `__getattribute__` a descriptior (in any usable/meaningful way)?

101 views Asked by At

How can we make code like the following emulate the __getattribute__ method inherited from object? I really want to modify the behavior of __getattribute__ but I'd like to begin by just getting the natural behavior.

class Descriptor:
    def __get__(*args):
        pass
   def __set__(*args)
       pass

class K:
    __getattribute__ = Descriptor()
1

There are 1 answers

3
Toothpick Anemone On

Yes, we can make __getattribute__ a descriptor! First of all, if a there is an integer member variable named x, then a descriptor __get__ method written for x should return an integer. Likewise, since __getattribute__ is a function, the __get__ for __getattribute__ will return a function. Initially, let's have __get__ return a silly function named foo, just so that we can run the program see what's going on:

class Descriptor:
    def __get__(*args):
        print("ENTERING __get__")
        print("Type(arg) for args passed into __get__:  ", end="")
        print(", ".join(map(lambda x: type(x).__name__, args)))
        def foo(*args):
            print("ENTERING `foo`")
            print("args passed into foo:  ", end="")
            print(", ".join(repr(arg) for arg in args))
            print("LEAVING `foo`")
            return "I AM THE RETURN VALUE OF FOO"
        return foo

class K:
    __getattribute__ = Descriptor()

Now, we try the following:

instance = K()
x = instance.x

Below are the statements seen on the the console (stdout):

ENTERING __get__
Type(arg) for args passed into __get__:  Descriptor, K, type
ENTERING `foo`
args passed into foo:  'x'
LEAVING `foo`

Note that foo doesn't receive the instance object through the usual self parameter. Instead, foo receives ONLY the attribute name 'x'

Normally, the following two pieces of code produce the same end results:

x = instance.x
x = K.__getattribute__(instance, 'x')

Let's try running K.__getattribute__(instance, 'x'):

ENTERING __get__
Type(arg) for args passed into __get__:  Descriptor, NoneType, type
ENTERING `foo`
args passed into foo:  <__main__.K object at 0x01AFE0B8>, 'x'
LEAVING `foo`

The input to __get__ and the input to foo are actually different from before.

+---------------------+-----------------------------+----------------------------+
| instance.x          | __get__(                    |  foo(<<string `x`>>)       |
|                     |   <<descriptor instance>>,  |                            |
|                     |   <<instance of K class>>,  |                            |
|                     |   <<K class itself>>        |                            |
|                     | )                           |                            |
+---------------------+-----------------------------+----------------------------+
| K.__getattribute__( | __get__(                    | foo(                       |
|    instance, 'x'    |    <<descriptor instance>>, |    <<instance of K class>>,|
| )                   |    <<None>>,                |    <<string `x`>>          |
|                     |    <<K class itself>>,      |                            |
|                     |                             |                            |
+---------------------+-----------------------------+----------------------------+

As such, we want __get__ to look like the following:

def __get__(descriptor, Kinstance, Kclass):
    if Kinstance:
        """
        return a function, `f`, which accepts as its only input
        the string name of an attribute.

           leftChild = f('leftChild')

        `f` is supposed to return that attribute 
        """
    else:  # Kinstance == None
        """
        return a function, `f`, which accepts as input two items:
        1) instance of some class
        2)the string name of an attribute.

           inst = Klass()
           leftChild = f(inst, 'leftChild')

        `f` is supposed to return that attribute 
        """

If you want the default behavior of __getattribute__, the following almost works:

class Descriptor:
    def __get__(descriptor, self, cls):
        if self:
            lamby = lambda attrname:\
                object.__getattribute__(self, attrname)
            return lamby
        else: # self == None
            return object.__getattribute__

class K:
    __getattribute__ = Descriptor()

The Problem of Global Variables

Having self be a global variable inside of the lambda function named lamby is extremely dangerous. When the lambda function eventually gets called, self won't be the same self that existed when the lambda function was defined. Consider the following example:

color = "white"

get_fleece_color = lambda shoop:\
    shoop + ", whose fleece was as " + color + " as snow."

color is a member variable inside of the lambda function get_fleece_color. When get_fleece_color was defined color was "white", but that might change:

print(get_fleece_color("Igor"))

# [... many lines of code later...]

color = "pink polka-dotted"
print(get_fleece_color("Igor's cousin, 3 times removed"))

The output is:

Igor, whose fleece was white as snow.
Igor's cousin, 3 times removed Igor, whose fleece was as pink polka-dotted as snow.

Functions make with the def keyword instead of lambda are equally dangerous.

lamby = lambda attrname:\
    object.__getattribute__(self, attrname)

def lamby(attrname):
    object.__getattribute__(self, attrname)

We want to use self as it existed at the same time lamby was defined, and not use teh self values which exists when lamby is called. There are several ways to do this, 3 of which are shown below:

Avoid Global Variable Solution 1

lamby = lambda attrname, *, self=self:\
    object.__getattribute__(self, attrname)

Avoid Global Variable Solution 2

class Descriptor:
def __get__(descriptor, self, cls):
    lamby = object.__getattribute__
    if self: # self != None
        Method = type(descriptor.__get__)
        lamby = Method(object.__getattribute__, self)
    return lamby

Avoid Global Variable Solution 3

class SamMethod:
    def __init__(self, func, arg):
        self.func = func
        self.arg = arg

    def __call__(self, *args, **kwargs):
        return self.func(self.arg, *args, **kwargs)

class Descriptor:
    def __get__(descriptor, self, cls):
        lamby = object.__getattribute__
        if self: # self != None
            lamby = SamMethod(object.__getattribute__, self)
        return lamby

class K:
    __getattribute__ = Descriptor()
    def foo(self):
        pass

Final remarks on the problem of global variables

Solution 2 has a problem in that the signature of the method class constructor might change in a future version of python. Solution 3 is my favorite, but feel free to pick your own.

Inheritance

Inheritance works just fine:

class SamMethod:
def __init__(self, func, arg):
    self.func = func
    self.arg = arg

def __call__(self, *args, **kwargs):
    return self.func(self.arg, *args, **kwargs)

#############################################################################

class Descriptor:
    def __get__(descriptor, self, cls):
        print(
            "MESSAGE"
        )
        lamby = object.__getattribute__
        if self:  # self != None
            lamby = SamMethod(object.__getattribute__, self)
        return lamby

###############################################################################

class ParentOfKlaus:
    __getattribute__  = Descriptor()

class Klaus(ParentOfKlaus):
    def foo(self):
        print("Klaus.foo")

class ChildOfKlaus(Klaus):
    def __init__(self):
        self.x = 99

    def foo(self):
        print("ChildOfKlaus.foo")

instance = ChildOfKlaus()
y = instance.x
print(y) # 99
instance.foo()

The console output is:

MESSAGE
99
MESSAGE
ChildOfKlaus.foo

Why make __getattribute__ a descriptor instead of overriding __getattribute__?

One advantage is that we have access to the class parameter passed into descriptor __get__ methods. Note that __get__ is called as __get__(descriptor, Kinstance, Kclass) We don't normally have direct access to Kclass, but if __getattribute__ is written as a descriptor, then it can access Kclass