描述符示例详解

代码

这里要创建一个描述符,根据要求(如隐藏敏感信息、正确地设置日期的格式)对属性的值进行变换,并返回修改后的版本:

from dataclasses import dataclass
from datetime import datetime
from functools import partial
from typing import Callable


class BaseFieldTransformation:

    def __init__(self, transformation: Callable[[], str]) -> None:
        print("初始化")
        self._name = None
        self.transformation = transformation

    def __get__(self, instance, owner):
        print("get执行")
        if instance is None:
            return self
        raw_value = instance.__dict__[self._name]
        return self.transformation(raw_value)

    def __set_name__(self, owner, name):
        print("set_name执行")
        self._name = name

    def __set__(self, instance, value):
        print("set执行")
        instance.__dict__[self._name] = value


ShowOriginal = partial(BaseFieldTransformation, transformation=lambda x: x)
HideField = partial(
    BaseFieldTransformation, transformation=lambda x: "**redacted**"
)
FormatTime = partial(
    BaseFieldTransformation,
    transformation=lambda ft: ft.strftime("%Y-%m-%d %H:%M"),
)


@dataclass
class LoginEvent:
    username: str = ShowOriginal()
    password: str = HideField()
    ip: str = ShowOriginal()
    timestamp: datetime = FormatTime()

    def serialize(self) -> dict:
        return {
            "username": self.username,
            "password": self.password,
            "ip": self.ip,
            "timestamp": self.timestamp,
        }


if __name__ == '__main__':
    le = LoginEvent("john", "secret password", "1.1.1.1", datetime.utcnow())

>>> le = LoginEvent("john", "secret password", "1.1.1.1", datetime.
utcnow())
>>> vars(le)
{'username': 'john', 'password': 'secret password', 'ip': '1.1.1.1',
'timestamp': ...}
>>> le.serialize()
{'username': 'john', 'password': '**redacted**', 'ip': '1.1.1.1',
'timestamp': '...'}
>>> le.password
'**redacted**'


这段代码使用了 Python 中的描述符(Descriptor)和数据类(Data Class)来实现一个 LoginEvent 类,该类用于表示用户的登录事件,并包含了一些敏感信息。其中,BaseFieldTransformation 类是一个基类,它定义了描述符的通用行为。LoginEvent 类继承了 dataclass 装饰器,这使得该类具有自动生成 init__、__repr 等方法的特性。

BaseFieldTransformation 描述符有三个方法:

__init__: 在对象创建时被调用,用于初始化对象状态;
__get__: 在属性被访问时被调用,用于获取属性的值;
__set__: 在属性被赋值时被调用,用于设置属性的值;
__set_name__: 在类中定义属性时被调用,用于设置描述符名称。
在该代码中,我们定义了三个具体的 BaseFieldTransformation 子类:ShowOriginal、HideField 和 FormatTime。这些子类通过 partial 函数将 transformation 参数预设为不同的函数,以便对应不同的字段转换方式。例如,ShowOriginal 字段保持原样,而 HideField 字段使用 "redacted" 来代替其真实值。

最后,我们使用 LoginEvent 类创建了一个数据记录。该数据记录用于存储用户的登录事件,包括用户名、密码、IP 地址和时间戳。其中,用户名和 IP 地址字段使用 ShowOriginal 描述符,这意味着它们不会被转换。密码字段使用 HideField 描述符,这意味着它将被替换为 "redacted"。时间戳字段使用 FormatTime 描述符,这意味着它将按照指定格式进行格式化。登录事件还包括一个 serialize 方法,用于将该事件序列化为 dict 格式的数据。



示例

class CelsiusWithDescriptor:
    def __init__(self, temperature=0):
        print("CelsiusWithDescriptor初始化")
        self._temperature = temperature
        # print(self._temperature)  # 0

    def __get__(self, instance, owner):
        print("Getting value...")
        return self._temperature

    def __set__(self, instance, value):
        if value < -273:
            raise ValueError("Temperature below -273 is not possible")
        print("Setting value.......................")
        print(value)  # 25
        self._temperature = value


class TemperatureWithDescriptor:
    def __init__(self, celsius):
        self.celsius = celsius  # 描述符对应的值25

    def __str__(self):
        return f"{self.celsius} degrees Celsius is {self.fahrenheit:.2f} degrees Fahrenheit"

    @property
    def fahrenheit(self):
        return self.celsius * 1.8 + 32


class TemperatureDataModel:
    temperature = CelsiusWithDescriptor()

    def __init__(self, temperature):
        self.temperature = temperature
        # print(self.temperature == TemperatureDataModel.temperature)  # True
        # print(self.temperature)  # 25

    def __str__(self):
        """打印对象时会执行此函数"""
        return str(TemperatureWithDescriptor(self.temperature))


if __name__ == '__main__':
    """代码分析TemperatureDataModel(25) 实例化
    1.类的执行顺序,会先执行类中定义的变量属性等
    2.会先执行CelsiusWithDescriptor()的初始化__init__方法,此时它的self._temperature为0
    3.执行初始化TemperatureDataModel的__init__方法,此时temperature为25
    4.其中的self.temperature描述符实际是调用CelsiusWithDescriptor()中的__set__方法,并将上面的temperature的值25当做value传递进去
    5.经过上面的__set__方法后,CelsiusWithDescriptor对象的self._temperature为25.
    6.打印tmp
    7.会执行TemperatureDataModel类的__str__方法
    8.此时的self.temperature为25,当做参数传递给TemperatureWithDescriptor类
    9.会先执行当做参数传递给TemperatureWithDescriptor类的__init__方法
    10.初始化实例属性self.celsius = 25
    11.外面的str,会执行TemperatureWithDescriptor类中定义的__str__方法,所以最终返回25 degrees Celsius is 77.00 degrees Fahrenheit
    
    """
    tmp = TemperatureDataModel(25)
    # print(tmp)
    tmp.temperature = 50  # 跟上面一样,执行__set__
    # print(tmp.temperature)  # 执行__get__

    tmp1 = TemperatureDataModel(59)
    print(tmp1.temperature)  # 59
    print(tmp.temperature)  # 59


如上代码有问题:可以看出不同的对象,却打印了同一个值。原因是CelsiusWithDescriptor类中get和set魔法方法中它自己存储数据,
而不是将数据存储到每个对象中去所以才会出现上面的问题。需要修改CelsiusWithDescriptor类中的代码
class CelsiusWithDescriptor:
    def __init__(self, temperature=0):
        self.name = None
        self._temperature = temperature
        # print(self._temperature)  # 0

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        print("Getting value...")
        return instance.__dict__[self.name]  # 必须要使用__dict__

    def __set__(self, instance, value):
        if value < -273:
            raise ValueError("Temperature below -273 is not possible")
        print("Setting value.......................")
        instance.__dict__[self.name] = value  # 必须要使用__dict__

    tmp = TemperatureDataModel(25)
    # print(tmp)
    tmp.temperature = 50  # 跟上面一样,执行__set__
    # print(tmp.temperature)  # 执行__get__

    tmp1 = TemperatureDataModel(59)
    print(tmp1.temperature)  # 59
    print(tmp.temperature)  # 50
posted @ 2023-04-08 21:19  我在路上回头看  阅读(26)  评论(0编辑  收藏  举报