零:环境
python3.6.5
JetBrains PyCharm 2018.1.4 x64
本文中链接均为锚链接,可点击快速跳到指定位置处查看
python的类和其他语言不太一样,类也是一种类型
我们在看一个类的类型的时候会发现是其类名,但是我们在对类再看类型(type函数)时就会发现都是type类型,感兴趣的朋友们可以百度metaclass,这里并不讨论
一:类的变量
由于python中没有常量标准,所以只讨论变量
类的变量有两大类,一个是实例变量一个是类变量
1、实例变量
是在方法里定义的变量,只能被实例化的对象所访问
例如
classTest():
参数1= 2
def __init__(self):
self.参数2= 3
pass
pass
其中参数2为为实例变量,因为是在方法里定义的,这里注意self不能少
根据习惯,我们往往在__init__中定义声明
2、类变量
是在类里定义的变量,被该类以及该类下所有的实例所共享,定义在类内且方法外
上面的参数1就是类变量,类变量既可以用实例对象访问也可以被类直接访问
3、变量的修改与绑定
对于变量的修改,实例变量的修改没有什么问题,修改了就是修改了,我们也没必要关注是不是还指向同一地址,我们只关心值
但是对于类变量来说就不一样了
因为python的绑定机制而导致类变量的修改产生了一些问题
在谈这个问题之前我们先看看什么是变量的绑定以及LEGB原则
3.1、LEGB原则
L (Local) E(Enclosing function locals) G(Global) B(BuiltIn)
LEGB原则实则寻找变量的原则
当编译器读到一个变量时,它会优先按照LEGB从前往后的顺序去寻找变量,找到就停止
L——局部变量,即和调用处处于同一作用域下的变量,关于作用域相关的知识不在本文讨论范围内,可以去百度
E——闭包函数区域,内部函数如果用到了外部函数内的变量则会先在L处找,然后再在外部函数处寻找
G——全局变量,即处于同文件下的变量,文件级变量
B——内置变量,就是系统提供的一些默认包含包中的变量
3.2、变量的绑定
说完了变量寻找原则之后就可以说变量的绑定了
变量的绑定是python提供的一个特殊机制,可以用于动态的为类添加变量或方法,添加的方法和属性是公有的
方法为 实例名或类名.属性或方法名 = 对应的值
其中 实例名.属性或方法名 = 对应的值:代表该添加的属性或方法只对当前实例有效
而 类名.属性或方法名 = 对应的值:代表该添加的属性或方法对该类下所有实例以及类本身有效
那么问题来了
这种方式和变量的修改方法一致,对于类变量来说,我们对类变量的修改会造成一个问题
我们可以看一下下面这个例子
classTest():
参数1= 2@classmethoddeff_测试2(cls):print("类{0}".format(cls.参数1))pass
passt,t1=Test(),Test()print(id(t.参数1),id(Test.参数1),id(t1.参数1))print(t.参数1,Test.参数1,t1.参数1)
Test.参数1= 7
print(id(t.参数1),id(Test.参数1),id(t1.参数1))print(t.参数1,Test.参数1,t1.参数1)
t.参数1=5
print(id(t.参数1),id(Test.参数1),id(t1.参数1))print(t.参数1,Test.参数1,t1.参数1)
这个例子的输出结果为
1522625584 1522625584 1522625584
2 2 2
1522625744 1522625744 1522625744
7 7 7
1522625680 1522625744 1522625744
5 7 7
第一波输出很正常,都指向同一个值,并且地址也都是同一个
当我们通过Test修改类变量时,却发现地址变了。这也很好理解,因为参数1是不可变数据类型,所以参数1有以下两种可能性
一是修改了指向的地址,指向新地址并且获得了新值
另一种可能性就是,不是变量的修改而是变量的绑定。在类里新建了一个同名的变量并且赋予了值
这里我们还看不出来到底是哪种可能性
当我们通关实例t修改类变量时,我们发现t指向的类变量和其他两个不一样
如果是第一种可能性的话,应该三个都一样的,然后地址变化
如果是第二种可能性的话就代表实例t在自己的实例里绑定了一个新的变量,与类变量同名,值为5,可以解释
为了证明我们的想法我们可以在类中加入打印类属性的方法然后调用一下看看 t.f_测试2()
打印的是 类7
我们可以看出和第二种假设符合
所以根据我们的假设,如果类变量是可变数据类型的话,我们修改是不会出问题的,会被解释成变量的修改而不是变量的绑定
具体效果看官们可以自己尝试
4、变量的类型——访问级别控制
类变量和实例变量是属性的一种分类方式,但是与此共存的还有另一种必须的分类方式
变量还分为三种类型,公开属性,保护属性,私有属性
公开属性前面没有下划线,可以在子类和本类中通过正常途径访问到
保护属性前面有一个下划线,可以通过子类和本类正常访问到,但是如果在其他包里通过“from moduel/文件夹名 import 文件名”,会点不出来但是能用。所以保护属性只起到提示作用
私有属性前面有两个下划线,只能在本类里访问到,无法在本类之外的任何地方访问到,但是可以通过该类提供的接口来间接的访问到
注意:公开属性和保护属性在类中通过dir内置函数或者__dir__方法查看时,实际名称是和定义的一样的,但是私有属性不一样,私有属性会在变量名前面加上"_类名’的前缀
而且如果在类变量和实例变量中定义相同名字时,虽说dir不会分别打印出来,会显示同一个名字,但是实际上并没有覆盖,还是可以通过实例调用实例属性,类调用类属性
二:类的方法
方法和属性一样都有访问级别的控制,都是用下划线的有无和个数判断
1:实例方法
实例方法和我们正常定义方法一样。
不同的是第一个参数是固定的代表实例对象,实例对象调用时会自动传进去第一个参数为调用者的实例
与实例属性一样都只能被实例对象调用,不过也可以用类调用然后第一个参数传实例对象来间接调用
2:类方法
类方法被@classmethod装饰器修饰
第一个参数为类对象,类或实例对象调用时会自动传进去第一个参数为类本身
可以被类或实例对象调用
3:静态方法
被@staticmethod装饰器修饰
无特殊参数
故往往被用于与类本身属性和方法无关的逻辑处理
当然你也可以通过参数传实例进去调用
三:魔法方法或属性(内置、双下)
用__xxx__来表示的方法或属性是特殊方法或属性
所有的特殊方法都有默认的定义,你不定义也会带有默认定义
接下来将介绍一些常用方法或属性的简介
1:__init__与__new__ 方法
一个类的构造的顺序是先调用__new__,然后根据返回值来判断接下来的步骤,如果返回父类的__new__实例的话,则会调用本类的__init__方法
如果调用了其他类的__new__的话,会调用其他类的__init__方法
如果没有返回值则不会调用__init__方法
__init__没有返回值
接下来这段
版权声明:本文为CSDN博主「SJ2050」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sj2050/article/details/81172022
绝大多数情况下,我们都不需要自己重写__new__方法,但在当继承一个不可变的类型(例如str类,int类等)时,它的特性就尤显重要了。我们举下面这个例子:
1 classCapStr(str):2 def __init__(self,string):3 string =string.upper()4
5 a = CapStr("I love China!")6 print(a)
执行结果为:
I love China!
classCapStr(str):def __new__(cls,string):
string=string.upper()return super().__new__(cls,string)
a= CapStr("I love China!")print(a)
执行结果为:
I LOVE CHINA!
我们可以根据上面的理论可以这样分析,我们知道字符串是不可改变的,所以第一个例子中,传入的字符串相当于已经被打下的疆域,而这块疆域除了将军其他谁也无法改变,__init__只能在这块领地上干瞪眼,此时这块疆域就是”I love China!“。而第二个例子中,__new__大将军重新去开辟了一块疆域,所以疆域上的内容也发生了变化,此时这块疆域变成了”I LOVE CHINA!“。
---------------------
版权声明:本文为CSDN博主「SJ2050」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sj2050/article/details/81172022
通过上面的分析我们可以看出对于不可变数据类型我们可以利用new来重新构造一块地方来修改变量
所有的类都有构造函数,如果你不重写的话会默认生成一个空的构造函数
类在初始化,也就是 XXX([参数1,[参数2,……]])时会调用构造函数来初始化
我们通常在这里做初始化实例变量的操作,可以通过对参数设置默认值来让构造函数实现重载
2:__str__,__repr__,__format__ 方法
这三个内置方法都是字符串打印用的
首先我们先说__format__,这个是在.format函数调用时,参数里如果有该对象则会调用该对象的__format__方法
如果没被定义的话再调用__str__方法,最后再调用__repr__方法
那么为什么用这么多字符串函数呢?
首先__format__方法只对format函数有效
但是另外两个不一样
__str__函数规范上是面向用户的,尽可能用户友好的显示,通常用于对象转换成字符串的时候调用
而__repr__是面向程序员的,要求详细,通常作为__str__的备胎或者其他情况比如说控制台打印或者序列里的元素转换成字符串的情况
比如说
classEmployee():def __repr__(self):return "Employee.__repr__"
def __str__(self):return "Employee.__str__"
def __format__(self, format_spec):if format_spec == "":return "Employee.__format__"
return "Employee.__format__"+format_specpasse=Employee()print(e)print([e],(e,),{e:e})print("{0}".format(e))print("{0:233}".format(e))print(format(e,"233"))
输出结果为
Employee.__str__[Employee.__repr__] (Employee.__repr__,) {Employee.__repr__: Employee.__repr__}
Employee.__format__Employee.__format__ 233Employee.__format__ 233
3:__class__ 属性
这是一个特殊属性,会返回该实例对象所属于的类型
4:__hash__ 方法
使用默认方法返回该对象哈希值,我们可以重写哈希算法来返回哈希值
5:__eq__,__ne__ 方法
该方法定义了这个对象的 == 的比较方法,可以重写实现复杂的比较,__ne__与之相反
6:__setattr__与__delattr__ 方法
这两个方法看名字就知道是变量的绑定和销毁,不细说
7:__dict__ 属性
这个内置属性返回所有的实例变量的全称和值,结构为{ "实例变量全名":值,…… }
实例变量的全名忘了的话参考上面的红字注意
8:__init_subclass__ 方法
这个内置方法会在被继承时自动调用,无需手动调用,默认为空方法,当然我们也可以手动调用
9:__model__ 属性
该内置属性会返回类所在的模块名
10:__sizeof__ 方法
返回对象所占空间字节大小
四:杂谈
一:@property
该装饰器可以讲get,set方法绑定到一个变量上来调用,在我们写代码时可以当做属性用,简化了代码,减少出错还能做复杂处理。日后维护也容易
使用方法有以下两种
classHouse():def __init__(self,pRoomId,pRoomPrice):
self.__roomId =pRoomId
self.__roomPrice =pRoomPricepass
#方法一
@propertydefroomId(self):return self.__roomId@roomId.setterdefroomId(self,value):
self.__roomId =value#方法二
def __getRoomPrice(self):return self.__roomPrice
def __setRoomPrice(self,value):
self.__roomPrice =value
roomPrice= property(__getRoomPrice,__setRoomPrice)pass
我们可以如下调用
h = House(1,6000)
h.roomId= 2h.roomPrice= 5000
print(h.roomId,h.roomPrice)
很一目了然吧,就不细说了
二:天然的多态性
python由于没有类型机制,所以函数里的参数如果每次调用都传不同类型的对象进去的话,会调用对应对象的对应方法或属性,天然性的实现了多态
无需像其他语言一样必须先让两个类继承自同一父类之后,规定参数传父类
三:继承与多继承
python继承写在class 类名(父类名1,父类名2,……)中,子类可以继承父类的公开对象和保护对象(保护对象实际没什么意义,详见变量的访问级别控制)
子类无法访问父类的私有对象,但是可以通过父类提供的接口间接访问
类的初始化顺序和其他语言一样是先初始化父类然后再初始化本类,只不过对父类的初始化是由我们手动调用的。我们可以选择不初始化前提我们的操作对原类的初始化操作相关东西无关
对于单继承,我们可以通过 super().__init__(参数1,参数2,……) 来初始化父类,我们也可以用 父类名.__init__(参数1,参数2,……) 来初始化,效果一样
对于多继承,super语法则会有BUG,目前算法不明
比如说
classA(object):def __init__(self,v):
self.__p1=vpass
pass
classB(object):deff(self):print("B_f()")pass
classC(object):def __init__(self,m,n):
self.__sss=m+npass
classAB(A,B,C):def __init__(self):#A.__init__(self,26)
#B.__init__(self)
#C.__init__(self,1,2)
super().__init__(5)#A的初始化
super(A,self).__init__(1,2)#C的初始化
super(C,self).__init__()#B的初始化
super(B,self).__init__(1,2)#C的初始化
passab= AB()
作者发现super在多继承中的初始化的对象寻找不到规律,故放弃,只能看出来了super()符合C3算法必定指向第一个父类
注释的三行则能准确的执行父类的初始化,所以建议用的 父类名.__init__(参数1,参数2,……)方法来初始化多继承父类
额外:C3算法
C3算法是子类寻找对象的顺序和优先级,C3算法实际上即为广度优先算法,优先寻找本类有没有对应属性或方法,没有就寻找第一个父类,再没有就寻找第二个父类,直到所有父类都找完
如果第一个父类还没有就如同最开始一样,以此为中心继续广度优先搜索,直到找到,最终还找不到就报错
我们可以用类方法mro来获取C3算法的顺序
四:子类调用父类方法
由于python没有类型机制,所以不能像其他语言一样直接强制转换,只能间接的以
ab =AB()
A.f(ab)
这种方式调用,即类调用对象并传入相应的实例对象
五:查看本类被什么类继承
类里有一个方法(注意不是实例对象是类)__subclass__(self),会返回直接子类的列表,想要输出间接的可以循环或递归
本文皆作者原创或学习产物,转载请注明出处