python支持面向对象的编程技术,python面向对象编程的三大特性
本文分为以下几个部分。
简介——以批量使用@property为例介绍函数描述符的基本理论和简单实例描述符的调用机制的细节。实例方法、静态方法和类方法的描述符原理。属性装饰器原理描述符的应用参考资料
简介正如我们在以前的python面向对象文章中提到的,我们可以用@property decorator将方法包装成属性。与其他属性相比,这种属性的优点是在给属性赋值时能够检查变量。示例代码如下:
A类:
def __init__(自己,名字,分数):
Self.name=name #公共属性
自我。_score=分数
@属性
定义分数(自我):
回归自我。_分数
@score.setter
定义分数(自我,价值):
打印(“在此设置分数”)
if isinstance(value,int):
自我。_score=值
否则:
打印(请输入一个整数)
a=A(鲍勃,90)
a .姓名# 鲍勃
得分# 90
a.name=1
A.name # 1,名字本身不应该被允许赋值为数字,但是这里不能控制它的赋值。
得分=83
a .分数# 83,当被赋予一个数值时,可以平稳运行。
a.score=bob #请输入整数
A.score # 83,当分配给一个字符串时,分数不变。当我们有很多这样的属性时,如果我们对每一个属性都使用@property,那么代码就太多余了。如下
A类:
def __init__(自己,姓名,分数,年龄):
Self.name=name #公共属性
自我。_score=分数
自我。_age=年龄
@属性
定义分数(自我):
回归自我。_分数
@score.setter
定义分数(自我,价值):
打印(“在此设置分数”)
if isinstance(value,int):
自我。_score=值
否则:
打印(请输入一个整数)
@属性
定义年龄(自己):
回归自我。_年龄
@age.setter
定义年龄(自我,价值):
打印(“在此设置年龄”)
if isinstance(value,int):
自我。_age=值
否则:
打印(请输入一个整数)
A=A(Bob ,90,20)因为每个测试的方法都是一样的,所以最好有一个方法可以批量实现,只写一次if isinstance。描述符可以用来实现这一点。
为了更清楚的理解描述符是如何实现的,我们就跳出这个话题,先说说描述符的基本理论。
描述符基本理论和简单实例描述符功能强大,应用广泛。它可以控制我们访问属性和方法的行为。是@property,super,static方法,class方法甚至是属性和实例背后的实现机制。是比较低级的设计,所以会比较难理解。
定义:从创建描述符开始,在一个类中定义了__get__ 、__set__ 、__delete__中的一个或多个,这个类的一个实例可以称为描述符。
为了真正理解描述符是什么,让我们首先看一个最简单的例子。这个例子没有实现任何功能,只是使用了一个描述符。
#创建一类描述符,它的实例是一个描述符
#这个类应该有方法_ _ get _ _ _ set _ _
#这个类是作为工具使用的,不是单独使用的。
M类:
def __init__(self,x=1):
self.x=x
def __get__(自身,实例,所有者):
返回self.x
def __set__(自身,实例,值):
self.x=值
#调用描述符的类
AA类:
M=M() # m是一个描述符。
aa=AA()
aa.m # 1
aa.m=2
Aa.m # 2我们来分析一下上面的例子。
创建一个aa实例和普通的类没有什么不同。我们从aa.m开始,我们看到aa.m是一个被aa实例调用的类属性。但是这个类属性不是一个普通的值,而是一个描述符,所以我们从访问这个类属性改为访问这个描述符。如果调用时得到一个描述符,Python会自动在内部触发一组使用机制。如果它访问,它将自动触发描述符的__get__方法。如果它修改了设置,它将自动触发描述符的__set__方法。这里aa.m触发了__get__方法并获取了self.x的值在前面的__init__中,1 aa.m=2的值触发了__set__方法,赋值2被传入value参数,改变了self.x的值,因此aa.m的下一次调用的值也发生了变化。进一步考虑:在访问属性时,我们不能直接给一个值,而是连接一个描述符,让它在访问和修改设置时自动调用。在__get__方法和__set__方法中进行一些处理后,可以更改操作属性的行为。这就是描述符的作用。
我相信有些读者已经认为介绍部分的例子是通过使用描述符来实现的。在讨论如何实现之前,我们需要了解更多关于描述符的调用机制。
描述符的调用机制,aa.m命令,其实就是寻找M属性的过程。程序会先找哪里,如果没有的话会找哪里?有一个顺序,这意味着需要使用__dict__方法来解释访问顺序。
先看下面的代码理解__dict__方法。
C类:
x=1
def __init__(self,y):
self.y=y
定义乐趣(自我):
打印(自我)
c=C(2)
#实例的属性是什么?
print(c.__dict__) # {y: 2}
#类的属性是什么?
Print(C.__dict__) #中带有x fun
打印(类型(c))。__dict__) #同上
Print(vars(c)) # __dict__也可以换成vars函数,它的作用完全一样。
#打电话
第二名
c.__dict__[y] # 2
#类型(c)。_ _ dict _ [fun] () #报错,说明函数不是这样调用的。__dict__方法返回一个字典,类和实例都可以调用它。关键是类或实例拥有的属性和方法。您可以使用这个字典来访问属性,但是不能像这样直接访问方法。原因我们以后再说。
我们来讨论一下调用aa.m时的访问顺序
程序首先会找出aa是否。_ _ dict _ [m]存在与否,然后在type(aa)中查找。_ _ dict _ [m]然后找到(aa)类型的父类。如果找到描述符,它将调用__get__方法。让我们来看看_ _
M类:
def __init__(self):
self.x=1
def __get__(自身,实例,所有者):
返回self.x
def __set__(自身,实例,值):
self.x=值
#调用描述符的类
AA类:
M=M() # m是一个描述符。
n=2
def __init__(self,score):
self.score=分数
aa=AA(3)
打印(aa。__dict__) # {score: 3}
打印(aa.score) # 3,看aa。__dict__,找到分数直接返回
打印(aa。__dict__[score]) # 3、上面的调用机制其实是这样的
打印(类型(aa))。__dict__) #其中有N和M
Print(aa.n) # 2,aa里找不到N。__dict__,所以我在type(aa)里找到了N。__dict__并返回其值。
打印(类型(aa))。_ _ dict _ _ [n]) # 2,其实就是上面那个的调用机制。
Print(aa.m) # 1,aa里找不到N。__dict__,所以我在type(aa)里找到了M。__词典_ _
# m是描述符对象,所以调用__get__方法返回self.x的值,即1。
打印(类型(aa))。_ _ dict _ _ [m]。_ _ get _ _ (aa,aa)) # 1,上面那个调用如下
在# __get__的定义中,除了self之外,还有instance和owner,它们实际上代表了描述符所在的实例和类。这里的细节我们以后再说。
打印(-*20)
Print(AA.m) # 1,以同样的方式调用描述符。
打印(aa。_ _ dict _ _ [m]。_ _ get _ _ (none,aa)) # class相当于调用这个。此外,还有与描述符类型相关的特殊情况。
同时定义__get__和__set__方法的描述符称为数据描述符,只定义__get__的描述符称为非数据描述符。两者的区别是:当属性名和描述符名相同时,在访问这个同名属性时,如果是数据描述符就先访问描述符,如果是非数据描述符就先访问属性。例如,有# both __get__和
M类:
def __init__(self):
self.x=1
def __get__(自身,实例,所有者):
Print(get m here) #打印一些信息以查看何时调用此方法
返回self.x
def __set__(自身,实例,值):
Print(set m here) #打印一些信息以查看何时调用此方法
Self.x=value 1 #在这里设置一个1可以更清楚的知道调用机制。
# Only __get__是非数据描述符。
N类:
def __init__(self):
self.x=1
def __get__(自身,实例,所有者):
Print (get here) #打印一些信息以查看何时调用此方法。
返回self.x
#调用描述符的类
AA类:
M=M() # m是一个描述符。
n=N()
def __init__(self,m,n):
Self.m=m #属性m与描述符m同名,调用时有些冲突。
Self.n=n #不是数据描述符,与m相比。
aa=AA(2,5)
打印(aa。__dict__) #只有N没有M,因为当数据描述符同名时,不会访问属性,而是直接访问描述符,所以在属性中找不到属性M。
打印(AA。__dict__) # m和N都有
Print(aa.n) # 5,当一个非数据描述符同名时,调用属性,就是传入的5
Print(AA.n) # 1,如果被类访问,它调用描述符并返回self.x的值
Print(aa.m) # 3其实在aa=AA(2,5)处创建实例时,进行了属性赋值,相当于aa.m=2。
#但是aa调用M时,并不是按照惯例调用属性M,而是调用数据描述符M。
#所以在定义实例aa的时候,实际上触发了M的__set__方法,将2传递给value,self.x就变成了3。
# aa.m也被调用时访问描述符,返回self.x的结果,为3。
#实际上,您可以通过查看打印的信息来判断__get__和__set__何时被调用。
Aa.m=6 #另外,给属性赋值也调用m的__set__方法。
Print(aa.m) # 7,调用__get__方法
打印(-*20)
#在代码中显式调用__get__方法
打印(AA。__dict__[n]。__get__(无,AA)) # 1
打印(aa。_ _ dict _ _ [n]。__get__ (aa,aa)) # 1注意:要创建只读数据描述符,需要同时定义__set__和__get__并在__set__中抛出AttributeError异常定义一个抛出异常的__set__方法就足以使描述符成为数据描述符。
描述符的详细信息本节分为以下两部分
__set__方法1中调用描述符__get__的原理及参数说明。一、调用描述符的原理。当调用一个属性并且该属性指向一个描述符时,为什么要调用这个描述符?其实这是由对象控制的。__getattribute__()方法,其中object是定义新类时默认继承的类,即py2编写的类(新定义的类继承object类,object类也继承__getattribute__方法。当访问b.x这样的属性时,会自动调用这个方法__getattribute__()。定义如下
def __getattribute__(self,key):
在Objects/typeobject.c 中模拟type_getattro()
v=对象。__getattribute__(self,key)
if hasattr(v, __get__ ):
return v.__get__(None,self)
上面对return v的定义说明,如果b.x是描述符对象,也就是能找到__get__方法,就会调用这个get方法,否则就用普通属性。如果在类中重写__getattribute__会改变描述符的行为,甚至关闭描述符的功能。
2._ _ get _ _和__set__方法中参数的说明。官网注明了这三个方法需要传入哪些参数,以及这些方法的返回结果是什么,如下图。
描述。__get__(self,obj,type=None) - value
描述。__set__(self,obj,value) -无
描述。_ _ delete _ _ (self,obj)-none我们需要知道的是self obj类型的值是什么。看下面这个例子。
M类:
def __init__(self,name):
self.name=name
def __get__(self,obj,type):
Print(获取第一个参数self:,self.name)
Print(获取第二个参数obj:,obj.age)
Print(获取第三个参数类型: ,type.name)
def __set__(self,obj,value):
obj。__dict__[self.name]=value
A类:
name=鲍勃
m=M(“年龄”)
def __init__(自身,年龄):
年龄=年龄
年龄是20岁
上午
#获取第一个参数self: age
#获取第二个参数obj: 20
#获取第三个参数类型:Bob
上午=30
a .年龄# 30总结如下
Self是描述符类中的实例obj m是调用描述符类中的实例类型a是调用描述符类Avalue是赋值这个属性时传入的值,也就是上面的30。上面的代码逻辑如下
A.m .访问描述符并调用__get__方法。三次打印分别调用m.name a.age A.name a.m=30并调用__set__方法,使A(即obj)的属性中的 age (即M(age )在这里传入的self.name)为30。
示例、静态方法和类方法的描述符原理本节解释了访问某些方法实际上是访问描述符,并解释了它们的调用顺序,以及类方法和静态方法描述符的python定义。
B类:
@classmethod
def print_classname(cls):
打印(“鲍勃”)
@静态方法
def print_staticname():
打印(“我的名字是鲍勃”)
def print_name(自身):
打印(“此姓名”)
b=B()
B.print_classname() #调用类方法
B.print_staticname() #调用静态方法
B.print_name() #调用实例方法
Print(B.__dict__) #有实例方法,静态方法,类方法#但实际上字典里并没有可以直接调用的函数。
print(b . _ _ dict _ _[ print _ class name ])
Print(b.print_classname) #与上面的不同
print(b . _ _ dict _ _[ print _ static name ])
Print(b.print_staticname) #与上的不同
print(B.__dict__[print_name])
Print(b.print_name) #与上面的不同
#0x 0000024 a 92 da 67 b 8处的classmethod对象
类“__main__”的# bound方法B.print_classname。 b
#0x 0000024 a 92 da 6860处的staticmethod对象
# function B.print_staticname位于0x0000024A92D889D8
# function b . print _ name at0x 0000024 a 92d 88158
# bound method b . print _ name of _ _ main _ _。b对象at0x000024a92da6828以上结果表明,直接调用实例时,类方法和实例方法都是boundmethod,而静态方法是function。因为静态方法本身就是类中定义的函数,不属于方法的范畴。
另外,直接调用实例后得到的结果可以直接跟一个括号,作为函数调用。但使用字典调用时,得到的结果与实例调用不同,不能直接作为带括号的函数使用。
事实上,从显示的结果中我们可以看到,staticmethods和classmethods实际上是被字典调用来获取两个类的对象,即static method和class method。这两个类实际上是定义描述符的类,所以字典访问的两个方法都获得描述符对象。它们需要一个__get__方法,后跟括号作为函数调用。
而普通的实例方法是由dictionary调用得到一个函数,也就是函数。理论上可以用括号直接调用,但是调用的时候说缺少self参数。其实也是一个描述符对象,通过__get__方法传入self来调用。
这三个方法本质上调用__get__方法,如下所示。
B.__dict__[print_classname]。__get__(None,B)()
B.__dict__[print_staticname]。__get__(None,B)()
B.__dict__[print_name]。__get__(b,B)()
print(b . _ _ dict _ _[ print _ class name ]。__get__(无,B))
print(b . _ _ dict _ _[ print _ static name ]。__get__(无,B))
print(B.__dict__[print_name])
Print (b. _ _ dict _ _ [print _ name]。_ _ get _ _ (none,b)) #这是没有传入实例的情况,也就是self,和直接从字典中调用结果是一样的。在python2中它是一个未绑定的方法。
print(B.__dict__[print_name]。__get__(b,B))
# B.print_name() #报告了一个错误,表示缺少一个self参数。
# B.print_name(B()) #这个名称输入实例不会报错,所以我们平时调用的方法本质上都是调用描述符对象,在访问描述符时自动调用__get__方法。
在调用上面的时候,注意到前两个__get__的第一个参数是None,而实例方法是a B,这是因为实例方法需要一个具体的实例来调用,不能被类直接调用。在python2中,用class直接调用instance方法得到一个未绑定的方法,用instance调用是一个绑定的方法(在python3中,未绑定方法的概念被删除,改为函数),class方法本身可以被class调用,所以当参数为None时是一个绑定的方法。所以说__get__的第一个参数使用B可以理解为方法的绑定过程。
因为这三个方法都调用描述符对象,所以这些对象是它们各自类的实例。它们的类是如何定义的?python中这些类的定义是由底层C语言实现的。为了理解它的工作原理,这里有一个用python语言实现classmethod decorator的方法,(源码),就是构建一个可以生成class方法对应的描述符对象的类。
类myclassmethod(对象):
def __init__(self,method):
自我方法=方法
def __get__(self,instance,cls):
返回lambda *args,**kw: self.method(cls,*args,**kw)
类别Myclass:
x=3
@myclassmethod
def方法(cls,a):
打印(cls.x a)
m=Myclass()
Myclass.method(a=2)我们来分析一下上面的代码。
我们看到使用@myclassmethod decorator和使用@classmethod decorator没有区别。首先,定义了myclassmethod类,其中使用了__get__方法,因此它的实例将是一个描述符对象。myclassmethod充当方法函数的装饰器。根据装饰者的知识,相当于设置method=myclassmethod(method),调用Myclass.method()调用改变后的method方法。也就是Myclass method(method)(a)myclassmethod(method)这是my class method的一个实例,也就是一个描述符。这里access调用__get__方法,返回一个匿名函数__get__,这个函数实际上是把owner(cls)部分传入method方法,因为methon是在我的类中调用的,这个owner就是我的类。这一步其实是提前引入了method的第一个参数cls,后面的参数A是由myclassmethod(method)(a)的第二个括号调用的。仔细分析上面的定义和调用过程会发现,我们经常说一个类方法的第一个参数是cls,其实是不对的。第一个参数可以是任意的,而且只占第一个位置,用来接收类实例引用类属性,可以改成任意变量。例如,以下代码正常运行类Myclass:
x=3
@classmethod
定义方法(b,a):
打印(黑白)
m=Myclass()
Myclass.method(a=2) # 5我们来看看staticmethod类的等价python定义(source)。
类mystaticmethod:
def __init__(self,可调用):
self.f=可调用
def __get__(self,obj,type=None):
回归自我
类别Myclass:
x=3
@mystaticmethod
定义方法(a,b):
打印(a b)
m=Myclass()
M.method(a=2,b=3)注意:从源的角度理解静态方法和类方法。
静态方法相当于不自动传入实例对象作为方法的第一个参数,类方法相当于将默认传入的第一个参数从实例改为类。使用@classmethod后,不管是类调用还是实例调用,都会自动作为第一个参数传递给class,不需要手动传入就可以调用类属性。但如果没有@classmethod,则需要手动传入没有@classmethod或@staticmethod的类,这样调用类时参数不会自动传入,调用实例时实例会自动作为第一个参数传入。所以添加了@classmethod,以便更容易调用类属性。添加@staticmethod是为了防止自动引入实例的干扰。另外需要注意的是,当属性和方法同名时,调用会自动访问属性,因为这些方法调用的描述符都是非数据描述符。当我们使用@property decorator时,新定义的get set方法会被自动调用,因为@property decorator是一个数据描述符。
下面是属性装饰器的原理。我们可以谈谈开头提出的问题,即@property decorator是如何使用descriptor实现的,调用机制是什么,如何通过descriptor多次简化和使用@property decorator。
首先要明确,属性有两种调用形式,一种是decorator形式,一种是类似函数形式。下面的例子将分别说明这两种形式的调用机制。
在下面贴上等价的python对属性的定义(来自官方网站的中文翻译)
类别属性(对象):
在Objects/desc object . c 中模拟PyProperty_Type()
def __init__(self,fget=None,fset=None,fdel=None,doc=None):
self.fget=fget
self.fset=fset
self.fdel=fdel
如果doc为None且fget不为None:
doc=fget。__doc__
自我。__doc__=doc
def __get__(self,obj,objtype=None):
如果obj为None:
回归自我
如果self.fget为None:
引发AttributeError(“不可读的属性”)
return self.fget(obj)
def __set__(self,obj,value):
如果self.fset为无:
引发AttributeError(“无法设置属性”)
self . offset(obj,value)
def __delete__(self,obj):
如果self.fdel为None:
引发AttributeError(“无法删除属性”)
self.fdel(obj)
def getter(self,fget):
返回类型(self)(fget,self . offset,self.fdel,self。__doc__)
定义设置器(自身,偏移):
返回类型(self)(self.fget,fset,self.fdel,self。__doc__)
定义删除器(self,fdel):
返回类型(self) (self。fget,self。偏移,fdel,自身。_ _ doc _ _)从上面的定义我们可以看到,定义分为两部分,一部分是__get__等方法的定义,一部分是getter的定义。同时我们注意到这个类要传入fget等三个函数作为属性。定义了Getter和其他方法,以便它可以完美地使用装饰表单。先不看这部分,先看不使用第一种形式的调用机制,也就是没有decorator的形式。
#像函数一样形成
A类:
def __init__(自己,名字,分数):
Self.name=name #公共属性
self.score=分数
def getscore(self):
回归自我。_分数
定义集合分数(自我,值):
打印(“在此设置分数”)
if isinstance(value,int):
自我。_score=值
否则:
打印(请输入一个整数)
score=property(getscore,setscore)
a=A(鲍勃,90)
a .姓名# 鲍勃
得分# 90
A.score=bob #请输入一个int来分析上面调用score的过程。
初始化的时候我开始访问score,发现有两个选项,一个是property,一个是property(getscore,setscore)对象。因为__get__和__set__方法是在后者中定义的,所以它是一个优先级高于属性的数据说明符。因此,我访问了这里的描述符。因为属性是在初始化期间设置的,所以我自动调用了描述符的__set__ method __set__来检查fset属性。这里,传入的setscore不是None,所以我调用了fset或setscore方法。这样就达到了设置属性时用自定义函数检查的目的。__get__也是如此。查询score时,调用__get__方法,这将触发getscore方法。这是使用属性的另一种方法
#装饰表单,即简介中的表单
A类:
def __init__(自己,名字,分数):
Self.name=name #公共属性
self.score=分数
@属性
定义分数(自我):
打印(“在此获取分数”)
回归自我。_分数
@score.setter
定义分数(自我,价值):
打印(“在此设置分数”)
if isinstance(value,int):
自我。_score=值
否则:
打印(请输入一个整数)
a=A(鲍勃,90)
# a .姓名# 鲍勃
得分90分
# a. score= bob #请为下面的分析输入一个int。
在第一个使用方法中,函数被传递到属性中,所以考虑get部分是否可以用decorator封装是非常简单的。当访问分数时,装饰器成为访问属性(分数)的描述符。这个分数也作为fget参数传入__get__来指定调用时的操作,但是set部分不起作用。所以有了setter和其他方法的定义,使用property和setter decorators的两个方法的名字还是score。通常,同名的方法会覆盖前一个方法。所以在调用时,它调用的是后面的setter decorator处理的score,所以如果两个decorator定义的位置互换了,就无法进行属性赋值操作。在调用setter decorator的分数时,我们面临一个问题。什么是装饰分数设置器?这是分数的设定方法,什么是分数?不是下面定义的分数,因为那个分数只相当于参数传入。我自动去其他位置看有没有现成的分数,找到了一个,是属性修改的分数。它是一个描述符。根据property的定义,里面确实有一个setter方法,返回property类被传入fset的结果,或者说是一个描述符。这个描述符被引入fget和fset,这是最新的分数。以后只要实例调用或者修改分数,都会用到这个描述符。如果有del,decorator中的分数会找到setter处理的分数。最新的分数将是三个函数都被传入的分数。最新分数的调用、赋值、删除和之前一样。财产原则在这里。从它的定义可以知道,它其实就是我们设置的函数,比如check,传入get set等方法,让我们可以自由操作属性。它是一个允许我们方便地导入其他操作的框架。当很多对象都要做同样的操作时,重复是不可避免的。如果想避免重复,就得写一个类似property的框架。这个框架不是引入我们想要的操作,而是把这些操作放在框架里。这个框架因为只能实现一个操作,所以不具有通用性,但是可以大大减少目前代码重复的问题。
在使用下面的描述符定义了Checkint类之后,你会发现A类简单多了。
类检查点:
def __init__(self,name):
self.name=name
def __get__(自身,实例,所有者):
如果实例为无:
回归自我
否则:
返回实例。__dict__[self.name]
def __set__(自身,实例,值):
if isinstance(value,int):
实例。__dict__[self.name]=value
否则:
打印(“请输入一个整数”)
#像函数一样形成
A类:
score=Checkint(score )
age=Checkint(age )
def __init__(自己,姓名,分数,年龄):
Self.name=name #公共属性
self.score=分数
年龄=年龄
a=A(鲍勃,90,30)
a .姓名# 鲍勃
得分# 90
# a.score=bob #请输入一个整数
# a.age=a #请输入一个整数
描述符的应用因为刚学描述符,对它的应用不太了解,这里只是我现在能想到的一些用法。如果我想到其他人,我会在后面补充。
首先是上面提到的。是实例方法、静态方法、类方法、属性的实现原理。在访问属性、分配属性或删除属性时,出现冗余操作,或者苦思冥想找不到答案,可以求助于描述符进行具体使用。1:缓存。比如调用一个类的方法需要很长时间的计算,结果会被其他方法重复使用。我们不想在每次使用与该类相关的函数时都再次运行该方法,所以我们可以设计在第一次计算后缓存结果,并将保存的结果用于将来的调用。只要使用描述符在__get__方法中,在判断语句下,obj。__dict__[self.name]=value。这样,每次再次调用这个方法时,都会从这个字典中获取值,而不是再次运行这个方法。(示例源的最后一个示例)
涉及
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。