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
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'

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

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

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

事实上,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

现在的效果是这样的

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
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

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


上下文管理器

引入

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

如:

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

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

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

如:

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

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

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

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

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

with语句初识
with open("123.txt","r") as file:
    content=file.wirte("123")

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

这就是上下文管理语句。

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

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

with 上下文管理器类型函数 as obj_to_use:
    内部代码
上下文管理器类型对象

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

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

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

懂得这个过程后,我们可以来亲手模拟一下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()

我们注意到,在上面的代码中,__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
"""   
上下文管理器类型函数

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

from contextlib import contextmanager

@contextmanager
def manage_func(a,b,c):
    codes_before
    yield obj
    codes_after

要想实现上下文管理的功能,被@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("文件已成功关闭...")

效果:

with MyOpen("123.txt","r") as file:
    print("已成功打开!文件操作中...")
    file.write("123")
    
"""
文件打开中...
已成功打开!文件操作中...
not writable
文件已成功关闭...
"""

生成器

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

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

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

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

def func():
    #...
    yield
    #...
generator=func()
result1=next(generator)  #获取生成的第一个数据
result2=next(generator)	#获取生成的第二个数据
#...
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
"""

第一次使用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
生成器推导式

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

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

深浅拷贝

引用拷贝

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

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

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

var1=value
var2=var1
修改var2
  • 无论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
  • 如果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
浅拷贝

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
深拷贝

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  ----------最内层的地址也不一样

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木子希卡利

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值