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