Skip to content

Python教程 - 7 面向对象(1)

面向对象,首先涉及到的两个概念就是:对象

什么是类?

类就是对现实事物的抽象设计。

例如设计学生的类,可能包括属性:学号,姓名、年龄、性别等。

设计狗的类,可能包括属性:名字、年龄、品种。

类表示的是对一个事物的抽象,不是具体的某个事物。

什么是对象?

对象就是一个具体的实例,这个实例是从类派生出来的。

我们将类的属性赋值,就产生了一个个实例,也就是对象。

例如通过学生的类,我们赋值:学号=001,姓名=张三,年龄=16,性别=男,班级=三年二班,产生了就是一个名字叫张三的具体的学生,这样我们通过给类可以派生出一个个不同属性值的对象,李四、王五、赵六...。

7.1 类和对象

1 类的定义

类中可以定义属性和行为,属性也叫类的成员变量,表示这个类有哪些数据信息,行为也叫成员方法,表示这个类能干什么。

例如,对于学生类而言,学号、姓名、年级就是属性,学习这个行为,可以定义为方法。

那么我们可以定义以下学生类:

python
# 定义类使用class关键字,后面跟类名
class Student:
    def __init__(self):
        self.sid = None  		# 定义学号
        self.name = None  	# 定义姓名
        self.age = None  		# 定义年龄

    def study(self):
        print(f"我是{self.name}, 我在学习")

在上面的类中,在 __init__ 方法中,通过 self 关键字定义了三个成员变量(sid、name、age), __init__ 方法是类的构造方法,会在使用类创建对象的时候自动调用。

定义了一个成员方法 study,定义实例方法和之前定义函数很相似,不同的是,实例方法的第一个参数是 self,后面才是我们自定义的参数。这个 self 现在暂时不用理会,固定的写法。

在成员方法中访问属性,需要使用 self.属性

2 类的使用

上面我们定义好类了,现在可以使用类来创建对象了。

python
"""定义类"""
class Student:
    def __init__(self):
        self.sid = None    # 定义学号
        self.name = None   # 定义姓名
        self.age = None    # 定义年龄

    def study(self):
        print(f"我是{self.name}, 我在学习")

"""使用类来创建对象"""
stu = Student()
stu.sid = "001"         # 为属性赋值
stu.name = "逗比笔记"
stu.age = 18
stu.study()             # 调用成员方法

stu.age = 19			# 修改实例变量
print(stu.age)          # 访问属性

通过 类名() 可以创建一个对象,得到对象后,我们可以通过 对象.属性 来给变量赋值、访问变量、修改变量的值。

使用 对象.方法() 可以调用实例方法,调用的时候忽略成员方法的第一个 self 参数。

执行结果:

我是逗比笔记, 我在学习
19

面向对象编程就是先设计类,然后通过类创建对象,由对象做具体的工作。

面向过程编程关心的是实现功能的步骤,而面向对象编程更关心由谁来实现功能。

3 self的作用

上面方法第一个参数的 self 表示什么?

self 表示调用当前方法的对象本身。

举个栗子:

下面的代码,我们定义了学生类,创建了两个学生的对象。

python
"""定义类"""
class Student:
    def __init__(self):
        self.sid = None     # 定义学号
        self.name = None    # 定义姓名
        self.age = None     # 定义年龄

    def study(self):
        print(f"我是{self.name}, 我在学习")

"""使用类来创建对象"""
stu1 = Student()		# 创建第一个对象
stu1.sid = "001"    # 为第一个对象赋值
stu1.name = "张三"
stu1.age = 18

stu2 = Student()	  # 创建第二个对象
stu2.sid = "002"    # 为第二个对象赋值
stu2.name = "李四"
stu2.age = 19

stu1.study()
stu2.study()

执行结果:

我是张三, 我在学习
我是李四, 我在学习

self 表示的当前调用成员方法的对象本身。

当我们使用 stu1 调用 study() 方法的时候,self 就是指 张三 这个对象,那么 self.name 的值就是 张三 ;当我们使用 stu2 调用study() 方法的时候,self 就是指 李四 这个对象,那么 self.name的值就是 李四

4 构造方法初始化

上面我们在创建对象后,分别使用 self.属性 来给对象的属性来进行赋值。我们也可以在创建对象的时候,直接传递属性值给 __init__ 方法进行初始化, __init__ 方法是类的构造方法。

举个例子:

python
"""定义类"""
class Student:
    def __init__(self, sid, name, age):
        self.sid = sid  # self.sid表示属性,sid表示构造方法参数
        self.name = name
        self.age = age

    def study(self):
        print(f"我是{self.name}, 我在学习")


"""使用类来创建对象"""
stu1 = Student("001", "张三", 18)
stu2 = Student("002", "李四", 19)

stu1.study()
stu2.study()

上面使用 __init__ 方法定义了多个参数,将参数赋值给成员变量,这样就可以在创建对象的时候,可以通过 类名(参数) 在括号中传递参数,代码要简洁很多。


5 定义一个空类

我们有时候定义一个类,但是类的细节还没有想好,只是想定义一个空类,那么就需要pass关键字:

python
class Student:
    pass						# 使用pass关键字来定义空类

我们在编写代码的时候,可能先构建功能实现的步骤,然后再完善具体的方法,所以我们可能需要定义空的方法,后面再来完善方法的细节。

使用pass关键字定义空方法:

python
def study():
    pass						# 使用pass关键字来定义空方法

6 类变量

上面我们定义的属性和方法,是实例变量和实例方法,也叫成员变量和成员方法。

实例变量对于每个实例而言,是独立的数据,每个对象之间相互不会影响。创建一个对象,就会开辟独立的内存空间保存对象的实例变量数据。但是无论创建多少对象,实例方法只有一份,所有对象共享,通过传入的第一个参数self,来确定是哪个对象调用了实例方法。

在类中还可以定义各个对象共享的数据,也就是类变量。

打个比方,我们定义了一个Student类,然后通过Student类来创建对象,我们想知道一共创建了多少个Student对象,应该如何操作呢?

我们可以通过定义类变量来实现:

python
class Student:

    stu_count = 0;

    def __init__(self, sid, name, age):
        self.sid = sid					# 学号
        self.name = name				# 姓名
        self.age = age					# 年龄
        Student.stu_count += 1

    def study(self):
        print(f"我是{self.name}, 我在学习")


"""使用类来创建对象"""
stu1 = Student("001", "张三", 18)
stu2 = Student("002", "李四", 19)
stu2 = Student("003", "王五", 17)
print(Student.stu_count)

在上面的代码中,我们定义了一个类,然后在类中定义了一个 stu_count 变量,该变量不在方法中定义,也就是累变量。

类变量通过 类名.变量 来赋值和访问。

执行结果:

3

7 类方法

除了类变量,还有类方法,类方法的作用就是用来操作类变量。

类方法需要在方法上添加 @classmethod 注解,类方法至少有一个形参,该形参用于绑定类,一般命名为 cls

python
class Student:

    stu_count = 0;

    def __init__(self, sid, name, age):
        self.sid = sid					# 学号
        self.name = name				# 姓名
        self.age = age					# 年龄
        Student.stu_count += 1

    @classmethod
    def get_stu_count(cls):
        print(f"一共创建了{Student.stu_count}个学生")


stu1 = Student("001", "张三", 18)
stu2 = Student("002", "李四", 19)
stu2 = Student("003", "王五", 17)
Student.get_stu_count()

在类方法中是不能访问实例变量的,因为没有传递self参数。

在实例方法中是可以访问类变量的,因为是通过类名访问的。

执行结果:

一共创建了3个学生

8 推荐的使用方式

实例变量和实例方法推荐使用 对象.变量对象.方法() 来调用的。

类变量和类方法推荐使用 类.变量类.方法() 来调用。

不过通过 对象.变量 也是可以访问类变量的,只是不推荐。

查看下面的代码:

python
class Student:

    stu_count = 0;

    def __init__(self, sid, name, age):
        self.sid = sid					# 学号
        self.name = name				# 姓名
        self.age = age					# 年龄
        Student.stu_count += 1


stu1 = Student("001", "张三", 18)
print(stu1.stu_count)							# 结果为1,访问的是类变量

stu1.stu_count = 10
print(stu1.stu_count)							# 结果为10,访问的是成员变量

print(Student.stu_count)					# 结果还是1,访问的是类变量

执行结果:

1
10
1

第一次执行 print(stu1.stu_count) 的时候,是使用对象调用了类变量 stu_count,所以执行结果是1;

当执行stu1.stu_count = 10 的时候,是给对象stu1添加了一个实例变量,并不是访问类变量,所以这个时候stu1是多了一个实例变量 stu_count,所以再次执行 print(stu1.stu_count) 的时候,执行结果是10;

此时执行 print(Student.stu_count),访问的是类变量 stu_count, 执行结果为1。

通过 对象.变量 = 值 的方式给对象的属性赋值,如果该对象没有该属性,就会给该对象添加相应的属性。所以我们之前在 init 构造方法中创建实例变量是一种推荐的方式,其实在任何地方,通过 对象.变量 = 值,都可以给对象创建实例变量,只是在类本身中,对象.变量 = 值 可以写成 self.变量 = 值,所以在类的其他方法中也可以通过self.变量 = 值 为对象创建实例变量。

但是我们推荐在 __init__ 构造方法中为对象创建实例变量。

9 静态方法

静态方法的作用是用来定义一些工具类函数。

在静态方法中不能访问实例成员和类成员。

使用 @staticmethod 注解来创建静态方法。

举个栗子:

我们可以定义数学运算的工具类,类中的方法可以通过类名来访问。

python
class Math_Util:

    @staticmethod
    def get_max(a, b):							# 获取大的数
        return a if a > b else b

    @staticmethod
    def get_min(a, b):							# 获取小的数
        return a if a < b else b


max_num = Math_Util.get_max(11, 15)
print(max_num)

min_num = Math_Util.get_min(11, 15)
print(min_num)

执行结果:

15
11

10 类的内置方法

上面说的__init__ 构造方法,是Python类内置的方法之一,Python类有很多内置的方法,这些内置方法我们称之为:魔术方法。

下面介绍几个常用的类内置方法。

1 __str__

__str__的方法的作用是将对象转换为字符串。

当我们直接打印对象,或将对象转换为字符串进行打印的时候,会显示地址信息。

举个栗子:

python
class Student:

    def __init__(self, sid, name, age):
        self.sid = sid
        self.name = name
        self.age = age


stu = Student("001", "张三", 18)
print(stu)
print(str(stu))

执行结果:

<__main__.Student object at 0x100498bd0>
<__main__.Student object at 0x100498bd0>

这些地址信息对于我们而言,不太关注,我们希望打印对象相关的信息,那么可以重写 __str__ 内置方法,自定义输出的内容:

python
class Student:

    def __init__(self, sid, name, age):
        self.sid = sid
        self.name = name
        self.age = age

    def __str__(self):
        return f"sid:{self.sid}, name:{self.name}, age:{self.age}"


stu = Student("001", "张三", 18)
print(stu)
print(str(stu))

执行结果:

sid:001, name:张三, age:18
sid:001, name:张三, age:18

2 __lt__

两个对象是无法进行比较大小的,但是通过重写 __lt__方法,来对两个对象进行比较,可以实现两个对象大于和小于的比较。

python
class Student:

    def __init__(self, sid, name, age):
        self.sid = sid
        self.name = name
        self.age = age

    def __lt__(self, other):					# other表示另一个对象,返回值为True或Flase
        return self.age < other.age


stu1 = Student("001", "张三", 18)
stu2 = Student("002", "李四", 19)
print(stu1 > stu2)

通过重写 __lt__ 方法,可以指定两个对象比较大小的判断方式。

其实还有一个 __gt__ 的方法,是大于符号的魔术方法,但是实现了 __lt__ 方法能同时比较大于和小于,所以就 __gt__ 就没必要实现了,两个实现一个就可以了。

3 __le__

__le__ 是 <= 的魔术方法,通过实现 __le__ 方法, 可以实现两个对象大于等于小于等于的比较。

python
class Student:

    def __init__(self, sid, name, age):
        self.sid = sid
        self.name = name
        self.age = age

    def __le__(self, other):					# other表示另一个对象,返回值为True或Flase
        return self.age <= other.age


stu1 = Student("001", "张三", 18)
stu2 = Student("002", "李四", 19)
print(stu1 <= stu2)

同样,>= 的魔术方法是 __ge__ ,不过 __le____ge__ 实现一个就可以了。

4 __eq__

不实现__eq__ 方法,两个对象也是可以进行等于比较的,但是是比较两个对象的内存地址,两个不同的对象==比较一定是Flase。

如果实现了__eq__方法,就可以按照自己的想法来判断两个对象是否相等。

python
class Student:

    def __init__(self, sid, name, age):
        self.sid = sid
        self.name = name
        self.age = age

    def __eq__(self, other):					# other表示另一个对象,返回值为True或Flase
        return self.age == other.age


stu1 = Student("001", "张三", 18)
stu2 = Student("002", "李四", 18)
print(stu1 == stu2)

上面实现了__eq__方法,通过年龄来判断两个对象是否相等。

5 __repr__

__repr__ 方法的作用是将对象转换为字符串。怎么和 __str__ 方法一样?

编写代码:

python
class Student:

    def __init__(self, sid, name, age):
        self.sid = sid
        self.name = name
        self.age = age

    def __repr__(self):
        return f"sid:{self.sid}, name:{self.name}, age:{self.age}"


stu = Student("001", "张三", 18)
print(stu)
print(str(stu))

执行结果:

sid:001, name:张三, age:18 sid:001, name:张三, age:18

真的和 __str__ 方法一样?

__repr__ 方法如果只是用来打印对象信息,那么和 __str__ 方法一样,但是__repr__ 方法更重要的是转换为解释器可识别的字符串。

需要搭配 eval() 函数才能显示其功能。

先介绍一下 eval() 函数。

举个栗子:

python
str = "1 + 2 * 3"
result = eval(str)
print(result)						# 执行结果:7

看出 eval() 函数的牛逼之处了吗?它将字符串作为代码执行了。

下面通过 __repr__ 方法和 eval 函数克隆对象:

首先 __repr__ 方法返回的字符串是可以执行的python语句,需要和 __init__ 构造方法一致,这样后面才能通过 eval 函数来执行这个语句,然后调用构造方法创建对象。

python
class Student:

    def __init__(self, sid, name, age):
        self.sid = sid
        self.name = name
        self.age = age

    def __repr__(self):
        return f"Student('{self.sid}', '{self.name}', {self.age})"


stu1 = Student("001", "张三", 18)
stu_str = repr(stu1)                # 获取__repr__()方法返回的string数据
print(stu_str)

stu2 = eval(stu_str)                # 将string执行,返回的是一个对象,相当于克隆了一个对象
print(stu2.name)
print(stu1 == stu2)

7.2 封装

面向对象编程,是一种编程思想,简单的理解就是:先有模板,也就是类,根据模板去创建实例,也就是对象,然后使用对象来完成功能开发。

我们经常说面向过程编程和面向对象编程,面向过程编程关注的是实现功能的步骤,面向对象编程更关注的是谁来实现功能。

面向对象编程有3大特性:

  • 封装
  • 继承
  • 多态

下面依次开始讲起。

在前面我们创建类,在类中定义了属性和方法,通过属性和方法来对现实世界的事物进行抽象的描述。

一个事物有很多的属性和方法,但是并不是所有属性和方法都需要开放出来。例如我们定义了一个手机类,我们可以使用手机打电话、拍照等,但是我们并不关心手机电压,驱动信息,也不关心内存的分配,CPU的调度等,虽然这些都属于手机的属性和行为。

我们可以将用户不关心的属性和方法封装并隐藏起来,只给类内部的方法调用,例如上网会用到4G模块,但是不是由用户来使用4G模块,而是由手机上网的功能来调用4G模块,只开放用户直接使用的信息和功能。

那么回过头来看,什么是封装?

它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象的内部信息。如果要访问这些信息,需要通过该类所提供的方法来实现对内部信息的操作和访问。

1 私有属性和方法

那么怎么将不想暴露的变量和方法隐藏起来呢,就需要用到私有成员变量和私有成员方法。

定义私有成员的方式:

  • 定义私有成员变量:变量名以 __开头,2个下划线开头
  • 定义私有成员方法:方法名以 __开头,2个下划线开头

举个栗子:

python
class Phone:

    def __init__(self):
        self.producer = "华为"        # 手机品牌
        self.__voltage = 12          # 电压

    def call(self):
        print("打电话")
        print(f"手机品牌:{self.producer}")
        print(f"手机电压:{self.__voltage}")

    # 定义一个私有方法
    def __get_run_voltage(self):
        print(f"当前电压:{self.__voltage}")


phone = Phone()
phone.call()
phone.producer = "小米"
phone.__voltage = 24        # 这里修改的不是私有属性的值
print(phone.producer)
print(phone.__voltage)      # 这里访问的不是私有属性
phone.call()

如果不想暴露的属性和方法可以定义为私有成员。私有属性和私有方法只能在类内部的方法中调用,不能通过对象来调用。

上面的代码 phone.__voltage = 24, 并不能修改私有属性的值,而是给对象添加了一个额外的属性。

执行结果:

打电话
手机品牌:华为
手机电压:12
小米
24
打电话
手机品牌:小米
手机电压:12

2 骗人的私有属性

我们上面私有属性和方法在类外部无法调用,只能在类内部使用,但这其实是一个障眼法的。

查看下面的代码:

python
class Phone:

    def __init__(self):
        self.producer = "华为"        # 手机品牌
        self.__voltage = 12          # 电压

    def call(self):
        print("打电话")
        print(f"手机品牌:{self.producer}")
        print(f"手机电压:{self.__voltage}")
        
    # 定义一个私有方法
    def __get_run_voltage(self):
        print(f"当前电压:{self.__voltage}")


phone = Phone()
print(phone.__dict__)					# 将对象属性以字典的形式输出

将对象以字典的形式输出,查看一下对象的结构。

执行结果:

{'producer': '华为', '_Phone__voltage': 12}

哦,哦,私有属性__voltage 变成了 _Phone__voltage 。那么我们能否修改属性 _Phone__voltage 来修改私有属性呢?

代码验证一下:

python
class Phone:

    def __init__(self):
        self.producer = "华为"        # 手机品牌
        self.__voltage = 12          # 电压

    def call(self):
        print("打电话")
        print(f"手机品牌:{self.producer}")
        print(f"手机电压:{self.__voltage}")

    # 定义一个私有方法
    def __get_run_voltage(self):
        print(f"当前电压:{self.__voltage}")

phone = Phone()
phone._Phone__voltage = 24					# 修改私有属性
phone.call()
phone._Phone__get_run_voltage()			# 调用私有方法

执行结果:

打电话
手机品牌:华为
手机电压:24
当前电压:24

所以,私有属性和私有方法是骗人的障眼法,完全可以通过 _类型__私有属性_类型__私有方法 的形式来调用。

但是,这是一个君子约定,非常不建议这么做。

3 封装的进阶

我们前面说到,封装是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象的内部信息。如果要访问这些信息,需要通过该类所提供的方法来实现对内部信息的操作和访问。

在面向对象编程思想中,我们一般会将所有的属性都设置为私有的,然后为每个属性提供两个对应的方法,分别用来获取和设置对应的属性,这两个方法称为getter和setter方法,在java中就是这么做的。

举个栗子:

python
class Phone:

    def __init__(self, producer, voltage):
        self.__producer = producer        # 手机品牌
        self.__voltage = voltage          # 电压

    def get_producer(self):
        return self.__producer

    def set_producer(self, producer):
        self.__producer = producer

    def get_voltage(self):
        return self.__voltage

    def set_voltage(self, voltage):
        if voltage < 36:
            self.__voltage = voltage
        else:
            raise ValueError("参数错误")        # 限制传入的参数值,进行报错处理

phone = Phone("华为", 12)
print(phone.get_producer())		# 通过getter方法获取变量
print(phone.get_voltage())

phone.set_producer("小米")		# 通过setter方法设置变量
phone.set_voltage(24)
print(phone.get_producer())
print(phone.get_voltage())

这样做的话,严格控制了属性获取和设置的入口,如果通过 对象.属性 来修改,代码很多的时候完全会不知道在哪里修改了属性导致出现了问题。另外我们可以在setter方法中对属性的设置进行限制。

执行结果:

华为
12
小米
24

4 getter和setter的初级进化

上面通过getter和setter方法来获取或设置属性显得略显复杂,有没有更好的方式呢?

肯定有的,不然还在这里哔哔?

直接上代码:

python
class Phone:

    def __init__(self, producer, voltage):
        self.producer = producer        # 访问的不是属性,是方法
        self.voltage = voltage          # 访问的不是属性,是方法

    def get_producer(self):
        return f"品牌:{self.__producer}"

    def set_producer(self, producer):
        print(f"设置producer:{producer}")
        self.__producer = producer

    def get_voltage(self):
        return f"电压:{self.__voltage}"

    def set_voltage(self, voltage):
        print(f"设置voltage:{voltage}")

        if voltage < 36:
            self.__voltage = voltage
        else:
            raise ValueError("参数错误")        # 限制传入的参数值,进行报错处理

    producer = property(get_producer, set_producer)
    voltage = property(get_voltage, set_voltage)


phone = Phone("华为", 12)
print(phone.producer)
print(phone.voltage)

phone.producer = "小米"
phone.voltage = 24
print(phone.producer)
print(phone.voltage)

我们在之前的代码上添加了两行代码:

python
producer = property(get_producer, set_producer)
voltage = property(get_voltage, set_voltage)

这是给属性绑定了getter和setter方法,这样,在访问属性的时候,会直接调用getter和setter方法。

所以 self.producerphone.producer 都是调用的是方法,而不是属性,通过执行结果可以看出来,或者通过断点调试。

执行结果:

设置producer:华为
设置voltage:12
品牌:华为
电压:12
设置producer:小米
设置voltage:24
品牌:小米
电压:24

5 getter和setter的终极写法

上面的getter和setter的写法有些繁琐,我们还可以使用 @property 注解进一步的优化。

代码如下:

python
class Phone:

    def __init__(self, producer, voltage):
        self.producer = producer        # 访问的不是属性,是方法
        self.voltage = voltage          # 访问的不是属性,是方法

    @property
    def producer(self):
        return self.__producer

    @producer.setter
    def producer(self, producer):
        self.__producer = producer

    @property
    def voltage(self):
        return self.__voltage

    @voltage.setter
    def voltage(self, voltage):
        if voltage < 36:
            self.__voltage = voltage
        else:
            raise ValueError("参数错误")        # 限制传入的参数值,进行报错处理


phone = Phone("华为", 12)
print(phone.producer)				# 调用的是方法,不是属性
print(phone.voltage)

phone.producer = "小米"			 # 调用的是方法,不是属性
phone.voltage = 24
print(phone.producer)
print(phone.voltage)

每个属性对应两个与属性名相同的方法,分别用于获取和设置属性,并在方法上添加 @property@属性.setter 注解。

执行结果:

华为
12
小米
24

如果想要属性是只读的,可以将setter删掉。

其实在Python中,我们有时候也没有必要针对所有属性都添加getter和setter,如果哪个属性有相关的需要,我们可以在添加,不论是否添加getter和setter,调用的代码是不变的,都是 对象.属性 ,只是有getter和setter,调用的是方法。

6 __slots__

我们之前可以通过 对象.属性 = 值 随时为一个对象添加属性。

__slots__ 的作用就是限制一个类创建的实例只能有固定的属性,不能随意添加属性。

举个栗子:

python
class Phone:

    __slots__ = ("producer", "__voltage")

    def __init__(self, producer, voltage):
        self.producer = producer
        self.__voltage = voltage


phone = Phone("华为", 12)
phone.size = 6          # 报错:AttributeError: 'Phone' object has no attribute 'size'

上面的类只能包含属性 __slots__ 指定的属性,添加别的属性就会报错。

优点:防止写错了属性名字而新增了新的属性,导致程序出现bug。

缺点:运行时不能为对象添加属性了,丧失了动态语言的灵活性。

这个功能可能这辈子也用不到,但是还是知道一下。