1204

我想了解内置函数是如何property工作的。令我困惑的是,property它也可以用作装饰器,但它只在用作内置函数时才需要参数,而不是在用作装饰器时。

此示例来自文档

class C:
    def __init__(self):
        self._x = None

    def getx(self):
        return self._x
    def setx(self, value):
        self._x = value
    def delx(self):
        del self._x
    x = property(getx, setx, delx, "I'm the 'x' property.")

property的参数是getx,setxdelx一个文档字符串。

在下面的代码中property用作装饰器。它的对象是x函数,但在上面的代码中,参数中没有对象函数的位置。

class C:
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """I'm the 'x' property."""
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        del self._x

在这种情况下如何创建x.setterx.deleter装饰器?

4

14 回答 14

1183

property()函数返回一个特殊的描述符对象

>>> property()
<property object at 0x10ff07940>

正是这个对象有额外的方法:

>>> property().getter
<built-in method getter of property object at 0x10ff07998>
>>> property().setter
<built-in method setter of property object at 0x10ff07940>
>>> property().deleter
<built-in method deleter of property object at 0x10ff07998>

这些也充当装饰者。它们返回一个新的属性对象:

>>> property().getter(None)
<property object at 0x10ff079f0>

那是旧对象的副本,但替换了其中一个功能。

请记住,@decorator语法只是语法糖;语法:

@property
def foo(self): return self._foo

真的意味着同样的事情

def foo(self): return self._foo
foo = property(foo)

所以foo函数被替换为property(foo),我们在上面看到的是一个特殊的对象。然后,当您使用 时@foo.setter(),您所做的是调用property().setter我在上面向您展示的方法,该方法返回属性的新副本,但这次将 setter 函数替换为修饰方法。

以下序列还通过使用这些装饰器方法创建了一个完整的属性。

首先,我们创建一些函数和一个property只有 getter 的对象:

>>> def getter(self): print('Get!')
... 
>>> def setter(self, value): print('Set to {!r}!'.format(value))
... 
>>> def deleter(self): print('Delete!')
... 
>>> prop = property(getter)
>>> prop.fget is getter
True
>>> prop.fset is None
True
>>> prop.fdel is None
True

接下来我们使用该.setter()方法添加一个setter:

>>> prop = prop.setter(setter)
>>> prop.fget is getter
True
>>> prop.fset is setter
True
>>> prop.fdel is None
True

最后我们使用.deleter()方法添加一个删除器:

>>> prop = prop.deleter(deleter)
>>> prop.fget is getter
True
>>> prop.fset is setter
True
>>> prop.fdel is deleter
True

最后但并非最不重要的一点是,该property对象充当描述符对象,因此它具有挂钩到实例属性获取、设置和删除的.__get__()方法.__set__().__delete__()

>>> class Foo: pass
... 
>>> prop.__get__(Foo(), Foo)
Get!
>>> prop.__set__(Foo(), 'bar')
Set to 'bar'!
>>> prop.__delete__(Foo())
Delete!

Descriptor Howto 包含该类型的纯 Python 示例实现property()

class Property:
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)
于 2013-06-26T20:54:27.500 回答
292

文档说这只是创建只读属性的快捷方式。所以

@property
def x(self):
    return self._x

相当于

def getx(self):
    return self._x
x = property(getx)
于 2013-06-26T20:52:51.733 回答
152

这是一个如何@property实现的最小示例:

class Thing:
    def __init__(self, my_word):
        self._word = my_word 
    @property
    def word(self):
        return self._word

>>> print( Thing('ok').word )
'ok'

否则word仍然是方法而不是属性。

class Thing:
    def __init__(self, my_word):
        self._word = my_word
    def word(self):
        return self._word

>>> print( Thing('ok').word() )
'ok'
于 2017-02-15T00:46:29.630 回答
90

第一部分很简单:

@property
def x(self): ...

是相同的

def x(self): ...
x = property(x)
  • 反过来,它是property仅使用 getter 创建 a 的简化语法。

下一步是使用设置器和删除器扩展此属性。这发生在适当的方法上:

@x.setter
def x(self, value): ...

返回一个新属性,该属性继承旧属性x和给定设置器的所有内容。

x.deleter以同样的方式工作。

于 2013-06-26T20:53:15.073 回答
84

下面是另一个示例,说明@property当必须重构取自此处的代码时如何提供帮助(我只在下面总结):

想象一下,你创建了一个这样的类Money

class Money:
    def __init__(self, dollars, cents):
        self.dollars = dollars
        self.cents = cents

并且用户根据他/她使用的这个类创建一个库,例如

money = Money(27, 12)

print("I have {} dollar and {} cents.".format(money.dollars, money.cents))
# prints I have 27 dollar and 12 cents.

现在让我们假设您决定更改您的Money类并摆脱dollarsandcents属性,而是决定只跟踪美分总数:

class Money:
    def __init__(self, dollars, cents):
        self.total_cents = dollars * 100 + cents

如果上述用户现在尝试像以前一样运行他/她的库

money = Money(27, 12)

print("I have {} dollar and {} cents.".format(money.dollars, money.cents))

这将导致错误

AttributeError:“金钱”对象没有属性“美元”

这意味着现在每个依赖于您的原始Money类的人都必须更改所有使用的代码行,dollarscents可能非常痛苦……那么,如何避免这种情况呢?通过使用@property

就是那样:

class Money:
    def __init__(self, dollars, cents):
        self.total_cents = dollars * 100 + cents

    # Getter and setter for dollars...
    @property
    def dollars(self):
        return self.total_cents // 100

    @dollars.setter
    def dollars(self, new_dollars):
        self.total_cents = 100 * new_dollars + self.cents

    # And the getter and setter for cents.
    @property
    def cents(self):
        return self.total_cents % 100

    @cents.setter
    def cents(self, new_cents):
        self.total_cents = 100 * self.dollars + new_cents

当我们现在从我们的图书馆打电话时

money = Money(27, 12)

print("I have {} dollar and {} cents.".format(money.dollars, money.cents))
# prints I have 27 dollar and 12 cents.

它会按预期工作,我们不必更改库中的任何一行代码!事实上,我们甚至不必知道我们所依赖的库发生了变化。

工作也setter很好:

money.dollars += 2
print("I have {} dollar and {} cents.".format(money.dollars, money.cents))
# prints I have 29 dollar and 12 cents.

money.cents += 10
print("I have {} dollar and {} cents.".format(money.dollars, money.cents))
# prints I have 29 dollar and 22 cents.

您也@property可以在抽象类中使用;我在这里举一个最小的例子。

于 2018-09-23T11:58:37.717 回答
58

以下是:

class C(object):
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """I'm the 'x' property."""
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        del self._x

是相同的:

class C(object):
    def __init__(self):
        self._x = None

    def _x_get(self):
        return self._x

    def _x_set(self, value):
        self._x = value

    def _x_del(self):
        del self._x

    x = property(_x_get, _x_set, _x_del, 
                    "I'm the 'x' property.")

是相同的:

class C(object):
    def __init__(self):
        self._x = None

    def _x_get(self):
        return self._x

    def _x_set(self, value):
        self._x = value

    def _x_del(self):
        del self._x

    x = property(_x_get, doc="I'm the 'x' property.")
    x = x.setter(_x_set)
    x = x.deleter(_x_del)

是相同的:

class C(object):
    def __init__(self):
        self._x = None

    def _x_get(self):
        return self._x
    x = property(_x_get, doc="I'm the 'x' property.")

    def _x_set(self, value):
        self._x = value
    x = x.setter(_x_set)

    def _x_del(self):
        del self._x
    x = x.deleter(_x_del)

这与以下内容相同:

class C(object):
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """I'm the 'x' property."""
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        del self._x
于 2017-05-24T18:38:10.683 回答
33

让我们从 Python 装饰器开始。

Python 装饰器是一个函数,它有助于向已定义的函数添加一些额外的功能。

在 Python 中,一切都是对象。Python 中的函数是一等对象,这意味着它们可以被变量引用、添加到列表中、作为参数传递给另一个函数等。

考虑以下代码片段。

def decorator_func(fun):
    def wrapper_func():
        print("Wrapper function started")
        fun()
        print("Given function decorated")
        # Wrapper function add something to the passed function and decorator 
        # returns the wrapper function
    return wrapper_func

def say_bye():
    print("bye!!")

say_bye = decorator_func(say_bye)
say_bye()

# Output:
#  Wrapper function started
#  bye!!
#  Given function decorated
 

在这里,我们可以说装饰器函数修改了我们的 say_bye 函数并添加了一些额外的代码行。

装饰器的 Python 语法

def decorator_func(fun):
    def wrapper_func():
        print("Wrapper function started")
        fun()
        print("Given function decorated")
        # Wrapper function add something to the passed function and decorator 
        # returns the wrapper function
    return wrapper_func

@decorator_func
def say_bye():
    print("bye!!")

say_bye()

让我们通过案例场景来了解所有内容。但在此之前,让我们先谈谈一些 OOP 原则。

Getter 和 setter 在许多面向对象的编程语言中使用,以确保数据封装的原则(这被视为将数据与操作这些数据的方法捆绑在一起。)

当然,这些方法是检索数据的 getter 和更改数据的 setter。

根据这一原则,将类的属性设为私有,以隐藏和保护它们免受其他代码的影响。

是的,@property基本上是一种使用 getter 和 setter 的 Python 方式。

Python 有一个很棒的概念,称为属性,它使面向对象程序员的生活变得更加简单。

让我们假设您决定创建一个可以以摄氏度为单位存储温度的类。

class Celsius:
def __init__(self, temperature = 0):
    self.set_temperature(temperature)

def to_fahrenheit(self):
    return (self.get_temperature() * 1.8) + 32

def get_temperature(self):
    return self._temperature

def set_temperature(self, value):
    if value < -273:
        raise ValueError("Temperature below -273 is not possible")
    self._temperature = value

重构代码,以下是我们如何使用“属性”来实现它。

在 Python 中,property() 是一个内置函数,它创建并返回一个属性对象。

属性对象具有三个方法,getter()、setter() 和 delete()。

class Celsius:
def __init__(self, temperature = 0):
    self.temperature = temperature

def to_fahrenheit(self):
    return (self.temperature * 1.8) + 32

def get_temperature(self):
    print("Getting value")
    return self.temperature

def set_temperature(self, value):
    if value < -273:
        raise ValueError("Temperature below -273 is not possible")
    print("Setting value")
    self.temperature = value

temperature = property(get_temperature,set_temperature)

这里,

temperature = property(get_temperature,set_temperature)

可能被分解为,

# make empty property
temperature = property()
# assign fget
temperature = temperature.getter(get_temperature)
# assign fset
temperature = temperature.setter(set_temperature)

注意事项:

  • get_temperature 仍然是属性而不是方法。

现在您可以通过写入来访问温度值。

C = Celsius()
C.temperature
# instead of writing C.get_temperature()

我们可以更进一步,不要定义名称get_temperatureset_temperature,因为它们是不必要的并且会污染类命名空间。

处理上述问题的pythonic方法是使用@property

class Celsius:
    def __init__(self, temperature = 0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

    @property
    def temperature(self):
        print("Getting value")
        return self.temperature

    @temperature.setter
    def temperature(self, value):
        if value < -273:
            raise ValueError("Temperature below -273 is not possible")
        print("Setting value")
        self.temperature = value

注意事项 -

  1. 用于获取值的方法用“@property”修饰。
  2. 必须充当 setter 的方法用“@temperature.setter”装饰,如果该函数被称为“x”,我们必须用“@x.setter”装饰它。
  3. 我们编写了“两个”具有相同名称和不同数量参数的方法,“def temperature(self)”和“def temperature(self,x)”。

如您所见,代码肯定不那么优雅。

现在,让我们谈谈一个现实生活中的实际场景。

假设您设计了一个类,如下所示:

class OurClass:

    def __init__(self, a):
        self.x = a


y = OurClass(10)
print(y.x)

现在,让我们进一步假设我们的类在客户中很受欢迎,并且他们开始在他们的程序中使用它,他们对对象进行了各种分配。

有一天,一位值得信赖的客户来找我们,建议“x”必须是 0 到 1000 之间的值;这真是一个可怕的场景!

由于属性,这很容易:我们创建“x”的属性版本。

class OurClass:

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

    @property
    def x(self):
        return self.__x

    @x.setter
    def x(self, x):
        if x < 0:
            self.__x = 0
        elif x > 1000:
            self.__x = 1000
        else:
            self.__x = x

这很棒,不是吗:您可以从可以想象的最简单的实现开始,以后可以自由迁移到属性版本,而无需更改接口!所以属性不仅仅是 getter 和 setter 的替代品!

你可以在这里查看这个实现

于 2018-09-17T00:01:12.207 回答
27

我在这里阅读了所有帖子,并意识到我们可能需要一个现实生活中的例子。为什么,实际上,我们有@property?因此,考虑一个使用身份验证系统的 Flask 应用程序。您在以下位置声明模型用户models.py

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(64), unique=True, index=True)
    username = db.Column(db.String(64), unique=True, index=True)
    password_hash = db.Column(db.String(128))

    ...

    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')

    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

在这段代码中,我们password通过使用which在您尝试直接访问它时@property触发断言来“隐藏”属性,而我们使用 @property.setter 来设置实际的实例变量。AttributeErrorpassword_hash

现在auth/views.py我们可以实例化一个用户:

...
@auth.route('/register', methods=['GET', 'POST'])
def register():
    form = RegisterForm()
    if form.validate_on_submit():
        user = User(email=form.email.data,
                    username=form.username.data,
                    password=form.password.data)
        db.session.add(user)
        db.session.commit()
...

password用户填写表单时来自注册表单的注意属性。密码确认发生在前端EqualTo('password', message='Passwords must match')(如果您想知道,但这是与 Flask 表单相关的不同主题)。

我希望这个例子会有用

于 2018-03-23T14:47:53.010 回答
18

很多人都已经清除了这一点,但这是我正在寻找的直接点。这就是我觉得从@property 装饰器开始很重要的一点。例如:-

class UtilityMixin():
    @property
    def get_config(self):
        return "This is property"

函数“get_config()”的调用将像这样工作。

util = UtilityMixin()
print(util.get_config)

如果您注意到我没有使用“()”括号来调用该函数。这是我正在寻找@property 装饰器的基本内容。这样您就可以像使用变量一样使用函数。

于 2018-11-09T13:00:56.490 回答
7

property@property是装饰器后面的一个类。

您可以随时检查:

print(property) #<class 'property'>

我重写了示例 fromhelp(property)以显示@property语法

class C:
    def __init__(self):
        self._x=None

    @property 
    def x(self):
        return self._x

    @x.setter 
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        del self._x

c = C()
c.x="a"
print(c.x)

在功能上与property()语法相同:

class C:
    def __init__(self):
        self._x=None

    def g(self):
        return self._x

    def s(self, v):
        self._x = v

    def d(self):
        del self._x

    prop = property(g,s,d)

c = C()
c.x="a"
print(c.x)

如您所见,我们如何使用该属性没有区别。

回答问题@property装饰器是通过property类实现的。


所以,问题是稍微解释一下这个property类。这一行:

prop = property(g,s,d)

是初始化。我们可以这样重写它:

prop = property(fget=g,fset=s,fdel=d)

和的含义fgetfsetfdel

 |    fget
 |      function to be used for getting an attribute value
 |    fset
 |      function to be used for setting an attribute value
 |    fdel
 |      function to be used for del'ing an attribute
 |    doc
 |      docstring

下一张图片显示了我们拥有的三元组,来自类property

在此处输入图像描述

__get__, __set__, 并且__delete__有被覆盖。这是 Python 中描述符模式的实现。

通常,描述符是具有“绑定行为”的对象属性,其属性访问已被描述符协议中的方法覆盖。

我们还可以使用 propertysetter和方法将函数绑定到 property getterdeleter检查下一个示例。s2该类的方法C将属性设置为doubled

class C:
    def __init__(self):
        self._x=None

    def g(self):
        return self._x

    def s(self, x):
        self._x = x

    def d(self):
        del self._x

    def s2(self,x):
        self._x=x+x


    x=property(g)
    x=x.setter(s)
    x=x.deleter(d)      


c = C()
c.x="a"
print(c.x) # outputs "a"

C.x=property(C.g, C.s2)
C.x=C.x.deleter(C.d)
c2 = C()
c2.x="a"
print(c2.x) # outputs "aa"
于 2019-05-26T16:27:55.700 回答
7

最好的解释可以在这里找到:Python @Property Explained – How to Use and When? (完整示例) by Selva Prabhakaran | 发表于 2018 年 11 月 5 日

它帮助我理解了 WHY 不仅是 HOW。

https://www.machinelearningplus.com/python/python-property/

于 2020-02-22T01:28:41.980 回答
4

装饰器是一个将函数作为参数并返回闭包的函数。闭包是一组内部函数和自由变量。内部函数正在关闭自由变量,这就是它被称为“闭包”的原因。自由变量是在内部函数之外并通过docorator传递给内部的变量。

顾名思义,装饰器就是装饰接收到的函数。

function decorator(undecorated_func):
    print("calling decorator func")
    inner():
       print("I am inside inner")
       return undecorated_func
    return inner

这是一个简单的装饰器功能。它收到“undecorated_func”并将其作为自由变量传递给 inner(),inner() 打印“I am inside inner”并返回undecorated_func。当我们调用时decorator(undecorated_func),它正在返回inner. 这是关键,在装饰器中,我们将内部函数命名为我们传递的函数的名称。

   undecorated_function= decorator(undecorated_func) 

现在内部函数被称为“undecorated_func”。由于 inner 现在被命名为“undecorated_func”,我们将“undecorated_func”传递给装饰器,我们返回“undecorated_func”并打印出“I am inside inner”。所以这个打印语句装饰了我们的“未装饰函数”。

现在让我们用属性装饰器定义一个类:

class Person:
    def __init__(self,name):
        self._name=name
    @property
    def name(self):
        return self._name
    @name.setter
    def name(self.value):
        self._name=value

当我们用@property() 修饰name() 时,发生了这样的事情:

name=property(name) # Person.__dict__ you ll see name 

property() 的第一个参数是 getter。这是第二次装饰中发生的事情:

   name=name.setter(name) 

正如我上面提到的,装饰器返回内部函数,我们用我们传递的函数的名称来命名内部函数。

这是需要注意的重要事项。“名称”是不可变的。在第一个装饰中,我们得到了这个:

  name=property(name)

在第二个中,我们得到了这个

  name=name.setter(name)

我们没有修改名称 obj。在第二个修饰中,python 看到这是属性对象并且它已经有了 getter。所以python创建了一个新的“name”对象,从第一个obj添加“fget”,然后设置“fset”。

于 2020-11-30T21:02:47.777 回答
1

可以通过两种方式声明属性。

  • 为属性创建 getter、setter 方法,然后将它们作为参数传递给属性函数
  • 使用@property装饰器。

你可以看看我写的关于python 属性的几个例子。

于 2017-07-13T09:20:51.367 回答
0

这是另一个例子:

##
## Python Properties Example
##
class GetterSetterExample( object ):
    ## Set the default value for x ( we reference it using self.x, set a value using self.x = value )
    __x = None


##
## On Class Initialization - do something... if we want..
##
def __init__( self ):
    ## Set a value to __x through the getter / setter... Since __x is defined above, this doesn't need to be set...
    self.x = 1234

    return None


##
## Define x as a property, ie a getter - All getters should have a default value arg, so I added it - it will not be passed in when setting a value, so you need to set the default here so it will be used..
##
@property
def x( self, _default = None ):
    ## I added an optional default value argument as all getters should have this - set it to the default value you want to return...
    _value = ( self.__x, _default )[ self.__x == None ]

    ## Debugging - so you can see the order the calls are made...
    print( '[ Test Class ] Get x = ' + str( _value ) )

    ## Return the value - we are a getter afterall...
    return _value


##
## Define the setter function for x...
##
@x.setter
def x( self, _value = None ):
    ## Debugging - so you can see the order the calls are made...
    print( '[ Test Class ] Set x = ' + str( _value ) )

    ## This is to show the setter function works.... If the value is above 0, set it to a negative value... otherwise keep it as is ( 0 is the only non-negative number, it can't be negative or positive anyway )
    if ( _value > 0 ):
        self.__x = -_value
    else:
        self.__x = _value


##
## Define the deleter function for x...
##
@x.deleter
def x( self ):
    ## Unload the assignment / data for x
    if ( self.__x != None ):
        del self.__x


##
## To String / Output Function for the class - this will show the property value for each property we add...
##
def __str__( self ):
    ## Output the x property data...
    print( '[ x ] ' + str( self.x ) )


    ## Return a new line - technically we should return a string so it can be printed where we want it, instead of printed early if _data = str( C( ) ) is used....
    return '\n'

##
##
##
_test = GetterSetterExample( )
print( _test )

## For some reason the deleter isn't being called...
del _test.x

基本上,与 C( object ) 示例相同,只是我使用x 代替...班级的......

输出是:

[ Test Class ] Set x = 1234
[ Test Class ] Get x = -1234
[ x ] -1234

如果我在init中注释掉 self.x = 1234那么输出是:

[ Test Class ] Get x = None
[ x ] None

如果我在getter函数中将_default = None设置为_default = 0(因为所有getter都应该有一个默认值,但它不是由我所看到的属性值传入的,所以你可以在这里定义它,并且它实际上还不错,因为您可以定义一次默认值并在任何地方使用它)即:def x(self,_default = 0):

[ Test Class ] Get x = 0
[ x ] 0

注意:getter 逻辑只是为了让值被它操纵以确保它被它操纵 - 对于 print 语句也是如此......

注意:我习惯了 Lua 并且能够在调用单个函数时动态创建 10 多个助手,并且我在不使用属性的情况下为 Python 做了类似的事情,它在一定程度上可以工作,但是,即使之前创建了函数被使用时,有时仍然存在问题,在创建之前调用它们,这很奇怪,因为它不是这样编码的......我更喜欢 Lua 元表的灵活性以及我可以使用实际的 setter / getter 的事实而不是本质上直接访问变量......我确实喜欢使用 Python 构建一些东西的速度 - 例如 gui 程序。虽然如果没有很多额外的库,我正在设计的一个可能是不可能的——如果我在 AutoHotkey 中编码,我可以直接访问我需要的 dll 调用,同样可以在 Java、C#、C++ 中完成,

注意:此论坛中的代码输出已损坏-我必须在代码的第一部分添加空格才能使其工作-复制/粘贴时确保将所有空格转换为制表符....我在 Python 中使用制表符,因为在一个 10,000 行的文件,文件大小可以是 512KB 到 1MB(带空格)和 100 到 200KB(带制表符),这相当于文件大小的巨大差异,并减少了处理时间......

选项卡也可以根据用户进行调整 - 因此,如果您更喜欢 2 个空格宽度、4 个、8 个或任何您能做到的,这意味着它对于有视力障碍的开发人员来说是周到的。

注意:由于论坛软件中的错误,类中定义的所有函数都没有正确缩进 - 确保在复制/粘贴时缩进

于 2018-08-07T11:08:27.873 回答