首页 > 解决方案 > 我们可以使 `__getattribute__` 成为一个描述符(以任何可用/有意义的方式)吗?

问题描述

我们如何使如下代码模拟__getattribute__继承自的方法object?我真的很想修改的行为,__getattribute__但我想从获得自然行为开始。

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

class K:
    __getattribute__ = Descriptor()

标签: pythonpython-3.xpropertydescriptor

解决方案


是的,我们可以做__getattribute__一个描述符!首先,如果存在一个名为 的整数成员变量x,那么__get__为其编写的描述符方法x应该返回一个整数。同样,因为__getattribute__is 是一个函数,所以__get__for__getattribute__将返回一个函数。最初,让我们__get__返回一个名为 的愚蠢函数foo,这样我们就可以运行程序看看发生了什么:

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()

现在,我们尝试以下方法:

instance = K()
x = instance.x

以下是在控制台 ( stdout) 上看到的语句:

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

请注意,foo不通过通常的self参数接收实例对象。相反,foo接收属性名称'x'

通常,以下两段代码产生相同的最终结果:

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

让我们尝试运行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`

input to__get__和 input tofoo实际上和以前不同了。

+---------------------+-----------------------------+----------------------------+
| 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>>,      |                            |
|                     |                             |                            |
+---------------------+-----------------------------+----------------------------+

因此,我们希望__get__如下所示:

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 
        """

如果您想要 的默认行为__getattribute__,以下几乎可以工作:

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()

全局变量问题

self命名的 lambda 函数中作为全局变量lamby是非常危险的。当 lambda 函数最终被调用时,self将与self定义 lambda 函数时存在的不同。考虑以下示例:

color = "white"

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

color是 lambda 函数内部的成员变量get_fleece_colorget_fleece_color定义的时间color"white",但这可能会改变:

print(get_fleece_color("Igor"))

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

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

输出是:

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

def使用关键字而不是make 的函数lambda同样危险。

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

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

我们希望使用在定义self的同时存在的值lamby,而不是使用被调用self时存在的值lamby。有几种方法可以做到这一点,其中 3 种如下所示:

避免全局变量解决方案 1

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

避免全局变量解决方案 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

避免全局变量解决方案 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

关于全局变量问题的最后评论

解决方案2存在一个问题,即method类构造函数的签名可能会在未来版本的 python 中发生变化。解决方案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):
        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()

控制台输出为:

MESSAGE
99
MESSAGE
ChildOfKlaus.foo

为什么要创建__getattribute__ 描述符而不是覆盖__getattribute__

一个优点是我们可以访问传递给描述符__get__方法的类参数。注意__get__被称为 as__get__(descriptor, Kinstance, Kclass)我们通常不能直接访问Kclass,但是如果__getattribute__写成描述符,那么它可以访问Kclass


推荐阅读