ljzsdut
GitHubToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeBack to homepage

15 面向对象

类的定义

class CLASS_NAME:   #class语句,产生类对象,并把类对象赋值给class关键字后的CLASS_NAME变量名,这点跟def语句类似。类名称后的括号可有可无,继承父类时,才有。
    def __init__(self,arg1,arg2...):  #构造方法
        self.attr1=arg1
        ...
        self.attr2=argument1 	#普通字段,保存在对象中
        ...
  def func1(self,arg1,arg2...):  #定义类的方法,注意固定参数:self参数
    pass
  def func2(self,arg1,arg2...):		#普通方法,保存在类中
    pass
  1. 在class语句内,任何赋值语句都会产生类属性或类方法,例如def语句、attr=attrbute等。这里的赋值语句包括=运算符、def语句等
  2. 类是一个命名空间,其内保存了类属性、类方法。

self参数的作用:

对象是由对应的类实例化而来的,当对象调用相应类的方法时,会将对象自身作为一个参数传递给类中的self参数,此参数由python内部自动传递,无需手动赋值。通俗地将:self参数指代类实例化后产生的那个实例对象。

image-20200305155519117

易错点:

class asd:
    def ad(self,a,b):
        return self.a + self.b  #AttributeError: 'asd' object has no attribute 'a'. self.a是使用前没有定义(赋值)
a = asd()
c = a.ad(1,2)
print(c)

# 正确方式:
class asd:
    def ad(self,a,b):
        self.a=a
        self.b=b
        return self.a + self.b    #以上3行或改为:return  a+b
a = asd()
c = a.ad(1,2)
print(c)

类的实例化

obj=CLASS_NAME(arg1,arg2...)  #根据类创建对象(创建一个CLASS_NAME的实例)。

#方法调用,必须先创建类的对象,之后通过对象来调用方法 
obj.func1(arg1,arg2...)  # 使用对象去执行类中方法

面向对象

面向对象之封装

封装概述

  • 是指隐藏对象的属性和实现细节,仅对外提供公共访问方式。

封装好处

  • 隐藏实现细节,提供公共的访问方式
  • 提高了代码的复用性
  • 提高安全性

封装原则

  • 将不需要对外提供的内容都隐藏起来
  • 把属性隐藏,提供公共方法对其访问

Python封装的理解

python封装实现方式

python不依赖语言特性去实现第二层面的封装,而是通过遵循一定的数据属性和函数属性的命名约定来达到封的效果。相关的命名约定如下:

约定一:任何一单下划线开头的名字都应该是内部的,私有的,但仍可以通过对象直接访问。

class People:
    _star='earth'
    def __init__(self,id,name,age,salary):
        self.id=id
        self.name=name
        self._age=age
        self._salary=salary
 
    def _get_id(self):
        print('我是私有方法啊,我找到的id是[%s]' %self.id)

print(People._star)    #>>  earth
#我们明明约定好了的,只要属性前加一个单下划线,那他就属于内部的属性,
#不能被外部调用了啊,为何还能调用?
p1=People('3706861900121221212','yj',18,10)
print(p1._age,p1._salary)
p1._get_id()

上面私有_属性:还能访问。 python并不会真的阻止你访问私有属性,这只是以一种约定。

约定二:双下划线开头的名字是本地的,不可以直接通过对象访问

双下划线开头的名字( __x)是类的本地变量/本地方法,在类的内部使用;类外部无法调用

如果它是定义在父类中,子类也是无法继承的。如果想访问某个类中的私有成员:obj._类名__成员,例如print(obj._Foo__name)

class People:
    __star='earth' #私有
    star = "moon"  #共有
    def __init__(self,id,name,age,salary):
        self.id=id
        self.name=name
        self.__age=age  #私有
        self._salary=salary #私有
 
    def _get_id(self):
        print('我是私有方法啊,我找到的id是[%s]' %self.id)
 
p1=People('333333','xixi',18,10)
print(People.star)#能访问
print(p1._salary) #能访问
print(People.__star)#不能访问
print(p1.__age)#不能访问

私有属性怎么可以访问:

class Site:
    def __init__(self, name, url):
        self.name = name       # public
        self.__url = url   # private
 
    def who(self):
        print('name  : ', self.name)
        print('url : ', self.__url)
 
    def __foo(self):          # 私有方法
        print('这是私有方法')
 
    def foo(self):            # 公共方法
        print('这是公共方法')
        self.__foo()
 
# print(Site.__dict__)
x = Site('baidu', 'www.baidu.com')
print(x.__dict__)#查看对象的属性字典
print(x._Site__url) #通过属性字典调用私有方法
# print(Site.__dict__)#查看类的属性字典
x._Site__foo()

为什么可以访问私有属性了?

python没有从根本上限制你的访问。python之所以这么设计, 原因就是:python做成非严格意义的封装,避免我们滥用封装。

封装在于明确区分内外,使得类实现则可以修改封装内的东西,而不影响外部调用者;而外部调用者也可以知道自己不可以碰哪里。这就提供一个良好的合作基础——或者说,只要接口这个基础约定不变,则代码改变不足为虑。

面向对象之继承

什么是继承?

继承,面向对象中的继承和现实生活中的继承相同,即:子可以继承父的内容。可以理解为将父类中的代码写入到子类中去,而且是写在子类代码块中的最前面,此时如果父类的某个方法名与子类自己定义的方法名重名,则子类的方法会覆盖父类的同名方法(代码块后面的同名方法会覆盖代码块前面的同名方法,相当于对方法变量名进行了重新赋值操作),也正是这个原因,实现了子类可以对父类的同名方法进行重新定制。

说明:

  • 父类、超类、基类都是同一个概念;
  • 子类、派生类是同一个概念。

如何实现继承?

定义类的时候,可以在类名加括号,并在括号中指明父类的名称。如果没有父类,默认继承自object类。例如:

class CLASS_NAME(parent_class1,parent_class2...)

  单继承:指明一个父类

  多继承:指定多个父类

继承搜索顺序

如果子类和父类具有同名的方法,在调用子类的该方法时,调用顺序:子类> 父类(如果是多继承,自左至右,顺序调用;共有父类,后者调用)后者调用是因为前者就算调用了,也会被后者调用时再次覆盖。

image-20200305162931678

验证:对于共有父类这种情况,比较靠谱的方法分析就是画出类树结构图。

class F:
    def test(self):
        print('fff')
class E(F):
    pass
class C(E):
    def test(self):
        print('ccc')
class D(F):
    pass
class B(D):
    pass
class A(B,C):
    pass

inst=A()
inst.test()  #ccc

使用IDE可以快速、准确地找到父类的方法。

易错点示例:

class A:
    def bar(self):
        print('BAR')
        self.f1()
class B(A):
    def f1(self):
        print('B')
class C:
    def f1(self):
        print('C')
class D(C,B):
    pass

d1=D()
d1.bar()
#BAR
#C

image-20200305163031900

说明:每个object.attribute都会开启新的独立搜索。Python会对每个<属性取出表达式object.attribute>进行对类树的独立搜索。这包括在class语句外对实例和类的引用(例如,X.attr),以及在类方法函数内对self实例参数属性的引用。方法中的每个self.attr表达式都会开启对self及其上层的类的attr属性的搜索。(attribute包括字段、方法、特性等类的成员)

子类调用父类同名方法

Python中子类调用父类的同名方法(覆盖/修改父类的方法)有两种方法能够实现:

  • 直接调用父类的方法(不推荐)
class Child(Parent):
    def __init__(self):
        Parent.__init__(self)  # self 对象调用了Parent的__init__()方法
  • 使用super函数(两者不要混用),推荐这种方法,调用顺序同继承的调用顺序

super()在子类中找出其父类,以便于调用其属性,可用于传入实例或类型对象,语法:super(subclass,[obj])

class Child(Parent):
    def method(self, arg):
    	super(Child, self).method(arg) #子类Child调用了Child的父类Parent的方法method,即:Parent.method(self,arg)

从运行结果上看,普通继承和super继承是一样的。但是其实它们的内部运行机制不一样,这一点在多重继承时体现得很明显。在super机制里可以保证公共父类仅被执行一次,至于执行的顺序,是按照mro进行的(E.mro)。

示例:

class Base(object):
  def __init__(self):
    print('Base create')

class childA(Base):
  def __init__(self):
    print('creat A ')
    Base.__init__(self)

class childB(Base):
  def __init__(self):
    print('creat B ')
    super(childB, self).__init__()

base = Base()
a = childA()
b = childB()

【推荐文章】深入思考python的super()

面向对象之多态

Python原生支持多态。

Python因为是弱类型语言,对于某个变量名,无需定义其数据类型,可以直接对其进行赋值。赋值后,数值的类型就决定了变量的类型,而变量的类型(准确地说时变量引用的数据的类型)决定了变量的操作方法,正式由于 这个原因,Python原生支持多态。

多态:“一个接口,完成多个方法”,传递不同的数据,完成不同的运算。

def plus(x,y):    #定义一个接口
    print(x+y)

plus(1,2)  #3   #使用的是数字的加法运算方法
plus('a','b')  #ab   #使用的是字符串连接方法
plus([1,2],['a','b'])  #[1, 2, 'a', 'b']   #使用的是列表的连接方法

类成员

类的成员可以分为三大类:字段、方法和属性。

image-20200305163824101

class  province:
    country = 'China'  # 静态字段(类属性),定义在class内、方法体之外,没有self,保存中类中
    def  __init__(self,name):  #构造方法
       self.name=name
       self.level="Province" #普通字段(实例属性),通过self.attribute=xxx定义,保存在对象中
    def  func1(self,arg1,arg2):#普通方法,保存在类中;至少有一个self参数,代指当前对象。类方法与普通的函数只有一个差别:方法的第一个参数self总是接收方法调用的隐性主体,也就是实例对象(方法调用者)。
        pass
    def  func2(self,arg1,arg2):
        pass

    @staticmethod   #修饰器:staticmethod()内置函数,用于装饰静态方法
    def static():   #静态方法:无self参数,可以有其他的参数
        print('static method')

    @classmethod    #修饰器
    def class_method(cls):  #类方法:至少有cls参数,cls代指当前类
        print('class_method')
        print(cls.country)

    @property   #特性/属性:将方法伪造成字段,调用时以字段的方式调用
    def proper(self):   #缺点:除self外,不能带其他参数。此方法用于获取特性的值
        return self.name
    @proper.setter      #修改特性的值的方法:特性方法名.setter
    def proper(self,value):#特性值无需直接修改,使用此方法实现修改特性proper的值
        self.name=value

shandong=province('山东')
shanxi=province('山西')
print(shandong.name)   		#普通字段的调用(对象.字段名)
print(province.country)     	#静态字段的调用(类.字段名)
# print(shandong.country)     	#不建议使用此种方法
province.static()       		#静态方法建议使用类来访问
# shandong.static()       		#不建议使用此种方法
province.class_method()     	#类方法建议使用类来访问
print(shandong.proper)      	#注意proper调用时未使用括号

shandong.name='ShanDong'  		#修改字段值
print(shandong.name)
shandong.proper="shandong"  	#修改特性的值
print(shandong.proper)

类的字段——数据属性

字段包括:普通字段和静态字段,他们在定义和使用中有所区别,而最本质的区别是内存中保存的位置不同(类对象中或实例对象中)。

  • 普通字段属于对象(实例属性/对象属性,通过self.attr=value定义在def语句内,从属于实例,只能被实例自己使用)

  • 静态字段属于类(类属性,通过attr=value定义在def语句之外,从属于类,可以被类实例化的所有实例使用)

teacher='全局变量'  #全局变量
class Person():
    country='China'  #类变量/静态变量
    def __init__(self,name,age):
        self.name=name  #实例变量,在方法中使用“self.变量名”引用
        self.age=age
        self.money=0
        var='局部变量'   #局部变量,只属于方法,只能在本方法内使用
        print(var)      #此处引用了局部变量,直接引用
        print(teacher)  #此处引用了全局变量,直接引用
        print(Person.country)  #此处引用了类变量,在方法中通过“类名.变量名”引用
    def func(self):
        print(self.name)    #此处引用了实例变量,在方法中使用“self.变量名”引用
        print(var)    #NameError: name 'var' is not defined,局部变量不能在其他方法中使用,只能在本方法中使用

每个实例对象都会继承类属性并获得自己的名称空间。所以类有一个命名空间,实例对象也有一个不同的命名空间。

image-20200305164324760

class Person():
    country='China'
    def __init__(self,name,age):
        self.name=name
        self.age=age
        self.money=0

a=Person('张三',17)

print(a.country) #China  #引用的是类中保存的类属性
a.country='USA'  #一旦通过实例对类中同名属性赋值,就会在实例对象中创建字段
print(a.country) #SA
del a.country 	#删除实例对象中的country字段后,“继承搜索”机制会使a.country指向类中的country字段
print(a.country) #China

print(a.age) #17
a.age=20
print(a.age) #20
del a.age
print(a.age) #AttributeError: 'Person' object has no attribute 'age'

类的方法——方法

方法包括:普通方法(也叫实例方法)、静态方法和类方法,三种方法在内存中都归属于类,区别在于调用方式不同。Python的类就是个语法糖。一个函数写在类里面和写在类外面没有区别,唯一的区别就是参数,所谓实例方法就是第一个参数是self,所谓类方法就是第一个参数是class(cls),而静态方法不需要额外的参数。

  • 普通方法:由对象调用;至少一个self参数;执行普通方法时,自动将调用该方法的对象赋值给self
  • 类方法:由调用; 至少一个cls参数;执行类方法时,自动将调用该方法的复制给cls
  • 静态方法:由调用;无默认参数;

**相同点:**对于所有的方法而言,均属于类(非对象),所以,在内存中也只保存一份。

**不同点:**方法调用者不同、调用方法时自动传入的参数不同。

image-20200305164608550

类的属性——特性

之所以叫属性,是因为这个方法可以像字段一样调用。不用带括号。

属性存在意义是:访问属性时可以制造出和访问字段完全相同的假象。

属性由方法变种而来,如果Python中没有属性,方法完全可以代替其功能。

image-20200305164912471

属性的定义和调用要注意一下几点:

  • 定义时,在普通方法的基础上添加 @property 装饰器;

  • 定义时,属性仅有一个self参数

  • 调用时,无需括号

    ​ 方法:foo_obj.func()

    ​ 属性:foo_obj.prop

修改属性值:

class  province:
    @property   #特性/属性:将方法伪造成字段,调用时以字段的方式调用
    def proper(self):   #缺点:除self外,不能带其他参数。此方法用于获取特性的值
        return self.name
    @proper.setter      #修改特性的值的方法:特性方法名.setter
    def proper(self,value):#特性值无需直接修改,使用此方法实现修改特性proper的值
        self.name=value

shandong=province('山东')
shandong.proper="shandong"  	#修改特性的值
print(shandong.proper)

总结

  1. 静态字段的意义:通过类创建对象时,如果某个字段在每个对象都具有相同的字段值,那么就使用静态字段
  2. 静态方法的意义:普通方法要想执行,必须先创建对象(需要使用对象中的数据);静态方法无需事先创建对象。静态方法就是在类中创建的普通的函数。定义时,无法使用对象中封装的内容。
  3. 对实例的属性进行赋值运算会在该实例(名称空间)内创建或修改变量名,而不是在类名称空间内。
  4. 通常的情况下,继承搜索只会在属性引用时发生,而不是在赋值运算时发生,因为对对象(类对象、实例对象)属性进行赋值会直接修改该对象 。(类对象–>类属性;实例对象–>实例属性)
  5. Python会自动把实例方法的调用映射为到类方法函数的调用,实例对象调用方法就像这样: instance.method(args...) Python会自动翻译成以下形式的类方法函数调用: class.method(instance, args...), class通过Python继承搜索流程找出方法名称所在之处。事实上,两种调用形式在Python中都有效。
  6. 类属性与实例对象属性:类属性在class语句作用域内定义(不是在def语句中),所以是类对象的属性,会被由这个类所创建的的所有实例对象所继承。注意继承并不是指保存在实例对象中,而是保存在类对象中。

总结:

通过类访问的有:静态字段、静态方法

通过对象访问的有:普通字段、类方法、特性

总之,与self相关的(类中使用self.attribute、def func(self) 定义)使用对象来调用;与self无关的(例如attribute=xxx),使用类来调用。

运算符重载

什么是运算符重载?

运算符重载:指在方法(例如__init__)中拦截内置的操作(例如cls()),当类的实例出现在内置操作中,python会自动调用自定义的方法,并且返回自定义方法的操作结果。例如调用某个运算符时(如obj.()、print()、-减号等),python会自动调用对象中的某个方法。

运算符重载的作用:

  • 运算符重载让类拦截常规的python运算;

    • 类可以重载所有python表达式运算符
    • 类可以重载打印、函数调用、属性点号运算风内置运算
  • 重载使类实例的行为更像内置类型;

  • 重载通过提供特殊名称的类方法实现;

说明:运算符重载并非必须,并且通常也不是默认的。

类的特殊成员(运算符重载方法)

__init__#类名()-->会执行类中的__init__方法(构建方法)
__del__ #(析构方法)执行del obj或python自动回收对象时会触发__del__方法执行对象的删除操作。
__call__#对象名()-->执行类中的__call__方法,如Foo()()就是执行Foo类中的__call__函数

__getitem__	#对象[args] -->执行类中的__getitem__方法
__setitem__	#对象[args] =xxx -->执行类中的__setitem__方法
__delitem__	#del 对象[args] -->执行类中的__delitem__方法

r[1:3] 							#(切片):执行__getslice__(python2.7)或__getitem__(python3.x)
r[1:3]=[11,22,33] 	#(切片):执行__setslice__(python2.7)或__setslice__(python3.x)
del r[1:3] 					#(切片):执行__delslice__(python2.7)或__delslice__(python3.x)

obj.__dict__		#对象的命名空间字典。获取对象的所有成员,包括自定义和默认定义的,保存在一个字典中。(具有命名空间的对象(具有__dict__的对象):模块、类、实例)
cls.__dict__		#类的命名空间字典。获取类的所有成员,包括自定义和默认定义的,保存在一个字典中
#说明:命名空间对象的属性通常都是以字典的形式实现的,而类继承树(一般而言)只是连接至其他字典的字典而已。__dict__属性是针对大多数基于类的对象的命名空间字典

__iter__		#执行for去迭代对象是 ,执行的是对象的__iter__方法
__str__		#当使用print(obj)或str(obj)时,会调用该类中的__str__方法。

obj.__name__:		#对象的名称(变量名),如类的名称,实例的名称 
cls.__name__:		#类的名称(就像模块一样),例如:object.__class__.__name__
obj.__class__:	#实例对象的实例类,提供了一个从实例到创建它的类的链接。
cls.__bases__:	#类对象的超类(元组),提供了超类的访问