Day 21 - 面向对象编程:类与对象基础

什么是面向对象编程(OOP)?

面向对象编程(Object-Oriented Programming,简称OOP)是一种程序设计范式,它使用"对象"来描述现实世界中的事物。Python 是一种多范式编程语言,完全支持面向对象编程。在 Python 中,一切皆为对象——数字、字符串、列表、字典,甚至函数都是对象。理解类和对象的概念是掌握 Python 编程的关键一步。

面向对象编程的核心思想是将数据和操作数据的方法封装在一起,形成一个独立的单元——类(Class)。类就像是一张蓝图或者模板,定义了某种类型对象所具有的属性(特征)和方法(行为)。而对象(Object)则是根据类这张蓝图创建出来的具体实例。打个比方,“汽车"是一个类,而一辆具体的红色 BMW 汽车就是一个对象;“学生"是一个类,而"张三"这位具体的学生就是一个对象。

类的定义与语法

在 Python 中,使用 class 关键字来定义一个类。类名通常采用 PascalCase 命名法(每个单词首字母大写),例如 StudentPersonBankAccount 等。类的定义体包含属性(数据)和方法(函数)。让我们从一个最简单的例子开始:

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

常见错误与注意事项

  1. 忘记 self 参数:在类方法中忘记包含 self 参数是最常见的错误之一。

  2. 拼写错误:属性名拼写错误会导致 AttributeError,例如 self.nameself.nmae 是两个不同的属性。

  3. 可变默认参数:不要使用可变对象(如列表、字典)作为默认参数,这会导致所有对象共享同一份数据。

  4. 类属性与实例属性混淆:初学者常常不小心修改了类属性而不是实例属性,导致意外影响所有对象。

总结

今天我们学习了 Python 面向对象编程的基础知识:

  • 类是对象的蓝图,定义了对象的属性和方法
  • 对象是类的实例,通过 类名() 的方式创建
  • self 参数指向当前对象,用于访问对象的属性和方法
  • 实例属性属于单个对象,类属性被所有对象共享
  • 理解 self、属性访问顺序、以及类与对象的关系是掌握 OOP 的基础

在接下来的几天里,我们将继续深入学习面向对象编程的其他重要概念,包括构造函数 __init__、字符串表示 __str____del__、私有属性与封装、继承与多态等。掌握这些知识将帮助你编写更加结构化、可维护和可扩展的 Python 代码。