我正在尝试将项目 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 的内部工作原理而进行的学习练习,因此答案最终可能是“否”。
我们可以使用
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
您的代码使用
__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)
,这样会容易得多。
将你的概念应用到课堂上有一个巨大的障碍。这就是装饰器所做的:
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