【python进阶】描述符协议介绍和应用(@property、静态方法、类方法)

一、什么是描述符

描述符(Descriptor)是一种用于管理对象属性访问的特殊协议。描述符对象需要实现特定的方法,这些方法在属性访问时被自动调用。

1.1 描述符的作用

描述符主要用于以下几个方面:

  1. 属性控制:控制属性的读取、赋值和删除操作。
  2. 数据封装:通过描述符可以封装属性访问的细节,使得属性访问更加安全和灵活。
  3. 属性重用:描述符可以在不同的类中重用,减少代码重复。
  4. 动态属性:描述符可以在运行时动态地定义属性的行为。

1.2 怎么样才算是描述符?

只要类对象实现了__get__、__set__和__delete__三种方法的其中一种,就可以判定为是描述符。

1.3 描述符分类

描述符分为两种:数据描述符和非数据描述符。两者区分的点在于是否只实现__get__方法,如果是,则是非数据描述符。

1.3.1 数据描述符(Data Descriptors)

指的是实现了__get__和__set__方法的类对象。通常用于控制属性的访问和赋值。

注意,数据描述符的__dict__中不存储实际的数据,而是通过__set__方法来设置数据。

数据描述符的作用

  1. 控制属性赋值:可以控制如何给属性赋值,例如,进行类型检查或值验证。
  2. 重写属性值:可以重写属性值的存储方式,例如,存储在实例的__dict__之外的地方。
  3. 自定义属性行为:可以给属性添加额外的行为,比如调用属性时添加日志。

1.3.2 非数据描述符(Non-Data Descriptors)

指只实现了__get__方法的类。通常用于提供属性的默认值或实现属性的读取功能,但不控制属性的赋值。

非数据描述符的作用:

  1. 提供默认值
  2. 读取属性值:控制如何读取属性值
  3. 避免属性覆盖:防止实例属性覆盖类属性

1.3.3 数据描述符(Data Descriptors)

指的是实现了__get__和__set__方法的类对象。通常用于控制属性的访问和赋值。

注意,数据描述符的__dict__中不存储实际的数据,而是通过__set__方法来设置数据。

二、描述符协议

2.1 描述符协议的三个方法

Python中的描述符协议包括三个方法:包含__get__、__set__和__delete__。

  1. __get__(self, instance, owner):
    当访问属性时调用。instance 是访问属性的对象实例(如果是类属性则为None),owner 是对象的类。
    作用:返回属性的值。
  2. __set__(self, instance, value):
    当设置属性值时调用。instance 是对象实例,value 是被设置的值。
    作用:实现这个方法可以控制属性赋值。
  3. __delete__(self, instance):
    当删除属性时调用。instance 是对象实例。
    作用:实现这个方法可以控制属性的删除。

协议三个方法的示例:

# 定义数据描述符
class DataDescriptor:
    def __init__(self):
        self.value = None

    def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        self.value = value

    def __delete__(self, instance):
        del self.value

# 使用数据描述符
class MyClass:
    x = DataDescriptor() # 使用描述符,必须把描述符实例化存储在变量当中

# 触发描述符协议
myClass = MyClass()
print(myClass.x) # 点操作符->触发__get__()
myClass.x = 5 # 点操作符、赋值运算符=->触发__get__()、__set__()
del myClass.x # del操作符->触发__delete__()

在上述代码,描述符需要实例化后存储在MyClass类的变量当中(这是必须步骤)。那么是如何触发描述符协议的呢?

  1. 当x = DataDescriptor() 这就说明变量x实际上是描述符实例对象。
  2. 当对x点操作,就是触发__get__()。
  3. 对x赋值(=等号运算符),就是触发__get__()和__set__()。
  4. 对x del操作,就是触发__delete__()。

三、描述符应用

3.1 描述符实现动态返回

在普通的属性查找当中,通过点运算符在类字典中找到键值对。在描述符管理属性的属性查找中,点操作符符找到描述符实例,这个实例被它的方法标识,然后调用该方法返回a.y__get__10。

注意,这个值【10】不是预先存储的,而是当需要的时候会计算出来(动态的)。【描述符是动态返回】

示例:描述符返回一个常量

# 简单的举例:描述符返回一个常量
# class Ten:
#     def __get__(self, obj, objtype=None):
#         return 10
#
# class A:
#     x = 5
#     y = Ten()  # 为了使用描述器,必须在类里面存储在变量当中
#
# a = A()
# print(a.x)
# print(a.y)  # Descriptor lookup

示例:描述符实现动态运算

# import os
#
# class DirectorySize:
#
#     def __get__(self, obj, objtype=None):
#         print(os.listdir(obj.dirname))
#         return len(os.listdir(obj.dirname)) # 描述符动态运算结果
#
# class Directory:
#
#     size = DirectorySize()
#
#     def __init__(self, dirname):
#         self.dirname = dirname
#
#
# s = Directory('songs')
# print(s.size)

3.2 管理属性 – 描述符不存储管理属性的值

描述器被赋值了实例化对象的公共属性,并存储到描述符类字典中。然而真正的数据是存储在实例化对象的私有属性字典中。

示例:描述符将管理属性的值存储在其他类当中

# import logging
#
# logging.basicConfig(level=logging.INFO)
#
# 描述符定义
# class LoggedAgeAccess:
#
#     def __get__(self, obj, objtpye=None)
#         """obj是实例化对象"""
#         value = obj._age # # 这里就是描述符不存储管理属性的值,而是从Person obj实例化对象获取私有属性 # 这里存在问题:属性硬编码
#         logging.info('Accessing %r giving %r', 'age', value) # 日志记录每次访问私有属性
#         return value
#
#     def __set__(self, obj, value):
#         """obj是实例化对象"""
#         logging.info('Updating %r to %r', 'age', value) # 描述符增加其他额外的操作,例如日志记录每次更新私有属性
#         obj._age = value # 这里就是描述符不存储管理属性的值,而是将管理属性存进Person obj实例化对象的私有属性,也就是Person instance.__dict__
#
# class Person:
#
#     age = LoggedAgeAccess()
#
#     def __init__(self, name, age):
#         self.name = name # 常规的实例化属性定义
#         self.age = age  # 由描述符管理的属性  Calls __set__()
#
#     def birthday(self):
#         # Calls both __get__() and __set()__
#         # self.age触发__get__(),获取到私有属性
#         # 赋值符号触发__set__(),将age+1后,作为value传入__set__()
#         self.age += 1
#
# mary = Person('Mary M', 30)
# print(mary.age)
# mary.birthday()
# print(vars(mary)) # return object.__dict__:{'name': 'Mary M', '_age': 31}
#
# dave = Person('David D', 40)
# print(dave.age)
# dave.birthday()
# print(vars(dave))

3.3 管理属性硬编码问题 - 自定义名称(Customized names)

在3.2的代码里面其实是存在一个问题,就是属性硬编码问题,就是在描述符的__get__和__set__方法中,对实例化的属性获取是硬编码。
其实,当一个类使用多个描述符,这个类可以告诉每个描述符是给哪个变量使用。

当Person类被定义时,它会调用LoggedAccess中的__set_name__()函数,以便记录字段名称,为每个描述符分配自己的public_name和private_name。

注意,使用__set_name__方法,一个属性对应一个描述符!!!

示例:描述符使用__set_name__方法来给多个管理属性分配描述符

import logging

logging.basicConfig(level=logging.INFO)

class LoggedAccess:
    # 这段是核心代码,__set_name__为每个描述符定义公共属性和私有属性。一个管理属性一一对应一个描述符。
    def __set_name__(self, owner, name):
        self.public_name = name         # self.name
        self.private_name = '_' + name  # self._name

    def __get__(self, obj, objtype=None):
        value = getattr(obj, self.private_name)  # 没有硬编码,需要通过getattr来获取__dict__里的数据
        logging.info('Accessing %s giving %r', self.public_name, value)
        return value

    def __set__(self, obj, value):
        logging.info('Updating %r to %r', self.public_name, value)
        # setattr作用是给对象设置属性(setattr(x, 'y', v) is equivalent to ``x.y = v'')
        # self.private_name是获取变量名
        setattr(obj, self.private_name, value) # 没有硬编码,需要通过setattr来设置__dict__里的数据

class Person:

    name = LoggedAccess()  #  First descriptor instance
    age = LoggedAccess()   # Second descriptor instance

    def __init__(self, name, age):
        self.name = name  # Calls the first descriptor # 在定义阶段,将name字段传给第一个描述符,声明name字段对应第一个描述符
        self.age = age    # Calls the second descriptor # 在定义阶段,将age字段传给第二个描述符,声明age字段对应第二个描述符

    def birthday(self):
        self.age += 1


pete = Person('Peter P', 10)
print(vars(vars(Person)['name'])) # LoggedAccess: {'public_name': 'name', 'private_name': '_name'}
print(vars(pete)) # pete: {'_name': 'Peter P', '_age': 10}

四、基于描述符实现的内置函数

描述符只能在类变量工作,如果放进实例里面,就失效。描述符的主要动机是提供一个钩子,使存储在类变量中的对象能够控制在进行属性查找时发生的操作。

描述符解释了一个函数如何成为绑定函数。classmethod(), staticmethod(), property(), and functools.cached_property(),__slots__都是基于描述符实现。

五、基于描述符实现-Property

调用property()是一个比较简洁的途径去创建一个数据描述符。当访问属性时候会触发这个property()函数。

5.1 property()函数详解

5.1.1 property()签名

property(fget=None, fset=None, fdel=None, doc=None) -> property

示例:官方文档展示一个经典的用法,关于定义一个管理属性x

class C:
    def getx(self): # 外部定义的获取属性值的函数 
        return self.__x
        
    def setx(self): # 外部定义的设置属性值的函数
        return self.__x = value
        
    def delx(self): # # 外部定义的删除属性的函数
        del self.__x
    # 这里就是把三个外部定义好的函数传入property类并初始化。并将变量x设置为管理属性。
    x = property(getx, setx, delx, "i'm the 'x' property")

c = C()
c.x = 1
print(c.x)
del c.x

如果c是实例对象,c.x会调用getter方法,c.x=value会调用setter方法,del c.x会调用deleter方法。

上述代码的解析:

  1. 先定义一个普通的类C,然后分别定义getx、setx、delx方法,此时这三个方法仍然是普通方法。
  2. 当把这三个方法作为参数传进property类,并把x变量作为管理属性(也就是x是Property类对象)后,
    将C实例化c=C(),c.x 等同于 Property().x,触发__get__()方法(触发数据描述符协议),进而返回外界函数处理结果(return self.fget(obj))
class Property:  # property()的内部__get__()      
    def __get__(self, obj, objtype=None):
            if obj is None:
                return self
            if self.fget is None:
                raise AttributeError('unreadale attribute')
            # 在property()内部__get__()方法中,用外部定义好的fget()方法对实例对象进行操作
            # 既达到装饰效果,又不改变原代码逻辑,并沿用原代码逻辑

            return self.fget(obj)  

所以执行顺序如下!!!:

  • c=C()->c.x->__get__()->return self.fget(obj)
  • c=C()->c.x=1->__set__()->self.fset(obj, value)
  • c=C()->del c.x->__delete__()->self.fdel(obj)

5.2 Property类(property())的纯Python的等效函数

# 要了解property()是如何根据描述符协议实现的,以下有一个纯Python的等效函数
class Property:
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        """
        :param fget:外界定义好的获取属性的函数
        :param fset:外界定义好的设置属性的函数
        :param fdel:外界定义好的删除属性的函数
        :param doc:
        """
        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):
        """
        把变量x赋值为管理属性(x=Property()),类.x 会因为点操作符触发__get__方法(数据描述符)。
        在__get__()内,返回由外界定义的获取属性的函数处理结果,从而达到效果
        :param obj:
        :param objtype:
        :return:
        """
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError('unreadale attribute')
        return self.fget(obj)

    def __set__(self, obj, value):
        """
        把变量x赋值为管理属性(x=Property()),类.x 会因为赋值操作符(=)触发__set__方法(数据描述符)。
        在__set__()内,返回由外界定义的设置属性的函数处理结果,从而达到效果
        :param obj:
        :param value:
        :return:
        """
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        """
        把变量x赋值为管理属性(x=Property()),类.x 会因为del操作符(=)触发__delete__方法(数据描述符)。
        在__delete__()内,返回由外界定义的删除属性的函数处理结果,从而达到效果
        :param obj:
        :return:
        """
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    # setter、setter、deleter是Property类用作装饰器的时候,在定义阶段用于返回Property类的作用,并将外部定义的函数作为property方法的参数入参。
    def getter(self, fget):
        # fget外部函数
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        # fget外部函数
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        # fget外部函数
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

5.2.1 额外讲解:type()

type除了验证数据类型外,type(self)还可以被用来动态创建一个新的描述符对象。这里的 self 是一个描述符实例,type(self) 获取了这个实例的类型,也就是描述符类本身。

然后,使用这个类型(描述符类)创建了一个新的实例,将 fget、self.fset、self.fdel 和 self.__doc__ 作为参数传递给这个类型的构造函数。

这种做法允许你在运行时动态地创建新的描述符对象.这是一种元编程技术,允许在运行时动态地创建类实例.

5.3 @property装饰器讲解

@property装饰器和上面的property()的目的都是一致,@property装饰器是property()的更简化版。

5.3.1 @property装饰器的使用

class C:
    def __init__(self):
        self._x = None # property装饰的属性,要用私有属性来定义
        self.count = 0
    
    # property装饰器装饰的函数是以私有属性去掉前下划线"_"来命名函数名
    @property  
    def x(self):
        return self._x # 对私有属性操作

    @x.setter  # 上面先定义好管理属性,才能使用setter
    def x(self, value):
        self.count += 1 # 触发描述符协议时候,统计管理属性被赋值次数
        self._x = value # 对私有属性操作

    @x.deleter # # 先定义好管理属性,才能使用deleter
    def x(self):
        del self._x # 对私有属性操作
        
# 和上面使用property()一样的使用操作
c = C()
c.x = 10
c.x = 20
print(c.x)
print(c.count)

5.4 property()的作用

property()是一个更简洁的数据描述符。

  • 封装属性访问:property 允许你控制对属性的访问,可以在属性被读取、赋值或删除时执行代码。
  • 实现计算属性:你可以创建动态计算的属性,而不是存储在实例中的值。
  • 输入验证:在设置属性值时,property 允许你验证输入值,确保它们满足特定的条件。
  • 只读属性:property 可以用来创建只读属性,不允许外部代码修改属性值。

六、基于描述符实现-常规函数与实例方法

python面向对象特性是基于函数环境。使用非数据描述符。

存储在类字典的函数,当被调用的时候,会转变成方法。

方法和常规函数的区别在于对象实例会在其他参数前面。

这里揭示了函数是如何变成方法。

6.1 示例:MethodType底层逻辑的Python语言等效代码

class MethodType:
    "Emulate PyMethod_Type in Objects/classobject.c"
    # 这个是MethodType的内部实现的核心代码
    # 这段代码的作用是将函数和实例对象绑定一起,返回实例方法
    def __init__(self, func, obj):
        """
        func: 函数对象
        obj: 实例对象
        """
        self.__func__ = func
        self.__self__ = obj

    def __call__(self, *args, **kwargs):
        func = self.__func__
        obj = self.__self__
        # 将函数和实例绑定一起,转成方法对象 
        # func(self, *args, **kwargs)-> self=obj,也就是实例对象
        return func(obj, *args, **kwargs) 

6.2 示例:通过MethodType手动创建实例方法

# 定义非数据描述符
# Function对象就是函数对象本身,Function.__name__就是函数的名称
class Function:
    # 该函数只实现__get__(),是非数据描述符
    ...
    def __get__(self, obj, objtype=None):
        # "Simulate func_descr_get() in Objects/funcobject.c"
        if obj is None: # 如果实例对象为None,返回Function对象本身
            return self
        # 如果实例对象存在,则将函数和实例对象绑定后返回方法
        # self=Function=函数本身。所以这里传入了函数对象和实例对象。
        return MethodType(self, obj)  

6.3 实例方法的实战

class D:
    def f(self, x):
        return x

print(D.f.__qualname__) # D.f
# 通过类字典去访问函数不会触发__get__(),只会返回底层函数对象
print(D.__dict__['f']) # <function D.f at 0x000001B7EBA0CB80>
# 就算通过类对象去点查询函数,__get__()也只会返回底层函数对象
print(D.f) # <function D.f at 0x000002723B05CB80>
# 神奇的是,如果通过实例对象去点查询函数,__get__()会返回实例方法
d = D()
print(d.f) # <bound method D.f of <__main__.D object at 0x0000021C43DF8E80>>
print(d.f.__func__) # <function D.f at 0x0000021C43DECB80>
print(d.f.__self__) # <__main__.D object at 0x0000021C43DF8E80>
print(d.__dict__) # {} #为空说明了无论是函数还是方法,都是存储在类字典内,而非实例对象的字典内。

6.3.1 额外介绍:qualname

__qualname__的作用:用于表示对象的“限定名称”(qualified name),即对象的完整路径名,包括其在模块和类中的嵌套路径。

这个属性主要用于提供更清晰的名称解析,尤其是在处理嵌套函数和类时。
示例:

def outer_function():
    def inner_function():
        pass
    return inner_function

# 获取嵌套函数的限定名称
inner = outer_function()
print(inner.__qualname__)  # 输出: outer_function.inner_function

七 基于描述符实现-静态方法

静态方法返回未变动的底层函数。

7.1 静态方法特性

  1. 无论是实例对象或类对象都可以直接访问这个函数。调用像c.f或C.f都是直接查询的,object.__getattribute__(c,‘f’)或
    object.__getattribute__(C,‘f’)一样的效果。
  2. 静态方法的适用对象:不需要引用self变量,也就是不需要引用类对象本身。相当于把一个普通函数强行和一个类关联起来,
    但是这个普通函数也不需要依赖或引用这个类。
  3. 外部调用被静态方法装饰的方法和类方法装饰的方法的操作一致,真正区别在于内部静态方法不需要依赖类本身就可以实现函数。

示例:静态方法的调用,无论实例对象或类对象,结果都一样

class E:
    @staticmethod  # 经过静态方法装饰后,被装饰函数不需要传入self
    def normarl_func(x):
        return x * 10

    def e(self): 
        return self.normarl_func(3)  # 类内部可以引用静态方法修饰的方法

# print(E.normarl_func(3)) # 30
# print(E().normarl_func(3)) # 30
# print(E().e()) # 30

7.2 示例:静态方法-纯python代码解释静态方法底层代码

# Using the non-data descriptor protocol, a pure Python version of staticmethod() would look like this:
# 以下是python版的基于非数据描述符协议实现的staticmethod()
class StaticMethod:
    def __init__(self, f):
        """
        :param f: 被静态方法装饰的函数对象
        """
        self.f = f

    # 非数据描述符,只有__get__()
    def __get__(self, obj, objtype=None):
        # 直接返回类对象本身的函数,而非实例对象的函数。所以静态函数可以不需要传实例对象self
        return self.f

八、基于描述符实现-类方法Class methods

8.1 类方法特性

  1. 适用对象:当方法只需要引用类本身,而不依赖于存储在实例中的数据时,这种行为就很有用.(也就是不需要引用实例,只需要类本身即可)
  2. 类方法装饰的方法的引用,对于类对象或实例对象引用都是一样的,没区别。
  3. 类方法装饰器的目的是可以让类内部的方法可以调用类对象本身,而不需要实例化。(而静态方法是不需要依赖实例化,也不需要依赖类自身本身)
    示例:被类方法装饰器装饰过的函数,可以引用类本身的数据,而不需要实例化
class F:
    @classmethod  # 被classmethod装饰过的函数,可以直接引用类本身的东西,不需要依赖实例
    def f(cls, x):
        return cls.__name__, x  # cls.__name__ 返回F类自身的name

    def e(self):
        return self.f(3)

# 类方法的引用,对于类对象或实例对象引用都是一样的,没区别。
# print(F.f(3))  # ('F', 3)
# print(F().f(3)) # ('F', 3)
print(F().e())

8.2 类方法的其中一个用途 – 创建替代的类构造方法

例如,字典对象的fromkeys()方法,利用键的列表来创建新的字典,值都是一样。
示例:原fromkeys()的使用实战

keys = ['a', 'b', 'c']
new_dict = dict.fromkeys(keys)
print(new_dict) # {'a': None, 'b': None, 'c': None}

示例:现在用类方法来重写实现字典对象的fromkeys()

# The pure Python equivalent is:
class Dict(dict):
    @classmethod
    def fromkeys(cls, iterable, value=None):
        print(cls)  # <class '__main__.Dict'> -> cls()就是类本身
        d = cls() # 为什么cls()是dict对象,因为Dict继承了dict对象,所以cls()等价于d = dict(),相当于创建空字典
        for key in iterable:
            d[key] = value
        return d

# 通过类方法装饰fromkeys方法,让fromkeys方法内部可以使用dict类对象本身,从而创建一个新的空字典对象
d = Dict.fromkeys('abracadabra')
print(type(d) is Dict)
print(d) # {'a': None, 'b': None, 'r': None, 'c': None, 'd': None}

8.3 示例:类方法-纯python代码解释类方法底层代码

# Using the non-data descriptor protocol, a pure Python version of classmethod() would look like this:
class ClassMethod:
    def __init__(self, f):
        self.f = f

    def __get__(self, obj, cls=None):
        """
        obj:实例对象
        cls:类对象
        """
        if cls is None:
            cls = type(obj)
            
       # 该代码在python3.9增加,用于支持装饰器链
        if hasattr(type(self.f), '__get__'): 
            return self.f.__get__(cls)
        return MethodType(self.f, cls)

8.3.1 额外讲解 – classmethod装饰器链介绍

示例:

class G:
    @classmethod
    @property
    def __doc__(cls):
        return f'A doc for {cls.__name__!r}'

print(G.__doc__) # A doc for 'G'

九、总结

以上代码都是根据官方文档给出的代码进行学习和翻译。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值