• Python之property装饰器,上下文管理器与生成器



    property属性

    引入

    学习面向对象编程的时候,我们知道Python类中能够添加所谓的私有属性(以__开头属性)。这些私有属性在外部不可直接访问,需要通过我们在类里面提供的setter,getter接口方法来访问、设置。

    如:

    class Woman:
        def __init__(self,real_age):
            """用真实年龄初始化"""
            self.__age=real_age
            
        def get_age(self):
            """将私有属性__age加工后返回"""
            age = self.__age-4 if self.__age>18 else self.__age
            return age
        
        def set_age(self,value):
            """一定条件下(传入的年龄小于18岁)设置年龄"""
            if value<=18:
            	self.__age=value
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    girlfriend=Woman(20)  #girlfriend.__age=20
    print(girlfriend.get_age())   #返回girlfriend.__age-4
    #16
    girlfriend.set_age(21)   #传入年龄太大,设置girlfriend.__age失败
    print(girlfriend.get_age())   
    #16
    print(girlfriend.__age)
    #AttributeError: 'Woman' object has no attribute '__age'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这里的get_age(),set_age()一看就是方法,调用的时候还必须加(),让人很不爽,且很难达到掩盖返回的年龄不一定是真实年龄的目的(bushi)

    在这里,我们想让getter和setter方法操作起来更像属性(property)一样。如

    girlfriend.age=20  #调用set_age(20)
    print(girlfriend.age)   #调用get_age()
    
    • 1
    • 2

    事实上,Python有两种 将方法变成property,能够像访问属性一样调用方法 的途径。

    property装饰器

    Python内置了一个property装饰器,它能将类的getter性质的方法装饰成类的属性。

    有了类的属性后,还可以对应地通过属性名.setter装饰器来配置对应的setter方法

    于是,前面的代码就可以改造成:

    class Woman:
        def __init__(self, real_age):
            """用真实年龄初始化"""
            self.__age = real_age
    
        @property
        def age(self):
            """将私有属性__age加工后返回"""
            age = self.__age - 4 if self.__age > 18 else self.__age
            return age
    
        @age.setter
        def age(self, value):
            """一定条件下(小于18岁)设置年龄"""
            if value <= 18:
                self.__age = value
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    现在的效果是这样的

    girlfriend=Woman(20)  #girlfriend.__age=20
    print(girlfriend.age)   #调用@property装饰的age方法,返回girlfriend.__age-4
    #16
    girlfriend.age=21   #调用@age.setter装饰的age方法,传入年龄太大,设置girlfriend.__age失败
    print(girlfriend.age)   
    #16
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    property类

    其实前面的property装饰器是一个类装饰器。

    所以我们也可以直接通过property类来设置方法为属性

    class Woman:
        def __init__(self, real_age):
            """用真实年龄初始化"""
            self.__age = real_age
    
        def get_age(self):
            """将私有属性__age加工后返回"""
            age = self.__age - 4 if self.__age > 18 else self.__age
            return age
    
        def set_age(self, value):
            """一定条件下(传入的年龄小于18岁)设置年龄"""
            if value <= 18:
                self.__age = value
                
    	age=property(get_age,set_age)   
        #给Woman类添加属性age(本质上是一个property对象),get_age是getter,set_age是setter
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    属性名=property(getter,setter),这样也可以达到相同的效果


    上下文管理器

    引入

    我们有很多操作都要对对象进行打开、关闭的操作。

    如:

    #文件打开与关闭
    file=open("123.txt","r")
    content=file.read()
    file.close()
    
    • 1
    • 2
    • 3
    • 4

    如果打开了却不关闭,不仅会消耗内存,还可能会泄露或破坏数据。

    然而,在打开与关闭之间,程序可能会出现异常,导致程序在还未执行关闭操作时,就已经停止运行。

    如:

    file=open("123.txt","r")
    content=file.wirte("123")   #读模式却写,产生异常,程序停止
    file.close()   #还没来得及关闭文件
    
    • 1
    • 2
    • 3

    这种时候,我们当然可以秀一秀异常捕捉操作:

    file=open("123.txt","r")
    try:
    	content=file.wirte("123")
    except Exception as error_info:
        print(error_info)
    finally:
    	file.close()   #无论是否出现异常,都执行文件关闭操作
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    但是,这样的代码有点长。

    其实,Python专门有一个with上下文管理语句是来处理这种情况的。

    with语句初识

    with open("123.txt","r") as file:
        content=file.wirte("123")
    
    • 1
    • 2

    这样,无论with语句内部缩进的代码是否会出现异常,都会自动关闭file对象。再也不用我们自己操心。

    这就是上下文管理语句。

    上下文管理语句的基本组成是:

    with 上下文管理器类型对象 as obj_to_use:
        内部代码
    
    • 1
    • 2

    with 上下文管理器类型函数 as obj_to_use:
        内部代码
    
    • 1
    • 2

    上下文管理器类型对象

    上下文管理器类型的对象,指的是那些 实现了__enter__()__exit__()方法的类 实例化出来的对象。

    上下文管理器类型的对象配合上with语句后,在执行with内部缩进代码之前,会先执行__enter__()方法,返回一个操作对象。with内部代码执行完毕后(无论是否出现异常),就会调用__exit()__方法。

    with manage_object:
        #首先调用manage_object.__enter__(),即上文操作
        内部代码
        #无论内部代码执行是否异常,都调用 manage_object.__exit(),即下文操作
    
    • 1
    • 2
    • 3
    • 4

    懂得这个过程后,我们可以来亲手模拟一下with语句打开关闭文件。

    class MyFile(object):
    
        # 初始化方法
        def __init__(self, file_name, file_model):
            # 定义变量保存文件名和打开模式
            self.file_name = file_name
            self.file_model = file_model
    
        # 上文操作
        def __enter__(self):
            print("文件打开中...")
            # 返回文件资源
            self.file = open(self.file_name,self.file_model)
            return self.file
    
        # 下文操作,形参固定这样写即可
        def __exit__(self, exc_type, exc_val, exc_tb):
            print("文件已成功关闭...")
            self.file.close()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    我们注意到,在上面的代码中,__enter__会返回一个对象。其实,with语句中可选的as obj_to_use,正是给这个返回的对象起名字。

    一般而言,就像这里一样,__enter__上文方法负责返回with上下文管理语句操作的对象,__exit__下文方法负责销毁或关闭操作对象。

    with MyFile("123.txt", "r") as file:
        print("已成功打开!文件操作中...")
        file.write("123")
    """
    文件打开中...
    已成功打开!文件操作中...
    文件已成功关闭...
    Traceback (most recent call last):
      File "C:\Users\34684\code\html\miniweb\static\web_logging.py", line 24, in <module>
        file.write("123")
    io.UnsupportedOperation: not writable
    """   
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    上下文管理器类型函数

    上下文管理器类型函数,指的是那些被@contextmanager装饰过的函数。

    from contextlib import contextmanager
    
    @contextmanager
    def manage_func(a,b,c):
        codes_before
        yield obj
        codes_after
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    要想实现上下文管理的功能,被@contextmanager装饰的函数的代码必须由两部分组成。

    codes_before(上文操作的代码)和codes_after(下文操作的代码)。

    两部分代码之间用一条yield语句分隔。

    yield语句是Python生成器的标志(稍后会讲),它可以暂停函数执行,暂时返回一个值,这里是with语句中将要操作的对象。

    这之后,将会执行with语句内部代码,直到出现异常或执行完毕,返回到yield这一行,继续执行下文操作的代码。

    我们上面见过的open()函数,就是一种上下文管理器类型函数。

    from contextlib import contextmanager
    
    @contextmanager
    def MyOpen(file_name, file_model):
        try:
            print("文件打开中...")
            file=open(file_name,file_model)
        #上文操作
        #-------------------------------------    
        	yield file  #返回with语句中操作的对象
            #这之后执行with语句内的代码
            #直到执行完毕或出现异常
            #返回这里,继续执行下文操作
        #-------------------------------------
        #下文操作
        except Exception as error_info:
        	print(error_info)
    	finally:
    		file.close()
            print("文件已成功关闭...")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    效果:

    with MyOpen("123.txt","r") as file:
        print("已成功打开!文件操作中...")
        file.write("123")
        
    """
    文件打开中...
    已成功打开!文件操作中...
    not writable
    文件已成功关闭...
    """
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    生成器

    生成器,顾名思义,它是一个可以一个又一个生成数据的对象。

    在函数中使用了yield关键字,调用这个函数就会获得一个generator(生成器)对象。

    拿到这个生成器对象后,可以通过next()一个一个获取其生成的数据。

    生成器每次调用只生成一个数据,可以节省大量的内存。

    def func():
        #...
        yield
        #...
    generator=func()
    result1=next(generator)  #获取生成的第一个数据
    result2=next(generator)	#获取生成的第二个数据
    #...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    yiled关键字

    从某种程度上来说,yiled和return有一定的相似度。

    return标识函数的返回值。return返回后,直接退出函数代码。

    而yield标识生成器对象的生成数据。yield返回生成数据后,下一次next还能继续函数中的代码。

    如:

    def one_two():
        i = 1
        while True:
            if i % 2 == 0:
                print(f"第{i}次生成数据")
                yield "Yes"    #>>----------------------1
            else:
                print(f"第{i}次生成数据")
                yield "No"	#>>--------------------------2
            i += 1
            print(f"将开始第{i}次生成数据...")
    
    
    one_two_g = one_two()
    print(next(one_two_g))
    """
    第1次生成数据
    No
    """
    print(next(one_two_g))
    """
    将开始第2次生成数据...
    第2次生成数据
    Yes
    """
    print(next(one_two_g))
    """
    将开始第3次生成数据...
    第3次生成数据
    No
    """
    print(next(one_two_g))
    """
    将开始第4次生成数据...
    第4次生成数据
    Yes
    """
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    第一次使用next,代码进入第一次循环,执行到1处停止,返回生成的数据"No"

    第二次使用next,接着刚刚暂停的地方,即1处,继续往下执行,跳过else语句,打印“将开始第2次生成数据…”。

    开启第二次循环,经过判断语句运行到2处停止,返回生成的数据“Yes"

    第三次使用next,接着刚刚暂停的地方,即2处,继续往下执行,打印“将开始第3次生成数据…”。

    开启第三次循环,经过判断语句运行到1处停止,返回生成的数据“No"

    循环往复

    从上面的例子来看,无限循环和生成器的相性很高。

    如果函数内部没有无限循环,生成器的生成次数(对应next使用次数)就会有限。如果超过这个生成次数再使用next,解释器将会报StopIteration异常。

    for循环与生成器

    生成器也是一种可迭代对象,它可与for循环搭配使用。

    def my_range(begin, end, step):
        i = begin
        while True:
            if i < end:
                yield i
            else:
                break
            i += step
            
    for i in my_range(1,10,2):    
        #my_range(1,10,2)返回一个生成器对象
        #for循环自动一次次读取生成器对象生成的数据,交给i
        print(i, end=" ")
    
    #1 3 5 7 9
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    生成器推导式

    就像列表推导式和字典推导式那样,生成器也支持推导式这种极具Python特色的语法。

    generator=(i for i in range(3))  #生成器推导式以()标识。注意,没有元组推导式这种东西
    print(next(generator))
    #0
    print(next(generator))
    #1
    print(next(generator))
    #2
    print(next(generator))
    #StopIteration
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    深浅拷贝

    引用拷贝

    变量“保存”值,其实是保存了值在计算机内存中的位置(地址)(可用id唯一表示),真正的说法应该是,变量通过地址引用了值。

    数据类型分为可变和不可变类型。不可变类型的值不会在原地址上修改,可变类型的值会在原地址上修改。

    在Python的基础数据类型中,int、float、str、tuple属于不可变类型,list、dict、set属于可变类型。

    var1=value
    var2=var1
    修改var2
    
    • 1
    • 2
    • 3
    • 无论var1的数据类型如何,var2=var1都是将var1的引用赋给var2
    • 如果var1是不可变类型,在修改var2时,计算机会在内存中开辟一片空间,用于储存新值。var2会抛弃对value的地址的引用,并将自己的引用修改为新值在内存的位置。(具体表现为id改变;修改var2与var1无关)
    var1=1
    print(id(var1))
    #2669384237296
    var2=var1
    print(id(var1))
    print(id(var2))
    #2669384237296
    #2669384237296
    var2=2
    print(var1)
    print(id(var1))
    #1
    #2669384237296
    print(var2)
    print(id(var2))
    #2
    #2669384237328
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 如果var1是可变类型,在修改var2(不是重新赋值)时,计算机会在原来的内存地址上直接对value进行修改。注意,这时var2引用的值的地址是不变的。(具体表现为id不变;修改var2,var1也会变)。
    var1=[1,2,3]
    print(id(var1))
    #2012908440768
    var2=var1
    print(id(var1))
    print(id(var2))
    #2012908440768
    #2012908440768
    var2.append(4)
    var2[2]=6
    print(var1)
    print(id(var1))
    #[1, 2, 6, 4]
    #2012908440768
    print(var2)
    print(id(var2))
    #[1, 2, 6, 4]
    #2012908440768
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    浅拷贝

    Python内置模块copy中的copy函数可以部分地打破引用拷贝

    它能够真正复制可变数据的最外层。即让计算机再找一片空间,储存可变数据的最外层中的不可变数据。

    浅拷贝可完全拷贝简单的单层的list、dict、set。

    import copy
    
    var1 = [1, 2, 3, [1, 2, 3]]
    print(id(var1))
    # 1393005601152
    var2 = copy.copy(var1)
    print(id(var1))
    print(id(var2))
    # 1393005601152
    # 1393005845824    ---------浅拷贝与引用拷贝的区别
    var2.append(4)
    var2[0] = 6
    print(var1)
    print(id(var1))
    # [1, 2, 3, [1, 2, 3]]
    # 1393005601152
    print(var2)
    print(id(var2))
    # [6, 2, 3, [1, 2, 3], 4]
    # 1393005845824
    var2[3].append(4)
    print(var1)
    print(id(var1))
    print(id(var1[3]))
    # [1, 2, 3, [1, 2, 3, 4]]
    # 1393005601152
    # 1392714992832
    print(var2)
    print(id(var2))
    print(id(var2[3]))
    # [6, 2, 3, [1, 2, 3, 4], 4] ----------内层列表依旧是相关联的(引用拷贝)
    # 1393005845824
    # 1392714992832
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    深拷贝

    Python内置模块copy中的deepcopy函数可以彻底地打破引用拷贝

    它能够真正复制可变数据,无论是否嵌套。即让计算机再找一大片空间,一重重地进行浅拷贝,直到最内层全是不可变类型数据。

    不过要消耗多一点的内存。

    import copy
    
    var1 = [1, 2, 3, [1, 2, 3]]
    print(id(var1))
    # 1922288502656
    var2 = copy.deepcopy(var1)
    print(id(var1))
    print(id(var2))
    # 1922288502656
    # 1922288681920    ---------外层地址不同
    var2.append(4)
    var2[0] = 6
    print(var1)
    print(id(var1))
    # [1, 2, 3, [1, 2, 3]]  --------他还是曾经那个少年,没有一丝丝改变
    # 1922288502656
    print(var2)
    print(id(var2))
    # [6, 2, 3, [1, 2, 3], 4]
    # 1922288681920
    var2[3].append(4)
    print(var1)
    print(id(var1))
    print(id(var1[3]))
    # [1, 2, 3, [1, 2, 3]]
    # 1922288502656
    # 1924142419136 ----------最内层的地址也不一样
    print(var2)
    print(id(var2))
    print(id(var2[3]))
    # [6, 2, 3, [1, 2, 3, 4], 4]
    # 1922288681920
    # 1922288682112  ----------最内层的地址也不一样
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

  • 相关阅读:
    226. 翻转二叉树
    高防服务器是怎样进行防御的?
    synchronized的工作原理
    KaiwuDB 受邀亮相 2023 中国国际“软博会”
    相关性分析-Pearson, Spearman, Kendall 三大相关系数+绘制热力图
    这个好用的办公网优化工具,官宣免费了
    一桩字符串长度引发的惨案
    【单细胞高级绘图】10.KEGG富集结果的圆圈图
    springboot幼儿园书刊信息管理系统毕业设计源码141858
    Excel导入和导出
  • 原文地址:https://blog.csdn.net/ncu5509121083/article/details/125546583