Python 的 property():向类添加托管属性
目录
使用 Python 的property()
,您可以在类中创建托管属性。当您需要修改其内部实现而不更改类的公共API时,您可以使用托管属性,也称为属性。提供稳定的 API 可以帮助您避免在用户依赖您的类和对象时破坏他们的代码。
属性可以说是最流行的以最纯粹的Pythonic风格快速创建托管属性的方式。
在本教程中,您将学习如何:
- 在您的类中创建托管属性或属性
- 执行惰性属性评估并提供计算属性
- 避免使用setter和getter方法让你的类更 Pythonic
- 创建read-only、read-write和write-only属性
- 为您的类创建一致且向后兼容的 API
您还将编写一些property()
用于验证输入数据、动态计算属性值、记录代码等的实际示例。为了充分利用本教程,您应该了解Python 中面向对象编程和装饰器的基础知识。
管理类中的属性
当您在面向对象的编程语言中定义一个类时,您可能最终会得到一些实例和类属性。换句话说,您最终会得到可通过实例、类或什至两者访问的变量,具体取决于语言。属性代表或保存给定对象的内部状态,您经常需要访问和改变它。
通常,您至少有两种方法来管理属性。您可以直接访问和改变属性,也可以使用methods。方法是附加到给定类的函数。它们提供对象可以使用其内部数据和属性执行的行为和操作。
如果您向用户公开您的属性,那么它们将成为您类的公共API的一部分。您的用户将直接在他们的代码中访问和改变它们。当您需要更改给定属性的内部实现时,就会出现问题。
假设您正在Circle
上课。最初的实现有一个名为.radius
. 您完成了该类的编码并将其提供给您的最终用户。他们开始Circle
在他们的代码中使用来创建许多很棒的项目和应用程序。做得好!
现在假设您有一个重要用户向您提出新要求。他们不想Circle
再存储半径。他们需要一个公共.diameter
属性。
此时,删除.radius
以开始使用.diameter
可能会破坏某些最终用户的代码。您需要以除删除.radius
.
Java和C++等编程语言鼓励您永远不要公开您的属性以避免此类问题。相反,您应该提供getter和setter方法,也分别称为accessors和mutators。这些方法提供了一种无需更改公共 API 即可更改属性的内部实现的方法。
最后,这些语言需要 getter 和 setter 方法,因为如果给定的需求发生变化,它们没有提供合适的方法来更改属性的内部实现。更改内部实现需要修改 API,这可能会破坏最终用户的代码。
Python 中的 Getter 和 Setter 方法
从技术上讲,没有什么可以阻止您在 Python 中使用 getter 和 setter方法。以下是这种方法的外观:
# point.py
class Point:
def __init__(self, x, y):
self._x = x
self._y = y
def get_x(self):
return self._x
def set_x(self, value):
self._x = value
def get_y(self):
return self._y
def set_y(self, value):
self._y = value
在此示例中,您Point
使用两个非公共属性 创建._x
并._y
保存手头点的笛卡尔坐标。
注: Python没有概念访问修饰符,如private
,protected
和public
,限制访问的属性和方法。在 Python 中,区别在于公共和非公共类成员。
如果要表示给定的属性或方法是非公开的,则必须使用众所周知的 Python约定,即在名称前加上下划线 ( _
)。这就是命名属性._x
和._y
.
请注意,这只是一个约定。它不会阻止您和其他程序员使用点表示法访问属性,如obj._attr
. 但是,违反此约定是不好的做法。
要访问和改变._x
or的值._y
,您可以使用相应的 getter 和 setter 方法。来吧,保存的上述定义Point
一个Python的模块和导入类到你的交互shell。
以下是您可以Point
在代码中使用的方法:
>>> from point import Point
>>> point = Point(12, 5)
>>> point.get_x()
12
>>> point.get_y()
5
>>> point.set_x(42)
>>> point.get_x()
42
>>> # Non-public attributes are still accessible
>>> point._x
42
>>> point._y
5
使用.get_x()
and .get_y()
,您可以访问._x
and的当前值._y
。您可以使用 setter 方法在相应的托管属性中存储新值。从这段代码中,您可以确认 Python 不限制对非公共属性的访问。是否这样做取决于您。
Pythonic 方法
尽管您刚刚看到的示例使用了 Python 编码风格,但它看起来并不像 Pythonic。在该示例中,getter 和 setter 方法不使用._x
和执行任何进一步处理._y
。你可以Point
用更简洁和 Pythonic 的方式重写:
>>> class Point:
... def __init__(self, x, y):
... self.x = x
... self.y = y
...
>>> point = Point(12, 5)
>>> point.x
12
>>> point.y
5
>>> point.x = 42
>>> point.x
42
这段代码揭示了一个基本原则。在 Python 中,向最终用户公开属性是正常和常见的。你不需要一直用 getter 和 setter 方法来弄乱你的类,这听起来很酷!但是,您如何处理似乎涉及 API 更改的需求更改?
与 Java 和 C++ 不同,Python 提供了方便的工具,允许您在不更改公共 API 的情况下更改属性的底层实现。最流行的方法是将您的属性转换为属性。
注意:提供托管属性的另一种常见方法是使用描述符。但是,在本教程中,您将了解属性。
属性表示普通属性(或字段)和方法之间的中间功能。换句话说,它们允许您创建行为类似于属性的方法。使用属性,您可以在需要时更改计算目标属性的方式。
例如,您可以将.x
和 都.y
转换为属性。通过此更改,您可以继续将它们作为属性进行访问。您还将拥有一个底层方法.x
,.y
这将允许您在用户访问和改变它们之前修改它们的内部实现并对它们执行操作。
注意:属性不是 Python 独有的。语言作为这样的JavaScript,C# ,科特林,和其他人还提供了工具和技术来创建属性类成员。
Python 属性的主要优点是它们允许您将属性作为公共 API 的一部分公开。如果您需要更改底层实现,那么您可以随时将属性转换为属性,而不会有太多痛苦。
在以下部分中,您将学习如何在 Python 中创建属性。
Python 入门 property()
Pythonproperty()
是避免在代码中使用正式的 getter 和 setter 方法的 Pythonic 方式。此功能允许您将类属性转换为属性或托管属性。由于property()
是内置函数,因此您无需导入任何内容即可使用它。此外,在 Cproperty()
中实现以确保最佳性能。
注意:通常property()
称为内置函数。但是,property
是一个旨在作为函数而不是常规类工作的类。这就是为什么大多数 Python 开发人员称其为函数的原因。这也是为什么property()
不遵循 Python命名 classes约定的原因。
本教程遵循调用property()
函数而不是类的常见做法。但是,在某些部分中,您会看到它被称为类以方便解释。
使用property()
,您可以将 getter 和 setter 方法附加到给定的类属性。这样,您就可以处理该属性的内部实现,而无需在 API 中公开 getter 和 setter 方法。您还可以指定处理属性删除的方法并为您的属性提供适当的文档字符串。
这是 的完整签名property()
:
property(fget=None, fset=None, fdel=None, doc=None)
前两个参数采用函数对象,它们将扮演 getter ( fget
) 和 setter ( fset
) 方法的角色。下面总结了每个参数的作用:
争论 | 描述 |
---|---|
fget |
返回托管属性值的函数 |
fset |
允许您设置托管属性值的函数 |
fdel |
定义托管属性如何处理删除的函数 |
doc |
表示属性的文档字符串的字符串 |
的返回值property()
是托管属性本身。如果您访问托管属性(如 )obj.attr
,则 Python 会自动调用fget()
. 如果您为属性分配一个新值(如 )obj.attr = value
,则 Pythonfset()
使用输入value
作为参数进行调用。最后,如果你运行一个del obj.attr
语句,那么 Python 会自动调用fdel()
.
注意:property()
取函数对象的前三个参数。您可以将函数对象视为没有调用括号对的函数名称。
您可以使用doc
为您的属性提供适当的文档字符串。您和您的其他程序员将能够使用 Python 的help()
. doc
当您使用支持文档字符串访问的代码编辑器和 IDE 时,该参数也很有用。
您可以将property()
其用作函数或装饰器来构建属性。在以下两节中,您将学习如何使用这两种方法。但是,您应该事先知道装饰器方法在 Python 社区中更受欢迎。
创建属性 property()
您可以通过property()
使用一组适当的参数调用并将其返回值分配给类属性来创建属性。的所有参数property()
都是可选的。但是,您通常至少提供一个setter function。
以下示例显示了如何创建一个Circle
具有方便属性的类来管理其半径:
# circle.py
class Circle:
def __init__(self, radius):
self._radius = radius
def _get_radius(self):
print("Get radius")
return self._radius
def _set_radius(self, value):
print("Set radius")
self._radius = value
def _del_radius(self):
print("Delete radius")
del self._radius
radius = property(
fget=_get_radius,
fset=_set_radius,
fdel=_del_radius,
doc="The radius property."
)
在此代码片段中,您创建Circle
. 类初始值设定项.__init__()
将其radius
作为参数并将其存储在名为 的非公共属性中._radius
。然后定义三个非公共方法:
._get_radius()
返回当前值._radius
._set_radius()
将其value
作为参数并将其分配给._radius
._del_radius()
删除实例属性._radius
一旦你有了这三个方法,你就可以创建一个类属性,调用它.radius
来存储属性对象。要初始化该属性,请将三个方法作为参数传递给property()
。您还可以为您的财产传递一个合适的文档字符串。
在此示例中,您使用关键字参数来提高代码可读性并防止混淆。这样,您就可以确切地知道每个参数使用哪个方法。
要Circle
试一试,请在您的 Python shell 中运行以下代码:
>>> from circle import Circle
>>> circle = Circle(42.0)
>>> circle.radius
Get radius
42.0
>>> circle.radius = 100.0
Set radius
>>> circle.radius
Get radius
100.0
>>> del circle.radius
Delete radius
>>> circle.radius
Get radius
Traceback (most recent call last):
...
AttributeError: 'Circle' object has no attribute '_radius'
>>> help(circle)
Help on Circle in module __main__ object:
class Circle(builtins.object)
...
| radius
| The radius property.
该.radius
属性隐藏了非公共实例属性._radius
,它现在是您在本示例中的托管属性。您可以.radius
直接访问和分配。计算机内部,Python会自动调用._get_radius()
并._set_radius()
在需要的时候。当您执行时del circle.radius
,Python 会调用._del_radius()
,这会删除底层的._radius
.
使用lambda
函数作为 Getter 方法显示隐藏
属性是管理实例属性的类属性。您可以将属性视为捆绑在一起的方法的集合。如果您检查.radius
仔细,那么你就可以找到你的所提供的原始方法fget
,fset
以及fdel
参数:
>>> from circle import Circle
>>> Circle.radius.fget
<function Circle._get_radius at 0x7fba7e1d7d30>
>>> Circle.radius.fset
<function Circle._set_radius at 0x7fba7e1d78b0>
>>> Circle.radius.fdel
<function Circle._del_radius at 0x7fba7e1d7040>
>>> dir(Circle.radius)
[..., '__get__', ..., '__set__', ...]
您可以通过相应的访问的getter,setter方法,并删除器在给定的属性方法.fget
,.fset
和.fdel
。
属性也是覆盖描述符。如果您使用dir()
检查给定属性的内部成员,那么您将在列表中找到.__set__()
和.__get__()
。这些方法提供了描述符协议的默认实现。
注意:如果你想更好地理解property
as 类的内部实现,那么查看文档中描述的纯 PythonProperty
类。
.__set__()
例如,的默认实现在您不提供自定义 setter 方法时运行。在这种情况下,您会得到一个,AttributeError
因为无法设置基础属性。
使用property()
作为装饰
装饰器在 Python 中无处不在。它们是将另一个函数作为参数并返回具有附加功能的新函数的函数。使用装饰器,您可以将预处理和后处理操作附加到现有函数。
当Python 2.2引入时property()
,装饰器语法不可用。正如您之前学到的,定义属性的唯一方法是传递 getter、setter 和 deleter 方法。装饰器语法是在Python 2.4中添加的,现在,property()
作为装饰器使用是 Python 社区中最流行的做法。
装饰器语法包括在要装饰的函数@
定义之前放置带有前导符号的装饰器函数的名称:
@decorator
def func(a):
return a
在这个代码片段中,@decorator
可以是一个函数或类来装饰func()
. 此语法等效于以下内容:
def func(a):
return a
func = decorator(func)
The final line of code reassigns the name func
to hold the result of calling decorator(func)
. Note that this is the same syntax you used to create a property in the section above.
Python’s property()
can also work as a decorator, so you can use the @property
syntax to create your properties quickly:
1# circle.py
2
3class Circle:
4 def __init__(self, radius):
5 self._radius = radius
6
7 @property
8 def radius(self):
9 """The radius property."""
10 print("Get radius")
11 return self._radius
12
13 @radius.setter
14 def radius(self, value):
15 print("Set radius")
16 self._radius = value
17
18 @radius.deleter
19 def radius(self):
20 print("Delete radius")
21 del self._radius
This code looks pretty different from the getter and setter methods approach. Circle
now looks more Pythonic and clean. You don’t need to use method names such as ._get_radius()
, ._set_radius()
, and ._del_radius()
anymore. Now you have three methods with the same clean and descriptive attribute-like name. How is that possible?
用于创建属性的装饰器方法需要使用底层托管属性的公共名称定义第一个方法,.radius
在本例中就是这样。此方法应实现 getter 逻辑。在上面的示例中,第 7 行到第 11 行实现了该方法。
第 13 到 16 行定义了 的 setter 方法.radius
。在这种情况下,语法完全不同。您无需@property
再次使用,而是使用@radius.setter
. 为什么你需要这样做?再看一下dir()
输出:
>>> dir(Circle.radius)
[..., 'deleter', ..., 'getter', 'setter']
此外.fget
,.fset
,.fdel
,和一堆其他特殊的属性和方法,property
还提供了.deleter()
,.getter()
和.setter()
。这三个方法各自返回一个新属性。
当您.radius()
使用@radius.setter
(第 13 行)装饰第二个方法时,您创建了一个新属性并重新分配了类级名称.radius
(第 8 行)来保存它。这个新属性包含第 8 行初始属性的相同方法集,并添加了第 14 行提供的新 setter 方法。最后,装饰器语法将新属性重新分配给.radius
类级名称。
定义删除器方法的机制是类似的。这一次,您需要使用@radius.deleter
装饰器。在该过程结束时,您将获得具有 getter、setter 和 deleter 方法的完整属性。
最后,当您使用装饰器方法时,如何为您的属性提供合适的文档字符串?如果您Circle
再次检查,您会注意到您已经在第 9 行的 getter 方法中添加了一个文档字符串。
新Circle
实现的工作方式与上一节中的示例相同:
>>> from circle import Circle
>>> circle = Circle(42.0)
>>> circle.radius
Get radius
42.0
>>> circle.radius = 100.0
Set radius
>>> circle.radius
Get radius
100.0
>>> del circle.radius
Delete radius
>>> circle.radius
Get radius
Traceback (most recent call last):
...
AttributeError: 'Circle' object has no attribute '_radius'
>>> help(circle)
Help on Circle in module __main__ object:
class Circle(builtins.object)
...
| radius
| The radius property.
您不需要使用一对括号.radius()
作为方法调用。相反,您可以.radius
像访问常规属性一样访问,这是属性的主要用途。它们允许您将方法视为属性,并负责自动调用底层方法集。
以下是使用装饰器方法创建属性时要记住的一些要点:
- 在
@property
装饰装修必须的getter方法。 - 文档字符串必须在getter 方法中。
- 该setter和删除器方法必须与getter方法加上的名字装饰
.setter
和.getter
分别。
到目前为止,您已经创建了property()
用作函数和装饰器的托管属性。如果您检查Circle
到目前为止的实现,那么您会注意到它们的 getter 和 setter 方法不会在您的属性之上添加任何真正的额外处理。
通常,您应该避免将不需要额外处理的属性转换为属性。在这些情况下使用属性可以使您的代码:
- 不必要的冗长
- 混淆其他开发者
- 比基于常规属性的代码慢
除非您需要的不仅仅是访问裸属性,否则不要编写属性。它们是在浪费CPU时间,更重要的是,它们是在浪费您的时间。最后,您应该避免编写显式的 getter 和 setter 方法,然后将它们包装在一个属性中。相反,使用@property
装饰器。这是目前最 Pythonic 的方式。
提供只读属性
可能最基本的用例property()
是在您的类中提供只读属性。假设您需要一个不可变 Point
类,它不允许用户改变其坐标的原始值,x
并且y
. 为了实现这个目标,你可以Point
像下面的例子一样创建:
# point.py
class Point:
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
return self._x
@property
def y(self):
return self._y
在这里,您将输入参数存储在属性._x
和 中._y
。正如您已经了解到的,_
在名称中使用前导下划线 ( ) 告诉其他开发人员它们是非公共属性,不应使用点表示法访问,例如在point._x
. 最后,定义两个 getter 方法并用@property
.
现在您有两个只读属性,.x
和.y
作为您的坐标:
>>> from point import Point
>>> point = Point(12, 5)
>>> # Read coordinates
>>> point.x
12
>>> point.y
5
>>> # Write coordinates
>>> point.x = 42
Traceback (most recent call last):
...
AttributeError: can't set attribute
此处point.x
和point.y
是只读属性的基本示例。它们的行为依赖于property
提供的底层描述符。正如您已经看到的,当您没有定义正确的 setter 方法时,默认.__set__()
实现会引发 an AttributeError
。
您可以Point
更进一步地实现此实现,并提供显式的 setter 方法,这些方法会引发带有更详细和特定消息的自定义异常:
# point.py
class WriteCoordinateError(Exception):
pass
class Point:
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
return self._x
@x.setter
def x(self, value):
raise WriteCoordinateError("x coordinate is read-only")
@property
def y(self):
return self._y
@y.setter
def y(self, value):
raise WriteCoordinateError("y coordinate is read-only")
在此示例中,您定义了一个名为 的自定义异常WriteCoordinateError
。此异常允许您自定义实现不可变Point
类的方式。现在,这两个 setter 方法都会通过更明确的消息引发您的自定义异常。来Point
试试你的改进吧!
创建读写属性
您还可以使用property()
提供具有读写功能的托管属性。在实践中,您只需要为您的属性提供适当的 getter 方法(“read”)和 setter 方法(“write”)即可创建读写托管属性。
假设你希望你的Circle
类有一个.diameter
属性。但是,在类初始值设定项中获取半径和直径似乎没有必要,因为您可以使用另一个来计算一个。这是一个Circle
管理.radius
和.diameter
作为读写属性的:
# circle.py
import math
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
self._radius = float(value)
@property
def diameter(self):
return self.radius * 2
@diameter.setter
def diameter(self, value):
self.radius = value / 2
在这里,您创建一个Circle
具有读写权限的类.radius
。在这种情况下,getter 方法只返回半径值。setter 方法转换半径的输入值并将其分配给 non-public ._radius
,这是您用来存储最终数据的变量。
在这个Circle
和它的.radius
属性的新实现中有一个微妙的细节需要注意。在这种情况下,类初始值设定项.radius
直接将输入值分配给属性,而不是将其存储在专用的非公共属性中,例如._radius
.
为什么?因为您需要确保作为半径提供的每个值,包括初始化值,都通过 setter 方法并转换为浮点数。
Circle
还将.diameter
属性实现为属性。getter 方法使用半径计算直径。setter 方法做了一些奇怪的事情。它不是将输入直径存储value
在专用属性中,而是计算半径并将结果写入.radius
.
以下是您的Circle
工作方式:
>>> from circle import Circle
>>> circle = Circle(42)
>>> circle.radius
42.0
>>> circle.diameter
84.0
>>> circle.diameter = 100
>>> circle.diameter
100.0
>>> circle.radius
50.0
.radius
和.diameter
在这些示例中都作为普通属性工作,为您的Circle
类提供干净和 Pythonic 的公共 API 。
提供只写属性
您还可以通过调整实现属性的 getter 方法的方式来创建只写属性。例如,您可以让您的 getter 方法在用户每次访问基础属性值时引发异常。
以下是使用只写属性处理密码的示例:
# users.py
import hashlib
import os
class User:
def __init__(self, name, password):
self.name = name
self.password = password
@property
def password(self):
raise AttributeError("Password is write-only")
@password.setter
def password(self, plaintext):
salt = os.urandom(32)
self._hashed_password = hashlib.pbkdf2_hmac(
"sha256", plaintext.encode("utf-8"), salt, 100_000
)
的初始值设定项User
将用户名和密码作为参数并将它们分别存储在.name
和 中.password
。您使用属性来管理您的类如何处理输入密码。AttributeError
每当用户尝试检索当前密码时,getter 方法都会引发。这变成.password
了只写属性:
>>> from users import User
>>> john = User("John", "secret")
>>> john._hashed_password
b'b\xc7^ai\x9f3\xd2g ... \x89^-\x92\xbe\xe6'
>>> john.password
Traceback (most recent call last):
...
AttributeError: Password is write-only
>>> john.password = "supersecret"
>>> john._hashed_password
b'\xe9l$\x9f\xaf\x9d ... b\xe8\xc8\xfcaU\r_'
在此示例中,您将创建john
一个User
具有初始密码的实例。setter 方法对密码进行哈希处理并将其存储在._hashed_password
. 请注意,当您尝试.password
直接访问时,您会得到一个AttributeError
. 最后,分配一个新值来.password
触发 setter 方法并创建一个新的散列密码。
在 setter 方法中.password
,您使用os.urandom()
生成一个 32 字节的随机字符串作为散列函数的salt。要生成散列密码,请使用hashlib.pbkdf2_hmac()
. 然后将生成的散列密码存储在非公共属性中._hashed_password
。这样做可确保您永远不会将明文密码保存在任何可检索的属性中。
将 Pythonproperty()
付诸行动
到目前为止,您已经学习了如何使用 Python 的property()
内置函数在类中创建托管属性。您用作property()
函数和装饰器,并了解了这两种方法之间的差异。您还学习了如何创建只读、读写和只写属性。
在以下部分中,您将编写一些示例代码,以帮助您更好地实际理解property()
.
验证输入值
最常见的用例之一property()
是构建托管属性,在存储甚至将其作为安全输入接受之前验证输入数据。数据验证是代码中的一个常见要求,它接受来自用户或您认为不可信的其他信息源的输入。
Pythonproperty()
提供了一种快速可靠的工具来处理输入数据验证。例如,回想着Point
例如,您可能需要的值.x
,并.y
为有效的数字。由于您的用户可以自由输入任何类型的数据,因此您需要确保您的点仅接受数字。
这Point
是管理此要求的实现:
# point.py
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
@property
def x(self):
return self._x
@x.setter
def x(self, value):
try:
self._x = float(value)
print("Validated!")
except ValueError:
raise ValueError('"x" must be a number') from None
@property
def y(self):
return self._y
@y.setter
def y(self, value):
try:
self._y = float(value)
print("Validated!")
except ValueError:
raise ValueError('"y" must be a number') from None
使用 Python EAFP样式验证输入数据的 setter 方法.x
和.y
使用try
...except
块。如果调用成功,则输入数据有效,您将进入屏幕。如果引发 a ,则用户会收到带有更具体消息的 a 。float()
Validated!
float()
ValueError
ValueError
注意:在上面的示例中,您使用语法raise
…from None
来隐藏与引发异常的上下文相关的内部详细信息。从最终用户的角度来看,这些细节可能会让人感到困惑,并使您的类看起来很粗糙。
查看文档中有关raise
声明的部分,了解有关此主题的更多信息。
重要的是要注意,直接分配.x
和.y
属性.__init__()
可确保在对象初始化期间也进行验证。不这样做是property()
用于数据验证时的常见错误。
以下是您的Point
课程现在的工作方式:
>>> from point import Point
>>> point = Point(12, 5)
Validated!
Validated!
>>> point.x
12.0
>>> point.y
5.0
>>> point.x = 42
Validated!
>>> point.x
42.0
>>> point.y = 100.0
Validated!
>>> point.y
100.0
>>> point.x = "one"
Traceback (most recent call last):
...
ValueError: "x" must be a number
>>> point.y = "1o"
Traceback (most recent call last):
...
ValueError: "y" must be a number
如果赋值.x
和.y
值float()
可以变成浮点数,则验证成功,该值被接受。否则,您将获得一个ValueError
.
这种实现Point
揭示了property()
. 你发现了吗?
就是这样!您有遵循特定模式的重复代码。这种重复违反了DRY(不要重复自己)原则,因此您需要重构此代码以避免它。为此,您可以使用描述符抽象出重复的逻辑:
# point.py
class Coordinate:
def __set_name__(self, owner, name):
self._name = name
def __get__(self, instance, owner):
return instance.__dict__[self._name]
def __set__(self, instance, value):
try:
instance.__dict__[self._name] = float(value)
print("Validated!")
except ValueError:
raise ValueError(f'"{self._name}" must be a number') from None
class Point:
x = Coordinate()
y = Coordinate()
def __init__(self, x, y):
self.x = x
self.y = y
现在你的代码有点短了。您通过定义在一个地方管理数据验证Coordinate
的描述符,设法删除了重复的代码。该代码的工作方式与您之前的实现方式相同。来试试看吧!
通常,如果您发现自己在代码周围复制和粘贴属性定义,或者发现像上面示例中那样重复的代码,那么您应该考虑使用适当的描述符。
提供计算属性
如果您需要一个在访问时动态构建其值的属性,那么property()
这就是您要走的路。这些类型的属性通常称为计算属性。当您需要它们看起来像热切的属性时,它们会很方便,但您希望它们是惰性的。
创建 Eager 属性的主要原因是在您经常访问属性时优化计算成本。另一方面,如果您很少使用给定的属性,那么惰性属性可以将其计算推迟到需要时,这可以使您的程序更高效。
以下是如何在类中property()
创建计算属性的示例:.area
Rectangle
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
return self.width * self.height
在此示例中,Rectangle
初始化程序将width
和height
作为参数并将它们存储在常规实例属性中。.area
每次访问时,只读属性都会计算并返回当前矩形的面积。
属性的另一个常见用例是为给定属性提供自动格式化的值:
class Product:
def __init__(self, name, price):
self._name = name
self._price = float(price)
@property
def price(self):
return f"${self._price:,.2f}"
在此示例中,.price
是格式化并返回特定产品价格的属性。要提供类似货币的格式,请使用带有适当格式选项的f 字符串。
注意:此示例使用浮点数来表示货币,这是不好的做法。相反,你应该使用decimal.Decimal
从标准库。
作为计算属性的最后一个示例,假设您有一个Point
使用.x
和.y
作为笛卡尔坐标的类。您想为您的点提供极坐标,以便您可以在一些计算中使用它们。极坐标系使用到原点的距离和与水平坐标轴的角度来表示每个点。
这是一个笛卡尔坐标Point
类,它也提供计算的极坐标:
# point.py
import math
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
@property
def distance(self):
return round(math.dist((0, 0), (self.x, self.y)))
@property
def angle(self):
return round(math.degrees(math.atan(self.y / self.x)), 1)
def as_cartesian(self):
return self.x, self.y
def as_polar(self):
return self.distance, self.angle
此示例说明如何Point
使用给定对象.x
和.y
笛卡尔坐标计算给定对象的距离和角度。下面是这段代码在实践中的工作方式:
>>> from point import Point
>>> point = Point(12, 5)
>>> point.x
12
>>> point.y
5
>>> point.distance
13
>>> point.angle
22.6
>>> point.as_cartesian()
(12, 5)
>>> point.as_polar()
(13, 22.6)
在提供计算属性或惰性属性时,这property()
是一个非常方便的工具。但是,如果您要创建一个经常使用的属性,那么每次都计算它既昂贵又浪费。一个好的策略是在计算完成后缓存它们。
缓存计算属性
有时您有一个经常使用的给定计算属性。不断重复相同的计算可能是不必要和昂贵的。要解决此问题,您可以缓存计算值并将其保存在非公共专用属性中以供进一步重用。
为了防止意外行为,您需要考虑输入数据的可变性。如果您有一个根据常量输入值计算其值的属性,那么结果将永远不会改变。在这种情况下,您只需计算一次该值:
# circle.py
from time import sleep
class Circle:
def __init__(self, radius):
self.radius = radius
self._diameter = None
@property
def diameter(self):
if self._diameter is None:
sleep(0.5) # Simulate a costly computation
self._diameter = self.radius * 2
return self._diameter
即使这个实现Circle
正确缓存了计算出的直径,它也有一个缺点,如果你改变了 的值.radius
,那么.diameter
将不会返回正确的值:
>>> from circle import Circle
>>> circle = Circle(42.0)
>>> circle.radius
42.0
>>> circle.diameter # With delay
84.0
>>> circle.diameter # Without delay
84.0
>>> circle.radius = 100.0
>>> circle.diameter # Wrong diameter
84.0
在这些示例中,您将创建一个半径等于 的圆42.0
。该.diameter
属性仅在您第一次访问时计算其值。这就是为什么您在第一次执行中看到延迟而在第二次执行中没有延迟的原因。请注意,即使您更改了半径值,直径也保持不变。
如果计算属性的输入数据发生变异,则需要重新计算属性:
# circle.py
from time import sleep
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
self._diameter = None
self._radius = value
@property
def diameter(self):
if self._diameter is None:
sleep(0.5) # Simulate a costly computation
self._diameter = self._radius * 2
return self._diameter
每次更改半径值时,.radius
属性的 setter 方法都会重置._diameter
为None
。通过这个小更新,.diameter
在每次更改 后第一次访问它时重新计算它的值.radius
:
>>> from circle import Circle
>>> circle = Circle(42.0)
>>> circle.radius
42.0
>>> circle.diameter # With delay
84.0
>>> circle.diameter # Without delay
84.0
>>> circle.radius = 100.0
>>> circle.diameter # With delay
200.0
>>> circle.diameter # Without delay
200.0
凉爽的!Circle
现在工作正常!它会在您第一次访问它以及每次更改半径时计算直径。
创建缓存属性的另一个选项是functools.cached_property()
从标准库中使用。此函数用作装饰器,允许您将方法转换为缓存属性。该属性仅计算一次其值,并在实例的生命周期内将其作为普通属性缓存:
# circle.py
from functools import cached_property
from time import sleep
class Circle:
def __init__(self, radius):
self.radius = radius
@cached_property
def diameter(self):
sleep(0.5) # Simulate a costly computation
return self.radius * 2
在这里,.diameter
在您第一次访问它时计算并缓存它的值。这种实现适用于那些输入值不会发生变化的计算。这是它的工作原理:
>>> from circle import Circle
>>> circle = Circle(42.0)
>>> circle.diameter # With delay
84.0
>>> circle.diameter # Without delay
84.0
>>> circle.radius = 100
>>> circle.diameter # Wrong diameter
84.0
>>> # Allow direct assignment
>>> circle.diameter = 200
>>> circle.diameter # Cached value
200
当您访问 时.diameter
,您将获得它的计算值。从现在开始,该值保持不变。但是,与 不同的是property()
,cached_property()
除非您提供适当的 setter 方法,否则不会阻止属性更改。这就是为什么您可以200
在最后几行中将直径更新为。
如果要创建不允许修改的缓存属性,则可以在以下示例中使用property()
and functools.cache()
like:
# circle.py
from functools import cache
from time import sleep
class Circle:
def __init__(self, radius):
self.radius = radius
@property
@cache
def diameter(self):
sleep(0.5) # Simulate a costly computation
return self.radius * 2
此代码堆叠@property
在@cache
. 两个装饰器的组合构建了一个缓存属性,以防止突变:
>>> from circle import Circle
>>> circle = Circle(42.0)
>>> circle.diameter # With delay
84.0
>>> circle.diameter # Without delay
84.0
>>> circle.radius = 100
>>> circle.diameter
84.0
>>> circle.diameter = 200
Traceback (most recent call last):
...
AttributeError: can't set attribute
在这些示例中,当您尝试为 分配新值时.diameter
,您会得到 ,AttributeError
因为 setter 功能来自 的内部描述符property
。
记录属性访问和变异
有时您需要跟踪代码的作用以及程序的运行方式。在 Python 中这样做的一种方法是使用logging
. 该模块提供了记录代码所需的所有功能。它将允许您不断观察代码并生成有关其工作方式的有用信息。
如果您需要跟踪访问和更改给定属性的方式和时间,那么您也可以利用property()
它:
# circle.py
import logging
logging.basicConfig(
format="%(asctime)s: %(message)s",
level=logging.INFO,
datefmt="%H:%M:%S"
)
class Circle:
def __init__(self, radius):
self._msg = '"radius" was %s. Current value: %s'
self.radius = radius
@property
def radius(self):
"""The radius property."""
logging.info(self._msg % ("accessed", str(self._radius)))
return self._radius
@radius.setter
def radius(self, value):
try:
self._radius = float(value)
logging.info(self._msg % ("mutated", str(self._radius)))
except ValueError:
logging.info('validation error while mutating "radius"')
在这里,您首先导入logging
并定义一个基本配置。然后Circle
使用托管属性实现.radius
。每次.radius
在代码中访问时,getter 方法都会生成日志信息。setter 方法记录您在 上执行的每个更改.radius
。它还记录由于输入数据错误而出现错误的情况。
以下是您可以Circle
在代码中使用的方法:
>>> from circle import Circle
>>> circle = Circle(42.0)
>>> circle.radius
14:48:59: "radius" was accessed. Current value: 42.0
42.0
>>> circle.radius = 100
14:49:15: "radius" was mutated. Current value: 100
>>> circle.radius
14:49:24: "radius" was accessed. Current value: 100
100
>>> circle.radius = "value"
15:04:51: validation error while mutating "radius"
记录来自属性访问和变异的有用数据可以帮助您调试代码。日志记录还可以帮助您识别有问题的数据输入的来源、分析代码的性能、发现使用模式等。
管理属性删除
您还可以创建实现删除功能的属性。这可能是 的罕见用例property()
,但在某些情况下,有一种删除属性的方法可能会很方便。
假设您正在实现自己的树数据类型。树是一种抽象数据类型,它在层次结构中存储元素。树组件通常称为节点。树中的每个节点都有一个父节点,除了根节点。节点可以有零个或多个子节点。
现在假设您需要提供一种方法来删除或清除给定节点的子节点列表。这是一个实现树节点的示例,该节点property()
用于提供其大部分功能,包括清除手头节点的子节点列表的能力:
# tree.py
class TreeNode:
def __init__(self, data):
self._data = data
self._children = []
@property
def children(self):
return self._children
@children.setter
def children(self, value):
if isinstance(value, list):
self._children = value
else:
del self.children
self._children.append(value)
@children.deleter
def children(self):
self._children.clear()
def __repr__(self):
return f'{self.__class__.__name__}("{self._data}")'
在此示例中,TreeNode
表示自定义树数据类型中的一个节点。每个节点将其子节点存储在 Python列表中。然后您将其实现.children
为一个属性来管理子项的基础列表。deleter 方法调用.clear()
子项列表以将它们全部删除:
>>> from tree import TreeNode
>>> root = TreeNode("root")
>>> child1 = TreeNode("child 1")
>>> child2 = TreeNode("child 2")
>>> root.children = [child1, child2]
>>> root.children
[TreeNode("child 1"), TreeNode("child 2")]
>>> del root.children
>>> root.children
[]
在这里,您首先创建一个root
节点来开始填充树。然后创建两个新节点并.children
使用列表将它们分配给它们。该del
语句触发内部删除器方法.children
并清除列表。
创建向后兼容的类 API
如您所知,属性将方法调用转换为直接属性查找。此功能允许您为您的类创建干净的 Pythonic API。您可以公开公开您的属性,而无需 getter 和 setter 方法。
如果您需要修改计算给定公共属性的方式,则可以将其转换为属性。属性可以执行额外的处理,例如数据验证,而无需修改您的公共 API。
假设您正在创建一个会计应用程序并且您需要一个基类来管理货币。为此,您创建了一个Currency
公开两个属性的类,.units
并且.cents
:
class Currency:
def __init__(self, units, cents):
self.units = units
self.cents = cents
# Currency implementation...
这个类看起来干净和 Pythonic。现在假设您的要求发生了变化,您决定存储美分的总数而不是单位和美分。从您的公共 API 中删除.units
和.cents
使用类似的东西.total_cents
会破坏多个客户端的代码。
在这种情况下,property()
保持当前 API 不变是一个很好的选择。以下是您可以如何解决该问题并避免破坏客户代码的方法:
# currency.py
CENTS_PER_UNIT = 100
class Currency:
def __init__(self, units, cents):
self._total_cents = units * CENTS_PER_UNIT + cents
@property
def units(self):
return self._total_cents // CENTS_PER_UNIT
@units.setter
def units(self, value):
self._total_cents = self.cents + value * CENTS_PER_UNIT
@property
def cents(self):
return self._total_cents % CENTS_PER_UNIT
@cents.setter
def cents(self, value):
self._total_cents = self.units * CENTS_PER_UNIT + value
# Currency implementation...
现在,您的类存储美分的总数,而不是独立的单位和美分。但是,您的用户仍然可以在他们的代码中访问和更改.units
和.cents
并获得与以前相同的结果。来试试看吧!
当您编写许多人要在其上构建的内容时,您需要保证对内部实现的修改不会影响最终用户如何使用您的类。
覆盖子类中的属性
当您创建包含属性的 Python 类并在包或库中发布它们时,您应该期望您的用户使用它们做很多不同的事情。其中之一可能是对它们进行子类化以自定义其功能。在这些情况下,您的用户必须小心并注意一个微妙的问题。如果您部分覆盖了一个属性,那么您将失去未被覆盖的功能。
例如,假设您正在编写一个Employee
类来管理公司内部会计系统中的员工信息。您已经有一个名为 的类Person
,并且您考虑将其子类化以重用其功能。
Person
具有作为.name
属性实现的属性。的当前实现.name
不满足以大写字母返回名称的要求。以下是您最终解决此问题的方法:
# persons.py
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
# Person implementation...
class Employee(Person):
@property
def name(self):
return super().name.upper()
# Employee implementation...
在 中Employee
,您重写.name
以确保在访问属性时,您获得大写的员工姓名:
>>> from persons import Employee, Person
>>> person = Person("John")
>>> person.name
'John'
>>> person.name = "John Doe"
>>> person.name
'John Doe'
>>> employee = Employee("John")
>>> employee.name
'JOHN'
伟大的!Employee
根据您的需要工作!它使用大写字母返回名称。然而,随后的测试发现了一个意想不到的行为:
>>> employee.name = "John Doe"
Traceback (most recent call last):
...
AttributeError: can't set attribute
发生了什么?好吧,当您覆盖父类中的现有属性时,您将覆盖该属性的全部功能。在此示例中,您仅重新实现了 getter 方法。因此,.name
失去了基类的其余功能。您不再有 setter 方法了。
这个想法是,如果您需要覆盖子类中的属性,那么您应该在手头的新版本的属性中提供您需要的所有功能。
结论
一个属性是一种特殊类型的类成员,它提供的功能是在常规属性和方法之间的某处。属性允许您在不更改类的公共 API 的情况下修改实例属性的实现。能够保持 API 不变有助于避免破坏用户在旧版本类之上编写的代码。
属性是在类中创建托管属性的Pythonic方式。它们在现实世界的编程中有几个用例,使它们成为您作为 Python 开发人员技能集的一个很好的补充。
在本教程中,您学习了如何:
- 使用 Python创建托管属性
property()
- 执行惰性属性评估并提供计算属性
- 避免带有属性的setter和getter方法
- 创建read-only、read-write和write-only属性
- 为您的类创建一致且向后兼容的 API
- 点赞
- 收藏
- 关注作者
评论(0)