Python3-Cookbook(第九章) - 元编程Part3

一、捕获类的属性定义顺序

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024-04-13 10:04
# @Author  : Maple
# @File    : 14-捕获类的属性定义顺序.py

from collections import OrderedDict


"""
你想自动记录一个类中属性和方法定义的顺序, 然后可以利用它来做很多操作(比如序列化、映射到数据库等等)。
"""

# A set of descriptors for various types
class Typed:
    _expected_type = type(None)
    def __init__(self, name=None):
        self._name = name

    def __set__(self, instance, value):
        if not isinstance(value, self._expected_type):
            raise TypeError('Expected ' + str(self._expected_type))
        instance.__dict__[self._name] = value

class Integer(Typed):
    _expected_type = int

class Float(Typed):
    _expected_type = float

class String(Typed):
    _expected_type = str

# Metaclass that uses an OrderedDict for class body
class OrderedMeta(type):
    def __new__(cls, clsname, bases, clsdict):
        d = dict(clsdict)
        order = []
        for name, value in clsdict.items():
            if isinstance(value, Typed):
                value._name = name
                order.append(name)
        # 为clsdict新增了一个类属性_order,然后里面存放的是(以Stock为例):[name,shares,price]
        d['_order'] = order
        # 创建类,
        return type.__new__(cls, clsname, bases, d)

    @classmethod
    def __prepare__(cls, clsname, bases):
        return OrderedDict()


class Structure(metaclass=OrderedMeta):
    def as_csv(self):
        # Structure类中有一个类属性:_order,并且里面存放的是[name,shares,price]
        # self._order --> 通过实例访问类属性
        return ','.join(str(getattr(self,name)) for name in self._order)

# Example use
class Stock(Structure):
    name = String()
    shares = Integer()
    price = Float()

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

二、定义有可选参数的元类

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024-04-13 10:23
# @Author  : Maple
# @File    : 15-定义有可选参数的元类.py

"""
为了使元类支持关键字参数,你必须确保在 __prepare__() , __new__() 和 __init__() 方法中都使用强制关键字参数

"""

class Mymeta(type):

    @classmethod
    def __prepare__(metacls, name, bases,*,debug=False, synchronize=True):
        return super().__prepare__(name,bases)


    def __new__(cls, name, bases,ns,*,debug=False, synchronize=True):
        return super().__new__(cls,name, bases,ns)

    def __init__(cls,name,bases,ns,*,debug=False,synchronize=True):
        super(Mymeta, cls).__init__(name,bases,ns)

class Spam(metaclass=Mymeta,debug= True,synchronize=False):

    def __init__(self,name):
        self.name = name

"""
应用举例:
假设我们在开发一个插件系统,希望能够自动注册所有插件类,而不需要在代码中显式注册它们。我们可以使用元类来捕获类的创建,并根据传入的额外参数决定是否注册这个类
PluginRegistryMeta 元类检查创建类时传入的 register 参数,如果为 True(默认),则将新创建的类注册到一个字典中
这样,我们就可以在不需要每次手动注册每个类的情况下,自动管理插件系统中的所有插件类
"""


class PluginRegistryMeta(type):
    registry = {}

    def __new__(mcs, name, bases, attrs, register=True, **kwargs):
        new_cls = super().__new__(mcs, name, bases, attrs)
        if register:
            mcs.registry[name] = new_cls
            print(f'Registered {name}')
        return new_cls

class BasePlugin(metaclass=PluginRegistryMeta):
    pass


# 这个类将被自动注册
class MyPlugin(BasePlugin):
    pass

# 这个类将不会被注册
class MyPrivatePlugin(BasePlugin, register=False):
    pass



if __name__ == '__main__':

    # Registered BasePlugin
    # Registered MyPlugin
    print(PluginRegistryMeta.registry) # {'BasePlugin': <class '__main__.BasePlugin'>, 'MyPlugin': <class '__main__.MyPlugin'>}



三、args和kwars的强制参数签名

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024-04-13 20:26
# @Author  : Maple
# @File    : 16-args和kwars的强制参数签名.py
import inspect
from inspect import Signature,Parameter

"""
你有一个函数或方法,它使用*args和**kwargs作为参数,这样使得它比较通用
但有时候你想检查传递进来的参数是不是某个你想要的类型。
"""


# 1. 定义标签对象
parms = [ Parameter('x', Parameter.POSITIONAL_OR_KEYWORD),
        Parameter('y', Parameter.POSITIONAL_OR_KEYWORD, default=42),
         Parameter('z', Parameter.KEYWORD_ONLY, default=None) ]
sig = Signature(parms)


# 2. 使用标签对象绑定参数
def fun(*args,**kwargs):
    bound_valus = sig.bind(*args,**kwargs)
    for name,value in bound_valus.arguments.items():
        print(name,value)

# 3.利用参数签名,强制函数遵循特定的规则

def make_sig(*names):
    parms = [Parameter(name,Parameter.POSITIONAL_OR_KEYWORD) for name in names]
    return Signature(parms)

# 定义基类
class Structure:
    __signature__ = make_sig()

    def __init__(self,*args,**kwargs):
        bound_values = self.__signature__.bind(*args,**kwargs)
        for name,value in bound_values.arguments.items():
            setattr(self,name,value)

class Stock(Structure):
    __signature__  =  make_sig('name','shares','price')

class Point(Structure):
    __signature__ = make_sig('x', 'y')


# 通过元类方式实现
from inspect import Signature, Parameter


def make_sig(*names):
    parms = [Parameter(name, Parameter.POSITIONAL_OR_KEYWORD)
            for name in names]
    return Signature(parms)


"""
当我们自定义签名的时候,将签名存储在特定的属性 __signature__ 中通常是很有用的
这样的话,在使用 inspect 模块执行内省的代码就能发现签名并将它作为调用约定
"""
class StructureMeta(type):
    def __new__(cls, clsname, bases, clsdict):
        # 为要创建的类添加一个__signature__属性,然后它的值从_fields属性中获取
        # 说明:clsdict是创建类时 传递的类中的属性和方法键值对,它会扫描创建类的定义体,然后获取对应的值
        #      比如本例的Stock类,定义了一个类属性_fields,其值为['name', 'shares', 'price']
        #      因此Stock.__signature__中会存放 Signature([Parameter(name,Parameter.POSITIONAL_OR_KEYWORD....])
        clsdict['__signature__'] = make_sig(*clsdict.get('_fields',[]))
        return super().__new__(cls, clsname, bases, clsdict)


class Structure(metaclass=StructureMeta):
    _fields = []
    def __init__(self, *args, **kwargs):
        # 通过实例访问类属性__signature__,获取存放在里面的值,然后绑定参数
        bound_values = self.__signature__.bind(*args, **kwargs)
        for name, value in bound_values.arguments.items():
            setattr(self, name, value)

# Example
class Stock2(Structure):
    _fields = ['name', 'shares', 'price']


class Point2(Structure):
    _fields = ['x', 'y']



if __name__ == '__main__':

    # 1.标签测试
    print(sig) #(x, y=42, *, z=None)

    # 2.标签对象绑定参数测试
    """
    x 1
    y 2
    z 5
    """
    fun(1,2,z= 5)

    # 3.强制签名测试
    s1 = Stock('ACME',100,490.1)
    try:
        s2 = Stock('ACME', 100)
    except Exception as e:
        print(e) # missing a required argument: 'price'

    # 4. 元类方式强制签名测试

    print('*******4. 元类方式强制签名测试*********************')
    s3 = Point2(1, 2)

    # 签名存储在特定的属性 __signature__中,inspect模块执行内省的代码就能发现签名
    print(inspect.signature(Stock2)) # (name, shares, price)
    print(s3) # <__main__.Point2 object at 0x0000012CC7D995B0>
    try:
        s4 = Point2('1')
    except Exception as e:
        print(e)  # missing a required argument: 'y'

四、类上强制使用编程规约

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024-04-13 21:40
# @Author  : Maple
# @File    : 17-类上强制使用编程规约.py


from inspect import signature
import logging

"""
你的程序包含一个很大的类继承体系,你希望强制执行某些编程规约(或者代码诊断)来帮助程序员保持清醒。
"""

class MatchSignaturesMeta(type):

    def __init__(self, clsname, bases, clsdict):
        super().__init__(clsname, bases, clsdict)
        # super() 的调用形式(即传递相同的参数两次)在元类中比较少见,因为它实际上创建了一个绑定到当前类的super代理对象
        # 这个对象将会从【父类】(也因此这段逻辑必须写在__init__中,而不能写在__new__中,因为new方法中,类还没有被创建)-开始查找方法或属性
        # 在这段代码中,sup 被用来访问在当前类的基类中已定义的方法

        # self是值当前要创建的类,因此super(self, self)就是当前要创建类的父类
        sup = super(self, self)
        # name和value是当前类的方法名和方法
        for name, value in clsdict.items():
            if name.startswith('_') or not callable(value):
                continue
            # 获取父类的中定义的方法,比如本例中的foo
            prev_dfn = getattr(sup,name,None)
            if prev_dfn:
                # 父类的方法签名
                prev_sig = signature(prev_dfn)
                # 当前类的方法签名
                val_sig = signature(value)
                if prev_sig != val_sig:
                    logging.warning('Signature mismatch in %s. %s != %s',
                                    value.__qualname__, prev_sig, val_sig)

# Example
class Root(metaclass=MatchSignaturesMeta):
    pass

class A(Root):
    def foo(self, x, y):
        pass

    def spam(self, x, *, z):
        pass

# Class with redefined methods, but slightly different signatures
class B(A):
    def foo(self, a, b):
        pass

    def spam(self,x,z):
        pass

if __name__ == '__main__':
    """WARNING:root:Signature mismatch in B.foo. (self, x, y) != (self, a, b)
       WARNING:root:Signature mismatch in B.spam. (self, x, *, z) != (self, x, z)
    """
    pass

五、以编程方式定义类

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024-04-13 22:01
# @Author  : Maple
# @File    : 18-以编程方式定义类.py
import abc
import operator
import sys


"""
以使用函数 types.new_class() 来初始化新的类对象
你需要做的只是提供类的名字、父类元组、关键字参数,以及一个用成员变量填充类字典的回调函数
"""

# stock.py
# Example of making a class manually from parts

# Methods
def __init__(self, name, shares, price):
    self.name = name
    self.shares = shares
    self.price = price

def cost(self):
    return self.shares * self.price

cls_dict = {
    '__init__' : __init__,
    'cost' : cost,
}

# Make a class
import types


class Base:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__() #这里不能传参
        cls.debug = kwargs.get('debug', False)
        cls.typecheck = kwargs.get('typecheck', True)



def named_tuple(classname, fieldnames):
    # 1. operator.itemgetter(n) 返回的是一个函数
    # 2. 该函数作用于一个对象(如元组、列表或任何支持索引操作的对象)并返回该对象的第 n 个元素
    # 3. 本例对类的对象的每个属性封装了一个 property,当通过对象obj.name访问属性时,实际上会调用该property
    #    如上所属,该描述器会返回对象(元组等)的第n个位置的元素

    cls_dict = { name: property(operator.itemgetter(n))
                for n, name in enumerate(fieldnames) }

    # Make a __new__ function and add to the class dict
    def __new__(cls, *args):
        # 这里实例化对象(并不是创建类,不要搞混了)
        if len(args) != len(fieldnames):
            raise TypeError('Expected {} arguments'.format(len(fieldnames)))
        # 使用父类(tuple)的__new__方法创建类
        return tuple.__new__(cls, args)

    cls_dict['__new__'] = __new__

    # Make the class
    cls = types.new_class(classname, (tuple,), {},
                        lambda ns: ns.update(cls_dict))

    # sys._getframe(1).f_globals['__name__'] 返回当前模块的名字
    cls.__module__ = sys._getframe(1).f_globals['__name__']
    return cls






if __name__ == '__main__':

    # 测试1
    # 第三个参数为{},相当于是创建了一个新的命名空间
    """
    类的命名空间是一个字典,它存储了类中定义的所有属性和方法。
    这包括类变量、类方法、静态方法、实例方法等。当类被定义时,所有在类块内部定义的函数和变量都会被加入到这个命名空间中。
    """
    # 第四个参数用来更新命名空间
    """
    本例中该函数的作用是将 cls_dict 字典中的__init__和cost方法添加到类 Stock 的命名空间中。
    # 备注:ns表示namespace
    """

    Stock = types.new_class('Stock', (), {}, lambda ns: ns.update(cls_dict))

    # __module__ 属性包含定义它的模块名
    print(Stock.__module__) # types

    #测试2: 如果想创建的类需要一个不同的元类,可以通过 types.new_class() 第三个参数传递给它
    print('*************测试2*******************')
    Stock2 = types.new_class('Stock', (), {'metaclass': abc.ABCMeta},
                             lambda ns: ns.update(cls_dict))

    print(Stock2.__dict__) # {..'_abc_impl': <_abc._abc_data object at 0x0000018428913B40>}}
    print(type(Stock2)) #<class 'abc.ABCMeta'>

    # 测试3:第三个参数还可以包含其他的关键字参数
    """
    比如要创建如下的类:
    class Spam(Base, debug=True, typecheck=False):
        pass
    """
    Spam = types.new_class('Spam', (Base,), {'debug': True, 'typecheck': False}, lambda ns: ns.update(cls_dict))
    print('Span',Spam) #  <class 'types.Spam'>

    # 测试4:自定义named_tuple
    Dog = named_tuple('Dog',['name','age'])
    print(Dog.__module__) # __main__
    dog = Dog('Lily',20)
    print(dog) # ('Lily', 20)

六、定义时初始化成员

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024-04-14 9:52
# @Author  : Maple
# @File    : 19-定义时初始化成员.py

"""
类被定义的时候就初始化一部分类的成员,而不是要等到实例被创建后
"""

import operator

class StructTupleMeta(type):
    def __init__(cls, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 在类定义的时候,就为其属性进行一些初始化操作
        # 此例是为每个属性设置一个描述器,该描述器实现`按照位置返回元组对象中的元素`功能
        # 关于property(operator.itemgetter(n))可参考18-以编程方式定义类
        for n, name in enumerate(cls._fields):
            setattr(cls, name, property(operator.itemgetter(n)))

class StructTuple(tuple, metaclass=StructTupleMeta):
    _fields = []
    def __new__(cls, *args):
        if len(args) != len(cls._fields):
            raise ValueError('{} arguments required'.format(len(cls._fields)))
        # 创建实例对象
        return super().__new__(cls,args)


class Stock(StructTuple):
    _fields = ['name', 'shares', 'price']

class Point(StructTuple):
    _fields = ['x', 'y']


if __name__ == '__main__':

    # 1. Stock测试
    stock = Stock('ACME',20,200)
    print(stock[0]) #ACME
    # 会调用描述器property(operator.itemgetter(0)),然后该描述器返回stock的第一个元素
    print(stock.name) # ACME

    try:
        stock.name = 'maple'
    except Exception as e:
        print(e) #  property of 'Stock' object has no setter

    # 2. Point测试
    p = Point(1,2)
    print(p.x)
    print(p.y)

七、利用函数注解实现方法重载

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024-04-14 13:11
# @Author  : Maple
# @File    : 20-利用函数注解实现方法重载.py

# multiple.py
import inspect
import types

class MultiMethod:
    '''
    Represents a single multimethod.
    '''
    def __init__(self, name):
        self._methods = {}
        self.__name__ = name

    def register(self, meth):
        '''
        Register a new method as a multimethod
        '''
        sig = inspect.signature(meth)

        # Build a type signature from the method's annotations
        types = []
        for name, parm in sig.parameters.items():
            if name == 'self':
                continue
            "检查比如方法bar中的参数x,后面是否带了int"
            if parm.annotation is inspect.Parameter.empty:
                raise TypeError(
                    'Argument {} must be annotated with a type'.format(name)
                )
            "检查比如方法bar中的参数x后面定义的数据类型(int,str等),是否为type的子类"
            if not isinstance(parm.annotation, type):
                raise TypeError(
                    'Argument {} annotation must be a type'.format(name)
                )
            if parm.default is not inspect.Parameter.empty:
                self._methods[tuple(types)] = meth
            types.append(parm.annotation)
        """
        比如对于Spam中的两个bar方法
        self._methods((<class 'int'>,<class 'int'>)] = <function Spam at OxOOO1....>
        self._methods((<class 'str'>,<class 'int'>)] = <function Spam at OxOOO2....>
        """
        self._methods[tuple(types)] = meth

    def __call__(self, *args):
        '''
        Call a method based on type signature of the arguments
        '''
        # 以调用s.bar(2,3)为例:(<class 'int'>, <class 'int'>)
        types = tuple(type(arg) for arg in args[1:])
        meth = self._methods.get(types, None)
        if meth:
            return meth(*args)
        else:
            raise TypeError('No matching method for types {}'.format(types))

    def __get__(self, instance, cls):
        '''
        self是MultiMethod实例,instance是Spam实例,types.MethodType的作用是将MultiMethod的实例做为一个属性绑定到后者上
        '''
        if instance is not None:
            # 1.将 MultiMethod 的实例(比如m)绑定到instance(比如Spam实例s)上,这样可以通过s.m访问到m实例
            # 2.因为s的属性bar中存放的其实是就是MultiMethod实例(register过程),所以s.bar本质上执行的就是s.m
            # 3.同时m实现了call方法,m(2,3)会触发该方法,进而根据传递进来的参数类型,执行m中对应的bar方法
            # 以调用s.bar(2,3)为例,返回一个绑定方法: <bound method bar of <__main__.Spam object at 0x000002134309EFF0>>
            return types.MethodType(self, instance)
        else:
            return self

class MultiDict(dict):
    '''
    Special dictionary to build multimethods in a metaclass
    '''
    def __setitem__(self, key, value):

        """
        1. 第一个bar注册的时候key not in self .self:{'__module__':'__main__','__qualname__':'Spam'},因此设置属性
           设置完成后self:{...,'bar':<function Spam at OxOOOO....>
        2. 第二个bar注册时候,key in self,但此时 current_value 是一个Spam.bar类型对象,因此先通过MultiMethod(key)实例化一个 MultiMethod
           对象,然后调用该对象的register方法,分别注册上一个bar方法和这一个bar方法,最后再setitem,此时self:{....,'bar':'<MultiMethod object...>'}
        3. 如果有第三个bar注册,key in self,此时 current_value 是MultiMethod对象,因此会在该对象中继续注册新的bar方法

        注意: 上述三个bar是重载的3个不同方法
        """

        if key in self:
            # If key already exists, it must be a multimethod or callable
            current_value = self[key]
            if isinstance(current_value, MultiMethod):
                current_value.register(value)
            else:
                mvalue = MultiMethod(key)
                mvalue.register(current_value)
                mvalue.register(value)
                super().__setitem__(key, mvalue)
        else:
            super().__setitem__(key, value)

class MultipleMeta(type):
    '''
    Metaclass that allows multiple dispatch of methods
    '''
    def __new__(cls, clsname, bases, clsdict):
        return type.__new__(cls, clsname, bases, dict(clsdict))

    @classmethod
    def __prepare__(cls, clsname, bases):
        return MultiDict()

class Spam(metaclass=MultipleMeta):
    def bar(self, x:int, y:int):
        print('Bar 1:', x, y)

    def bar(self, s:str, n:int = 0):
        print('Bar 2:', s, n)


# 描述器实现方式
class multimethod2:
    def __init__(self, func):
        self._methods = {}
        self.__name__ = func.__name__
        self._default = func

    def match(self, *types):
        def register(func):
            ndefaults = len(func.__defaults__) if func.__defaults__ else 0
            for n in range(ndefaults+1):
                # types[:len(types) - n]表示截取从开始位置-  len(types) - n位置的元素(但不包括 len(types) - n 这个位置的元素)
                # 如果方法有默认值,比如本例中的bar2,_methods中会存放两个键值对,分别是self._methods[(<class 'str'>,<class 'int'>)]
                #                                                      和self._methods[(<class 'str'>)]
                self._methods[types[:len(types) - n]] = func
            return self
        return register

    def __call__(self, *args):
        types = tuple(type(arg) for arg in args[1:])
        meth = self._methods.get(types, None)
        if meth:
            return meth(*args)
        else:
            return self._default(*args)

    def __get__(self, instance, cls):
        if instance is not None:
            return types.MethodType(self, instance)
        else:
            return self


class Spam2:
    @multimethod2
    def bar(self, *args):
        # Default method called if no match
        raise TypeError('No matching method for bar')

    @bar.match(int, int)
    def bar(self, x, y):
        print('Bar 1:', x, y)

    @bar.match(str, int)
    def bar(self, s, n = 0):
        print('Bar 2:', s, n)



if __name__ == '__main__':

    s = Spam()
    s.bar(2,3)

    print('*******************')

    s2 = Spam2()
    s2.bar('maple',3)

八、避免重复属性的方法

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024-04-14 17:41
# @Author  : Maple
# @File    : 21-避免重复属性的方法.py



# 需要写很多重复代码
class Person:
    def __init__(self, name ,age):
        self.name = name
        self.age = age

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError('name must be a string')
        self._name = value

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if not isinstance(value, int):
            raise TypeError('age must be an int')
        self._age = value


# 定义一个生成描述器的函数
def typed_property(name, expected_type):
    storage_name = '_' + name

    @property
    def prop(self):
        return getattr(self, storage_name)

    @prop.setter
    def prop(self, value):
        if not isinstance(value, expected_type):
            raise TypeError('{} must be a {}'.format(name, expected_type))
        setattr(self, storage_name, value)

    return prop

# Example use
class Person:
    name = typed_property('name', str)
    age = typed_property('age', int)

    def __init__(self, name, age):
        # self.name会调用描述器的setter方法
        self.name = name
        self.age = age


if __name__ == '__main__':

    p = Person('Maple',12)
    print(p.name) # Maple
    print(p.age) #12

    print('******************')
    try:
        p2 = Person('Maple','12')
    except Exception as e:
        print(e) # age must be a <class 'int'>

九、在局部变量域中执行代码

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024-04-14 17:51
# @Author  : Maple
# @File    : 22-在局部变量域中执行代码.py

import time
from contextlib import contextmanager

"""
实现一个新的上下文管理器的最简单的方法就是使用 contexlib 模块中的 @contextmanager 装饰器。
"""

# 上下文管理器的常规写法
import time

class timethis:
    def __init__(self, label):
        self.label = label

    def __enter__(self):
        self.start = time.time()

    def __exit__(self, exc_ty, exc_val, exc_tb):
        end = time.time()
        print('{}: {}'.format(self.label, end - self.start))


# 利用装饰器实现上下文管理器

@contextmanager
def timethis(label):
    print('yield之前的代码')
    start = time.time()
    try:
        # yield之前的代码会在上下文管理器中作为__enter__()方法执行
        yield
        print('yield之后的代码')
        # yield 之后的代码会作为 __exit__() 方法执行。 如果出现了异常,异常会在yield语句那里抛出
    finally:
        end = time.time()
        print('{}: {}'.format(label, end - start))


@contextmanager
def list_transaction(orig_list):
    working = list(orig_list)
    yield working
    orig_list[:] = working





if __name__ == '__main__':

    # 案例1
    """
    yield之前的代码
    yield之后的代码
    counting: 0.5752782821655273

    """
    with timethis('counting'):
        n = 10000000
        while n > 0:
            n -= 1

    # 案例2
    items  = [1,2,3]
    with list_transaction(items) as working:
        """
           1. items赋值给working,停在working
           2. 执行working.append
           3. 将working从新赋值给items
        """

        working.append(4)
        working.append(5)

    print(items) # [1, 2, 3, 4, 5]

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值