Python面向对象编程核心概念:类、对象与继承机制详解

橘子海
橘子海
发布时间:2026-10-13 | 3人浏览过
原创

本文深入解析Python面向对象编程核心概念,涵盖类与对象的定义、实例化过程中的__init__方法作用、数据属性与方法属性的区别。详细讲解继承机制下的代码复用、多继承与MRO列表,以及组合解决“有什么”关系的设计模式。此外,还介绍了抽象类、接口模拟及super()方法的优雅用法,帮助开发者构建高扩展性的Python程序。

1.什么是面向对象

面向对象(OOP)是一种抽象化的思维模式,旨在理解世界万物。世间万物均可被抽象为对象,一切事物皆由对象构成。在编程领域,这是一种开发方法,将对象作为程序的基本构建单元。

2.面向对象与面向过程的区别

前文已介绍过面向过程,其核心在于“过程”,即解决问题的具体步骤。面向过程的设计类似于设计流水线,体现的是一种机械式的思维方式。

优点:使复杂问题简单化、流程化。 缺点:扩展性较差。

主要应用场景包括Linux内核、Git以及HTTP服务等。

面向对象的程序设计核心在于对象,对象是特征(变量)与技能(函数)的结合体。

优点:解决了程序扩展性差的问题。 缺点:可控性较差,难以预测最终结果。

主要应用场景是需求经常变化的软件,即与用户交互较为频繁的软件。

需要注意的是:面向对象程序设计并不能解决所有问题,其主要目的是解决扩展性问题。当然,当前的互联网软件中,扩展性是最为重要的考量因素。

3.对象与类的概念

在Python中,一切皆对象。一个对象应具备自己的属性(即特征)以及自己的功能(即方法)。

在Python中,特征通过变量表示,功能通过函数表示,因此对象就是变量与函数的结合体。

从众多对象中抽取具有相同特征和功能的部分,就构成了类。因此,类是一系列对象共同特征与功能的结合体。 下面我们来定义一个类,其方法与定义函数有些类似:

#定义一个中国人的类
class Chinese:
 #共同的特征
 country='China'
 
 #共同的技能
 def talk(self):
 print('is talking Chinese')
 def eat(self):
 print('is eating Chinese food')

这样我们就定义好了一个类,注意以下几点:

1. 定义类使用class关键字。 2. 类名通常首字母大写,且冒号前不需要括号(非必须,有括号也不报错,但通常需要继承object类以保证是新式类),这与函数定义有所区别。 3. 与函数不同,类在定义阶段就会执行类里面的代码。 4. 类有两种属性:共同的特征称为数据属性,共同的功能称为函数属性。

如何由这个类产生一个对象呢?这涉及到实例化:

#实例化的方式产生一个对象
p1=Chinese()
p2=Chinese()

我们可以得出结论:无论现实世界如何,在程序中,确实是先有类,才有的对象。

我们已经通过实例化的方式得到了两个对象,但这里有一个问题:得到的两个对象,其特征和功能都是一样的,这与“万物皆对象”的理念完全不符啊。理想情况下,每个对象都应该是不同的,这样的世界才有趣。

事实上,我们在定义类时,忘记了定义 __init__() 这个函数。正确的定义方法应该是这样的:

#定义一个中国人的类
class Chinese:
 #共同的特征
 country='China'
 #初始化
 def __init__(self,name,age):
 self.name=name #每个对象都有自己的名字
 self.age=age #每个对象都有自己的年龄
 #共同的技能
 def talk(self):
 print('is talking Chinese')
 def eat(self):
 print('is eating Chinese food')
#实例化的方式产生一个对象
p1=Chinese('zhang',18)

类名加括号即为实例化,实例化会自动触发 __init__ 函数运行。我们可以利用它来为每个对象定制自己的特征。

我们在定义 __init__ 函数时,括号里有三个参数,但在实例化调用时却只传了两个值,为什么不报错呢?这是因为 self 的作用:在实例化时,自动将对象本身传给 __init__ 函数的第一个参数。当然,self 只是一个约定俗成的名字。

注意:这种自动传值的机制仅在实例化时体现。类除了实例化,还有一种作用是属性引用,即通过类名.属性访问。

从上述报错代码可以看出,属性引用时没有自动传值这回事。如果是类调用类里面的方法,需要手动把类当作参数传给它。

Chinese.talk(Chinese)

我们学过名称空间的概念,定义变量或函数都会在内存中开辟一块空间。类里面也有定义变量(数据属性)和函数(函数属性),它们也有名称空间,可以通过 __dict__ 方法查看。

p1=Chinese('zhang',18)
print(Chinese.__dict__)
#{'__module__': '__main__', 'country': 'China', '__init__': <function Chinese.__
# init__ at 0x000002187F35D158>, 'talk': <function Chinese.talk at 0x000002187F35D1E0>, 
# 'eat': <function Chinese.eat at 0x000002187F35D268>, '__
# dict__': <attribute '__dict__' of 'Chinese' objects>,
# '__weakref__': <attribute '__weakref__' of 'Chinese' objects>, '__doc__': None}
print(p1.__dict__)
#{'name': 'zhang', 'age': 18}

通过上述代码显示的结果我们得知,打印实例化后的对象的名称空间,只显示其特有的属性。如果想要找到与其他对象共有的属性,则需要去类的名称空间中查找。

还有一个问题:对象的名称空间中没有函数属性,当然也是去类里面找。但是,不同对象指定的函数,是同一个函数吗?

p1=Chinese('zhang',18)
p2=Chinese('li',19)
print(Chinese.talk)#<function Chinese.talk at 0x000001B8A5B7D1E0>
print(p1.talk) #<bound method Chinese.talk of <__main__.Chinese object at 0x000001B8A5B7BD68>>
print(p2.talk) #<bound method Chinese.talk of <__main__.Chinese object at 0x000001B8A5B7BDA0>>

可以看到,并不是,它们的内存地址都不一样。而且注意 bound method,这是绑定方法。

对象本身只有数据属性,但Python的class机制将类的函数也绑定到对象上,称为对象的方法,或者叫绑定方法。绑定方法唯一绑定一个对象,同一个类的方法绑定到不同的对象上,属于不同的方法。我们可以验证一下:

当用到这个函数时:类调用的是函数属性,既然是函数,就是函数名加括号,有参数传参数。

而对象用到这个函数时,对象没有函数属性,它是绑定方法。绑定方法怎么用呢?也是直接加括号,但不同的是,绑定方法会默认把对象自己作为第一个参数。

class Chinese:
 country='China'
 def __init__(self,name,age):
 self.name=name 
 self.age=age 
 def talk(self):
 print('%s is talking Chinese'%self.name)
 def eat(self):
 print('is eating Chinese food')

p1=Chinese('zhang',18)
p2=Chinese('li',19)
Chinese.talk(p1) #zhang is talking Chinese
p1.talk()  #zhang is talking Chinese

只要是绑定方法,就会自动传值!其实我们以前就接触过这个,在Python3中,类型就是类。数据类型如list、tuple、set、dict这些,实际上也都是类,我们以前用的方法如l1.append(3),还可以这样写:l1.append(l1,3)。

继承与派生

我们说过,Python中一切皆对象。我们从对象中抽取了共同特征和技能,得到了类的概念。类与类之间也有共同特征,我们可以从有共同特征和技能的类中提取共同的技能和特征,叫做父类。

比如老师和学生,都有名字、年纪、生日、性别等,都会走、说话、吃饭……我们就可以从老师和学生中总结出来一个“人”类,称为父类。那老师和学生就是“人”类的子类,子类继承父类,就有了父类的特征和方法。

继承是一种“是什么”什么的关系,继承是一种产生新类的方法,当然目的也是为了减少代码重用。

继承的基本形式是:

class People:
 pass
class Student(People):#People称为基类或者父类
 pass

1. 在Python中支持多继承,一个子类可以继承多个父类。

我们可以通过 __bases__ 的方法查看继承的所有父类,会返回一个元组。

class People:
 pass
class Animals:
 pass
class Student(People,Animals):
 pass

print(Student.__bases__)#(<class '__main__.People'>, <class '__main__.Animals'>)
print(People.__bases__)#(<class 'object'>,)

可以看到,在People父类中,默认也继承了一个object类,这就是新式类和经典类的区别:

凡是继承了object类的类及其子类,都称为新式类;没有继承object类的类,称为经典类。

在Python 3中,默认就是新式类;而在Python2.X中,默认都是经典类。

继承怎么减少代码呢?看例子。

class People:
 def __init__(self,name,age):
 self.name=name
 self.age=age
 def walk(self):
 print('%s is walkig'%self.name)
class Teacher(People):
 def __init__(self,name,age,level):
 People.__init__(self,name,age)
 self.level=level
t1=Teacher('zhang',18,10)
print(t1.level) #10
print(t1.name) #zhang  子类可以用父类定义的属性
t1.walk() #zhang is walking 子类无需定义就可以用父类的方法
print(issubclass(Teacher,People)) #True查看Teacher类是不是People类的子类

从上面的例子中可以看到,Teacher类继承了父类People类,但是Teacher又有自己特有的属性level。子类也可以定义自己独有的方法,甚至可以和父类的方法重名,但是执行时会以子类定义的为准。

这就叫做派生。

2.组合

继承是解决“是什么”什么的问题,那还有一种场景就是“有什么”什么。比如老师有生日,学生也有生日,生日有年月日这些属性。如果每个类都写的话,又是重复代码。但是又不能让学生和老师继承生日类。这时就用到了组合。组合就是解决“有什么”什么的问题。看例子。

class Date:
 def __init__(self,year,mon,day):
 self.year=year
 self.mon=mon
 self.day=day
 def tell_birth(self):
 print('出生于%s年%s月%s日'%(self.year,self.mon,self.day))

class Teacher:
 def __init__(self,name,age,year,mon,day):
 self.name=name
 self.age=age
 self.birth=Date(year,mon,day)
t=Teacher('egon',19,2010,10,10)
print(t.birth)  #<__main__.Date object at 0x0000017E559380F0>
t.birth.tell_birth() #出生于2010年10月10日

什么?嫌参数太多?*args学过吧,你高兴就好。

class Date:
 def __init__(self,year,mon,day):
 self.year=year
 self.mon=mon
 self.day=day
 def tell_birth(self):
 print('出生于%s年%s月%s日'%(self.year,self.mon,self.day))
class Teacher:
 def __init__(self,name,age,*args):
 self.name=name
 self.age=age
 self.birth=Date(*args)
t=Teacher('egon',19,2010,10,10)
print(t.birth)  #<__main__.Date object at 0x0000017E559380F0>
t.birth.tell_birth() #出生于2010年10月10日

3.抽象类与接口

继承有两种用途:

1. 代码重用,子类继承父类的方法。 2. 声明某个子类兼容于某父类,定义一个接口类Interface,接口类中定义了一些接口名(就是函数名)且并未实现接口的功能,子类继承接口类,并且实现接口中的功能。

需要注意的是,Python中并没有接口的关键字,我们只能是模仿接口的功能。

比如在Python中,一切皆文件嘛,那程序是文件,硬件是文件,文本文档也是文件。我们知道什么叫文件呢?就是能读能写。那程序、文本文档这些,都应该有读和写的功能,我们来模拟一下。

class Interface:
 def read(self):
 pass
 def write(self):
 pass
class Txt(Interface):
 def read(self):
 print('文本文档的读取方式')
 def write(self):
 print('文本文档的写入方式')
class Sata(Interface):
 def read(self):
 print('硬盘文件的读取方式')
 def write(self):
 print('硬盘文件的写入方式')
class process(Interface):
 def read(self):
 print('进程数据的读取方式')
 def write(self):
 print('进程数据的写入方式')

这么做的意义就是:我们不需要知道子类有什么具体的方法,既然它们继承了文件类,那它们就是文件,那它们就有读和写这两个功能。

父类限制了子类必须有read和write这两个方法,而且名字也必须一样(当然现在只是我们主观上的限制,一会我们说完抽象类,就可以从代码级别上限制了)。这样就实现了统一,模拟了接口的概念,这就是归一化设计。在归一化设计中,只要是基于一个接口设计的类,那么所有这些类实例化出来的对象,在用法上是一样的。

我们再来说一下抽象类:

Python中的抽象类需要导入一个模块来实现。抽象类只能被继承,不能被实现。

抽象类的写法:

import abc
class File(metaclass=abc.ABCMeta):
 @abc.abstractmethod
 def read(self):
 pass
 @abc.abstractmethod
 def write(self):
 pass
#父类使用了抽象类,那子类就必须继承父类的方法,而且名字也必须一样
#这样就实现了代码级别的限制

class Txt(File):
 def read(self):
 print('文本文档的读取方式')
 def write(self):
 print('文本文档的写入方式')

继承原理:

当我们定义一个类后,Python就会根据上面的继承规律解析出一个继承顺序的列表(MRO列表),可以通过mro()查看,但是这个方法只有在新式类中才有,经典类没有。

super()方法

我们之前用继承是怎么用的来着,

class Parent(object):
 def __init__(self,name,age):
 self.name=name
 self.age=age

class Child(Parent):
 def __init__(self,name,age,salary):
 Parent.__init__(self,name,age,salary)
 self.salary=salary

这其实是和继承没啥关系的写法,如果父类名字改了,在子类中也要改。更优雅的写法是用super()。

class Parent(object):
 def __init__(self,name,age):
 self.name=name
 self.age=age

class Child(Parent):
 def __init__(self,name,age,salary):
 super().__init__(name,age)
 self.salary=salary

这是python3中的写法,如果是python2,super后面的括号里要写(Child,self)。

注意:super()方法只适用于新式类。

如果是多继承的关系,就用到mro列表。如果就是要继承多个父类的方法,那就还是乖乖的用以前指名道姓的方法引用。

看完这篇,可以继续参阅:

Python面向对象编程基础解析(二)

总结

以上就是本文关于Python面向对象编程基础解析的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站:Python探索之ModelForm代码详解、Python_LDA实现方法详解等,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表