python入门11——面向对象(进阶01):描述符及常用的魔法方法

一、描述符

描述符本质就是一个新式类,在这个新式类中,至少实现了__get____set____del__中的一个,亦称为“描述符协议”。

__get__():调用一个属性时触发。 __set__():为一个属性赋值时触发。 __del__():采用del删除属性时触发

1. 描述符的作用

描述符的作用是用来代理另外一个类的属性的(必须把描述符定义成这个类的类属性,不能定义到构造函数中)。

包含上述三个方法的新式类称为描述符,由这个类产生的实例进行属性的调用/赋值/删除,并不会触发这三个方法,如下实例所示:

class Foo:
    def __get__(self, instance, owner):
        print('触发get')

    def __set__(self, instance, value):
        print('触发set')

    def __delete__(self, instance):
        print('触发delete')

f1 = Foo()
f1.name = 'egon'
print(f1.name)  # egon
del f1.name
# 用该类的实例进行属性调用/赋值/删除是不会触发这三个方法的

只实现__get__方法的对象是非数据描述符,意味着在初始化之后它们只能被读取。而同时实现__get__和__set__的对象是数据描述符,意味着这种属性是可读写的。

2. 描述符的实例

一个简单的实例:

# 描述符Str
class Str:
    def __get__(self, instance, owner):
        print('Str调用')

    def __set__(self, instance, value):
        print('Str设置...')

    def __delete__(self, instance):
        print('Str删除...')


# 描述符Int
class Int:
    def __get__(self, instance, owner):
        print('Int调用')

    def __set__(self, instance, value):
        print('Int设置...')

    def __delete__(self, instance):
        print('Int删除...')


class People:
    name = Str() 
    age = Int()

    def __init__(self, name, age):  # name被Str类代理,age被Int类代理,
        self.name = name
        self.age = age

描述符Str的使用:

p1 = People('alex', 18)  # Str设置...  Int设置...

print(p1.name)  # Str调用  None
del p1.name  # Str删除...

描述符Int的使用:

p1 = People('alex', 18)  # Str设置...  Int设置...

print(p1.age)  # Int调用  None
p1.age = 18  # Int设置...
del p1.age  # Int删除...
3. 描述符的应用

【应用】先定义一个温度类,然后定义两个描述符类用于描述摄氏度和华氏度两个属性。要求两个属性会自动进行转换,也就是说我们可以给摄氏度这个属性赋值,然后打印的华氏度属性是自动转换后的结果。

# 摄氏度类
class Celsius:
    def __init__(self, value=26.0):
        self.value = float(value)  # 将初始摄氏度固定为26度并将类型改为float

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

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

# 华氏度类
class Fahrnheit:
    def __get__(self, instance, owner):
        return instance.cel * 1.8 + 32  # 将得到的摄氏度cel根据公式计算出华氏度

    def __set__(self, instance, value):
        instance.cel = (float(value) - 32) / 1.8  # 根据华氏度计算摄氏度cel

# 描述符类
class Temperature:
    cel = Celsius()  # cel是摄氏度实例
    f = Fahrnheit()  # f是华氏度实例


temp = Temperature()  # temp为描述符类实例
print(temp.cel)  # 可以查看摄氏度的值
print(temp.f)  # 并且可以直接计算出华氏度

temp.cel = 37.8  # 设置摄氏度的值
print(temp.f)  # 依据该值可计算出华氏度

该实例的运行结果为:
在这里插入图片描述
通过以上实例可以看出描述符具由许多优点:保护属性不受修改、属性类型检查和自动更新某个依赖属性的值等。
【应用】python是弱类型语言,即参数的赋值没有类型限制,下面我们通过描述符机制来实现类型限制功能。即通过描述符机制实现name参数只能是str类型。
为name属性定义一个(数据)描述符类,其中实现了__get__和__set__方法,如下:

class Type(object):
    def __init__(self):
        self.__name = None

    def __get__(self, instance, owner):
        print('此处是__get__方法!')
        return self.__name

    def __set__(self, instance, value):
        print('此处是__set__方法!')
        if isinstance(value, str):  # 使用isinstance方法判断是否为str类型
            self.__name = value
        else:
            raise TypeError("必须为字符串类型!")

定义一个测试类Test:

class Test(object):
    name = Type()  # 将name属性的代理权给Type类

当name值不是字符串时,会报TypeError: 必须为字符串类型!的错误。

t = Test()
t.name = 123  # 此时name为int型
print(t.name)

运行结果如下所示:
在这里插入图片描述
当输入的name符合要求,即为str类型时,运行结果如下:

t = Test()
t.name = 'zhai'  # 此处是__set__方法!
print(t.name)  # 此处是__get__方法! # zhai

二、setitem、getitem、delitem

如果在类中定义了__getitem__()方法,那么他的实例对象(假设为f)就可以这样f[key]取值。当实例对象做f[key]运算时,就会调用类中的__getitem__()方法。
一个完整的实例如下所示:

class Foo:
    def __getitem__(self, item):
        print('执行__getitem__()')
        print(self.__dict__[item])
        return self.__dict__  # get方法需要返回值

    def __setitem__(self, key, value):
        print('执行__setitem__()')
        self.__dict__[key] = value

    def __delitem__(self, key):
        print('当执行del obj[key]时,触发的是__delitem__()')
        self.__dict__.pop(key)

f1 = Foo()
print('-------实例化一个Foo对象f1-------')
print(f1.__dict__)
print('---------赋值给name和age--------')
f1['name'] = 'zhai'  # 注意和以前学的区分,以前是执行obj.key时会触发setattribute方法
f1['age'] = 18
print(f1.__dict__)
print('----------输出age的值----------')
print(f1['age'])
print('-----------删除name值----------')
del f1['name']
print(f1.__dict__)

运行结果为:
在这里插入图片描述

三、str、repr、format

1. __str__方法

__str__()可以改变对象的字符串显示格式。
当使用print输出对象的时候,若定义了__str__(self)方法,打印对象时就会从这个方法中打印出return的字符串数据。
每个类中都默认定义了__str__()方法,如下所示:

class Foo:
    pass

f = Foo()
print('类中默认有str方法,其输出是:')
print(f)  # 实际上是在触发str(f),本质上就是触发f.__str()__,因为该方法没有被重写,所以返回默认值
# 输出结果为:<__main__.Foo object at 0x00000163C0AB1E20>

我们可以通过重写该方法来控制打印信息的格式,如下:

class Foo:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):  # 该方法必须要return
        return '姓名:%s 年龄:%s' % (self.name, self.age)


f = Foo('zhai', 21)  # 实现了自己控制打印信息的格式
print(f)  # 内部逻辑是调用str()方法,其实就是执行f.__str__()
# 结果为:姓名:zhai 年龄:21
2. __repr__方法

repr能把一个对象用字符串的形式表达出来以便辨认,这就是“字符串表示形式”。repr 就是通过 repr 这个特殊方法来得到一个对象的字符串表示形式的。如果没有实现 repr,当我们在控制台里打印一个向量的实例时,得到的字符串可能会是 <Vector object at 0x10e100070>

如果方法中找不到str方法,但是存在repr方法,那么实例对象调用该方法时会按照repr中定义的格式输出。如下实例所示:

class Foo:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # def __str__(self):  # 该方法必须要return
    #     return '姓名:%s 年龄:%s' % (self.name, self.age)

    def __repr__(self):
        return '来自repr方法——姓名:%s 年龄:%s' % (self.name, self.age)

f = Foo('zhai', 21)
print(f)  # str(f)---->f.__str__()---->f.__repr__()
# 来自repr方法——姓名:zhai 年龄:21

如果repr方法和str方法同时存在于一个方法内部,那么执行时只显示按str格式输出的内容,实例如下:

class Foo:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):  # 该方法必须要return
        return '来自str方法——姓名:%s 年龄:%s' % (self.name, self.age)

    def __repr__(self):
        return '来自repr方法——姓名:%s 年龄:%s' % (self.name, self.age)

f = Foo('zhai', 21)
print(f)
# 来自str方法——姓名:zhai 年龄:21

repr 和 str 的区别在于,后者是在 str() 函数被使用,或是在用 print 函数打印一个对象的时候才被调用的,并且它返回的字符串对终端用户更友好。如果你只想实现这两个特殊方法中的一个,repr 是更好的选择,因为如果一个对象没有 str 函数,而 Python 又需要调用它的时候,解释器会用 repr 作为替代。

str函数或者print函数—>obj.str()
repr或者交互式解释器—>obj.repr()
如果__str__没有被定义,那么就会使用__repr__来代替输出

注意:这俩方法的返回值必须是字符串,否则抛出异常。

3. __format__方法

现在希望能输出多种格式的年月日,用以前学过的知识做出来如下所示:

class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

d = Date(2021, 8, 18)
x = '{0.year}年{0.month}月{0.day}日'.format(d)  # format()这个函数本质上就是在执行__format__()
y = '{0.year}/{0.month}/{0.day}'.format(d)
z = '{0.year}.{0.month}.{0.day}'.format(d)
print(x+'\n', y+'\n', z)
# 2021年8月18日
#  2021/8/18
#  2021.8.18

这种做法在输出用户想要的格式时需要不断手动挨个调整格式,于是我们想到一个更好的方法可以灵活的解决需求。我们可以自己定制一个输出格式,使用自定制格式化字符串__format__。具体代码实现如下所示:

# 将所有输出格式存放在字典数据中
format_dic = {
    'y.m.d': '{0.year}.{0.month}.{0.day}',
    'y/m/d': '{0.year}/{0.month}/{0.day}',
    'y年m月d日': '{0.year}年{0.month}月{0.day}日'
}

class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    def __format__(self, format_spec):
        # 当用户未选择格式或用户格式输入错误时,默认使用年月日格式
        if not format_spec or format_spec not in format_dic:
            format_spec = 'y年m月d日'
        fm = format_dic[format_spec]  # 在字典中取key值为format_spec对应的value值
        return fm.format(self)  # 返回要输出的格式


d = Date(2021, 8, 18)
print(format(d, 'y/m/d'))  # 2021/8/18
print(format(d))  # 2021年8月18日 (注:这里是没输入格式的情况)
print(format(d, 'y-m-d'))  # 2021年8月18日 (注:这里是格式输入错误的情况)

四、其他方法总结

1. __del__方法

析构方法,当对象在内存中被释放时,自动触发执行。
注:如果产生的对象仅仅只是python程序级别的(用户级),那么无需定义__del__,如果产生的对象的同时还会向操作系统发起系统调用,即一个对象有用户级与内核级两种资源,比如(打开一个文件,创建一个数据库链接),则必须在清除对象的同时回收系统资源,这就用到了__del__。
一个简单的例子:

class Foo:
    def __init__(self, name):
        self.name = name

    def __del__(self):
        print('正在执行__del__()')


f1 = Foo('zhai')
del f1.name  # 删除实例对象的属性是不会触发__del__方法的
print('------->')
# 运行结果为:
# ------->
# 正在执行__del__()   //执行完删除后触发,是因为该文件资源在内存中被释放自动触发的__del__

只有删除实例对象时才会触发__del__

f1 = Foo('zhai')
del f1  # 删除实例对象时才会触发__del__
print('------->')
# 运行结果为:
# 正在执行__del__()
# ------->

【应用场景】创建数据库类,用该类实例化出数据库链接对象,对象本身是存放于用户空间内存中,而链接则是由操作系统管理的,存放于内核空间内存中。当程序结束时,python只会回收自己的内存空间,即用户态内存,而操作系统的资源则没有被回收,这就需要我们定制__del__,在对象被删除前向操作系统发起关闭数据库链接的系统调用,回收资源。

2. 其他常见方法
方法名含义
__class__方法表示当前操作的对象的类是什么
__call__方法对象后面加括号会触发执行
__doc__方法获取到注释内容
__file__方法获取到当前的文件路径
__module__方法表示当前操作的对象在那个模块
__name__方法获取到函数的名称
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值