Python面向对象(一):对象找到了嘛?

84 阅读11分钟

面向过程VS面向对象

面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行。为了简化程序设计,面向过程把函数继续切分为子函数,即把大块函数通过切割成小块函数来降低系统的复杂度

面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接受其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递

举个例子来说明面向过程和面向对象在程序流程上的不同之处

假设我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个dict表示:

std1 = { 'name': 'Michael', 'score': 98 }
std2 = { 'name': 'Bob', 'score': 81 }

处理学生成绩可以通过函数实现,比如打印学生的成绩:

def print_score(std):
    print('%s: %s' % (std['name'], std['score']))

如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程,而是Student这种数据类型应该被视为一个对象,这个对象拥有namescore两个属性(Property)。如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个print_score消息,让对象自己把自己的数据打印出来

class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score

    def print_score(self):
        print('%s: %s' % (self.name, self.score))

给对象发消息实际上就是调用对象对应的关联函数,称为对象的方法(Method)

bart = Student('Bart Simpson', 59)
lisa = Student('Lisa Simpson', 87)
bart.print_score()
lisa.print_score()

面向对象的设计思想是从自然界中来的,在自然界中,类(Class)和实例(Instance)的概念是很自然的。

Class是一种抽象概念,我们定义的Class——Student,是值学生这个概念,而实例(Instance)则是一个个具体的学生,bart、lisa就是两个具体的Student

面向对象的设计思想就是抽象出Class,根据Class创建Instance

面向对象的抽象程度比函数要高,因为一个Class既包含数据,又包含操作数据的方法

对象,你找到了吗?

我们通过一段最基本的Python面向对象的应用代码来详细理解面向对象的思想

class Document():
    def __init__(self, title, author, context):
        print('init function called')
        self.title = title
        self.author = author
        self.__context = context # __开头的属性是私有属性

    def get_context_length(self):
        return len(self.__context)

    def intercept_context(self, length):
        self.__context = self.__context[:length]

harry_potter_book = Document('Harry Potter', 'J. K. Rowling', '... Forever Do not believe any thing is capable of thinking independently ...')

print(harry_potter_book.title)
print(harry_potter_book.author)
print(harry_potter_book.get_context_length())

harry_potter_book.intercept_context(10)

print(harry_potter_book.get_context_length())

print(harry_potter_book.__context)

########## 输出 ##########

init function called
Harry Potter
J. K. Rowling
77
10

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-5-b4d048d75003> in <module>()
     22 print(harry_potter_book.get_context_length())
     23 
---> 24 print(harry_potter_book.__context)

AttributeError: 'Document' object has no attribute '__context'
  • 类:一群有着相似性的事物集合,对应Python的Class
  • 对象:集合中的一个事物,对应由Class生成的某一个object,代码中的harry_potter_book
  • 属性:对象的某个静态特征,比如代码中的title、author和__context
  • 函数:对象的某个动态能力,比如代码中的intercept_context()、get_context_length()函数

类:一群有着相同属性和函数的对象的集合

class Document 定义了Document类,往下它有三个函数

  • init表示构造函数,一个对象生成时会被自动调用的函数 harry_potter_book = Document(...)这一行代码被执行的时候,'init function called'字符串会被打印出来
  • get_context_length() intercept_context() 则为类的普通函数,我们调用它们来对对象的属性做一些事情。

class Document有三个属性,titleauthor__context

  • 如果一个属性以__开头,默认这个属性是私有属性,私有属性指不希望在类的函数之外的地方被访问和修改的属性
  • title和author能够很自由的被打印出来,但print(harry_potter_book.__context)就会报错

再深入一点吧

在工程实践中,随着复杂度的提升,我们会遇到一些问题

  • 如何在一个类中定义一些常量,每个对象都可以方便访问这些常量而不用重新构造?
  • 如果一个函数不涉及到访问修改这个类的属性,而放到类外面有点不恰当,怎么做才能更优雅?
  • 既然类是一群相似的对象的集合,那可以不可是一群相似的类的集合呢?
class Document():
    
    WELCOME_STR = 'Welcome! The context for this book is {}.'
    
    def __init__(self, title, author, context):
        print('init function called')
        self.title = title
        self.author = author
        self.__context = context
    
    # 类函数
    @classmethod
    def create_empty_book(cls, title, author):
        return cls(title=title, author=author, context='nothing')
    
    # 成员函数
    def get_context_length(self):
        return len(self.__context)
    
    # 静态函数
    @staticmethod
    def get_welcome(context):
        return Document.WELCOME_STR.format(context)


empty_book = Document.create_empty_book('What Every Man Thinks About Apart from Sex', 'Professor Sheridan Simove')


print(empty_book.get_context_length())
print(empty_book.get_welcome('indeed nothing'))

########## 输出 ##########

init function called
7
Welcome! The context for this book is indeed nothing.
  1. 第一个问题,在Python的类中,只需要和函数并列的声明并赋值,就可以实现这一点 例如这段代码中的 WELCOME_STR,一种很常规的做法,是用全大写来表示常量,因此我们可以在类中使用 self.WELCOME_STR ,或者在类外使用 Entity.WELCOME_STR 来表达这个字符串。

  2. 针对第二个问题,我们提出了类函数、成员函数和静态函数三个概念。前两者产生的影响是动态的,能够访问或者修改对象的属性;而静态函数则与类没有什么关联,最明显的特征便是,静态函数的第一个参数没有任何特殊性。

    静态函数可以用来做一些简单独立的任务,方便测试,优化代码结构。静态函数可以通过函数前一行加上装饰器@staticmethod 来表示

    类函数的第一个参数一般为cls,表示必须传一个类进来。类函数最常用的功能是实现不同的init构造函数,我们使用 create_empty_book 类函数,来创造新的书籍对象,其 context 一定为 'nothing'。这样比直接构造要清晰一些。类函数需要装饰器 @classmethod 来声明。

  3. 成员函数是最正常的类的函数,不需要任何装饰器声明,第一个参数self代表当前对象的引用,可以通过此函数,来实现想要的查询/修改来的属性等功能

继承下去!

第三个问题,既然类是一群相似的对象的集合,那可以不可是一群相似的类的集合呢?当然可以,只要抽象得好,类可以描述成任何事物的集合

类的继承,顾名思义指的是一个类即拥有另一个类的特征,也拥有不同于另一个类的独特特征

class Entity():
    def __init__(self, object_type):
        print('parent class init called')
        self.object_type = object_type
    
    def get_context_length(self):
        raise Exception('get_context_length not implemented')
    
    def print_title(self):
        print(self.title)

class Document(Entity):
    def __init__(self, title, author, context):
        print('Document class init called')
        Entity.__init__(self, 'document')
        self.title = title
        self.author = author
        self.__context = context
    
    def get_context_length(self):
        return len(self.__context)
    
class Video(Entity):
    def __init__(self, title, author, video_length):
        print('Video class init called')
        Entity.__init__(self, 'video')
        self.title = title
        self.author = author
        self.__video_length = video_length
    
    def get_context_length(self):
        return self.__video_length

harry_potter_book = Document('Harry Potter(Book)', 'J. K. Rowling', '... Forever Do not believe any thing is capable of thinking independently ...')
harry_potter_movie = Video('Harry Potter(Movie)', 'J. K. Rowling', 120)

print(harry_potter_book.object_type)
print(harry_potter_movie.object_type)

harry_potter_book.print_title()
harry_potter_movie.print_title()

print(harry_potter_book.get_context_length())
print(harry_potter_movie.get_context_length())

########## 输出 ##########

Document class init called
parent class init called
Video class init called
parent class init called
document
video
Harry Potter(Book)
Harry Potter(Movie)
77
120

Document和Video都有相应的标题、作者和内容等属性,我们可以从中抽象出一个叫做Entity的类,作为他们俩的父类

每个类都有构造函数,继承类在生成对象的时候,是不会自动调用父类的构造函数的,因此必须在init()函数中显示调用父类的构造函数。

执行顺序: 子类的构造函数 ——> 父类的构造函数


父类get_context_length() 函数。如果使用Entity直接生成对象,调用 get_context_length() 函数,就会 raise error 中断程序的执行,这种写法叫做函数重写,可以使子类必须重新写一遍get_context_length() 函数,来覆盖掉原有函数。


print_title()函数定义在父类中,但是子类的对象可以用它来打印title,这就体现了继承的优势:减少重复的代码,降低系统的复杂度


抽象函数与抽象类

from abc import ABCMeta, abstractmethod

class Entity(metaclass=ABCMeta):
    @abstractmethod
    def get_title(self):
        pass

    @abstractmethod
    def set_title(self, title):
        pass

class Document(Entity):
    def get_title(self):
        return self.title
    
    def set_title(self, title):
        self.title = title

document = Document()
document.set_title('Harry Potter')
print(document.get_title())

entity = Entity()

########## 输出 ##########

Harry Potter

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-7-266b2aa47bad> in <module>()
     21 print(document.get_title())
     22 
---> 23 entity = Entity()
     24 entity.set_title('Test')

TypeError: Can't instantiate abstract class Entity with abstract methods get_title, set_title

抽象类是一种特殊的类,它生下来就是作为父类存在的,一旦对象化就会报错。同样,抽象函数定义在抽象类中,子类必须重写该函数才能使用,抽象函数使用装饰器@abstractmethod来表示

代码中entity = Entity()直接报错,只有通过 Document 继承 Entity 才能正常使用。


这是软件工程中一个很重要的概念,定义接口。大型工程往往需要很多人合作开发,在idea提出之后,开发组和产品组首先会召开产品设计会,PM(Product Manage)产品经理写出产品需求文档,然后迭代,TL(Team Leader)项目经理编写开发文档,开发文档中会定义不同模块的大致功能和接口、每个模块之间如何写作、单元测试和集成测试、线上灰度测试、监测日志等一系列开发流程

抽象类就是一种自上而下的设计风范,你只需要用少量的代码描述清楚要做的事情,定义好接口,然后就可以交给不同开发人员去开发对接

菱形继承

 --->B---
A-      -->D
 --->C---

这种继承方式,叫做菱形继承,BC 继承了 A,然后 D 继承了 BC,创造一个 D 的对象。那么构造函数调用顺序又是怎样的呢?

class A():
    def __init__(self):
        print('enter A')
        print('leave A')

class B(A):
    def __init__(self):
        print('enter B')
        super().__init__()
        print('leave B')

class C(A):
    def __init__(self):
        print('enter C')
        super().__init__()
        print('leave C')

class D(B, C):
    def __init__(self):
        print('enter D')
        super().__init__()
        print('leave D')

D()

enter D
enter B
enter C
enter A
leave A
leave C
leave B
leave D

菱形继承潜在的问题:一个基类的初始化函数可能被调用两次。在一般的工程中,这显然不是我们所希望的。正确的做法应该是使用 super 来召唤父类的构造函数,而且 python 使用一种叫做方法解析顺序的算法(具体实现算法叫做 C3),来保证一个类只会被初始化一次。

OOP面向对象编程总结

OOP (Object-Oriented Programming),面向对象编程是一种是一种程序设计思想,是基于对象和类的编程范式,通过将程序的功能组织为多个对象,每个对象包含数据和操作数据的方法。 OOP强调模块化、代码重用和抽象化,提供了一种直观、自然的编程犯法,是现代软件开发中最常用的编程方法之一。

OOP的核心概念

类(Class): 类是对象的蓝图或模板,它定义了一组属性和方法。属性是对象的特征,而方法是对象的行为。例如,"车"可以作为一个类,其中包含属性如颜色、型号,以及方法如启动、刹车。

对象(Object): 对象是类的实例,是类中定义的属性和方法的具体实现。每个对象都有独立的属性值,但共享同一类的方法。创建对象的过程称为实例化。

封装(Encapsulation): 封装是将数据和操作数据的方法绑定在一起,并隐藏对象的内部实现,只允许通过公开的方法访问和修改对象的属性。这有助于保护数据完整性和提高代码安全性。

继承(Inheritance): 继承是通过创建一个新类来复用现有类的属性和方法。新类(子类)继承旧类(父类)的所有特性,同时可以添加新的特性或修改现有特性。这种机制促进了代码重用和系统扩展。

多态(Polymorphism): 多态性允许对象以不同的形式表现。当一个方法可以应用于不同类型的对象时,就体现了多态性。通过继承和接口实现多态,使得相同的方法在不同对象上可以有不同的表现方式。

面向对象编程的四要素

  • 抽象:本质就是抽取不同类的相同方法(函数)和属性,作为父类的属性和方法
  • 封装:就是把功能封装抽象得的方法和其他属性和方法
  • 继承:子类继承父类的抽象出来的属性和方法
  • 多态:就是重写抽象的方法(函数)

参考资料

  • 极客时间《Python核心技术与实践》
  • 廖雪峰的官方网站Python教程