英文:
adding __getitem__ accessor to Python class method
问题
I'll provide the translated code and discussion without addressing the specific issue in your Python code:
我正在尝试向类方法添加一个项目获取器(`__getitem__`,以提供`[]`语法),以便我可以使用一些独特的语法来为函数提供类型,而不是使用正常的括号,就像以下示例中的最后一行语法(在这个第一部分的代码段中)的目标实际上是整个努力的目标。
此外,我希望保留已定义的函数可以像函数一样调用的直观行为,可能还可以使用类型的默认值,例如:
在更广泛的背景下,这将作为API适配器的一部分实现,该适配器实现了通用请求处理程序,并且我希望数据以特定格式从API适配器中提取出来。因此,实际使用中的方式可能如下所示:
我正在尝试使用装饰器来实现这种行为,以便我可以将其应用于任何要启用此行为的函数。以下是我的当前完整实现:
如果您运行此代码,它将失败,指出'TypedMethod'对象没有属性'my_other_method'。进一步检查揭示了'compute_typed_value'的第一行没有以代码所期望的方式打印:
> '<__main__.TypedMethod object at 0x10754e790> () {'__requested_type__': <class 'int'>}'
具体而言,第一个打印出的项目是'TypedMethod',而不是'MyClass'实例。
基本上,想法是使用'__getitem__'调用生成'functools.partial',以便结果函数的后续调用包含已知的"magic" 'kwargs'值中的'__getitem__'键,这应该可以工作,但现在'compute_typed_value'中的可用于'self'引用实际上是由包装器生成的'TypedMethod'实例,而不是预期的'MyClass'实例。我尝试过很多方法来将'MyClass'实例作为'self'传递,但由于它是作为装饰器实现的,所以在装饰时实例不可用,这意味着在函数执行时需要以绑定方法的方式存在,我认为。
---
我知道我可以将此值作为第一个位置参数传递,但我*希望*它能够使用方括号注释,因为我认为这样更酷而且更可读。这主要是为了理解Python内部工作原理而进行的学习练习,因此答案最终可能是"不行"。
Please note that the translation above may not retain the original formatting and structure of your code, but it conveys the general idea and discussion. If you have any further questions or need assistance with the Python code, feel free to ask.
英文:
I'm attempting to add an item getter (__getitem__
, to provide the []
syntax) to a class method so that I can use some unique-ish syntax to provide types to functions outside the normal parentheses, like the following. The syntax on the last line (of this first snippet) is really the goal for this whole endeavor.
class MyClass:
@typedmethod
def compute_typed_value(self, value, *args, **kwargs):
print(self, args, kwargs)
result = TypedMethod.requested_type(kwargs)(value)
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') # returns int value 12345
Additionally, I'd like to retain the intuitive behavior that a defined function can be called like a function, potentially with a default value for the type, like so:
a = MyClass()
a.compute_typed_value('12345')
# should return whatever the default type is, with the value of '12345',
# or allow some other default behavior
In a broader context, this would be implemented as a piece of an API adapter that implements a generic request processor, and I'd like the data to come out of the API adapter in a specific format. So the way that this might look in actual use could be something like the following:
@dataclass
class MyAPIData:
property_a: int = 0
property_b: int = 0
class MyAPIAdapter:
_session
def __init__(self, token):
self._init_session(token)
@typedmethod
def request_json(self, url, **kwargs):
datatype = TypedMethod.requested_type(kwargs)
response_data = self._session.get(url).json()
if datatype:
response_data = datatype(**response_data)
return response_data
def fetch_myapidata(self, search):
return self.request_json[MyAPIData](f"/myapi?q={search}")
I'm attempting to achieve this kind of behavior with a decorator that I can throw onto any function that I want to enable this behavior. Here is my current full implementation:
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')
If you run this code, it will fail stating that 'TypedMethod' object has no attribute 'my_other_method'. Further inspection reveals that the first line of compute_typed_value
is not printing what one would intuitively expect from the code:
> <__main__.TypedMethod object at 0x10754e790> () {'__requested_type': <class 'int'>}
Specifically, the first item printed, which is a TypedMethod
instead of a MyClass
instance
Basically, the idea is use the __getitem__
callout to generate a functools.partial
so that the subsequent call to the resulting function contains the __getitem__
key in a known "magic" kwargs
value, which should hypothetically work, except that now the self
reference that is available to MyClass.compute_typed_value
is actually a reference to the TypedMethod
instance generated by the wrapper instead of the expected MyClass
instance. I've attempted a number of things to get the MyClass
instance passed as self
, but since it's implemented as a decorator, the instance isn't available at the time of decoration, meaning that somehow it needs to be a bound method at the time of function execution, I think.
I know I could just pass this value in as like the first positional argument, but I want it to work with the square bracket annotation because I think it'd be cool and more readable. This is mostly a learning exercise to understand more of Python's inner workings, so the answer could ultimately be "no".
答案1
得分: 3
你的代码在 __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 # 类查找
return TypeIndexer(instance, self.method)
class TypeIndexer:
def __init__(self, instance, method):
self.instance = instance
self.method = method
def __getitem__(self, type):
return partial(self.method, self.instance, _secret_kwarg=type)
我省略了将名称 _secret_kwarg
隐藏在某个类变量中的逻辑,以及创建一个用于从 kwargs
字典中获取它的公共 API 的逻辑。如果你将类型作为公共参数之一传递给方法,可能会更容易一些。或者将其作为第一个位置参数传递给方法,或者使用具有有意义名称的关键字参数?用户实际上不需要直接提供它,可能不会比 TypedMethod.requested_type(kwargs)(value)
现在更令人困惑。
当然,如果我们将这个逻辑继续推演下去,你可以重写整个 obj.method[type](args)
模式为 obj.method(type, args)
,这将会更加简单。
英文:
Your code is doing some odd stuff with __call__
that doesn't quite work. Fixing those issues will likely make self
refer to what you expect in compute_typed_value
.
The main problems:
- Assigning a new function to the
__call__
attribute of an instance doesn't work to change the object's behavior when it's actually called. You attempt this twice, but theTypedMethod
object's hard-coded__call__
method is getting called instead of any of the other things you try (you first set_method.__call__
and separatelywrapper
to be called, neither of which make much sense to me). - Your
typed_method
decorator returns theTypedMethod
object it creates, rather than the wrapper function. BecauseTypedMethod
is not a descriptor, there's no binding logic forMyClass.compute_typed_value
, so there's no good way for the instance ofMyClass
to get passed in anywhere. Normally this works because functions are descriptors, returning bound method objects. However, it's going to be a bit complicated to make that work here, since you want a__getattr__
to work on the bound object.
So, I think you should change things up to use two different classes.
The first is a descriptor class, that when looked up, has binding behavior so that you can get the self
value to pass in to the method. When bound, it returns an instance of the second class.
The second class handles the indexing by type. It has a __getitem__
method, which returns a partial
that passes both the self
value that the first class captured, and the type that it has been indexed with (as a secret keyword argument).
Here's what that looks like:
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)
I've left out the logic to hide the name _secret_kwarg
in a class variable somewhere, and to have a public API for getting it out of a kwargs
dict. It would actually be a whole lot easier if you just passed the type in to the method as a public argument. Maybe make it the first positional argument after self
, or a kwarg with a meaningful name? The fact that the user doesn't actually supply it directly wouldn't be much more confusing than TypedMethod.requested_type(kwargs)(value)
is now.
Of course, if we follow that logic to its conclusion, you could rewrite the whole obj.method[type](args)
pattern to be obj.method(type, args)
and it would be a whole lot easier.
答案2
得分: 2
以下是您提供的代码的翻译:
我们可以创建一个SomeInstance
类,其中包含一个getter
方法,通过调用描述符并将其设置为类属性来返回MyClass
的实例,当初始化MyClass
时。在这里,MyClass._instance
是 MyClass
的实例(它是MyClass
的self
),我们可以将其传递给装饰的computed_typed_value
方法内的实例方法my_other_method
:
class SomeInstance:
def __get__(self, instance, owner):
if instance is None:
print('instance is None')
return self
print(f'通过{self.__class__.__name__}获取{owner.__name__}的实例')
return instance
class MyClass:
_instance = SomeInstance()
def __init__(self):
MyClass._instance = self._instance
print(f'MyClass._instance的类型是{type(MyClass._instance)}')
def my_other_method(self):
print('做一些其他事情!')
return 3
@typedmethod
def compute_typed_value(self, value, *args, **kwargs):
print(self, args, kwargs)
result = TypedMethod.requested_type(kwargs)(value)
print(result)
x = MyClass._instance.my_other_method()
print(x)
return result
a = MyClass()
a.compute_typed_value[int]('12345')
输出:
使用Typed Method包装compute_typed_value:<function MyClass.compute_typed_value at 0x7f5dce1ed790>
<function MyClass.compute_typed_value at 0x7f5dce1ed790>
通过SomeInstance获取MyClass的实例
MyClass._instance的类型是<class '__main__.MyClass'>
获取类型化值:<class 'int'>
WRAPPER 12345 () {'__requested_type': <class 'int'>}
('12345',) {'__requested_type': <class 'int'>}
<__main__.TypedMethod object at 0x7f5deed5a7c0> () {'__requested_type': <class 'int'>}
12345
做一些其他事情!
3
12345
请注意,代码中的中文翻译部分以粗体显示。
英文:
We can make SomeInstance
class with a getter
that return
s MyClass
's instance by invoking the descriptor and setting it as a class attribute when init
'ing MyClass
. Here MyClass._instance
is the instance of MyClass
(it's MyClass
's self
), which we can pass to the instance method my_other_method
within the decorated computed_typed_value
method:
class SomeInstance:
def __get__(self, instance, owner):
if instance is None:
print('instance is None')
return self
print(f'getting instance of {owner.__name__} via {self.__class__.__name__}')
return instance
class MyClass:
_instance = SomeInstance()
def __init__(self):
MyClass._instance = self._instance
print(f'MyClass._instance type is {type(MyClass._instance)}')
def my_other_method(self):
print('Doing some other things!')
return 3
@typedmethod
def compute_typed_value(self, value, *args, **kwargs):
print(self, args, kwargs)
result = TypedMethod.requested_type(kwargs)(value)
print(result)
x = MyClass._instance.my_other_method()
print(x)
return result
a = MyClass()
a.compute_typed_value[int]('12345')
Outputs:
wrapping compute_typed_value with a Typed Method: <function MyClass.compute_typed_value at 0x7f5dce1ed790>
<function MyClass.compute_typed_value at 0x7f5dce1ed790>
getting instance of MyClass via SomeInstance
MyClass._instance type is <class '__main__.MyClass'>
getting typed value: <class 'int'>
WRAPPER 12345 () {'__requested_type': <class 'int'>}
('12345',) {'__requested_type': <class 'int'>}
<__main__.TypedMethod object at 0x7f5deed5a7c0> () {'__requested_type': <class 'int'>}
12345
Doing some other things!
3
12345
答案3
得分: 2
将您提供的代码中的注释部分翻译如下:
应用您的概念到一个类上存在一个巨大的障碍。这就是装饰器的作用:
def typewrap(func):
def wrapper(self, *args, **kwargs):
#...进行类型检查
#调用装饰的方法
func(self, *args, **kwargs)
#这里没有存储`self`或`func`
#并且作用域正在改变
#这里抛弃了`func`的`self`
return TypedMethod(wrapper)
您需要一个对func
的self
的引用,可以在运行wrapper
之前传递给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必须是`type`的实例")
return partial(self._cb, key)
def typedmethod(func):
def wrapper(argtype: type, *args, **kwargs):
#测试所有参数
for arg in args:
if not isinstance(arg, argtype):
raise TypeError(f"所有参数必须是类型: {argtype.__name__}")
for _, v in kwargs.items():
if not isinstance(v, argtype):
raise TypeError(f"所有参数必须是类型: {argtype.__name__}")
#调用包装的方法
func(*args, **kwargs)
#将wrapper分配为回调
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
英文:
Applying your concept to a class has a giant roadblock. This is what the decorator does:
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)
You need a reference to self
of func
that you can pass to TypedMethod
, before wrapper is run, but it doesn't exist yet. It also won't exist, because wrapper
will be called from the TypedMethod
instance, instead of your class instance.
If you get rid of the class scope, your idea becomes very simple to implement. A bunch of your original logic is not necessary (mainly __call__
). Here is an example:
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
答案4
得分: 0
这是您提供的一段Python代码和说明。我将为您翻译代码的关键部分:
我从未想过会这样使用Python类,但如果模式匹配...
我在[此Github Gist](https://gist.github.com/AndroxxTraxxon/e9b0ca46108fc17cdc1996d34a724d38)中提供了一个清理后的示例,显示相同的实现和用法。
问题是,由于装饰器重新分配函数本身,类实例引用(`self`)被覆盖。我的解决方案基本上是添加另一个类级装饰器,手动将该引用放回。这与我最初提出的问题有些不同,因为更改了命名,但本质仍然相同。我们定义一个类,它本身用作装饰器。Python之所以允许这样做,是因为当调用类类型时,它们本身也是函数调用,用于创建和初始化类的新实例。这也允许我们提供更多的魔术方法,以使其他更方便的事项更容易。以下是代码示例。
希望这对您有所帮助。如果您需要更多翻译,请告诉我。
英文:
I never thought I would use a Python class this way, but if the pattern fits...
I put a cleaned-up example of the below answer in this github gist that shows the same implementation and usage.
The issue I was having was that the class instance reference (self
) was being overridden due to the reassignment of the function itself via a decorator. My solution is basically to add another class-level decorator to manually put that reference back. This looks a little different than my originally asked question due to a change in naming, but the essence is still the same. We define a class that is itself used as the decorator. Python just allows this because class types, when called, are function calls themselves, to create and init a new instance of the class. This also allows us to provide a few more magic methods to make other quality of life items easier. Here's what that looks like:
from functools import partial, wraps
from types import MethodType
from functools import partial, wraps
from types import MethodType, FunctionType
from typing import Any, Callable
class subscriptable:
_SUBSCRIPT_KEY = '___SUBSCRIPT_KEY'
_HAS_SUBSCRIPTABLE_METHODS = '___HAS_SUBSCRIPTABLE_METHODS'
_callout: Callable
def __init__(self, callout: Callable, instance: Any = None):
if instance is not None and isinstance(callout, FunctionType):
self._callout = MethodType(callout, instance)
else:
self._callout = callout
def bind(self, instance):
return self.__class__(self._callout, instance=instance)
def __getitem__(self, specified_type):
return partial(self.__call__, **{self.__class__._SUBSCRIPT_KEY: specified_type})
def __call__(self, *args, **kwargs):
"""A transparent passthrough to the wrapped method"""
return self._callout(*args, **kwargs)
def __str__(self):
return f"<{self.__class__.__name__} {self._callout}>"
@classmethod
def has_key(cls, foo_kwargs):
"""A utility method to determine whether the provided kwargs has the expected subscript key"""
return cls._SUBSCRIPT_KEY in foo_kwargs
@classmethod
def key(cls, foo_kwargs:dict):
"""A utility method that allows the subscript key to be consumed by the wrapped method, without needing to know the inner workings"""
return foo_kwargs.pop(cls._SUBSCRIPT_KEY, None)
@classmethod
def container(cls, clazz):
"""A decorator for classes containing `subscriptable` methods"""
if not hasattr(clazz, cls._HAS_SUBSCRIPTABLE_METHODS):
orig_init = clazz.__init__
@wraps(clazz.__init__)
def __init__(self, *args, **kwargs):
for attr_name in dir(self):
attr_value = getattr(self, attr_name)
if isinstance(attr_value, cls):
setattr(self, attr_name, attr_value.bind(self))
orig_init(self, *args, **kwargs)
clazz.__init__ = __init__
setattr(clazz, cls._HAS_SUBSCRIPTABLE_METHODS, True)
return clazz
As a bonus feature, As some previously written answers allude to, class functions are not the only place that something like this might be useful, so this also allows for standalone functions to exhibit the same behavior. See this example, and its output:
@subscriptable
def other_typed_value(value, **kwargs):
subscript = subscriptable.key(kwargs)
print(subscript, value)
value = other_typed_value[int]('12345')
value = other_typed_value('12345')
print("Standard function str:", other_typed_value)
Produces the output:
<class 'int'> 12345
None 12345
Standard function str: <subscriptable <function other_typed_value at 0x000001DC809384C0>>
And finally, the original point of the question, whether we can apply this pattern to class methods. The answer is yes, but with the assistance of yet another decorator. This is where subscriptable.container
steps in. Since we can't access the instance at the time of class definition, I used an additional decorator to provide a pre-init hook that initializes all the functions so they are usable as expected (as properly bound class methods, even!), available even in the __init__
function. This kind of processing is probably pretty slow, but for my use case, it's mostly for singletons anyway.
@subscriptable.container
class MyClass:
@subscriptable
def compute_typed_value(self, value, *args, **kwargs):
print(self, args, kwargs)
if subscriptable.has_key(kwargs):
value = subscriptable.key(kwargs)(value)
self.my_other_method()
return value
def my_other_method(self):
print('Doing some other things!')
return 3
a = MyClass()
value = a.compute_typed_value[int]('12345')
print(value, type(value))
value = a.compute_typed_value('12345')
print(value, type(value))
print("Class Method str:", a.compute_typed_value)
Anyhoo, the above code yields the following output, which you'll notice has all the correct references in the places they were missing before. great success!
Doing some other things!
12345 <class 'int'>
<__main__.MyClass object at 0x000001DC808EB1C0> () {}
Doing some other things!
12345 <class 'str'>
Class Method str: <subscriptable <bound method MyClass.compute_typed_value of <__main__.MyClass object at 0x000001DC808EB1C0>>>
I was hoping to do this without a second decorator, but a single class decorator to enable the desired behavior when I'm already using one is a price I'm willing to pay.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论