第一章 初始 Python
第二章 认识 Python 变量、类型、运算符
第三章 认识 条件分支、循环结构
第四章 认识 Python的五种数据结构
第五章 认识 Python 函数、模块
第六章 认识面向对象三大特性
面向对象编程是一种非常流行的编程范式(programming paradigm),所谓编程范式就是程序设计的方法论,简单的说就是程序员对程序的认知和理解以及他们编写代码的方式。
在前面的文档中,我们说过“程序是指令的集合”,运行程序时,程序中的语句会变成一条或多条指令,然后由CPU(中央处理器)去执行。为了简化程序的设计,我们又讲到了函数,把相对独立且经常重复使用的代码放置到函数中,在需要使用这些代码的时候调用函数即可。如果一个函数的功能过于复杂和臃肿,我们又可以进一步将函数进一步拆分为多个子函数来降低系统的复杂性。
编程是让我们以计算机的方式控制机器完成任务,但这种思维方式与人类正常思维方式不同,导致编程的乐趣减少。
复杂系统的开发会使代码过于复杂,增加开发和维护的难度。
面向对象编程作为一种新的编程范式,它把数据和操作数据的函数视为一个整体,称为对象。
在面向对象编程中,对象可以接收并响应消息。
通过发送和接收消息,多个对象可以协同工作,从而构建出复杂的系统并解决现实中的问题。
这种基于消息传递的协作方式,使得程序中的对象能够相互沟通,实现更加灵活和可扩展的解决方案。
这种思想使得程序的设计更加接近现实世界,提高了软件开发的效率和可维护性。
面向对象编程:把一组数据和处理数据的方法组成对象,把行为相同的对象归纳为类,通过封装隐藏对象的内部细节,通过继承实现类的特化和泛化,通过多态实现基于对象类型的动态分派。
我们先说说类和对象这两个词。
在面向对象编程中,类是一个抽象的概念,对象是一个具体的概念。
我们把同一类对象的共同特征抽取出来就是一个类,比如我们经常说的人类,这是一个抽象概念,而我们每个人就是人类的这个抽象概念下的实实在在的存在,也就是一个对象。
简而言之,类是对象的蓝图和模板,对象是类的实例,是可以接受消息的实体。
在面向对象编程的世界中,一切皆为对象,对象都有属性和行为,每个对象都是独一无二的,而且对象一定属于某个类。
对象的属性是对象的静态特征,对象的行为是对象的动态特征。
按照上面的说法,如果我们把拥有共同特征的对象的属性和行为都抽取出来,就可以定义出一个类。
在Python中,可以使用
class
关键字加上类名来定义类,通过缩进
我们可以确定类的代码块。
在类的代码块中,我们需要写一些函数,前面说过类是一个抽象概念
,那么这些函数就是对一类对象共同的动态特征的提取
。
写在类里面的函数我们通常称之为方法
,方法就是对象的行为
,也就是对象可以接收的消息。
方法的第一个参数通常都是self
,它代表了接收这个消息的对象本身。
class Student:
def study(self, course_name):
print(f'学生正在学习{course_name}.')
def play(self):
print(f'学生正在玩游戏.')
上述定义了一个学生(Student)类,它拥有学习(study)和玩(play)两个方法。
在定义完一个类之后,我们可以借助构造器语法轻松地创建该类的实例对象。
student1 = Student()
student2 = Student()
print(student1) # <__main__.Student object at 0x10ad5ac50>
print(student2) # <__main__.Student object at 0x10ad5acd0>
# 通过hex函数,将整输的唯一标识转换为16进制标识,你会发现和我们实例化出来的对象地址一模一样
print(hex(id(student1)), hex(id(student2))) # 0x10ad5ac50 0x10ad5acd0
在
类的名字后跟上圆括号
就是所谓的构造器语法,上面的代码创建了两个学生对象,一个赋值给变量student1
,一个赋值给变量student2
。
当我们用student1
和student2
两个变量时,我们会看到输出了对象在内存中的地址(十六进制形式),跟我们用id
函数查看对象标识获得的值是相同的。
我们定义的变量其实保存的是一个对象在内存中的逻辑地址(位置),通过这个逻辑地址,我们就可以在内存中找到这个对象。
所以student3 = student2
这样的赋值语句并没有创建新的对象,只是用一个新的变量保存了已有对象的地址。
接下来尝试给对象发消息,即
调用对象的方法
。
在Student
类中我们定义了study
和play
两个方法,两个方法的第一个参数self
代表了接收消息的学生对象,study
方法的第二个参数是学习的课程名称。
Python中,给对象发消息有两种方式,请看下面的代码。
# 通过“类.方法”调用方法,第一个参数是接收消息的对象,第二个参数是学习的课程名称
Student.study(student1, 'Python程序设计') # 学生正在学习Python程序设计.
# 通过“对象.方法”调用方法,点前面的对象就是接收消息的对象,只需要传入第二个参数
student1.study('Python程序设计') # 学生正在学习Python程序设计.
Student.play(student2) # 学生正在玩游戏.
student2.play() # 学生正在玩游戏.
大家可能已经注意到了,刚才我们创建的学生对象只有行为没有属性,如果要给学生对象定义属性,我们可以修改
Student
类,为其添加一个名为__init__
的方法。
在我们调用Student
类的构造器创建对象时,首先会在内存中获得保存学生对象所需的内存空间,然后通过自动执行__init__
方法,完成对内存的初始化操作
,也就是把数据放到内存空间中。
所以我们可以通过给Student
类添加__init__
方法的方式为学生对象指定属性,同时完成对属性赋初始值的操作,正因如此,__init__
方法通常也被称为初始化方法。
我们对上面的Student
类稍作修改,给学生对象添加name
(姓名)和age
(年龄)两个属性。
class Student:
"""学生类"""
def __init__(self, name, age):
"""初始化属性"""
self.name = name
self.age = age
def study(self, course_name):
"""学习方法"""
print(f'{self.name}正在学习{course_name}.')
def play(self):
"""玩耍方法"""
print(f'{self.name}正在玩游戏.')
重新执行一次,看看程序的执行结果有什么变化。
# 由于初始化方法除了self之外还有两个参数
# 所以调用Student类的构造器创建对象时要传入这两个参数
student1 = Student('张三', 18)
student2 = Student('李四', 15)
student1.study('Python程序设计') # 张三正在学习Python程序设计.
student2.play() # 李四正在玩游戏.
Python的面向对象编程提供了封装这一重要特性,可以隐藏对象的内部实现细节,只暴露必要的接口。
通过使用__init__方法,可以在创建对象时为其绑定属性和初始值
。
而以
双下划线__开头和结尾
的方法,被称为魔术方法或魔法方法
,它们具有特殊用途和意义
。
为了在打印对象时
显示自定义信息,而不是对象的地址,可以使用__repr__魔术方法。
该方法返回的字符串决定了使用print函数打印对象时显示的内容。
通过定义__repr__方法,可以更好地控制对象的输出形式,为使用者提供更友好的信息展示。
class Student:
"""学生类"""
def __init__(self, name, age):
"""初始化属性"""
self.name = name
self.age = age
def study(self, course_name):
"""学习方法"""
print(f'{self.name}正在学习{course_name}.')
def play(self):
"""玩耍方法"""
print(f'{self.name}正在玩游戏.')
def __repr__(self):
return f'{self.name}: {self.age}'
student1 = Student('张三', 18)
print(student1) # 张三: 18
students = [student1, Student('李四', 19), Student('王五', 20)]
print(students) # [张三: 18, 李四: 19, 王五: 20]
面向对象编程有三大特性:
封装
、继承
和多态
。
概念:
封装是把对象的属性和方法捆绑在一起,隐藏对象的内部细节,只暴露必要的操作,保护数据不受外部代码的干扰。它提高了代码的安全性和可维护性,因为外部代码无法随意修改内部属性,且只能通过定义的方法操作对象。
在Python中,封装是一种
让代码更安全、更易于管理的方式
。它意味着你可以隐藏一个对象的内部工作原理
,只让用户看到和使用他们需要的功能。
想象一下,你有一个大象玩具,你把它放入一个冰箱模型中。
你不想让用户看到你是如何把大象放进去的,或者冰箱里面是怎么样的,你只想让他们知道如何打开和关闭冰箱门。
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def eat(self):
print(f'{self.name},正在吃饭!')
def sleep(self):
print(f'{self.name},正在睡觉!')
person = Person("张三", 18)
person.eat() # 张三,正在吃饭
person.sleep() # 张三,正在睡觉
外部使用Person类中的方法时,外界不需要关注方法内部是如何实现的,只需要知道如何调用的方法需要传递什么参数即可。
概念:
继承是从已有的类派生出新类,新类继承了父类的所有属性和方法。它减少了代码重复,允许子类继承父类的功能并添加或覆盖方法以定制功能。
在Python中,允许我们创建一个新类(子类),
继承另一个已存在的类(父类)的属性和方法
。
在Python中,如果没有指定父类,那么默认的父类是object类
。
在Python中,还支持多重继承
,即一个类可以有一个或多个父类。
在Python中,子类除了可以从父类继承属性外,还可以定义自己特有的属性和方法
,使得子类拥有比父类更丰富的功能。
在子类的初始化方法中,我们可以使用super().__init__()
来调用父类的初始化方法。
子类对象可以替换掉父类对象
,这是面向对象编程中的常见行为,也叫做“里氏替换原则”。
子类继承了父类的特性,也继承了其方法。但如果子类想有自己的行为,可以对父类的方法进行重写。这就意味着,同样的方法在程序运行时,根据调用的对象不同,可以有不同的实现。这就是多态,也是面向对象编程中最核心的概念。
class Person:
"""
人类(基类)
提示:无论是学生还是老师,都是人类,
我们为此设计一个人类共有属性、共有行为的类,作为基类
所有学生都会具有吃饭、睡觉的属性,我们继承此类,就不需要自己再实现了
"""
def __init__(self, name, age):
self.name = name
self.age = age
def eat(self):
print(f'{self.name},正在吃饭!')
def sleep(self):
print(f'{self.name},正在睡觉!')
class Student(Person):
"""学生类"""
def __init__(self, name, age):
super().__init__(name, age)
def study(self, course_name):
print(f'{self.name},正在学习{course_name}!')
class Teacher(Person):
"""老师类"""
def __init__(self, name, age, title):
super().__init__(name, age)
self.title = title
def teach(self, course_name):
print(f'{self.name}{self.title},正在讲授{course_name}!')
# 学生1
student1 = Student('张三', 18)
student1.eat()
student1.study('Python程序设计')
student1.sleep()
# 学生2
student2 = Student('李四', 19)
student2.sleep()
# 老师1
teacher1 = Teacher('王五', 55, '教授')
teacher1.teach('Python编程')
# 老师2
teacher1 = Teacher('赵六', 55, '教授')
teacher1.teach('Java编程')
仔细观察上述代码,每个学生和教师都调用了吃饭和睡觉的方法,但自己都没有实现,就是因为继承了基类(Person)。
概念:
多态是指子类可以以自己的方式实现父类的接口。同一个消息可以发送给不同的对象,产生不同的结果。它提高了代码的灵活性和可扩展性,允许在运行时动态确定对象的类型。
多态,听起来很高级,其实就是“多种形态”的意思,想象一下这个世界上有很多同名的人,在大街上大喊“XX”名字,或许有多个人回头或给出不同的反应和行为
class Person:
"""
人类(基类)
提示:无论是学生还是老师,都是人类,
我们为此设计一个人类共有属性、共有行为的类,作为基类
所有学生都会具有吃饭、睡觉的属性,我们继承此类,就不需要自己再实现了
"""
def __init__(self, name, age):
self.name = name
self.age = age
def eat(self):
print(f'{self.name},正在吃饭!')
def sleep(self):
print(f'{self.name},正在睡觉!')
def go_home(self):
print(f'{self.name},走着回家!')
class Student(Person):
"""学生类"""
def __init__(self, name, age):
super().__init__(name, age)
def study(self, course_name):
print(f'{self.name},正在学习{course_name}!')
def go_home(self):
print(f'{self.name},骑自行车回家!')
class Teacher(Person):
"""老师类"""
def __init__(self, name, age, title):
super().__init__(name, age)
self.title = title
def teach(self, course_name):
print(f'{self.name}{self.title},正在讲授{course_name}!')
def go_home(self):
print(f'{self.name},开汽车回家!')
# 将三个不同的类对象存储在列表中
all_object = [Person("张三", 18), Student("李四", 19), Teacher("王五", 50, "教授")]
# 遍历列表,让每个对象调用同一个方法名
for obj in all_object:
obj.go_home()
"""
输出如下:
张三,走着回家!
李四,骑自行车回家!
王五,开汽车回家!
"""
上面的
Student
、Teacher
两个类都继承自Person
,他们都分别重写了go_home
方法。
重写就是子类对父类已有的方法重新做出实现。
大家仔细观察上述的代码,包括父类以及两个子类中的go_home
方法都不太一样,所以这个方法在程序运行时会产生多态行为,多态简单的说就是调用相同的方法,不同的子类对象做不同的事情。
面向对象编程中的
私有属性(private)和受保护属性(protected)
是为了保护对象内部状态,防止外部代码直接修改。
公开的方法是对象提供的接口,让外部可以控制对象的行为。
在Python中,我们通过给属性名添加前缀下划线来表示其访问可见性,如__name表示私有属性(变量名前两个底划线),_name表示受保护属性(变量名前一个底划线)。
这能帮助初学者理解Python面向对象编程的基本概念和原则,以及如何控制对象的访问和修改权限。
class Student:
def __init__(self, name, age):
self.__name = name
self.__age = age
def study(self, course_name):
print(f'{self.__name}正在学习{course_name}.')
student = Student('张三', 20)
student.study('Python')
print(student.__name) # 报错:AttributeError: 'Student' object has no attribute '__name'
上面的代码,在外部访问了内部私有属性,导致抛出了异常。
由此可见,以__
开头的属性__name
是私有的,在类的外面无法直接访问,但是类里面的study
方法中可以通过self.__name
访问该属性。
Python并没有从语法上严格保证私有属性的私密性
,但通过给私有属性和方法更换名字,可以阻挠对它们的访问。
尽管如此,如果我们知道更换名字的规则,仍然可以访问到它们。
为了更好地保护对象的内部状态,我们应该遵循面向对象编程中的访问权限原则,并谨慎处理私有属性和方法。
class Student:
def __init__(self, name, age):
self.__name = name
self.__age = age
def study(self, course_name):
print(f'{self.__name}正在学习{course_name}.')
student = Student('张三', 20)
student.study('Python')
print(student._Student__name, student._Student__age) # 依旧会有警告,但可以获取到内部属性
在Python中,我们可以使用
property装饰器
为私有属性提供读取和修改的方法。装饰器通常放在类、函数或方法的声明之前,通过一个@
符号表示将装饰器应用于类、函数或方法。
通过使用装饰器,我们可以更灵活地控制私有属性的访问和修改
,从而更好地保护对象的内部状态。
@property 属性访问装饰器
作用:使一个方法看起来像一个属性。这样,当其他代码尝试访问该属性时,该方法将被自动调用,并返回相应的值。
class Person:
def __init__(self, name):
self.__name = name
@property
def name(self):
return self.__name
person = Person("张三")
print(person.name) # 张三
外部代码可以像这样使用name属性:person.name
实际上,它是在调用name()方法。
@xxxx.setter 属性修改装饰器
作用:为属性提供一个设置方法。这样,当其他代码尝试修改该属性时,该方法将被自动调用。
class Person:
def __init__(self, name):
self.__name = name
@property
def name(self):
return self.__name
@name.setter
def name(self, value):
if not value:
raise ValueError("姓名不可为空")
self.__name = value
person = Person("张三")
print(person.name) # 张三
person.name = "李四"
print(person.name) # 李四
person.name = ""
# print(person.name) # ValueError: 姓名不可为空
综合使用
class Student:
def __init__(self, name, age):
self.__name = name
self.__age = age
# 属性访问器(getter方法) - 获取__name属性
@property
def name(self):
return self.__name
# 属性访问器(getter方法) - 获取__name属性
@property
def age(self):
return self.__age
# 属性修改器(setter方法) - 修改__name属性
@name.setter
def name(self, name):
# 如果name参数不为空就赋值给对象的__name属性
# 否则将__name属性赋值为原属性
self.__name = name or self.__name
stu = Student('张三', 18)
print(stu.name, stu.age) # 张三 18
stu.name = ''
print(stu.name) # 张三
stu.name = '李四'
print(stu.name) # 李四
# stu.age = 20 # AttributeError: can't set attribute
面向对象的编程思想确实很棒,也更容易符合人类的思维方式。
但要真正掌握抽象、封装、继承和多态这些面向对象的核心概念,确实需要长时间的积累和实践。
这些技能不是一蹴而就的,需要持续学习和探索。
对于初学者来说,这可能会是一个挑战,但也是一个充满探索和成长的旅程。记住,学习是一个持续的过程,不要怕犯错误,勇于尝试和探索,你将会在面向对象的编程世界中找到自己的道路。