Fluent Python2 【Chapter23_QA】
1. 如何理解下面这个错误的原因

在Python中,描述符是一个对象,它定义了属性的访问方式。描述符通过实现__get__、__set__和__delete__方法来控制属性的读取、修改和删除。描述符可以用于创建具有特殊行为的属性,例如类型检查、缓存、惰性计算等。
在描述符的__get__和__set__方法中,self参数指的是描述符本身的实例,而instance参数指的是托管实例,即拥有描述符的实例。描述符的目的是管理托管实例的属性,因此描述符应该将属性的值存储在托管实例中,而不是存储在描述符自身中。
下面是一个描述符的例子,用于说明正确和错误的存储属性值的方式:
class Descriptor: def __init__(self, storage_name): self.storage_name = storage_name def __get__(self, instance, owner): if instance is None: return self return instance.__dict__[self.storage_name] def __set__(self, instance, value): # 正确的写法:将值存储在托管实例中 instance.__dict__[self.storage_name] = value # 错误的写法:不应该将值存储在描述符实例中 # self.__dict__[self.storage_name] = value class LineItem: weight = Descriptor('weight') # 这两行其实就体现了描述符对属性的管理 price = Descriptor('price') def __init__(self, weight, price): self.weight = weight self.price = price # 创建LineItem的实例 item = LineItem(10, 20)
在这个例子中,Descriptor类是一个描述符,它管理着LineItem实例的weight和price属性。
在Descriptor的__set__方法中,我们正确地将值存储在托管实例instance的__dict__中。这样,每个LineItem实例都会有自己的weight和price属性值。【换言之,各个实例的weight, price可以各不相同】
如果我们错误地将值存储在描述符实例self的__dict__中,那么所有的LineItem实例将会共享同一个weight和price值【换言之,所有实例的weight和price都完全相同,这显然不符合常理】,
因为描述符实例是类属性,由所有实例共享。这将导致不符合预期的行为,因为每个实例的属性值应该独立于其他实例。
总结来说,描述符应该将托管实例的属性值存储在托管实例自身中,而不是存储在描述符实例中。这样做可以确保每个实例都有自己独立的属性值,符合面向对象编程的原则。
2. 对于__set_name__ 和如下代码几个变体的理解 [bulkfood_v4.py]
class Quantity: def __set_name__(self, owner, name): self.storage_name = name # print(self.storage_name) def __set__(self, instance, value): if value > 0: instance.__dict__[self.storage_name] = value else: msg = f'{self.storage_name} must be > 0' raise ValueError(msg) class LineItem: weight = Quantity() # 体会:Quantity()的实例,给别人在这里当属性, 实现Quantity描述符管理LineItem的属性,同时这个Quantity也是两个类的链接枢纽, # print('weight.storage_name: ', weight.storage_name) # 要点1理解:这里会报错,因为在尝试访问 storage_name 属性时,Quantity 类的实例还没有被完全初始化。 price = Quantity() # print('price.storage_name: ', price.storage_name) def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price L1 = LineItem('basketball', 11, 1) print('weight.storage_name:', L1.weight.storage_name) # print('price.storage_name:', L1.price.storage_name)
要点1理解:
错误信息 “AttributeError: ‘Quantity’ object has no attribute ‘storage_name’” 表示在尝试访问 weight 和 price 的 storage_name 属性时出现了问题。这个错误是因为在尝试访问 storage_name 属性时,Quantity 类的实例还没有被完全初始化。
在Python中,当你在类定义中创建一个描述符实例(如 weight = Quantity())时,这个实例会被创建,但是 __set_name__ 方法还没有被调用。__set_name__ 方法是在类定义完成后,当你创建 LineItem 的实例时被调用的。因此,在 LineItem 类的定义中直接访问 weight.storage_name 和 price.storage_name 时,这些属性还没有被设置,所以会抛出 AttributeError。
要修复这个问题,你需要确保在 __set_name__ 方法被调用之后,也就是在 LineItem 实例被创建之后,再访问 storage_name 属性。下面是修改后的代码:(实际上这里修改代码后,还是报错)
class Quantity: def __set_name__(self, owner, name): self.storage_name = name print(self.storage_name) def __set__(self, instance, value): if value > 0: instance.__dict__[self.storage_name] = value else: msg = f'{self.storage_name} must be > 0' raise ValueError(msg) class LineItem: weight = Quantity() price = Quantity() def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price # 创建LineItem的实例 L1 = LineItem('basketball', 11, 1) print(L1.weight) # 现在__set_name__已经调用,可以安全访问storage_name print(L1.price) # 同上 # 打印weight和price的storage_name
# print(L1.weight) 此时的L1.weight=11,已经覆盖了之前Quantity()
print('weight.storage_name:', L1.weight.storage_name) # 要点2理解:写到了实例初始化完成后,为什么还是报错?
print('price.storage_name:', L1.price.storage_name)
要点2理解:
在上述代码中,weight 和 price 实际上是 LineItem 类的实例属性,它们在 __init__ 方法中被赋值为整数(11 和 1)。
这些属性在创建 LineItem 实例后被覆盖,不再是 Quantity 的实例,因此它们没有 storage_name 属性。
在您的代码中,Quantity 类的实例在 LineItem 类的定义中作为类属性创建,但是在创建 LineItem 实例后,这些属性被新的实例属性(整数)覆盖。因此,尝试访问 L1.weight.storage_name 或 L1.price.storage_name 会抛出 AttributeError,因为 L1.weight 和 L1.price 此时是整数,而不是 Quantity 的实例。
如果您想访问 Quantity 实例的 storage_name 属性,您需要在 LineItem 类的定义之外,即在创建 LineItem 实例之前访问它们。下面是正确的代码:
class Quantity: def __set_name__(self, owner, name): self.storage_name = name print(self.storage_name) # 这行可有可无,只是为了调试研究 def __set__(self, instance, value): if value > 0: instance.__dict__[self.storage_name] = value else: msg = f'{self.storage_name} must be > 0' raise ValueError(msg) class LineItem: weight = Quantity() price = Quantity() def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price # 打印weight和price的storage_name print('LineItem.weight.storage_name:', LineItem.weight.storage_name) # 返回: weight print('LineItem.price.storage_name:', LineItem.price.storage_name) # 返回: price # 创建LineItem的实例 L1 = LineItem('basketball', 11, 1) print(L1.weight) # 返回11
3. 请给我详细解释下为什么如下代码中,Quantity()类知道要把描述符分别绑定给哪个变量(weight, price)?
书上说是因为有__set_name__这个特殊方法,请给我详细解释下这个过程。
class Quantity: def __set_name__(self, owner, name): # <1> self.storage_name = name # <2> def __set__(self, instance, value): # <3> if value > 0: instance.__dict__[self.storage_name] = value else: msg = f'{self.storage_name} must be > 0' raise ValueError(msg) # no __get__ needed # <4> class LineItem: weight = Quantity() # <5> 为什么这里Quantity()知道自动分配weight, 还是price? price = Quantity() def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price
这是一个非常好的问题,涉及到 Python 中描述符协议的工作原理。让我来详细解释一下 __set_name__ 特殊方法在这个过程中的作用。
在上述的代码中,Quantity 类被用作描述符(descriptor)。描述符是一种特殊的类,它可以控制对属性的访问和赋值操作。
在这个例子中,Quantity 描述符被用于管理 LineItem 类中的 weight 和 price 属性。
当你在 LineItem 类中定义 weight = Quantity() 和 price = Quantity() 时,Python 会自动调用 Quantity 类的 __set_name__ 特殊方法。这个方法的签名如下:
def __set_name__(self, owner, name): # ...
其中:
self是当前描述符实例。owner是定义该描述符的类,也就是LineItem。name是描述符在owner类中的名称,即'weight'或'price'。
在以上代码中,当执行 weight = Quantity() 时,Python 会自动调用 Quantity 类的 __set_name__ 方法,传入 owner 为 LineItem、name 为 'weight'。
同样地,当执行 price = Quantity() 时,Python 会再次调用 __set_name__ 方法,但这次 name 参数是 'price'。
在 __set_name__ 方法内部,你将 name 参数的值保存到了描述符实例的 self.storage_name 属性中。这就是 Quantity 类知道要把描述符绑定给哪个变量的关键所在。
当你后续在 LineItem 的实例上访问或修改 weight 或 price 属性时,Python 会自动调用 Quantity 描述符的 __get__ 或 __set__ 方法。在你的代码中,你只定义了 __set__ 方法,用于控制对属性的赋值操作。
在 __set__ 方法内部,你使用了 self.storage_name 来确定要存储值的属性名。如果是对 weight 属性进行赋值,self.storage_name 就是 'weight'。
如果是对 price 属性进行赋值,self.storage_name 就是 'price'。这样,你就可以正确地将值存储到实例的 __dict__ 字典中。
总的来说,__set_name__ 特殊方法的作用是在定义描述符时,让 Python 自动将描述符与它所绑定的属性名关联起来。
这个关联信息被保存在描述符实例的 self.storage_name 属性中。后续,当 Python 调用描述符的 __get__ 或 __set__ 方法时,描述符就可以根据 self.storage_name 的值,正确地确定要操作的属性是哪个。
这种机制使得同一个描述符类可以被多次实例化,每个实例都可以控制不同的属性,而无需在描述符类中硬编码属性名。这提高了描述符的灵活性和可重用性。
4. 为什么weight.storage_name会打印报错,对于问题2更好的解答 【牵涉出python定义类时对语句的处理,很细的细节】
class Quantity: def __set_name__(self, owner, name): self.storage_name = name print(self.storage_name) def __set__(self, instance, value): if value > 0: instance.__dict__[self.storage_name] = value else: msg = f'{self.storage_name} must be > 0' raise ValueError(msg) class LineItem: weight = Quantity() print('weight.storage_name: ', weight.storage_name) # ?这里报错的深度理解,因为比较反直觉 price = Quantity() def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price # 创建LineItem的实例 L1 = LineItem('basketball', 11, 1) print(L1.weight)
让我详细解释一下为什么在访问weight.storage_name时,Python还没有完成对LineItem类的创建。
在Python中,当定义一个类时,Python会按照以下顺序处理类定义体中的语句:
- 收集所有语句:Python首先会收集类定义体中的所有语句,包括简单语句、函数定义、类定义等,但不会执行它们。
- 执行可执行语句:接下来,Python会按照语句在类定义体中出现的顺序,逐一执行那些可执行的语句,比如简单赋值语句。
- 创建类对象:最后,Python会利用已执行的语句创建类对象。
在你的示例代码中:
class LineItem: weight = Quantity() print('weight.storage_name: ', weight.storage_name) # <-- 这行代码 price = Quantity() def __init__(self, description, weight, price): ...
当Python处理LineItem类定义时,会经历以下步骤:
- Python收集
LineItem类定义体中的所有语句,包括weight = Quantity()、打印语句、price = Quantity()以及__init__方法。 - Python开始执行可执行语句。首先执行
weight = Quantity()语句,创建一个Quantity实例,并调用其__set_name__方法,将'weight'赋值给self.storage_name属性。 - Python继续执行打印语句
print('weight.storage_name: ', weight.storage_name)。但是在这个时候,LineItem类还没有完全创建完毕。Python只是在执行LineItem类定义体中的可执行语句, 但还没有完成类对象的创建。因此,当你试图访问weight.storage_name时,weight实例虽然已经创建好了,但是它还没有被真正地绑定到LineItem类上。这就导致了AttributeError错误。 - Python继续执行
price = Quantity()语句,创建另一个Quantity实例,并调用其__set_name__方法。 - Python执行
__init__方法的定义,但不会调用它。 - 最后,Python利用已执行的语句创建
LineItem类对象,并将weight和price属性正确地绑定到该类上。
所以,当你试图在类定义体内访问weight.storage_name时, LineItem类还处于创建过程中的中间阶段, weight实例虽然已经创建好了,但还没有被正确绑定到类上, 导致了AttributeError错误。
这个过程确实有些反直觉,因为我们通常认为Python会逐行执行类定义体中的语句。但实际上,Python采用了这种"收集-执行-创建"的处理流程,以支持一些特性,比如描述符协议。
5. 在如下这段话中,如何理解_get__ 和__set__这两个方法都必须定义,否则,实例的同名属性会遮盖描述符??请举例说明有无__set__两种情况,进行对比
“只读描述符必须要有__set__方法,使用描述符类实现只读属性时要记住,__get__ 和__set__这两个方法都必须定义,否则,实例的同名属性会遮盖描述符。
只读属性的__set__方法只需抛出AttributeError异常,并提供合适的错误消息即可。”
在Python中,描述符(descriptor)是一种特殊的对象,它定义了当属性被访问、修改或删除时的行为。
描述符是通过实现特定的魔法方法(__get__, __set__, __delete__)来工作的。当一个类中定义了这些方法中的任何一个时,这个类的实例就成为了描述符。
在你提供的段落中,提到如果定义了__get__方法但没有定义__set__方法,那么实例的同名属性会遮盖描述符。
这意味着,如果你为一个属性定义了__get__方法来控制属性的读取行为,但没有定义__set__方法来控制属性的写入行为,那么当你尝试在类的实例上设置这个属性时,你会直接创建或修改一个实例属性,而不是使用描述符。
先举两例,后总结,就秒懂
class Descriptor: def __get__(self, instance, owner): return 'get from descriptor' # 这个描述符没有定义__set__方法
现在,我们创建一个使用这个描述符的类:
class MyClass: readonly = Descriptor()
然后我们尝试使用这个类:
obj = MyClass() print(obj.readonly) # 输出: get from descriptor obj.readonly = 'new value' # 尝试设置属性 print(obj.readonly) # 输出: new value,因为没有__set__,所以这里创建了一个实例属性
在这个例子中,当我们尝试设置obj.readonly时,我们没有得到一个错误,而是成功地创建了一个名为readonly的实例属性,它遮盖了描述符。
为了防止这种情况,即使你想要一个只读属性,你也应该定义一个__set__方法,并让它抛出一个AttributeError异常。
下面是一个修正后的描述符例子:
class ReadOnlyDescriptor: def __get__(self, instance, owner): return 'get from descriptor' def __set__(self, instance, value): # 加上了__set__方法 raise AttributeError('This attribute is read-only')
使用这个修正后的描述符:
class MyClass: readonly = ReadOnlyDescriptor() obj = MyClass() print(obj.readonly) # 输出: get from descriptor try: obj.readonly = 'new value' # 尝试设置属性 except AttributeError as e: print(e) # 输出: This attribute is read-only
在这个修正后的例子中,当我们尝试设置obj.readonly时,我们得到了一个错误,提示我们这个属性是只读的。
这样,描述符的行为就不会被实例的同名属性遮盖了。
通过体会上述两个例子差异,便能更好理解描述符能够控制实例属性访问(设置、删除)方式这句话了。
6.如何理解如下这段话的"用于验证的描述符可以只有__set__方法"?
"用于验证的买哦书夫可以只有__set__方法,在仅用于验证的描述符中,__set__方法应该检查value参数获得的值,
如果有效,就使用描述符实例的名称作为键,直接在实例的__dict__属性中设置。这样从实力中读取同名属性的速度很快,因为不用经过__get__方法处理。"
这段话讨论的是一种特殊的描述符,它仅用于验证属性的值。这种描述符通常不需要实现__get__方法,因为它不涉及属性的读取行为,只关心属性的设置。在这种情况下,描述符的作用是确保设置的属性值满足某些条件或约束。
当描述符只有__set__方法时,它会在属性值被设置时触发,以便进行验证。如果验证通过,描述符会将值直接存储在实例的__dict__中,这样做的目的是为了优化属性的读取速度。
因为如果描述符实现了__get__方法,那么每次读取属性时都会调用这个方法,这可能会增加额外的开销。但是,如果描述符只存储值而不拦截读取操作,那么属性的读取就会直接从实例的__dict__中获取,这样更快。
下面是一个例子,说明如何实现一个只有__set__方法的验证描述符:
class ValidatingDescriptor: def __set__(self, instance, value): if value < 0: raise ValueError("Value must be non-negative") instance.__dict__[self.__name__] = value # 使用描述符的类 class MyClass: attribute = ValidatingDescriptor() def __init__(self): self.attribute.__name__ = 'attribute' # 因为self.__name__在上面不是自动设置的,需要手动设置,否则会在此处报错,说在实例中找不到__name__属性 # 实例化对象并设置属性 obj = MyClass() obj.attribute = 10 # 设置有效值,不会抛出异常 print(obj.attribute) # 输出: 10 # 尝试设置无效值 try: obj.attribute = -5 except ValueError as e: print(e) # 输出: Value must be non-negative
在这个例子中,ValidatingDescriptor是一个只有__set__方法的描述符。当尝试设置attribute属性时,__set__方法会检查值是否为非负数。
如果是,它将该值直接存储在实例的__dict__中。这样,当我们读取obj.attribute时,我们实际上是从实例的__dict__中直接读取,而不需要经过__get__方法。
这种方法的一个缺点是,描述符的__name__属性并不是自动设置的,我们需要在类定义中手动设置它。这通常通过在类的__init__方法中为描述符的__name__属性赋值来实现,如上方红色部分注释标注:
7.如理理解这句话"仅有__get__方法的描述符可以实现高效缓存",
如果只编写了__get__方法,那么得到的是非覆盖性描述符。这种描述符可以用于执行某些耗费资源的计算,然后为实例设置同名属性,缓存结果。
同名实例属性会遮盖描述符,因此后续访问直接从实例的__dict__属性中获取值,不再触发描述符的__get__方法。@functools.cached_property装饰器创建的其实就是非覆盖型描述符。
import time class CachingDescriptor: def __get__(self, instance, owner): if instance is None: return self # 计算并缓存结果 value = self.compute_value(instance) # 将结果存储在实例的__dict__中,以便后续快速访问 instance.__dict__[self.__name__] = value return value def compute_value(self, instance): # 模拟一个耗时的计算 print("Computing value...") time.sleep(5) return 42 class MyClass: cached_attribute = CachingDescriptor() # 在类定义外为描述符设置名称 MyClass.cached_attribute.__name__ = 'cached_attribute' # 实例化对象并访问属性 obj = MyClass() start_time = time.time() print(obj.cached_attribute) # 输出: Computing value... 42 print("First access took:", time.time() - start_time, "seconds") # 再次访问属性,这次不会触发__get__方法,因此速度更快 start_time = time.time() print(obj.cached_attribute) # 输出: 42 print("Second access took:", time.time() - start_time, "seconds")
在这个例子中,CachingDescriptor是一个只有__get__方法的描述符。当第一次访问obj.cached_attribute时,__get__方法会被调用,它模拟了一个耗时的计算,并将结果缓存到实例的__dict__中。
后续再次访问obj.cached_attribute时,由于实例的__dict__中已经有了同名属性,因此直接从__dict__中获取值,而不需要再次执行计算。
@functools.cached_property装饰器是Python标准库中的一个实现,它创建的就是这种非覆盖型描述符。
它的工作方式与上面的例子类似,只不过@functools.cached_property更加简洁和易于使用。下面是一个使用@functools.cached_property的例子:
import time import functools class MyClass: @functools.cached_property # 这种装饰器修饰的方法可以用于执行某些耗费资源的计算,然后为实例设置同名属性(此例是cached_attribute), 缓存结果。 def cached_attribute(self): # 模拟一个耗时的计算 print("Computing value...") time.sleep(1) return 42 # 实例化对象并访问属性 obj = MyClass() start_time = time.time() print(obj.cached_attribute) # 输出: Computing value... 42 print("First access took:", time.time() - start_time, "seconds") # 再次访问属性,这次不会触发计算,因此速度更快 start_time = time.time() print(obj.cached_attribute) # 输出: 42 print("Second access took:", time.time() - start_time, "seconds")
在这个例子中,@functools.cached_property装饰器将cached_attribute方法转换为一个描述符(非覆盖型描述符),它会在第一次访问时执行方法体中的代码,并将结果缓存到实例的__dict__中。
后续的访问将直接从__dict__中获取缓存的结果,从而避免了重复的计算。
8. 如何理解如下这段话中的"非特殊方法可以被实例属性覆盖"?
函数和方法只实现了__get__方法,属于非覆盖型描述符。像my_obj.the_method = 7这样简单赋值之后,后续通过该实例访问the_method,得到的是数值7, 但是不影响类或其他实例。
然而,特殊方法不受这个问题影响。解释器只在类中寻找特殊方法,也就是说, repr(x)执行的其实是x.__class__.__repr__(x), 因此x的__repr__属性对repr(x)方法调用没有影响。出于同样的原因,实例的__getter__属性不会破坏常规的属性访问规则
这段话讨论的是在Python中描述符的覆盖问题。在Python中,描述符是通过实现特定的魔法方法(__get__, __set__, __delete__)来控制属性的访问、设置和删除行为的。
如果一个描述符只实现了__get__方法,那么它被称为非覆盖型描述符。这意味着,如果一个实例属性与描述符同名,那么这个实例属性会覆盖描述符。
让我们通过一个例子来理解这一点:
class Descriptor: def __get__(self, instance, owner): return 'get from descriptor' class MyClass: the_method = Descriptor() # 创建MyClass的实例 obj = MyClass() # 访问the_method,此时会调用描述符的__get__方法 print(obj.the_method) # 输出: get from descriptor # 为实例obj的the_method属性赋值 obj.the_method = 7 # 再次访问the_method,此时得到的是实例属性覆盖的值 print(obj.the_method) # 输出: 7 # 再创建个其他实例,可以验证,其他实例的返回值果然是不受影响的 obj2 = MyClass() print(obj2.the_method) # 不是7,依然是输出: get from descriptor
在这个例子中,MyClass有一个名为the_method的描述符。当我们第一次访问obj.the_method时,它会调用描述符的__get__方法,并输出"get from descriptor"。
然而,当我们为obj.the_method赋值后,这个实例属性就会覆盖描述符。因此,当我们再次访问obj.the_method时,我们得到的是数值7,而不是描述符返回的值。
这与特殊方法(如__repr__)不同。特殊方法是由解释器直接在类中寻找的,而不是通过实例的__dict__。因此,即使实例有一个与特殊方法同名的属性,也不会影响对特殊方法的调用。例如:
class MyClass: def __repr__(self): return 'MyClass object' # 创建MyClass的实例 obj = MyClass() # 调用特殊方法__repr__ print(repr(obj)) # 输出: MyClass object # 尝试覆盖特殊方法 obj.__repr__ = 7 # 再次调用特殊方法__repr__,仍然会调用类定义中的方法 print(repr(obj)) # 输出: MyClass object
在这个例子中,即使我们为obj.__repr__赋值,也不会影响repr(obj)的调用。这是因为repr(obj)实际上是调用obj.__class__.__repr__(obj),而不是直接访问实例的__dict__。
总的来说,非特殊方法(如描述符的__get__方法)可以被实例属性覆盖,而特殊方法则不会受到实例属性的影响。
9. 追问from 8: 对于最后一句话,如何理解。
出于同样的原因,实例的__getter__属性不会破坏常规的属性访问规则。
在Python中,属性的访问通常遵循特定的规则。当您访问一个对象的属性时,Python会首先检查该对象的__dict__字典中是否存在该属性。如果存在,则直接返回该值。如果不存在,Python会继续检查该对象的类定义中是否有该属性。如果类定义中有一个描述符(实现了__get__方法的对象),则调用描述符的__get__方法来获取属性值。
特殊方法,如__repr__,是由Python解释器直接在类中查找的,而不是通过常规的属性访问规则。这意味着,即使实例的__dict__中有一个与特殊方法同名的属性,也不会影响对特殊方法的调用。
__getter__属性是Python中的另一种特殊方法,它是属性装饰器@property的实现。@property装饰器允许您定义一个只读属性,并为该属性提供一个“getter”方法。这个“getter”方法实际上是一个特殊方法,它不会受到实例属性的影响。
让我们通过一个例子来理解这一点:
class MyClass: @property def my_property(self): return 'getter called' # 创建MyClass的实例 obj = MyClass() # 访问my_property,此时会调用getter方法 print(obj.my_property) # 输出: getter called # 尝试为实例的my_property属性赋值 obj.my_property = 'new value' # 再次访问my_property,此时仍然会调用getter方法 print(obj.my_property) # 输出: getter called
在这个例子中,my_property是一个只读属性,由@property装饰器提供。当我们尝试为obj.my_property赋值时,这个赋值操作会被忽略,因为my_property是一个只读属性。
当我们再次访问obj.my_property时,我们得到的是由getter方法返回的值,而不是我们尝试设置的值。
这个例子说明了实例的__getter__属性(即通过@property装饰器定义的getter方法)不会破坏常规的属性访问规则。
即使实例的__dict__中有一个与__getter__同名的属性,也不会影响对__getter__的调用。
这是因为@property装饰器定义的属性实际上是特殊方法,它们的调用是由Python解释器直接在类中处理的,而不是通过常规的属性访问机制。
10. 那如何理解 特性是覆盖性描述符,不能被实例属性覆盖。
在Python中,描述符(descriptor)是一种特殊对象,它定义了当属性被访问、修改或删除时的行为。描述符可以通过实现特定的魔法方法(__get__, __set__, __delete__)来工作。
如果一个描述符实现了__get__和__set__方法,那么它被称为覆盖性描述符(overriding descriptor)。
覆盖性描述符的特点是,当一个实例属性与描述符同名时,描述符的行为不会被实例属性所遮盖。也就是说,即使实例有一个与描述符同名的属性,描述符的__get__和__set__方法仍然会被调用。
下面是一个例子,说明如何实现一个覆盖性描述符:
class OverridingDescriptor: def __get__(self, instance, owner): return 'get from descriptor' def __set__(self, instance, value): instance.__dict__[self.__name__] = value class MyClass: overriding_attribute = OverridingDescriptor() overriding_attribute.__name__ = 'overriding_attribute' # 创建MyClass的实例 obj = MyClass() # 访问overriding_attribute,此时会调用描述符的__get__方法 print(obj.overriding_attribute) # 输出: get from descriptor # 为实例obj的overriding_attribute属性赋值 obj.overriding_attribute = 7 # 再次访问overriding_attribute,此时仍然会调用描述符的__get__方法 print(obj.overriding_attribute) # 输出: get from descriptor
在这个例子中,MyClass有一个名为overriding_attribute的覆盖性描述符。当我们第一次访问obj.overriding_attribute时,它会调用描述符的__get__方法,并输出"get from descriptor"。
当我们为obj.overriding_attribute赋值后,描述符的__set__方法会被调用,并将值存储在实例的__dict__中。
当我们再次访问obj.overriding_attribute时,__get__方法仍然会被调用,而不是直接从实例的__dict__中获取值。
这与非覆盖性描述符(只实现了__get__方法)不同,后者会直接从实例的__dict__中获取值,而不是调用描述符的方法。
总结来说,覆盖性描述符的特点是,即使实例有一个与描述符同名的属性,描述符的行为也不会被实例属性所遮盖。这种行为是由描述符的__get__和__set__方法实现的。

浙公网安备 33010602011771号