Day 21 - 面向对象编程:类与对象基础
什么是面向对象编程(OOP)?
面向对象编程(Object-Oriented Programming,简称OOP)是一种程序设计范式,它使用"对象"来描述现实世界中的事物。Python 是一种多范式编程语言,完全支持面向对象编程。在 Python 中,一切皆为对象——数字、字符串、列表、字典,甚至函数都是对象。理解类和对象的概念是掌握 Python 编程的关键一步。
面向对象编程的核心思想是将数据和操作数据的方法封装在一起,形成一个独立的单元——类(Class)。类就像是一张蓝图或者模板,定义了某种类型对象所具有的属性(特征)和方法(行为)。而对象(Object)则是根据类这张蓝图创建出来的具体实例。打个比方,“汽车"是一个类,而一辆具体的红色 BMW 汽车就是一个对象;“学生"是一个类,而"张三"这位具体的学生就是一个对象。
类的定义与语法
在 Python 中,使用 class 关键字来定义一个类。类名通常采用 PascalCase 命名法(每个单词首字母大写),例如 Student、Person、BankAccount 等。类的定义体包含属性(数据)和方法(函数)。让我们从一个最简单的例子开始:
class Person:
"""一个人类的简单表示"""
def say_hello(self):
"""打招呼的方法"""
print("你好!")
# 创建对象
p = Person()
p.say_hello()
在这个例子中,我们定义了一个名为 Person 的类。类中有一个 say_hello 方法。注意方法定义中的第一个参数 self,这是面向对象编程中最重要的概念之一。
self 参数详解
self 参数是类方法(除了类方法和静态方法之外)的第一个参数,它指向对象本身。当我们调用 p.say_hello() 时,Python 内部会将这个调用转换为 Person.say_hello(p),也就是说,self 接收到了调用该方法的对象 p 本身。
self 这个名字并不是关键字,你可以用其他名字来代替它,但按照 Python 的惯例,始终使用 self 是一个良好的编程习惯。使用其他名字虽然不会报错,但会让你的代码难以被其他程序员理解,因为 Python 社区已经形成了使用 self 的共识。
self 的主要作用体现在以下几个方面:
- 访问对象的属性(实例变量)
- 调用对象的其他方法
- 在方法之间传递对象本身
- 判断两个对象是否是同一个对象(使用
is运算符)
class Dog:
def __init__(self, name, age):
self.name = name # 将name存储在当前对象中
self.age = age # 将age存储在当前对象中
def bark(self):
print(f"{self.name} 在叫:汪汪汪!")
def info(self):
print(f"名字:{self.name},年龄:{self.age}岁")
dog1 = Dog("旺财", 3)
dog2 = Dog("小白", 5)
dog1.bark() # 输出:旺财 在叫:汪汪汪!
dog2.bark() # 输出:小白 在叫:汪汪汪!
dog1.info() # 输出:名字:旺财,年龄:3岁
dog2.info() # 输出:名字:小白,年龄:5岁
类与对象的关系
类和对象之间的关系是"模板"与"实例"的关系。类定义了创建对象的蓝图,描述了一类对象共同的特征和行为;对象是类的具体实现,每个对象都有自己独立的属性值,但共享类中定义的方法。
class Car:
# 类属性(所有对象共享)
wheels = 4
def __init__(self, brand, model, year):
# 实例属性(每个对象独立)
self.brand = brand
self.model = model
self.year = year
def drive(self):
print(f"{self.brand} {self.model} 正在行驶...")
# 创建不同的汽车对象
car1 = Car("丰田", "卡罗拉", 2022)
car2 = Car("本田", "雅阁", 2023)
print(Car.wheels) # 4(通过类名访问类属性)
print(car1.wheels) # 4(通过对象访问类属性)
print(car2.wheels) # 4(通过对象访问类属性)
car1.drive() # 丰田 卡罗拉 正在行驶...
car2.drive() # 本田 雅阁 正在行驶...
实例属性与类属性
在 Python 类中,属性分为两大类:实例属性和类属性。实例属性是与特定对象绑定的属性,每个对象都有自己独立的实例属性副本;类属性是所有对象共享的属性,存储在类本身而非对象中。
实例属性在 __init__ 方法中通过 self.属性名 的方式创建,也可以在其他地方动态创建。类属性直接在类体中定义,不在任何方法内部,它们被所有该类的对象共享。
理解实例属性和类属性的区别非常重要:
- 修改类属性会影响所有对象(如果对象没有覆盖它)
- 修改实例属性只影响当前对象
- 对象可以直接访问类属性,但无法直接修改类属性(除非使用特殊方法)
class Student:
# 类属性:所有学生共享的学校名称
school_name = "清华大学"
def __init__(self, name, student_id):
# 实例属性:每个学生独立的属性
self.name = name
self.student_id = student_id
self.grades = [] # 每个学生有自己的成绩列表
def add_grade(self, subject, score):
"""添加成绩"""
self.grades.append({"科目": subject, "成绩": score})
def show_info(self):
"""显示学生信息"""
print(f"学号:{self.student_id}")
print(f"姓名:{self.name}")
print(f"学校:{self.school_name}") # 可以访问类属性
print(f"成绩:{self.grades}")
# 创建学生对象
student1 = Student("张三", "2023001")
student2 = Student("李四", "2023002")
# 通过类名访问类属性
print(Student.school_name) # 清华大学
# 通过对象访问类属性
print(student1.school_name) # 清华大学
# 修改类属性(通过类名)
Student.school_name = "北京大学"
print(student1.school_name) # 北京大学
print(student2.school_name) # 北京大学
# 修改实例属性
student1.school_name = "复旦大学" # 这只是给student1添加了一个新的实例属性
print(student1.school_name) # 复旦大学(实例属性)
print(student2.school_name) # 北京大学(类属性,没受影响)
print(Student.school_name) # 北京大学
对象的创建过程
当你使用 类名() 的方式创建对象时,Python 内部会执行一系列操作:首先,创建一个新的空对象;然后,调用 __init__ 方法(如果类定义了它)并将新对象作为 self 参数传入;最后,返回这个已经初始化好的对象。这个过程叫做"实例化”(Instantiation)。
class Book:
def __init__(self, title, author, pages):
print("__init__ 方法被调用!")
self.title = title
self.author = author
self.pages = pages
def read(self):
print(f"正在阅读《{self.title}》...")
print("开始创建对象...")
book = Book("活着", "余华", 500)
print("对象创建完成!")
book.read()
执行结果:
开始创建对象... __init__ 方法被调用! 对象创建完成! 正在阅读《活着》...魔术方法 dict
每个对象都有一个 __dict__ 属性,它是一个字典,包含了对象的所有实例属性。类也有 __dict__ 属性,但它包含的是类的属性和方法定义。你可以使用这个属性来动态地检查和操作对象的属性。
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
p = Person("小明", 20)
# 查看对象的实例属性
print(p.__dict__) # {'name': '小明', 'age': 20}
# 动态添加属性
p.gender = "男"
print(p.__dict__) # {'name': '小明', 'age': 20, 'gender': '男'}
# 动态删除属性
del p.age
print(p.__dict__) # {'name': '小明', 'gender': '男'}
类型与身份
在 Python 中,每个对象都有两个重要的标识:类型(type)和身份(id)。类型决定了对象可以执行哪些操作,身份是对象在内存中的唯一地址(可以使用 id() 函数获取)。使用 type() 函数可以查看对象的类型,使用 is 运算符可以比较两个对象是否是同一个对象。
class Animal:
def __init__(self, name):
self.name = name
a = Animal("猫")
b = Animal("猫")
print(type(a)) # <class '__main__.Animal'>
print(type(b)) # <class '__main__.Animal'>
print(type(a) == type(b)) # True
print(id(a)) # 140234567890
print(id(b)) # 140234567891(不同的内存地址)
print(a is b) # False(不是同一个对象)
print(a is not b) # True
c = a # 让c指向a同一个对象
print(c is a) # True(是同一个对象)
属性访问的优先级
当访问一个对象的属性时,Python 会按照特定的优先级顺序进行查找:首先在对象的实例属性中查找,然后在类的类属性中查找,最后在父类中查找(关于继承和父类,我们会在后面的章节详细介绍)。
class MyClass:
class_attr = "类属性"
def __init__(self):
self.instance_attr = "实例属性"
obj = MyClass()
# 访问实例属性
print(obj.instance_attr) # 实例属性
# 访问类属性(通过对象)
print(obj.class_attr) # 类属性
# 访问不存在的属性会引发 AttributeError
# print(obj.nonexistent) # AttributeError: 'MyClass' object has no attribute 'nonexistent'
对象的字符串表示
每个对象都可以通过 str() 或 repr() 函数转换为字符串。默认的实现通常不太友好(显示对象的内存地址),你可以通过定义 __str__ 和 __repr__ 方法来定制对象的字符串表示。我们会在 Day 23 中详细讨论这些魔术方法。
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
s = Student("王五", 95)
print(str(s)) # <__main__.Student object at 0x7f8a9c123456>
print(repr(s)) # <__main__.Student object at 0x7f8a9c123456>
练习题
练习 1:创建简单的银行账户类
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
"""存款"""
if amount > 0:
self.balance += amount
print(f"{self.owner} 存款 {amount} 元,当前余额:{self.balance} 元")
else:
print("存款金额必须为正数!")
def withdraw(self, amount):
"""取款"""
if amount <= 0:
print("取款金额必须为正数!")
elif amount > self.balance:
print(f"余额不足!当前余额:{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 元,当前余额:1300 元
account.withdraw(2000) # 余额不足!当前余额:1300 元
练习 2:创建矩形类
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
"""计算面积"""
return self.width * self.height
def perimeter(self):
"""计算周长"""
return 2 * (self.width + self.height)
rect = Rectangle(10, 5)
print(f"矩形宽:{rect.width},高:{rect.height}")
print(f"面积:{rect.area()}") # 50
print(f"周长:{rect.perimeter()}") # 30
练习 3:创建计数器类
class Counter:
count = 0 # 类属性,记录所有计数器的总数
def __init__(self):
Counter.count += 1
self.current = 0
def increment(self):
"""计数值加1"""
self.current += 1
def reset(self):
"""重置当前计数器"""
self.current = 0
@classmethod
def get_total_count(cls):
"""获取创建的计数器总数"""
return cls.count
c1 = Counter()
c2 = Counter()
c3 = Counter()
print(f"创建的计数器总数:{Counter.get_total_count()}") # 3
c1.increment()
c1.increment()
c2.increment()
print(f"c1的计数值:{c1.current}") # 2
print(f"c2的计数值:{c2.current}") # 1
print(f"c3的计数值:{c3.current}") # 0
常见错误与注意事项
-
忘记 self 参数:在类方法中忘记包含
self参数是最常见的错误之一。 -
拼写错误:属性名拼写错误会导致
AttributeError,例如self.name和self.nmae是两个不同的属性。 -
可变默认参数:不要使用可变对象(如列表、字典)作为默认参数,这会导致所有对象共享同一份数据。
-
类属性与实例属性混淆:初学者常常不小心修改了类属性而不是实例属性,导致意外影响所有对象。
总结
今天我们学习了 Python 面向对象编程的基础知识:
- 类是对象的蓝图,定义了对象的属性和方法
- 对象是类的实例,通过
类名()的方式创建 self参数指向当前对象,用于访问对象的属性和方法- 实例属性属于单个对象,类属性被所有对象共享
- 理解
self、属性访问顺序、以及类与对象的关系是掌握 OOP 的基础
在接下来的几天里,我们将继续深入学习面向对象编程的其他重要概念,包括构造函数 __init__、字符串表示 __str__ 和 __del__、私有属性与封装、继承与多态等。掌握这些知识将帮助你编写更加结构化、可维护和可扩展的 Python 代码。