向 Python 类方法添加 __getitem__ 访问器

问题描述 投票:0回答:3

我正在尝试将项目 getter 注释添加到类方法中,以便我可以使用一些独特的注释来为普通括号之外的函数提供类型,如下所示。 (第一个片段的)最后一行的语法确实是整个努力的目标。

class MyClass:

    @typedmethod
    def compute_typed_value(self, value, *args, **kwargs):
        print(self, args, kwargs)
        print(TypedMethod.requested_type(kwargs)(value))
        self.my_other_method()

    def my_other_method(self):
        print('Doing some other things!')
        return 3


a = MyClass()
a.compute_typed_value[int]('12345')

我正在尝试使用一个装饰器来实现这种行为,我可以将其添加到我想要启用此行为的任何函数上。这是我目前的完整实施:


from functools import partial

class TypedMethod:

    _REQUESTED_TYPE_ATTR = '__requested_type'

    def __init__(self, method):
        self._method = method
        print(method)
        self.__call__ = method.__call__

    def __getitem__(self, specified_type, *args, **kwargs):
        print(f'getting typed value: {specified_type}')
        if not isinstance(specified_type, type):
            raise TypeError("Only Type Accessors are supported - must be an instance of `type`")
            
        return partial(self.__call__, **{self.__class__._REQUESTED_TYPE_ATTR: specified_type})
    
    def __call__(self, *args, **kwargs):
        print(args, kwargs)
        return self._method(self, *args, **kwargs)
    
    @classmethod
    def requested_type(cls, foo_kwargs):
        return foo_kwargs[cls._REQUESTED_TYPE_ATTR] if cls._REQUESTED_TYPE_ATTR in foo_kwargs else None

def typedmethod(foo):
    print(f'wrapping {foo.__name__} with a Typed Method: {foo}')
    _typed_method = TypedMethod(foo)
    def wrapper(self, *args, **kwargs):
        print('WRAPPER', self, args, kwargs)
        return _typed_method(self, *args, **kwargs)
    _typed_method.__call__ = wrapper
    return _typed_method

class MyClass:

    @typedmethod
    def compute_typed_value(self, value, *args, **kwargs):
        print(self, args, kwargs)
        result = TypedMethod.requested_type(kwargs)(value)
        print(result)
        self.my_other_method()
        return result

    def my_other_method(self):
        print('Doing some other things!')
        return 3


a = MyClass()
a.compute_typed_value[int]('12345')

如果您运行此代码,它将失败,说明“TypedMethod”对象没有属性“my_other_method”。进一步检查显示

compute_typed_value
的第一行没有打印出人们直觉上对代码的期望:

<__main__.TypedMethod object at 0x10754e790> () {'__requested_type': <class 'int'>}

具体来说,打印的第一个项目是一个

TypedMethod
而不是
MyClass
实例

基本上,这个想法是使用

__getitem__
标注生成一个
functools.partial
,以便随后对结果函数的调用包含已知“魔法”
__getitem__
值中的
kwargs
键,这应该假设有效,除了现在
self
可用的
MyClass.compute_typed_value
引用实际上是对包装器生成的
TypedMethod
实例的引用,而不是预期的
MyClass
实例。我已经尝试了很多事情来让
MyClass
实例作为
self
传递,但是由于它是作为装饰器实现的,所以该实例在装饰时不可用,这意味着它需要以某种方式绑定我认为函数执行时的方法。


我知道我可以像第一个位置参数一样传递这个值,但我 want 它与方括号注释一起使用,因为我认为它会很酷并且更具可读性。这主要是为了了解更多 Python 的内部工作原理而进行的学习练习,因此答案最终可能是“否”。

python python-decorators python-typing
3个回答
1
投票

我们可以使用

getattribute
得到
some_other_method
然后调用它?不确定你要做什么,但这有效:

class MyClass:

    @typedmethod
    def compute_typed_value(self, value, *args, **kwargs):
        print(self, args, kwargs)
        result = TypedMethod.requested_type(kwargs)(value)
        print(result)
        MyClass().__getattribute__(MyClass.my_other_method.__name__)()
        return result

    def my_other_method(self):
        print('Doing some other things!')
        return 3

a = MyClass()
a.compute_typed_value[int]('12345')

输出:

wrapping compute_typed_value with a Typed Method: <function MyClass.compute_typed_value at 0x7f192b55d670>
<function MyClass.compute_typed_value at 0x7f192b55d670>
getting typed value: <class 'int'>
WRAPPER 12345 () {'__requested_type': <class 'int'>}
('12345',) {'__requested_type': <class 'int'>}
<__main__.TypedMethod object at 0x7f19292ce9d0> () {'__requested_type': <class 'int'>}
12345
Doing some other things!
12345

...因为你的例子有

return 3
(也许你想用
some_other_method
的返回值做一些事情)我们可以将它分配给
x
(......然后用
x
做一些事情):

@typedmethod
def compute_typed_value(self, value, *args, **kwargs):
    print(self, args, kwargs)
    result = TypedMethod.requested_type(kwargs)(value)
    print(result)
    x = MyClass().__getattribute__(MyClass.my_other_method.__name__)
    x()
    return result

1
投票

您的代码使用

__call__
做一些奇怪的事情,但效果不佳。解决这些问题可能会使
self
参考您在
compute_typed_value
中的期望。

主要问题:

  • 将新函数分配给实例的
    __call__
    属性并不能改变对象在实际调用时的行为。您尝试了两次,但是调用了
    TypedMethod
    对象的硬编码
    __call__
    方法,而不是您尝试的任何其他方法(您首先设置
    _method.__call__
    并单独调用
    wrapper
    ,两者都不会对我来说很有意义)。
  • 您的
    typed_method
    装饰器返回它创建的
    TypedMethod
    对象,而不是包装函数。因为
    TypedMethod
    不是描述符,所以没有
    MyClass.compute_typed_value
    的绑定逻辑,所以没有很好的方法让
    MyClass
    的实例在任何地方传递。通常这是可行的,因为函数是描述符,返回绑定方法对象。但是,在这里进行这项工作会有点复杂,因为您希望
    __getattr__
    在绑定对象上工作。

所以,我认为你应该改变一下,使用两个不同的类。

第一个是描述符类,当查找时,它具有绑定行为,因此您可以获得

self
值以传递给方法。绑定时,它返回第二个类的实例。

第二类按类型处理索引。它有一个

__getitem__
方法,它返回一个
partial
,它传递第一个类捕获的
self
值,以及它被索引的类型(作为秘密关键字参数)。

这是它的样子:

class typedmethod:
    def __init__(self, method):
        self.method = method

    def __get__(self, instance, owner=None):
        if instance is None: return self # class lookup
        return TypeIndexer(instance, self.method)

class TypeIndexer:
    def __init__(self, instance, method):
        self.instance = instance
        self.method = method

    def __getitem__(self, type):
        return partial(method, self.instance, _secret_kwarg=type)

我省略了将名称

_secret_kwarg
隐藏在某处的类变量中的逻辑,并拥有一个公共 API 以将其从
kwargs
字典中取出。如果您只是将类型作为公共参数传递给方法,实际上会容易得多。也许让它成为
self
之后的第一个位置争论,或者一个有意义的名字的 kwarg?用户实际上并不直接提供它的事实不会比现在的
TypedMethod.requested_type(kwargs)(value)
更令人困惑。

当然,如果我们按照这个逻辑得出结论,您可以将整个

obj.method[type](args)
模式重写为
obj.method(type, args)
,这样会容易得多。


1
投票

将你的概念应用到课堂上有一个巨大的障碍。这就是装饰器所做的:

    def typewrap(func):
        def wrapper(self, *args, **kwargs):
            #...do type checking

            #call decorated method
            func(self, *args, **kwargs)
            
        #there is no `self` of `func` to store
        #and scope is changing
        #`self` of `func` is abandoned here
        return TypedMethod(wrapper)

您需要引用

self
func
,您可以在包装器运行之前将其传递给
TypedMethod
,但它尚不存在。它也不会存在,因为
wrapper
上的范围已更改为
TypedMethod
实例。


如果你摆脱类作用域,你的想法将变得非常容易实现。一堆你原来的逻辑就不用了(主要是

__call__
)。这是一个例子:

from functools import partial

class TypedMethod:
    def __init__(self, callback):
        self._cb = callback

    def __getitem__(self, key):
        if not isinstance(key, type):
            raise TypeError("key must be an instance of `type`")
            
        return partial(self._cb, key)
       
       
def typedmethod(func):
    def wrapper(argtype:type, *args, **kwargs):
        #test all arguments
        for arg in args:
            if not isinstance(arg, argtype):
                raise TypeError(f"all arguments must be of type: {argtype.__name__}")
        for _,v in kwargs.items():
            if not isinstance(v, argtype):
                raise TypeError(f"all arguments must be of type: {argtype.__name__}")
                
        #call wrapped method
        func(*args, **kwargs)
        
    #assign wrapper as the callback
    return TypedMethod(wrapper)
def dofunc(a):
    print(a)
    
@typedmethod
def dotype(x,y,z,a):
    print(x,y,z)
    dofunc(a)

dotype[int](22, 35, 2, a=12345)
#22, 35, 2
#12345
© www.soinside.com 2019 - 2024. All rights reserved.