property
的可能陷阱在文章如何使用property掌控属性访问?中,我们学习了如何使用property
来实现在获取或设置“实例属性”的同时进行额外自定义操作,如:类型检查或验证等功能,但在仅了解这些的情况下使用property
可能会产生如下述代码一样的问题:
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def first_name(self):
return self._first_name
@first_name.setter
def first_name(self, value):
if not isinstance(value, str):
raise TypeError('姓名必须是字符串格式!')
self._first_name = value
# 以下为重复代码,仅仅是换了一个名字而已
@property
def last_name(self):
return self._last_name
@last_name.setter
def last_name(self, value):
if not isinstance(value, str):
raise TypeError('姓名必须是字符串格式!')
self._last_name = value
def main():
python_guru = Person('Raymond', 'Hettinger')
print(python_guru.first_name)
print(python_guru.last_name)
print('-' * 50)
python_creator = Person(1, 2)
if __name__ == '__main__':
main()
上述代码的运行结果为:
Raymond
Hettinger
--------------------------------------------------
Traceback (most recent call last):
…
TypeError: 姓名必须是字符串格式!
上述# 1
、# 2
和# 3
、# 4
处代码都可以实现类型检查等功能(因为使用int
数值尝试创建Person
类的对象时会报错),但代码除了变量名称不一样外,其余完全一致,即代码重复了,这样的代码不仅无美感可言,而且修改比较麻烦。这就是标题所说的陷阱所指。
property
的陷阱为了避免上述问题,下面代码使用本文主角描述器(在本文稍后会立马给出描述器的定义,这里仅给读者对其做一个感性认识)来实现同样功能:
class Name:
def __init__(self, storage_name):
self.storage_name = storage_name
def __get__(self, instance, owner): # 1
print('Name类中的__get__方法正在被调用...')
if instance is None:
return self
else:
return instance.__dict__[self.storage_name] # 3
def __set__(self, instance, value): # 2
print('Name类中的__set__方法正在被调用...')
if not isinstance(value, str):
raise TypeError('姓名必须是字符串格式!')
instance.__dict__[self.storage_name] = value # 4
class Person:
first_name = Name('first_name') # 5
last_name = Name('last_name') # 6
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
def main():
python_guru = Person('Raymond', 'Hettinger')
print('-' * 50)
print(python_guru.first_name)
print(python_guru.last_name)
print('-' * 50)
python_creator = Person(1, 2)
if __name__ == '__main__':
main()
上述代码的运行结果为:
Name类中的__set__方法正在被调用…
Name类中的__set__方法正在被调用…
--------------------------------------------------
Name类中的__get__方法正在被调用…
Raymond
Name类中的__get__方法正在被调用…
Hettinger
--------------------------------------------------
Name类中的__set__方法正在被调用…
Traceback (most recent call last):
…
TypeError: 姓名必须是字符串格式!
由上述代码及其运行结果可知,该代码也可以实现和使用property
相同的功能。实际上,在上述代码中,类Name
其实就是一个描述器,而此时Person
类的实例将设置和获取实例属性的功能代理给了描述器中的__set__
和__get__
方法,进而在这两个方法中通过操作Person类实例的__dict__
属性来设置或获取属性的值。
定义:描述器是一个类,该类实现了
__get__
、__set__
、__delete__
三个方法中的至少一个方法1,描述器可以为对象访问多个属性时提供一种相同的逻辑和操作。
仅从上述定义并不能看出描述器究竟有多么强大的功能,但实际上描述器可以说是Python实现面向对象编程范式的重要基础之一,因为Python中的函数、方法、属性、类方法装饰器@classmethod
、静态方法装饰器@staticmethod
等都基于描述器实现。对此,这个系列的文章将为大家一一道来。
在上述代码中,你可能对于这几个位置有疑问:
# 1
,# 2
方法处的参数instance
和owner
的作用;# 3
,# 4
处__dict__
属性的功能;# 5
,# 6
处存在和实例对象同名类属性的必要性。为了更好地深入理解Python的描述器,下面先对这三点进行详细分析:
在描述器实现的几个方法中,各个方法的参数含义为:
instance
:表示Person
类的一个实例;owner
:表示实例化得到上述instance
的类,即Person
类。验证代码如下:
class Name:
def __init__(self, storage_name):
self.storage_name = storage_name
def __get__(self, instance, owner):
print('描述器Name的__get__方法正在被调用...')
print('instance = ', instance, 'owner = ', owner)
return instance.__dict__[self.storage_name]
def __set__(self, instance, value):
print('描述器Name的__set__方法正在被调用...')
print('instance = ', instance, 'value = ', value)
if not isinstance(value, str):
raise TypeError('姓名必须是字符串格式!')
instance.__dict__[self.storage_name] = value
class Person:
"""描述一个人的类"""
first_name = Name('first_name')
last_name = Name('last_name')
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
def main():
python_guru = Person('Guido', 'Rossum')
print(python_guru.first_name)
if __name__ == '__main__':
main()
上述代码的运行结果为:
描述器Name的__set__方法正在被调用…
instance = <__main__.Person object at 0x7f6d01576630> value = Guido
描述器Name的__set__方法正在被调用…
instance = <__main__.Person object at 0x7f6d01576630> value = Rossum
描述器Name的__get__方法正在被调用…
instance = <__main__.Person object at 0x7f6d01576630> owner =
Guido
__dict__
属性在Python中,不管是类还是用类创建的实例都是对象(关于类也是一种对象,以及类这种对象是由什么实例化而来,请见文章为什么说元类是你最常使用却也最陌生的Python语言特性之一?),这两类对象都有一个名为__dict__
的属性,该属性都是一个字典,其中:
__dict__
属性保存了该类的命名空间,其中以键值对形式包含:类属性、实例方法、帮助文档信息等;__dict__
属性以键值对形式包含实例属性。关于上述说明,可由下列代码来运行确认:
class Name:
def __init__(self, storage_name):
self.storage_name = storage_name
def __get__(self, instance, owner):
if instance is None:
return self
else:
return instance.__dict__[self.storage_name]
def __set__(self, instance, value):
if not isinstance(value, str):
raise TypeError('姓名必须是字符串格式!')
instance.__dict__[self.storage_name] = value
class Person:
"""描述一个人的类"""
first_name = Name('first_name')
last_name = Name('last_name')
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
def main():
print('Person.__dict__ = ', Person.__dict__)
python_guru = Person('Raymond', 'Hettinger')
print('python_guru.__dict__ = ', python_guru.__dict__)
if __name__ == '__main__':
main()
上述代码的运行结果为:
Person.__dict__ = {…, ‘__doc__’: ‘描述一个人的类’, ‘first_name’: <__main__.Name object at 0x7f0ae2eb99e8>, ‘last_name’: <__main__.Name object at 0x7f0ae11a0550>, ‘__init__’:
, ‘__dict__’: , …}
python_guru.__dict__ = {‘first_name’: ‘Raymond’, ‘last_name’: ‘Hettinger’}
在Python中,当使用obj.attr
(请记住,实例方法的参数self
也引用了实例对象,这对理解本节有较大帮助)的语法设置/删除某指定名称的属性时,根据以下几点:
__get__
、__set__
、__delete__
中的哪些方法(其中:仅定义__get__
方法的类叫做非数据型描述器,仅定义__set__
或__delete__
方法的类叫做数据型描述器);结论1:在使用
obj.attr
的语法设置/删除指定名称的属性时,数据型描述器具有最高优先级,其次为实例属性。
下面对上述结论做依次验证:
验证1.1:当满足下列情形(即对应上图中的
# 1
--># 2
--># 3
--># 4
),则描述器对应方法被优先调用:
- 创建对象的类中有指定名称的类属性;
- 该类属性为数据类描述器;
- 设置属性时数据类描述器实现了
__set__
方法;- 删除属性时数据描述其实现了
__delete__
方法。
验证代码如下图所示:
class Name:
def __init__(self, storage_name):
self.storage_name = storage_name
def __get__(self, instance, owner):
if instance is None:
return self
else:
return instance.__dict__[self.storage_name]
def __set__(self, instance, value):
print('描述器Name的__set__方法正在被调用...')
if not isinstance(value, str):
raise TypeError('姓名必须是字符串格式!')
instance.__dict__[self.storage_name] = value
def __delete__(self, instance):
print('描述器Name的__delete__方法正在被调用...')
del instance.__dict__[self.storage_name]
class Person:
"""描述一个人的类"""
first_name = Name('first_name')
middle_name = Name('middle_name')
last_name = Name('last_name')
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
def main():
print('Person.__dict__ = ', Person.__dict__)
python_guru = Person('Guido', 'Rossum')
python_guru.middle_name = 'van'
print('python_guru.__dict__ = ', python_guru.__dict__)
del python_guru.first_name
del python_guru.middle_name
print('python_guru.__dict__ = ', python_guru.__dict__)
if __name__ == '__main__':
main()
上述代码的运行结果为:
Person.__dict__ = {…, ‘first_name’: <__main__.Name object at 0x7f1b6d0bd5f8>, ‘middle_name’: <__main__.Name object at 0x7f1b6d0bd630>, ‘last_name’: <__main__.Name object at 0x7f1b6d0bd668>, …}
描述器Name的__set__方法正在被调用…
描述器Name的__set__方法正在被调用…
描述器Name的__set__方法正在被调用…
python_guru.__dict__ = {‘first_name’: ‘Guido’, ‘last_name’: ‘Rossum’, ‘middle_name’: ‘van’}
描述器Name的__delete__方法正在被调用…
描述器Name的__delete__方法正在被调用…
python_guru.__dict__ = {‘last_name’: ‘Rossum’}
由上述代码还可以知道:
当使用
obj.attr
的语法设置属性时,并不要求实例对象中有和类属性同名的实例属性。
验证1.2:在使用
obj.attr
的语法设置/删除指定名称的属性时,当满足下列情形(即对应上图中的# 1
--># 2
--># 3
--># 5
流程)时程序会优先尝试调用对应的描述器方法,调用失败则抛出AttributeError
异常:
- 创建对象的类中有使用描述器创建的指定名称类属性;
- 设置属性时描述器仅实现了
__delete__
方法;- 删除属性时描述器仅实现了
__set__
方法。
class Name:
def __init__(self, storage_name):
self.storage_name = storage_name
def __delete__(self, instance):
print('描述器Name的__delete__方法正在被调用...')
del instance.__dict__[self.storage_name]
class Person:
"""描述一个人的类"""
first_name = Name('first_name')
def __init__(self, first_name):
self.first_name = first_name
def main():
python_guru = Person('Guido')
if __name__ == '__main__':
main()
上述代码的运行结果为:
AttributeError: __set__
验证1.3:在使用
obj.attr
的语法设置/删除指定名称的属性时,即使Person
类中定义了由描述器创建的对象作为类属性,只要描述器仅实现了__get__
方法(即仅为数据型描述器),程序都会直接对实例对象的__dict__
属性进行相应操作。
当使用obj.attr
的语法获取属性时,属性查找的优先级要比设置/删除属性时要复杂得多,因为解释器将根据下列不同情况进行属性查找:
具体流程可以用下图来表示:
结论2:在使用
obj.attr
获取属性时,属性查找的优先级为:数据类描述器的优先级最高,其次为实例属性,再次为非数据型描述器,然后是普通类属性。
下面对上述结论做依次验证:
验证2.1:当类中有由数据型描述器创建的类属性,且描述器该描述器同时为非数据型描述器(即实现了
__get__
方法),则优先调用描述器中的__get__
获取属性(对应流程# 1
--># 2
--># 3
--># 4
);如果描述器仅实现__delete__
方法而未实现__set__
方法,则__get__
方法也不会被调用。
class Name:
def __init__(self, storage_name):
self.storage_name = storage_name
def __get__(self, instance, owner):
print('描述器Name的__get__方法正在被调用...')
if instance is None:
return self
else:
return instance.__dict__[self.storage_name]
def __set__(self, instance, value):
print('描述器Name的__set__方法正在被调用...')
if not isinstance(value, str):
raise TypeError('姓名必须是字符串格式!')
instance.__dict__[self.storage_name] = value
class Person:
"""描述一个人的类"""
first_name = Name('first_name')
last_name = Name('last_name')
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
def main():
python_guru = Person('Guido', 'Rossum')
print(python_guru.first_name)
print(type(python_guru).__dict__['first_name'].__get__(python_guru, type(python_guru)))
if __name__ == '__main__':
main()
上述代码的运行结果为:
描述器Name的__set__方法正在被调用…
描述器Name的__set__方法正在被调用…
描述器Name的__get__方法正在被调用…
Guido
描述器Name的__get__方法正在被调用…
Guido
即此时通过obj.attr
的方式获取属性相当于type(obj).__dict__['attr'].__get__(obj, type(obj))
。
验证2.2:当类中没有由数据型描述器创建的类属性,而实例属性中有待获取属性,则返回实例属性(对应流程
# 1
--># 2
--># 6
--># 7
–># 8
)。
class Name:
def __init__(self, storage_name):
self.storage_name = storage_name
class Person:
"""描述一个人的类"""
first_name = Name('first_name')
last_name = Name('last_name')
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
def main():
python_guru = Person('Guido', 'Rossum')
print(python_guru.first_name)
if __name__ == '__main__':
main()
上述代码的运行结果为:
Guido
验证2.3:当实例中不具有待获取属性,而有同名类属性,但类属性是一个非数据型描述器创建的对象,此时调用
__get__
方法(对应流程# 1
--># 2
--># 6
--># 7
–># 8
–># 9
--># 10
–># 11
)。(注意此时虽然调用了__get__
方法,但是程序会报错!)
class Name:
def __init__(self, storage_name):
self.storage_name = storage_name
def __get__(self, instance, owner):
print('描述器Name的__get__方法正在被调用...')
if instance is None:
return self
else:
return instance.__dict__[self.storage_name]
class Person:
"""描述一个人的类"""
first_name = Name('first_name')
last_name = Name('last_name')
def __init__(self, first_name, last_name):
self.last_name = last_name
def main():
python_guru = Person('Guido', 'Rossum')
print(python_guru.first_name)
if __name__ == '__main__':
main()
上述代码的运行结果为:
描述器Name的__get__方法正在被调用…
Traceback (most recent call last):
…
KeyError: ‘first_name’
验证2.4:当实例对象中没有待获取属性,且类属性是由任意未实现任何描述器方法的类创建,则直接返回类属性((对应流程
# 1
--># 2
--># 6
--># 7
–># 8
–># 9
--># 10
–># 12
))。
class Name:
def __init__(self, storage_name):
self.storage_name = storage_name
class Person:
"""描述一个人的类"""
first_name = Name('first_name')
last_name = Name('last_name')
def __init__(self, first_name, last_name):
self.last_name = last_name
def main():
python_guru = Person('Guido', 'Rossum')
print(python_guru.first_name)
if __name__ == '__main__':
main()
上述代码的运行结果为:
<__main__.Name object at 0x7ff9e759e748>
这就是描述器协议,即只要一个类实现了__get__
、__set__
、__delete__
三个方法其中之一,则其就是一个描述器,而不管该类是否还实现了其他任何方法,这也是鸭子类型的一种体现,关于协议和鸭子类型这两个名词,更深入了解请见文章浅谈Python中的注解和类型提示。 ↩︎