小柴的故事
小柴是一名计算机专业的大学生。这个学期,他将要学习面向对象编程。这可把小柴给急坏了:他可是母胎单身十八年,一个对象也没有,这门课不会挂科吧! 挂科可是会影响他保研!
于是他心急火燎地翻遍了微信通讯录,给每一个女生都发了消息:
“嗨,你好!想问一下你能当我的对象吗?”
不出意外,所有女生都拉黑了他。
然而,“世上无难事,只要肯登攀”,在寻找女朋友失败后,他重整旗鼓,开始寻找男朋友。
他又开始翻通讯录,给每一个男生都发了消息。他将信息发送给我们公众号的编辑卡咪同学后,卡咪同学为了避免他骚扰其他男同学,赶紧回复了他:
“小柴同学,你好!我觉得你这个问题很有意思,我来帮你解答一下吧!”
“事实上,掌握面向对象编程,并不意味着一定要有一个对象。” 卡咪同学说。
那好,就让我们和小柴同学一起,来看看什么是面向对象编程吧!
此“对象”非彼“对象”
在我们提到的面向对象编程中,“对象”这个概念,其实指的并不是男女朋友。在面向对象编程中,我们把程序中的一个东西叫做“对象”。这个东西可以是一个人、一个动物、一辆车,甚至是一个概念,比如“爱情”。
我们先说明一些简单的概念。
1. 类 (Class)
所有的对象都是由类创建的。类就像是一个蓝图或者模板,定义了对象的属性和方法。 比如,我们可以有一个“人”类,这个类定义了人的属性(比如名字、年龄)和方法(比如说话、走路)。当我们根据这个类创建一个具体的人时,这个人就是一个对象。
一个类,把人的特质(名字、年龄)和人的行为(说话、走路)都捆绑在了一起。我们可以根据这个类创建很多不同的人对象,比如小柴同学、卡咪同学等等。
在Python中,我们可以这样定义一个类:
classPerson:# 我们等下会讲这个函数是干什么的def__init__(self, name, age): self.name = name # 定义属性 self.age = age# 定义方法defspeak(self): print(f"{self.name} says: Hello!")defwalk(self): print(f"{self.name} is walking.")
2. 实例 (Instance)
当我们根据一个类创建一个具体的对象时,这个对象就叫做这个类的一个实例。比如,我们可以创建一个小柴同学的实例:
xiaochai = Person("小柴", 20)
这个时候,xiaochai就是Person类的一个实例。
我们是怎么创建实例的呢?事实上,我们调用的是类的一个特殊函数,叫做__init__函数。它是英文 "initialize" 的缩写,意思是“初始化”。这个函数在创建实例的时候会被自动调用,用来初始化实例的属性。在上面的例子中,我们传入了名字和年龄,这些信息就被用来初始化了小柴同学这个实例的属性。
classPerson:def__init__(self, name, age): self.name = name # 定义属性 self.age = age
这个函数的每一行都是什么意思呢?第一个参数self是一个特殊的参数,代表了当前实例本身。当我们创建一个实例时,Python会自动把这个实例传递给self参数。这样,我们就可以在类的内部通过self来访问和修改这个实例的属性。在上面的例子中,我们通过self.name和self.age来定义了这个实例的属性。
如果有些难理解,我们可以先记住:基本所有的类都会有一个__init__函数,而类内函数的第一个参数都是self,这个参数代表了当前实例(即小柴)本身。
我们在创建实例的时候,Python会自动把这个实例(小柴)传递给self参数,这样我们就可以在类的内部通过self来访问和修改这个实例的属性。
3. 属性 (Attribute)
属性是用来描述对象的特征的,在上面的例子中,name和age就是这个类的属性。我们可以通过实例来访问这些属性:
print(xiaochai.name) # 输出: 小柴print(xiaochai.age) # 输出: 20
我们也可以修改这些属性:
xiaochai.age = 21print(xiaochai.age) # 输出: 21
可以看到,我们使用了点号(.)来访问和修改实例的属性。这是面向对象编程中最基本的句法。
4. 方法 (Method)
方法是定义在类中的函数,用来描述对象的行为。比如,在上面的例子中,speak和walk就是这个类的方法。我们可以通过实例来调用这些方法:
xiaochai.speak() # 输出: 小柴 says: Hello!xiaochai.walk() # 输出: 小柴 is walking.
方法和属性一样,也是通过点号来访问的。方法可以让我们对对象进行操作,或者让对象执行一些动作。
那好,我们写一个完整的例子来看看这些概念是如何结合在一起的:
classPerson:def__init__(self, name, age): self.name = name # 定义属性 self.age = agedefspeak(self): print(f"{self.name} says: Hello!")defwalk(self): print(f"{self.name} is walking.")# 创建一个实例xiaochai = Person("小柴", 20)kami = Person("卡咪", 19)# 访问属性print(xiaochai.name) # 输出: 小柴print(kami.age) # 输出: 19# 调用方法xiaochai.speak() # 输出: 小柴 says: Hello!kami.walk() # 输出: 卡咪 is walking.
有人问了,面向对象编程,和我们之前的编程有什么区别吗?我们可以试着用之前的方式实现一下这整个例子:
defcreate_person(name, age):return {'name': name, 'age': age} # 创建一个字典来表示一个人defshow_age(person): print(f"{person['name']} is {person['age']} years old.")defshow_name(person): print(f"{person['name']} is the person's name.")defchange_name(person, new_name): person['name'] = new_namedefchange_age(person, new_age): person['age'] = new_agedefspeak(person): print(f"{person['name']} says: Hello!")defwalk(person): print(f"{person['name']} is walking.")# 创建一个人xiaochai = create_person("小柴", 20)kami = create_person("卡咪", 19)# 访问属性show_name(xiaochai) # 输出: 小柴 is the person's name.show_age(kami) # 输出: 卡咪 is 19 years old.# 调用方法speak(xiaochai) # 输出: 小柴 says: Hello!walk(kami) # 输出: 卡咪 is walking.
有什么区别呢?
在面向对象编程中,我们把数据(属性)和操作数据的函数(方法)都捆绑在了一起,这样我们就可以更方便地管理和使用这些数据。而在之前的编程方式中,我们需要把数据和函数分开来管理,这样就比较麻烦。
我们定义了很多函数来操作这些数据,而且每次操作都需要传入这个数据(在这个例子中就是被操作的人)作为参数,这样就很麻烦。而在面向对象编程中,我们只需要调用实例的方法,用点记号就可以了,不需要再传入这个实例作为参数了。
所以,面向对象编程让我们的代码更清晰、更结构化、更易于维护。我们可以把相关的数据和操作捆绑在一起,这样就更容易理解和使用了。
试着比较:
speak(xiaochai) # 之前的方式,需要传入作为参数xiaochai.speak() # 面向对象编程的方式,直接调用实例,实例作为方法的隐式参数self,不需要再传入了
可见,面向对象编程的核心思想就是把数据和操作数据的函数捆绑在一起,这样我们就可以更方便地管理和使用这些数据了。
一些其他的例子
银行账户也是一个很好的例子。我们可以定义一个BankAccount类,来表示一个银行账户:
classBankAccount:def__init__(self, owner, balance=0):# balance是余额的意思,是可选参数,默认值是0 self.owner = owner self.balance = balancedefdeposit(self, amount):# 存款方法,参数是存款金额 self.balance += amount print(f"{self.owner} 存了 {amount}。当前余额: {self.balance}")defwithdraw(self, amount):# 取款方法,参数是取款金额if amount > self.balance: print(f"{self.owner} 余额不足。当前余额: {self.balance}")else: self.balance -= amount print(f"{self.owner} 取出了 {amount}。当前余额: {self.balance}")# 创建一个银行账户实例account = BankAccount("小柴", 1000)# 存款account.deposit(500) # 输出: 小柴 存了 500。当前余额: 1500# 取款account.withdraw(200) # 输出: 小柴 取出了 200。当前余额: 1300account.withdraw(1500) # 输出: 小柴 余额不足。当前余额: 1300
我们可以注意到,Python中的类通常是以大写字母开头的,这是一种命名约定,可以方便我们区分类和变量。
我们再举一个虚数的例子。我们可以定义一个ComplexNumber类,来表示一个复数:
classComplexNumber:def__init__(self, real, imag): self.real = real # 实部 self.imag = imag # 虚部defdisplay(self):# 定义这个方法是为了让我们在打印这个对象的时候,能够得到一个友好的输出 print(f"{self.real} + {self.imag}i")defadd(self, other):# 定义一个加法方法,参数是另一个复数对象 new_real = self.real + other.real new_imag = self.imag + other.imagreturn ComplexNumber(new_real, new_imag) # 返回一个新的复数对象defmultiply(self, other):# 定义一个乘法方法,参数是另一个复数对象 new_real = self.real * other.real - self.imag * other.imag new_imag = self.real * other.imag + self.imag * other.realreturn ComplexNumber(new_real, new_imag) # 返回一个新的复数对象# 创建两个复数实例c1 = ComplexNumber(2, 3) # 2 + 3ic2 = ComplexNumber(4, 5) # 4 + 5i# 对这两个复数进行加法和乘法操作c3 = c1.add(c2) # c3是c1和c2的和c4 = c1.multiply(c2) # c4是c1和c2的乘积# 打印结果print("c1: ", end="")c1.display() # 输出: 2 + 3iprint("c2: ", end="")c2.display() # 输出: 4 + 5iprint("c3: ", end="")c3.display() # 输出: 6 + 8iprint("c4: ", end="")c4.display() # 输出: -7 + 22i
我们一直都在用Python中的面向对象编程
面向对象编程从来不是什么很高大上的事情。其实,我们在使用Python的时候,一直都在用面向对象编程。比如,我们创建一个列表:
my_list = [1, 2, 3] # 可以改写成 my_list = list([1, 2, 3]),这里的list就是一个类,我们创建了一个列表对象my_list,这个对象就是list类的一个实例。my_list.append(4) # 调用列表对象的方法,这里的append方法就是list类定义的方法,用来向列表中添加元素。print(my_list) # 输出: [1, 2, 3, 4]
我们用Python的字符串方法来操作字符串时:
my_string = "Hello, World!"print(my_string.upper()) # 调用字符串对象的方法,把字符串转换成大写,print(my_string.lower()) # 调用字符串对象的方法,把字符串转换成小写
我们调用海龟库绘制图形时:
from turtle import Turtle # 首字母大写的Turtle是一个类t = Turtle() # 创建一个Turtle对象(实例)t.forward(100) # 调用Turtle对象的方法,让它向前移动100单位t.left(90) # 调用Turtle对象的方法,让它向左转90t.forward(100) # 调用Turtle对象的方法,让它向前移动100单位
结语
听完卡咪同学的讲解,小柴同学终于明白了,原来面向对象编程中的“对象”并不是指男女朋友,而是指程序中的一个东西,比如一个人、一个动物、一辆车,甚至是一个概念。通过定义类,我们可以创建很多不同的对象,每个对象都有自己的属性和方法,这样我们就可以更方便地管理和使用这些数据了。
小柴同学高兴地表示:我可以继续单身下去了!但说完这句话,他不知道为何感觉心里有点空落落的。
晚上回宿舍,小柴同学想用运算符实现上文提的两个复数的加减法 print(c1 + c2),这就需要重载运算符。小柴同学觉得好玩儿,创建了一个“动物”类,又创建了“猫”类和“狗”类,他想把这三者联系起来,看看它们之间有什么关系。卡咪同学告诉他,这就是面向对象编程中的“继承”。还有各种各样,五花八门的概念,我们后面如果可以再介绍。
免责声明
本文「小柴同学」相关故事均为虚构,与现实人物、事件、团体无关,请勿对号入座。
如果本文内容让你产生了情绪波动、精神内耗、深夜emo、找对象焦虑、甚至对 OOP 产生了人生困惑,请立刻放下手机,深呼吸,好好吃饭睡觉,严重者请及时寻求专业心理咨询帮助,你的身心健康永远比代码和对象重要。
如有冒犯与困扰,我们深表歉意,请勿当真。