添加 __getitem__ 访问器到 Python 类方法

huangapple go评论95阅读模式
英文:

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(&#39;Doing some other things!&#39;)
        return 3


a = MyClass()
a.compute_typed_value[int](&#39;12345&#39;) # 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(&#39;12345&#39;) 
# should return whatever the default type is, with the value of &#39;12345&#39;, 
# 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&quot;/myapi?q={search}&quot;)

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 = &#39;__requested_type&#39;

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

    def __getitem__(self, specified_type, *args, **kwargs):
        print(f&#39;getting typed value: {specified_type}&#39;)
        if not isinstance(specified_type, type):
            raise TypeError(&quot;Only Type Accessors are supported - must be an instance of `type`&quot;)
            
        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&#39;wrapping {foo.__name__} with a Typed Method: {foo}&#39;)
    _typed_method = TypedMethod(foo)
    def wrapper(self, *args, **kwargs):
        print(&#39;WRAPPER&#39;, 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(&#39;Doing some other things!&#39;)
        return 3


a = MyClass()
a.compute_typed_value[int](&#39;12345&#39;)

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:

> &lt;__main__.TypedMethod object at 0x10754e790&gt; () {&#39;__requested_type&#39;: &lt;class &#39;int&#39;&gt;}

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__ 部分存在一些奇怪的问题,无法正常运行。修复这些问题可能会使 selfcompute_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 the TypedMethod object's hard-coded __call__ method is getting called instead of any of the other things you try (you first set _method.__call__ and separately wrapper to be called, neither of which make much sense to me).
  • Your typed_method decorator returns the TypedMethod object it creates, rather than the wrapper function. Because TypedMethod is not a descriptor, there's no binding logic for MyClass.compute_typed_value, so there's no good way for the instance of MyClass 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的实例(它是MyClassself),我们可以将其传递给装饰的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 returns 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(&#39;instance is None&#39;)
            return self
        print(f&#39;getting instance of {owner.__name__} via {self.__class__.__name__}&#39;)
        return instance

class MyClass:
    _instance = SomeInstance()
    def __init__(self):
        MyClass._instance = self._instance
        print(f&#39;MyClass._instance type is {type(MyClass._instance)}&#39;)

    def my_other_method(self):
        print(&#39;Doing some other things!&#39;)
        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](&#39;12345&#39;)

Outputs:

wrapping compute_typed_value with a Typed Method: &lt;function MyClass.compute_typed_value at 0x7f5dce1ed790&gt;
&lt;function MyClass.compute_typed_value at 0x7f5dce1ed790&gt;
getting instance of MyClass via SomeInstance
MyClass._instance type is &lt;class &#39;__main__.MyClass&#39;&gt;
getting typed value: &lt;class &#39;int&#39;&gt;
WRAPPER 12345 () {&#39;__requested_type&#39;: &lt;class &#39;int&#39;&gt;}
(&#39;12345&#39;,) {&#39;__requested_type&#39;: &lt;class &#39;int&#39;&gt;}
&lt;__main__.TypedMethod object at 0x7f5deed5a7c0&gt; () {&#39;__requested_type&#39;: &lt;class &#39;int&#39;&gt;}
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)

您需要一个对funcself的引用,可以在运行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(&quot;key must be an instance of `type`&quot;)
            
        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&quot;all arguments must be of type: {argtype.__name__}&quot;)
        for _,v in kwargs.items():
            if not isinstance(v, argtype):
                raise TypeError(f&quot;all arguments must be of type: {argtype.__name__}&quot;)
                
        #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 = &#39;___SUBSCRIPT_KEY&#39;
    _HAS_SUBSCRIPTABLE_METHODS = &#39;___HAS_SUBSCRIPTABLE_METHODS&#39;

    _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):
        &quot;&quot;&quot;A transparent passthrough to the wrapped method&quot;&quot;&quot;
        return self._callout(*args, **kwargs)
    
    def __str__(self):
        return f&quot;&lt;{self.__class__.__name__} {self._callout}&gt;&quot;
    
    @classmethod
    def has_key(cls, foo_kwargs):
        &quot;&quot;&quot;A utility method to determine whether the provided kwargs has the expected subscript key&quot;&quot;&quot;
        return cls._SUBSCRIPT_KEY in foo_kwargs
    
    @classmethod
    def key(cls, foo_kwargs:dict):
        &quot;&quot;&quot;A utility method that allows the subscript key to be consumed by the wrapped method, without needing to know the inner workings&quot;&quot;&quot;
        return foo_kwargs.pop(cls._SUBSCRIPT_KEY, None)
    
    @classmethod
    def container(cls, clazz):
        &quot;&quot;&quot;A decorator for classes containing `subscriptable` methods&quot;&quot;&quot;
        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](&#39;12345&#39;)
value = other_typed_value(&#39;12345&#39;)
print(&quot;Standard function str:&quot;, other_typed_value)

Produces the output:

&lt;class &#39;int&#39;&gt; 12345
None 12345
Standard function str: &lt;subscriptable &lt;function other_typed_value at 0x000001DC809384C0&gt;&gt;

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(&#39;Doing some other things!&#39;)
return 3
a = MyClass()
value = a.compute_typed_value[int](&#39;12345&#39;)
print(value, type(value))
value = a.compute_typed_value(&#39;12345&#39;)
print(value, type(value))
print(&quot;Class Method str:&quot;, 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 &lt;class &#39;int&#39;&gt;
&lt;__main__.MyClass object at 0x000001DC808EB1C0&gt; () {}
Doing some other things!
12345 &lt;class &#39;str&#39;&gt;
Class Method str: &lt;subscriptable &lt;bound method MyClass.compute_typed_value of &lt;__main__.MyClass object at 0x000001DC808EB1C0&gt;&gt;&gt;

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.

huangapple
  • 本文由 发表于 2023年3月21日 02:03:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/75793794.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定