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实例的weightprice属性。

Descriptor__set__方法中,我们正确地将值存储在托管实例instance__dict__中。这样,每个LineItem实例都会有自己的weightprice属性值。换言之,各个实例的weight, price可以各不相同

如果我们错误地将值存储在描述符实例self__dict__中,那么所有的LineItem实例将会共享同一个weightprice换言之,所有实例的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 类中的 weightprice 属性。

当你在 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__ 方法,传入 ownerLineItemname'weight'

同样地,当执行 price = Quantity() 时,Python 会再次调用 __set_name__ 方法,但这次 name 参数是 'price'

__set_name__ 方法内部,你将 name 参数的值保存到了描述符实例的 self.storage_name 属性中。这就是 Quantity 类知道要把描述符绑定给哪个变量的关键所在。

当你后续在 LineItem 的实例上访问或修改 weightprice 属性时,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会按照以下顺序处理类定义体中的语句:

  1. 收集所有语句:Python首先会收集类定义体中的所有语句,包括简单语句、函数定义、类定义等,但不会执行它们。
  2. 执行可执行语句:接下来,Python会按照语句在类定义体中出现的顺序,逐一执行那些可执行的语句,比如简单赋值语句。
  3. 创建类对象:最后,Python会利用已执行的语句创建类对象。

在你的示例代码中:

class LineItem:
    weight = Quantity()
    print('weight.storage_name: ', weight.storage_name)  # <-- 这行代码
    price = Quantity()
    
    def __init__(self, description, weight, price):
        ...

当Python处理LineItem类定义时,会经历以下步骤:

  1. Python收集LineItem类定义体中的所有语句,包括weight = Quantity()、打印语句、price = Quantity()以及__init__方法。
  2. Python开始执行可执行语句。首先执行weight = Quantity()语句,创建一个Quantity实例,并调用其__set_name__方法,将'weight'赋值给self.storage_name属性。
  3. Python继续执行打印语句print('weight.storage_name: ', weight.storage_name)但是在这个时候,LineItem类还没有完全创建完毕。Python只是在执行LineItem类定义体中的可执行语句, 但还没有完成类对象的创建。因此,当你试图访问weight.storage_name时,weight实例虽然已经创建好了,但是它还没有被真正地绑定到LineItem类上。这就导致了AttributeError错误。
  4. Python继续执行price = Quantity()语句,创建另一个Quantity实例,并调用其__set_name__方法。
  5. Python执行__init__方法的定义,但不会调用它。
  6. 最后,Python利用已执行的语句创建LineItem类对象,并将weightprice属性正确地绑定到该类上。

所以,当你试图在类定义体内访问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__方法实现的。

 

posted @ 2024-03-25 19:46  AlphaGeek  阅读(20)  评论(0)    收藏  举报