目录
声明:文章基于廖雪峰的Python教程
面向对象编程
类和实例
- 在python中,我们需要注意的一点是,像‘__XXX__’这样的变量在表示的是特殊变量,有特殊用途,例如:我们知道,其实函数名也是一个变量,函数有很多的属性,比如__name__,__qualname __,__class__等变量都是__XXX__这种形式。像‘__XXX’这种形式的变量在类中表示的是私有变量或函数,不应该直接引用,这里之所以说的‘不应该’,而非像c++和java中‘无法’,是因为python无法完全限制你对私有变量的访问!
- 在python中,我们可以自由地给一个实例变量绑定属性,比如:
In [34]: class Man(object): ...: pass ...: In [35]: m = Man() In [36]: m.name = 'Trump' In [37]: m.name Out[37]: 'Trump'
__init__
方法的第一个参数永远是self
,表示创建的实例本身,这跟c++中的this指针是一个意思,只不过c++中是隐式传入,而在python中需要我们显示写出,但是我们在传参时不需要传入self,Python解释器会自动将本实例变量传入self中。这样,在__init__
方法内部,就可以把各种属性绑定到self
,因为self
就指向创建的实例本身。这里提醒一下,在类内,凡是属于实例的属性都要用self.xxx的形式来访问,注意区别实例的属性和类的属性两个不同的概念。在类外,我们使用“实例.方法”来对方法进行调用时,会自动将本实例传入作为self的参数:In [86]: class A(): ...: def fun(): ...: pass ...: In [87]: a = A() In [88]: a.fun() Traceback (most recent call last): File "<ipython-input-88-7301c6f31cb5>", line 1, in <module> a.fun() TypeError: fun() takes 0 positional arguments but 1 was given
可以看到我在类A中定义了一个fun()方法,但是我没给该方法赋self这个参数,在使用A的实例a调用fun()方法时抛出了“fun() takes 0 positional arguments but 1 was given”这个错误,这是因为解释器默认将本实例作为一个实参传入了该方法,本来是要由self来接收该参数,但是我们没定义,因此出现了以上错误。无论是在类内还是在类外,对实例的属性进行访问时,都要用“实例.属性”这种方式进行,只不过在类内实例用self,即“self.xxx”,在类外用“a.xxx”这种形式。而对方法的调用来说,若采用“实例.方法”(类内为“self.xxx()”这种形式,类外为“a.xxx()”这种形式)形式调用方法,则解释器会自动把本实例作为一个参数传入该方法的self形参中,若采用“类名.方法”的这种形式调用方法时不会,这时就可以自由地选择将哪个实例作为实参传入self了。这段红色划线部分字是先打声招呼,若不懂可看以下“继承和多态”第5点的具体讲解!
- 和静态语言不同,python可以实现任何的属性或方法动态的绑定到已经创建的实例当中,因此对于同一个类创建的多个实例可能会有不同的属性或方法:
In [49]: class A(object):
...: pass
...:
In [50]: a = A()
In [51]: b = A()
In [52]: a.x = 1
In [53]: b.y = 2
In [54]: a.x
Out[54]: 1
In [55]: a.y #a没有y这个属性,故出错
Traceback (most recent call last):
File "<ipython-input-55-59a3db86364c>", line 1, in <module>
a.y
AttributeError: 'A' object has no attribute 'y'
In [56]: b.x #b没有x这个属性,故出错
Traceback (most recent call last):
File "<ipython-input-56-252ebe2d9b6c>", line 1, in <module>
b.x
AttributeError: 'A' object has no attribute 'x'
In [57]:
In [57]: b.y
Out[57]: 2
访问限制
- 若我们想在对象(实例)中添加私有属性,即类外无法进行直接访问的属性,那么可以试着给对象添加‘__XXX’这样的属性,这样在类外对该属性进行访问时就会抛出错误:
class A(): def __init__(self, x): self.__x = x a = A(7) print(a.__x)
以上代码抛出的错误是:。表面上好像不能在类外直接访问,实际上还是可以!只不过python解释器对外将变量名改成了‘_A__x’:
In [74]: print(a._A__x) 7
所以如果你尝试用以下方法在类外对私有属性进行修改就是错误的做法:
a.__x = 2
这里的__x已非私有属性,而是新绑定的一个属性,区别于_A__x。
继承和多态
- 我们先定义一个父类A,再定义一个子类B继承自A,这样B的对象就能够使用继承自A的方法:
In [82]: class A(): ...: def play(self): ...: print('playing') ...: In [83]: class B(A): ...: pass ...: In [84]: b = B() In [85]: b.play() playing
另外,我们也可以在子类中定义和父类中名字和参数都相同的方法,实现对父类方法的‘覆盖’,其实学过其他编程语言的都能体会到,这些语言都是相通的,这里的覆盖和java中的重写是不是一个意思。
- 当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样:
class Dog(): pass d = Dog() # d是Dog类型 l = list() # l是list类型 s = str() # s是str类型 i = int() # i是int类型 d = dict() # d是dict类型
- 在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类:
In [99]: class Human(): ...: pass ...: In [100]: class Man(Human): ...: pass ...: In [101]: Trump = Man() In [102]: isinstance(Trump, Man) # 判断一个变量是否是某个类型的实例 Out[102]: True In [103]: isinstance(Trump, Human) Out[103]: True
但是反过来却不成立:
In [104]: herm = Human() In [105]: isinstance(herm, Man) Out[105]: False
- 在c++中,多态指的是定义一个父类型的对象指针,指向子类的对象,那么就可以将它指向不同子类的对象从而调用不同的子类各自的功能。对于python中多态,我们先来看下面的例子:
class Animal(): def show(self): print("I'm a animal") class Dog(Animal): def show(self): print("I'm a doggy") class Cat(Animal): def show(self): print("I'm a cat") class Bird(Animal): def show(self): print("I'm a bird")
我们先定义一个父类Animal,和它的三个子类Dog、Cat、Bird,都分别实现了一个show(self)方法,接下来我们再定义一个函数Who(animal),参数为animal,即我们希望传入的是一个Animal类型的实例:
In [2]: def Who(animal): ...: animal.show()
接下来我们就可以使用这个函数根据其传入的具体类型(Dog、Cat、Bird)来调用不同“动物”的show方法,其实多态的含义就体现在这句话。
In [3]: Who(Dog()) I'm a doggy In [4]: Who(Cat()) I'm a cat In [5]: Who(Bird()) I'm a bird
现在想想,python中的多态和上面讲到的c++的多态是不是相通的😁!其实看到这里,我们很容易就会想到一个问题:是不是我自己随便创建一个含有show方法的类都可以使用Who函数对其实例进行调用?完全正确!!!不信看实验结果:
In [6]: class Trump(): ...: def show(self): ...: print("I'm Trump, and No one knows more about the new coronavirus than I do!") ...: In [7]: Who(Trump()) I'm Trump, and No one knows more about the new coronavirus than I do!
这在python中称为“鸭子类型”,即“一个东西,只要它看起来像鸭子,走起路来也像鸭子,那么可以认为它就是鸭子!”
-
方法的调用有两种形式:①实例.方法;②类名.方法。再上面我们提到过,采用形式①对方法进行调用时,解释器会自动将“本实例”作为一个参数传入到方法中,而形式②却不会,来看下面例子:
In [1]: class Trump(): ...: def show(self): ...: print('Nobody knows more about the new coronavirus than I do!') ...: In [2]: t = Trump() In [3]: t.show() #用形式 1 调用方法 Nobody knows more about the new coronavirus than I do! In [4]: Trump.show() #用形式 2 调用方法 Traceback (most recent call last): File "<ipython-input-4-bbdbbbf6b35e>", line 1, in <module> Trump.show() TypeError: show() missing 1 required positional argument: 'self' In [5]: In [5]: Trump.show(t) #用形式 2 调用方法 Nobody knows more about the new coronavirus than I do!
从上面例子可以看到,在使用t.show()对方法进行调用时,会自动将t这个实例传入show方法中,并由self接收,因此对self这个形参我们不需要由自己再传入实参。在使用Trump.show()时,解释器就不会自动将实例传入了,而show方法中,我们定义了一个形参self,但是在调用该方法时却没有对它传入任何参数,因而就抛出了“缺失参数”的错误!在这种情况下我们在调用show方法就需要由自己传入一个实参到self中,即Trump.show(t),将t传入作为self的实参。
-
在子类中,若没有定义构造方法,那么在创建子类的实例时,默认调用的是父类的构造方法:
class Animal(): def __init__(self, gender): self.Gender = gender class Dog(Animal): pass
In [17]: d = Dog('male') In [18]: d.Gender Out[18]: 'male'
若子类中也定义了构造方法,那么子类的构造方法就会覆盖父类的构造方法,创建子类的实例时默认调用的时子类的构造方法:
class Cat(Animal): def __init__(self, name): self.Name = name
In [29]: c = Cat('Lily') In [30]: c.Name Out[30]: 'Lily'
如果尝试输出c.Gender就会出错:
In [31]: c.Gender Traceback (most recent call last): File "<ipython-input-31-61c005fa9dd2>", line 1, in <module> c.Gender AttributeError: 'Cat' object has no attribute 'Gender'
这是因为在创建实例时,调用的是Cat的构造方法,而该方法中没有对实例绑定Gender这个属性!因此我们要对Cat的构造函数进行修改:
class Cat(Animal): def __init__(self, gender, name): self.Name = name Animal.__init__(self, gender)
现在就不会出现问题了,试试看:
In [34]: c = Cat('male', 'Mike') In [35]: c.Gender Out[35]: 'male' In [36]: c.Name Out[36]: 'Mike'
我们在Cat的构造方法中增加了‘Animal.__init__(self, gender)’这句,如果弄懂了上面讲的第5点那么这句代码就很好理解了!首先明确,采用的是形式②对方法进行调用,即“类名.方法”的形式,这就告诉我们解释器不会自动将某个实例传入方法的形参中,需要我们自己传!在Animal的构造方法中定义了两个参数:self和gender,因此在调用时我们传入了两个参数。不过这里推荐使用super()函数来实现对父类方法的调用,super 是用来解决多重继承问题的,直接用类名调用父类方法在使用单继承的时候没问题,但是如果使用多继承,会涉及到查找顺序(MRO)、重复调用(钻石继承)等种种问题。
class Cat(Animal): def __init__(self, name, gender): super(Cat, self).__init__(gender) #Python3.x版本的直接可以写super().__init__(gender) self.Name = name
In [384]: c = Cat('Dolly', 'femal') In [385]: c.Name Out[385]: 'Dolly' In [386]: c.Gender Out[386]: 'femal'
这两种方式都是在子类中方法与父类中重名的情况下使用的,若不重名则可以直接调用父类方法。super()在调用父类的方式时和方法解析顺序(mro)有关,该顺序基于广度优先搜索。关于super()在多重继承中的使用,可以参考Python中多继承与super()用法和python super()调用多重继承函数问题这两篇文章。
获取对象信息
- 当创建了一个对象(变量/实例),想要知道他是什么类型时,我们可以使用type()函数:
In [37]: type(12) Out[37]: int In [38]: type('hello') Out[38]: str In [39]: type(1.2) Out[39]: float In [40]: type([]) Out[40]: list In [41]: type(()) Out[41]: tuple In [42]: type({1: 'a'}) Out[42]: dict
想知道某个对象是否是某个类型时:
In [42]: import types In [43]: def fun(): ...: pass ...: In [45]: type(123) == int Out[45]: True In [46]: type('hello') == str Out[46]: True In [47]: type((1,)) == tuple Out[47]: True In [48]: type(fun) == types.FunctionType Out[48]: True In [49]: type(abs) == types.BuiltinFunctionType Out[49]: True In [50]: class A(): ...: pass ...: In [51]: a = A() In [52]: type(a) == A Out[52]: True
- dir()函数可以获取某个对象的所有属性和方法的名字,返回的是一个str元素的list:
In [61]: class A(): ...: def __init__(self): ...: self.i = 1 ...: self.f = 1.2 ...: self.s = 'hello world' ...: In [62]: dir(A()) Out[62]: ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'f', 'i', 's']
- 类似
__xxx__
的属性和方法在Python中都是有特殊用途的,比如__len__
方法返回长度。在Python中,如果你调用len()
函数试图获取一个对象的长度,实际上,在len()
函数内部,它自动去调用该对象的__len__()
方法,所以,下面的代码是等价的:In [64]: len("hello") Out[64]: 5 In [65]: 'hello'.__len__() Out[65]: 5
如果我们要想自己写的类也能用len()方法的话,类中就要实现__len__()方法:
In [66]: class A(): ...: def __len__(self): ...: return 10 ...: In [67]: len(A()) Out[67]: 10
- 使用
getattr()
、setattr()
以及hasattr()函数
,可以操作一个对象的状态:In [68]: class A(): ...: def __init__(self): ...: self.x = 1 ...: In [69]: a = A() In [71]: hasattr(a, 'x') #判断对象a是否有属性'x' Out[71]: True In [72]: hasattr(a, 'y') #判断对象a是否有属性'y' Out[72]: False In [73]: getattr(a, 'x') #返回对象a的属性'x' Out[73]: 1 In [75]: setattr(a, 's', 'hello') #给对象a再绑定一个属性's',并设定其值为'hello' In [76]: a.s Out[76]: 'hello'
- 在类中,属性可分为两种:类属性和实例属性。以上知识涉及到的都是实例属性,何所谓类属型和实例属性?其实看名字就了然于胸了!类属型就是属于类的属性,实例属性就是属于实例的属性。实例的属性是通过“实例.属性”这种形式来进行绑定的,比如:
class A(): def __init__(self): self.x = 1 #在类中进行绑定 a = A() a.y = 2.1 #在类外创建出实例后再进行动态绑定
类属型的定义要这样:
class B(): x = 1 #类属型的定义 def __init__(self): pass
我们知道实例的属性是不能直接通过类名来访问的,即无法使用‘类名.属性’这种形式来访问,只能通过‘实例.属性’来进行访问。那么类属型该怎么访问呢?来看下面例子:
In [83]: class B(): ...: x = 1 ...: def __init__(self): ...: pass ...: In [84]: B.x #通过类名访问类属型 Out[84]: 1 In [85]: b = B() In [86]: b.x #通过实例访问类属型 Out[86]: 1 In [87]: b.x = 2 #给实例绑定属性x In [88]: b.x #访问实例属性 Out[88]: 2 In [89]: B.x #访问类属型 Out[89]: 1
从上面例子我们可以看到,类属型可通过类名访问,而且也可以通过实例访问,实际上所有实例都能够访问到类属型(无论对类属型还是实例属性进行访问都不能省略前面的类名或实例名即只有“类名.属性”和“实例.属性”两种形式)。我们可以将实例属性理解为某些实例所独有的属性,而将类属型理解为所有实例都共享的属性。这里共享的意思是所有实例同享一个,而不是各个实例各自拥有!在上面例子中,我们还给b绑定了一个属性x,这里要注意区分了,实例b和类都拥有了名为‘x’的属性,但是这二者不一样!也就是这时实例属性和类属型同名了。在实例属性和类属型同名的情况下,采用“实例.属性”形式对属性访问时,访问到的会是实例属性而非类属性,因为实例属性比类属型优先级高!这里看起来有点绕!理解了这句话,最后两行代码便很好理解了,b.x访问的是b刚刚绑定的属性,而B.x访问的是类属型。我们可以通过del关键字删除实例b的属性x:
In [106]: del b.x In [107]: b.x #访问类属型 Out[107]: 1
删除后就只有类属型了,这时使用实例对类属型进行访问时就不会由于实例属性优先级高而覆盖了类属型。