@property 装饰器在 Python 中如何工作?
- 2024-11-21 08:33:00
- admin 原创
- 5
问题描述:
我想了解内置函数的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
、setx
和delx
文档字符串。
在下面的代码中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.setter
和装饰器是如何创建的?x.deleter
解决方案 1:
该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 函数被替换为装饰方法。
以下序列还使用这些装饰器方法创建了一个完整属性。
首先我们创建一些函数:
>>> def getter(self): print('Get!')
...
>>> def setter(self, value): print('Set to {!r}!'.format(value))
...
>>> def deleter(self): print('Delete!')
...
然后,我们创建一个property
仅具有 getter 的对象:
>>> 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!
描述符 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__)
解决方案 2:
文档说这只是创建只读属性的快捷方式。所以
@property
def x(self):
return self._x
相当于
def getx(self):
return self._x
x = property(getx)
解决方案 3:
@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'
解决方案 4:
下面是另一个关于@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
类别并摆脱dollars
和cents
属性,但决定只跟踪总美分金额:
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”对象没有属性“dollars”
这意味着现在每个依赖你原来的Money
类的人都必须更改使用dollars
和的所有代码cents
行,这可能非常痛苦...那么,如何避免这种情况呢?通过使用@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
您也可以在抽象类中使用;我在这里给出了一个最小的例子。
解决方案 5:
第一部分很简单:
@property
def x(self): ...
等同于
def x(self): ...
x = property(x)
这反过来又是
property
仅使用 getter 创建的简化语法。
下一步是使用 setter 和 deleter 扩展此属性。这需要使用适当的方法:
@x.setter
def x(self, value): ...
返回一个新属性,该属性继承了旧属性的所有内容x
以及给定的 setter。
x.deleter
以同样的方式工作。
解决方案 6:
以下:
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
解决方案 7:
让我们从 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_temperature和set_temperature,因为它们是不必要的并且会污染类命名空间。
处理上述问题的Python 方式是使用@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
注意事项 -
用于获取值的方法用“@property”修饰。
必须作为 setter 运行的方法用“@temperature.setter”修饰,如果该函数被称为“x”,我们必须用“@x.setter”修饰它。
我们编写了“两个”具有相同名称但参数数量不同的方法“deftemperature(self)”和“deftemperature(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 的替代品!
您可以在此处检查此实现
解决方案 8:
我读了这里的所有帖子,意识到我们可能需要一个现实生活中的例子。为什么我们有@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
使用“隐藏”属性,当您尝试直接访问它时@property
会触发AttributeError
断言,而我们使用@property.setter 来设置实际的实例变量password_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 表单相关的不同主题)。
我希望这个例子有用
解决方案 9:
上面的许多人都已经澄清了这一点,但这是我正在寻找的直接观点。我认为这是使用 @property 装饰器开始的重要内容。例如:-
class UtilityMixin():
@property
def get_config(self):
return "This is property"
函数“get_config()”的调用将像这样进行。
util = UtilityMixin()
print(util.get_config)
如果您注意到,我没有使用“()”括号来调用该函数。这是我在 @property 装饰器中寻找的基本内容。这样您就可以像使用变量一样使用您的函数。
解决方案 10:
property
是装饰器后面的一个类@property
。
您可以随时检查这一点:
print(property) #<class 'property'>
我重写了示例,以help(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)
fget
、fset
和的含义fdel
:
| 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 中描述符模式的实现。
一般来说,描述符是一个具有“绑定行为”的对象属性,其属性访问已被描述符协议中的方法覆盖。
我们还可以使用属性setter
和方法将函数绑定到属性。查看下一个示例。该类的方法getter
将设置属性doubled。deleter
`s2`C
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"
解决方案 11:
装饰器是一个以函数为参数并返回闭包的函数。闭包是一组内部函数和自由变量。内部函数对自由变量进行封闭,这就是它被称为“闭包”的原因。自由变量是位于内部函数外部并通过装饰器传递到内部函数的变量。
顾名思义,装饰器就是对接收到的函数进行装饰。
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)
现在 inner 函数名为“undecorated_func”。由于 inner 现在名为“undecorated_func”,我们将“undecorated_func”传递给装饰器,并返回“undecorated_func”并打印出“I am inside inner”。所以这个打印语句装饰了我们的“undecorated_func”。
现在让我们定义一个带有属性装饰器的类:
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)
我们没有修改 name obj。在第二个修饰中,python 看到这是属性对象,并且它已经有 getter。因此 python 创建了一个新的“name”对象,从第一个 obj 添加“fget”,然后设置“fset”。
解决方案 12:
属性可以用两种方式声明。
为属性创建 getter、setter 方法,然后将它们作为参数传递给属性函数
使用@property装饰器。
您可以看看我写的有关Python 中的属性的几个示例。
解决方案 13:
下面我举了一个例子来阐明@property
考虑一个由两个变量命名的类Student
:name
和class_number
,并且您希望class_number
其范围在 1 到 5 之间。
现在我将解释两个错误的解决方案,最后解释正确的解决方案:
下面的代码是错误的,因为它没有验证class_number
(在 1 到 5 的范围内)
class Student:
def __init__(self, name, class_number):
self.name = name
self.class_number = class_number
尽管经过验证,但这个解决方案也是错误的:
def validate_class_number(number):
if 1 <= number <= 5:
return number
else:
raise Exception("class number should be in the range of 1 to 5")
class Student:
def __init__(self, name, class_number):
self.name = name
self.class_number = validate_class_number(class_number)
因为class_number
仅在创建类实例时检查验证,之后不再检查(可以class_number
使用 1 到 5 范围之外的数字进行更改):
student1 = Student("masoud",5)
student1.class_number = 7
正确的解决方案是:
class Student:
def __init__(self, name, class_number):
self.name = name
self.class_number = class_number
@property
def class_number(self):
return self._class_number
@class_number.setter
def class_number(self, class_number):
if not (1 <= class_number <= 5): raise Exception("class number should be in the range of 1 to 5")
self._class_number = class_number
解决方案 14:
下面是另一个示例:
##
## 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 '
'
##
##
##
_test = GetterSetterExample( )
print( _test )
## For some reason the deleter isn't being called...
del _test.x
基本上,与 C(对象)示例相同,只是我使用x...我也不在 __init 中初始化- ...好吧.. 我这样做了,但它可以被删除,因为 __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 逻辑只是为了让它操作值以确保它被它操作 - 打印语句也是一样...
注意:我习惯使用 Lua,当我调用单个函数时,它能够动态创建 10 多个辅助函数,而且我在不使用属性的情况下为 Python 做了一些类似的事情,并且在一定程度上可以工作,但是,即使函数是在使用前创建的,有时仍然存在问题,即它们在创建之前被调用,这很奇怪,因为它不是那样编码的...我更喜欢 Lua 元表的灵活性,以及我可以使用实际的 setter / getter 而不是直接访问变量的事实...不过,我确实喜欢用 Python 构建某些东西的速度 - 例如 GUI 程序。虽然我正在设计的东西可能没有很多额外的库就无法实现 - 如果我用 AutoHotkey 编写它,我可以直接访问我需要的 dll 调用,并且同样可以在 Java、C#、C++ 和更多中完成 - 也许我还没有找到合适的东西,但对于那个项目,我可能会从 Python 切换。
注意:本论坛中的代码输出已损坏 - 我必须在代码的第一部分添加空格才能使其工作 - 复制/粘贴时确保将所有空格转换为制表符......我对 Python 使用制表符,因为在一个 10,000 行的文件中,文件大小可以是 512KB 到 1MB(带有空格),而带有制表符的文件大小可以是 100 到 200KB,这相当于文件大小的巨大差异,并减少了处理时间......
标签也可以按用户进行调整 - 因此如果您喜欢 2 个空格宽度、4 个空格宽度、8 个空格宽度或其他任何宽度,您都可以这样做,这意味着它对视力有缺陷的开发人员很贴心。
注意:由于论坛软件中的一个错误,类中定义的所有函数都没有正确缩进 - 确保在复制/粘贴时缩进
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件