真锋
永远保持一颗学习和专注的心
嵌入式视觉笔记

Python3 面向对象编程进阶

本文主要参考廖雪峰的《Python3教程》书籍,加以编排目录,同时对内容进行修改和加上自己的理解,目前仅仅是方便个人理解学习和给初学者参考。顺便说一下 Python 导入模块,应该避免通配符导入,因为它们使名称空间中存在哪些名称不清楚。 为了清楚起见,坚持常规导入更好。

面向对象编程

面向对象编程——Object Oriented Programming,简称 OOP,是一种程序设计思想,把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。在 OOP 编程中对象是 OOP 程序的基本单元,一个对象包含了数据和操作数据的函数,在 Python 中,所有数据类型都可以视为对象,也可以自定义对象,自定义的对象数据类型就是面向对象中的类( Class)的概念。

面向对象的设计思想来源于现实世界,因为现实界中,类( Class)和实例( Instance)的概念是很自然的。 Class 是一种抽象概念,比如我们定义的 Class——Student,是指学生这个概念,而实例( Instance)则是一个个具体的 Student,比如, Harley Zhang 和 HongGao Zhang 是两个具体的 Student。

所以,面向对象的设计思想是抽象出 Class,根据 Class 创建 Instance。面向对象的抽象程度又比函数要高,因为一个 Class 既包含数据,又包含操作数据的方法。

数据封装、继承和多态是面向对象的三大特点,其理解会在后文内容给出。

OPP 与 OOP

面向过程编程 OPP(Procedure Oriented Programming):面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行。为了简化程序设计,面向过程把函数继续切分为子函数,即把大块函数通过切割成小块函数来降低系统的复杂度。
面向对象编程 OOP:面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。
总结:在实际编程中,以处理学生成绩表为例,如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程,而是 Student 这种数据类型应该被视为一个对象,这个对象拥有 name和 score 这两个属性( Property)。

类的理解与定义

类的理解和面向对象编程不是看一篇文章和一本书能彻底学会的,需要反复学习和不断的实践才能彻底掌握,可以找一本经典的书籍来看,可惜,我目前也没有找到经典的 Python 面向对象编程的书籍。

类的理解-类也是对象

在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段,在 Python 中这一点也是一样的。可以说类是对象的抽象化,对象是类的实例化,类不代表具体的事物,而对象表示具体的事物,对象=属性(特征)+方法(行为),类是一个可以创建对象(类实例)的对象。(类也是对象的理解参考这里

Python 中的每个数字、字符串、数据结构、函数、类、模块等等,都是在 Python 解释器的自有“盒子”内,都被认为是 Python 对象。每个对象都有类型(例如,字符串或函数)和内部数据,也就是说这些对象都有相应的类,对象一般都是由类创建的。在实际中,这可以让语言非常灵活,因为函数和类也可以被当做对象使用,具体用法可以参阅其他资料。

class ObjectCreator(object):
    """定义一个空类"""
    pass

mObject = ObjectCreator()
print(mObject)

输出结果:

<main.ObjectCreator object at 0x00000000023EE048>

但是,Python 中的类有一点跟大多数的编程语言不同,在 Python 中,可以把类理解成也是一种对象。对的,这里没有写错,就是对象。为什么呢?因为只要使用关键字 class ,Python解释器在执行的时候就会创建一个对象。程序运行上面代码的时候,就会在内存中创建一个对象,名字就是 ObjectCreator这个对象(类)自身拥有创建对象(类实例)的能力,而这就是为什么它是一个类的原因。但是,它的本质仍然是一个对象,于是我们可以对它做如下的操作:

class ObjectCreator(object):
    """定义一个空类"""
    pass

def echo(ob):
    print(ob)

# 1,变量 mObject 指向的就是一个 ObjectCreator 的实例,后面的 0x10a67a590 是内存地址,每个 实例 的地址都不一样
mObject = ObjectCreator()  
print(mObject)

print(ObjectCreator)  # 2,可以直接打印一个类,因为它其实也是一个对象
echo(ObjectCreator)   # 3,可以直接把一个类作为参数传给函数(注意这里是类,是没有实例化的)
objectCreator = ObjectCreator  # 4,也可以直接把类赋值给一个变量
print(objectCreator)

输出的结果如下:

类的定义

和 C++ 类似,类定义也是以关键字 class 开头,后跟类的名称并以冒号结尾。定义好类后,创建实例是通过类名 + ()实现的。一个更复杂类的定义及创建类实例代码如下。

import torch
import torch.nn as nn

class Softmax_Model(nn.Module):
    r"""A simple softmax model base class.."""
    def __init__(self, out_chan):
        super(Softmax_Model, self).__init__() 
        self.__out_chan = out_chan
        self.convbn = nn.Sequential(nn.Conv2d(in_channels=1, out_channels=out_chan, kernel_size=3, stride=1, padding=1,dilation=1, bias=False),
                                    nn.BatchNorm2d(out_chan), 
                                    nn.ReLU(inplace=True))
        self.convbn2 = nn.Sequential(nn.Conv2d(in_channels=out_chan, out_channels=out_chan, kernel_size=3, stride=1, padding=1,dilation=1, bias=False),
                                    nn.BatchNorm2d(out_chan), 
                                    nn.ReLU(inplace=True))
        self.softmax = nn.Softmax(dim=1)
    def forward(self, x):
        x1 = self.convbn(x)
        x2 = self.softmax(x1)  
        x2 = self.convbn2(x2)
        x2 = self.softmax(x2)
        return x1, x2
if __name__ == "__main__":
    # 1, Define model structure
    input_shape = (13, 48, 80)
    net = Softmax_Model(input_shape[1])   # Create class instance
  • __out_chan 变量是一个实例变量,可以在内部类但不可以在外部类使用 Softmax.__out_chan 访问。
  • 第一种方法 init() 方法是一种特殊的方法,被称为类的构造函数或初始化方法,当创建了这个类的实例时就会调用该方法。
  • Softmax 这个类是继承自 nn.Module 类的,Softmax 被称为子类, nn.Module 是父类,子类继承了父类的全部功能(方法)。如果子类和父类都存在相同的 forward() 方法时,可以说,子类 Softmax 的 forward() 覆盖了父类 nn.Module 的 forward(),在代码实际运行的时候,总是会调用子类的 forward(),这就是继承的另一个好处:多态。

类和实例

面向对象最重要的概念就是类( Class)和实例( Instance),必须牢记类是抽象的模板,比如下面的 Student 类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。Student 类定义代码如下:

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))
    def set_score(self, score):
        if 0 <= score <= 100:
            self.__score = score
        else:
            raise ValueError('bad score')
    def get_score(self):
        return self.__score

__init__ 方法的第一个参数永远是 self,表示创建的实例本身,因此,在 __init__ 方法内部,就可以把各种属性(数据)绑定到 self,self 就指向创建的实例本身。有了 __init__ 方法,在创建实例的时候,就不能传入空的参数了,必须传入与 __init__ 方法匹配的参数,但 self 不需要传,Python 解释器自己会把实例变量传进去。

定义好了 Student 类,就可以根据 Student 类创建出 Student 的实例(即根据模板创建对象),创建实例是通过 类名+() 实现的:

harley = Student('harley', 96)
print(harley)
print(Student)

程序运行结果如下:

变量 harley 指向的就是一个 Student 的实例,后面的 0x000001D7AF158A08 是内存地址,每个 object 的地址都不一样,而 Student 本身则是一个类。值得注意的是,我们可以自由地给一个实例变量绑定属性,比如,给实例 harley 绑定一个 height属性。

harley.height = 174
print(harley.height)
####输出 174 ####

数据封装

数据封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。简单来说就是数据和逻辑被类“封装”起来了,类的实例调用很容易,不用知道内部实现的细节。

总结

  • 类是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;
  • 方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据;
  • 通过在实例上调用方法,我们就直接操作了对象内部的数据,但无需知道方法内部的实现细节。
  • 和静态语言 C++ 不同, Python 允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同。

访问限制

在 Class 内部有属性和方法,外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。但是,一般情况下,如果没有给属性的前面加上双下划线 __,外部代码是可以自由地修改一个 Student 类实例的 name 属性:

>>> harley = Student('Harleys Zhang', 90)
>>> harley.name
>>> 'Harleys Zhang'

因此要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,如 Student 类的 score 属性,是无法从外部访问实例变量 .__score 的,代码如下:

给类的属性添加访问限制,是为了确保外部代码不能随意修改对象内部的状态,使代码更加健壮。

在 Python 中,变量名类似 __xxx__ 的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是 private 变量。Python 中也存在一些以双下划线 “__” 开头和结尾的特殊方法,被称位 Python 的魔法函数,比如 __init__()__len()__ 和 __str__ 等。需要注意的是,Python 官方推荐永远不要将这样的命名方式应用于自己的变量或函数,而是应该按照文档说明来使用 Python 内置的这些特殊成员变量和方法。

类的变量总结

单下划线和双下划线在 Python 变量和方法名称中都各有其含义。有一些含义仅仅是依照约定,被视作是对程序员的提示,而有一些含义是由 Python 解释器严格执行的。

  • 私有变量 private:以 __ 开头的实例变量,只有内部可以访问,外部不能访问。
  • 特殊变量:以双下划线开头,并且以双下划线结尾的变量,特殊变量可以直接访问,private 变量不能。
  • 单个下划线前缀的实例变量:按照约定俗成的规定:单个下划线是一个 Python 命名约定,通常不会由 Python 解释器强制执行(通配符导入除外)。意思是这个名称是供内部使用的,当你看到这样的变量,虽然你可以从外部访问,但最好不要随意访问。

获取对象信息

使用 type()

在 Python 中,我们可以使用 type() 函数判断对象类型,type() 会返回对象对应的 Class 类型,基本的数据类型 int、str 等可以直接判断;但如果要判断一个对象是否是函数,需要使用 types 模块中定义的常量。

import types
def fn():
    pass

print(type(fn) == types.FunctionType)       # True
print(type(abs )== types.BuiltinFunctionType) # True
print(type(lambda x: x) == types.LambdaType)  # True
print(type((x for x in range(10))) == types.GeneratorType)  # True

使用 isinstance()

能用 type() 判断的基本类型也可以用 isinstance() 判断,同时 isinstance() 还可以判断一个变量是否是某些类型中的一种。判断 class 类型一般用 isinstance() 函数,因为对于 class 的继承关系来说,使用 type()就很不方便。

使用 dir()

如果要获得一个对象的所有属性和方法,可以使用 dir() 函数,它返回一个包含字符串的 list,比如,获得一个 int 对象的所有属性和方法。

类似 __xxx__ 的属性和方法在 Python 中都是有特殊用途的,比如 __len__ 方法返回长度。在 Python 中,如果你调用 len() 函数试图获取一个对象的长度,实际上,在 len() 函数内部,它自动去调用该对象的 __len__() 方法,所以,下面的代码是等价的:

>>> len('ABCD')
4
>>> 'ABCD'.__len__()
4

小结

通过 Python 内置的一系列函数,我们可以对任意一个 Python 对象进行剖析,获取其内部的数据。值得注意的是,只有在不知道对象信息的时候,我们才会去获取对象信息。

继承和多态

在开发程序的过程中,如果我们定义了一个类 A,然后又想新建立另外一个类 B,但是类 B 的大部分内容与类 A 的相同时,我们不用从头开始写一个类 B,这就用到了面向对象的三大特性(封装、继承、多态)之一:继承的概念。
通过继承的方式新建类 B,让 B 继承 A,B 会‘遗传’ A 的所有属性(数据属性和函数属性),实现代码重用。通过继承创建的新类成为子类或派生类,被继承的类成为父类、基类或超生类。

Python3 继承的核心原则有以下两条:

  • 子类在调用某个方法或变量的时候,首先在自己内部查找,如果没有找到,则开始根据继承机制在父类里查找。
  • 根据父类定义中的顺序,以深度优先的方式逐一查找父类!

理解多态可以参考以下项目中的代码实例,简单代码实例网上很多,这里的代码是 mmdetection 框架中的多态应用。

class Dataset(object):
    """An abstract class representing a Dataset.
    All other datasets should subclass it. All subclasses should override
    ``__len__``, that provides the size of the dataset, and ``__getitem__``,
    supporting integer indexing in range from 0 to len(self) exclusive.
    """
    def __getitem__(self, index):
        raise NotImplementedError
    def __len__(self):
        raise NotImplementedError
    def __add__(self, other):
        return ConcatDataset([self, other])

class TensorDataset(Dataset):
    """Dataset wrapping tensors.
    Each sample will be retrieved by indexing tensors along the first dimension.
    Arguments:
        *tensors (Tensor): tensors that have the same size of the first dimension.
    """
    def __init__(self, *tensors):
        assert all(tensors[0].size(0) == tensor.size(0) for tensor in tensors)
        self.tensors = tensors
    def __getitem__(self, index):
        return tuple(tensor[index] for tensor in self.tensors)
    def __len__(self):
        return self.tensors[0].size(0)
a = TensorDataset(torch.tensor(np.ones([13, 1, 1, 1])))
len(a)  # 13

以上代码中,当子类和父类都存在相同的 len() 方法时,我们说,子类的 len() 覆盖了父类的 len(),在代码运行的时候,总是会调用子类的 len(),输出结果13,而不是 产生 raise NotImplementedError 错误。这样,我们就获得了继承的另一个好处:多态

super() 函数

在前面的知识中,我们知道,当子类如果有和父类同名的方法时,那就会覆盖掉父类同名的方法,但有时,我们希望能同时实现父类的功能,调用父类的同名方法有两种方式:

1,调用未绑定的父类方法,这种方式简单,但是在多重继承中会出现问题,不建议使用,单重继承的例子,可参考这篇文章,代码示例如下:

class Base:
    def __init__(self):
        print('Base.__init__')

class A(Base):
 
    """单重继承"""
    def __init__(self):
        Base.__init__(self)
        print('A.__init__')

2,使用 super() 函数来调用。super() 最常见用法是在子类中调用父类的初始化方法 __init__(),从而确保父类被正确的初始化了;super() 的另外一个常见用法出现在覆盖 Python 特殊方法的代码中。

MRO 列表

单重继承使用 super 比较好理解,多重继承使用 super 是较难理解的,这涉及到 MRO 的理解。
事实上,对于你定义的每一个类,Python 会计算出一个方法解析顺序(Method Resolution Order, MRO)列表,它代表了类继承的顺序,我们可以使用下面的方式获得某个类的 MRO 列表:

"""
多重继承调用 super方法
"""
class Base(object):
    def __init__(self):
        print("enter Base")
        print("leave Base")
class A(Base):
    def __init__(self):
        print("enter A")
        super(A,self).__init__()
        print("leave A")
class B(Base):
    def __init__(self):
        print("enter B")
        super(B,self).__init__()
        print("leave B")
class C(A,B):
    def __init__(self):
        print("enter C")
        super(C,self).__init__()
        print("leave C")
c=C()
# 获取类的 MRO 列表
print(C.mro())

程序输出结果如下:

enter C
enter A
enter B
enter Base
leave Base
leave B
leave A
leave C
[, , , , ]

MRO 列表真实的列出了类 C 的继承顺序:C->A->B->Base->object。在方法调用时,是按照这个顺序查找的。MRO 列表的顺序是通过一个 C3 线性化算法来实现的,这里我们不去深究这个算法,感兴趣的读者可以自己去了解一下,总的来说,一个类的 MRO 列表就是合并所有父类的 MRO 列表,并遵循以下三条原则:

  • 子类永远在父类前面。
  • 如果有多个父类,会根据它们在列表中的顺序被检查。
  • 如果对下一个类存在两个合法的选择,选择第一个父类。

super 原理

super 的工作原理如下:

def super(cls, inst):
    mro = inst.__class__.mro()
    return mro[mro.index(cls) + 1]

其中,cls 代表类,inst 代表实例,上面的代码做了两件事:

  • 获取 inst 的 MRO 列表;
  • 查找 cls 在当前 MRO 列表中的 index, 并返回它的下一个类,即 mro[index + 1]。

当你使用 super(cls, inst) 时,Python 会在 inst 的 MRO 列表上搜索 cls 的下一个类。

实例属性和类属性

  • Python 是动态语言,根据类创建的实例可以绑定任何属性,给实例绑定属性的方法是通过实例变量,或者通过 self 变量。
  • 在 Python 中,类是一个特殊的对象,类对象可以拥有自己的属性和方法,类属性通常用来记录与这个类相关的特征;给类绑定属性可以直接在 class 中定义,这种属性虽然是类属性,归类所有,但是类的所有实例都可以访问。

简单代码示例如下:

class Student(object):
    name = 'Harley'
    def __init__(self, score):
        self.score = score
s = Student(96)
print(s.name)  # 打印 name 属性,因为实例并没有 name 属性,所以会继续查找 class 的 name 属性
s.name = 'Michael'  # 给实例绑定 name 属性,不影响类属性
print(s.name)  # 由于实例属性优先级比类属性高,因此,如果实例也绑定了 name 属性,它会屏蔽掉类的 name 属性,但是类属性并未消失,用 Student.name 仍然可以访问
#########
# Harley
# Michael
#########

在编写程序的时候,千万不要把实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。

一般来说,实例变量用于每个实例的唯一数据,而类变量用于类的所有实例共享的属性和方法。所以利用类属性,可以知道创建了多少个实例对象,代码如下:

class Tool(object):
    # 使用赋值语句,定义类属性,记录创建工具对象的总数
    count = 0
    def __init__(self, name):
        self.name = name
        # 针对类属性做一个计数+1
        Tool.count += 1
# 创建工具对象
tool1 = Tool("斧头")
tool2 = Tool("铲子")
tool3 = Tool("铁锹")
# 知道使用 Tool 类到底创建了多少个对象
print("现在创建了 %d 个工具" % Tool.count)
##################
# 现在创建了 3 个工具
##################

使用 @property

可以使用 @property 装饰器来创建只读属性,Python 内置的 @property 装饰器就是负责把一个方法变成属性调用的。@property 装饰器会将方法转换为相同名称的只读属性,可以与所定义的属性配合使用,这样可以防止属性被修改。

@property 的实现比较复杂,我们先考察如何使用。把一个 getter 方法变成属性,只需要加上 @property 就可以了,此时, @property 本身又创建了另一个装饰器@score.setter,负责把一个 setter 方法变成属性赋值,于是,我们就拥有一个可控的属性操作。

参考资料

《廖雪峰-Python3教程》
Python中下划线的5种含义
面向对象的三大特性(封装、继承、多态)
Python 类属性和类方法
彻底搞懂python super函数的作用
封装、继承和多态
Python 中类也是对象
Python3.10.0a2官方文档–9. 类

赞赏

发表评论

textsms
account_circle
email

嵌入式视觉笔记

Python3 面向对象编程进阶
本文主要参考廖雪峰的《Python3教程》书籍,加以编排目录,同时对内容进行修改和加上自己的理解,目前仅仅是方便个人理解学习和给初学者参考。顺便说一下 Python 导入模块,应该避免通配…
扫描二维码继续阅读
2020-11-02