📊 阅读时长:22分钟 | 关键词:Python面向对象、类、对象、self、init、实例属性、类属性

引言:从面向过程到面向对象

到目前为止,我们写的代码都是面向过程的——数据是数据,函数是函数,两者分开。这在处理简单问题时没问题,但程序一旦复杂,就会变得难以维护。

让我们看一个具体的例子。假设你要模拟"学生的一天":

# 面向过程写法:数据和函数是分离的
def get_up(name):
    print(f'{name}睁开眼睛')
    print(f'{name}起身')
    print(f'{name}穿好衣服')

def wash(name):
    print(f'{name}刷牙')
    print(f'{name}洗脸')

def eat(name):
    print(f'{name}吃菜')
    print(f'{name}扒饭')

def study(name):
    print(f'{name}看视频')
    print(f'{name}查资料')
    print(f'{name}写代码')

# 使用时
stu_name = '张三'
stu_age = 18
stu_grade = '高三'

print(f'大家好!我是{stu_name},今年{stu_age}岁,在读{stu_grade}!')
get_up(stu_name)
wash(stu_name)
eat(stu_name)
study(stu_name)

这种写法的问题很明显:

  • 学生相关的数据(name、age、grade)散落在各处
  • 每个函数都要传 name,重复且容易出错
  • 如果要模拟"老师的一天",需要再写一套几乎一样但又不完全一样的代码
  • 数据和操作数据的函数没有关联

面向对象编程(OOP) 解决了这些问题。它的核心思想是:把数据和操作数据的方法绑定在一起,形成一个"对象"

# 面向对象写法:数据和函数封装在一起
class Student:
    def __init__(self, name, age, grade):
        self.name = name
        self.age = age
        self.grade = grade

    def introduce(self):
        print(f'大家好!我是{self.name},今年{self.age}岁,在读{self.grade}!')

    def get_up(self):
        print(f'{self.name}睁开眼睛')
        print(f'{self.name}起身')
        print(f'{self.name}穿好衣服')

# 使用时
stu = Student('张三', 18, '高三')
stu.introduce()
stu.get_up()

一、类与对象:基本概念

1.1 什么是类?什么是对象?

用生活中的类比来理解:

概念 生活中的类比 Python 中
类(Class) 汽车设计图纸 class Student:
对象(Object) 根据图纸造出的具体汽车 stu = Student('张三', 18, '高三')
属性(Attribute) 汽车的颜色、排量 self.nameself.age
方法(Method) 汽车能做的事情(加速、刹车) def study(self):

类是模板,对象是模板的实例。 一个类可以创建无数个对象。

1.2 定义类和创建对象
# 定义类
class Student:
    """学生类"""
    school = '深兰教育'       # 类属性(所有实例共享)

    def __init__(self, name, age):
        """初始化方法:在创建对象时自动调用"""
        self.name = name      # 实例属性(每个实例独有)
        self.age = age

    def study(self, course):
        """实例方法"""
        print(f'{self.name}在学习{course}课!')

# 创建对象(实例化)
stu1 = Student('张三', 28)
stu2 = Student('李四', age=32)

# 调用方法
stu1.study('Python')     # 张三在学习Python课!
stu2.study('机器学习')    # 李四在学习机器学习课!

在这里插入图片描述

二、__init__self:两个必须理解的概念

2.1 self 是什么?

self 代表调用该方法的实例对象本身。它是方法定义中的第一个参数,Python 会自动传入。

class Student:
    def __init__(self, name):
        self.name = name    # self.name 是实例属性,name 是传入的参数

    def study(self):
        print(f'{self.name}在学习')   # 通过 self 访问实例属性

stu = Student('张三')
stu.study()
# Python 内部实际执行的是:Student.study(stu)
# self 就是 stu

关键理解

  • self 不是 Python 关键字,只是约定俗成的名字(你可以用别的名字,但千万别这么做)
  • self 在方法定义时是第一个参数,调用时不需要手动传入
  • 通过 self.xxx 定义和访问的变量是实例属性
2.2 __init__:初始化方法

__init__ 是一个魔术方法(前后各有两个下划线),在创建对象时自动调用,用来给实例属性赋初始值。

class Student:
    def __init__(self, name, age):
        print(f'初始化方法被调用了!name={name}, age={age}')
        self.name = name
        self.age = age

# 创建对象时,__init__ 自动执行
stu = Student('张三', 18)
# 输出:初始化方法被调用了!name=张三, age=18

__new__ vs __init__

实际上,创建对象的过程分两步:

  1. __new__(cls)构造方法,创建实例对象并返回
  2. __init__(self)初始化方法,对实例对象进行属性定制
# 简化理解
# 当你写 stu = Student('张三', 18) 时,Python 内部大致是:
# obj = Student.__new__(Student)     # 步骤1:创建空对象
# Student.__init__(obj, '张三', 18)  # 步骤2:初始化属性
# stu = obj                          # 步骤3:返回对象

日常开发中,99% 的情况你只需要关注 __init__,不需要重写 __new__

在这里插入图片描述

三、实例属性 vs 类属性

这是面向对象中最容易搞混的概念之一。

特性 实例属性 类属性
定义位置 __init__ 中用 self.xxx 类体中直接定义
归属 每个实例独有 所有实例共享
访问方式 实例.属性 类.属性(推荐)或 实例.属性
修改方式 实例.属性 = 新值 类.属性 = 新值
存储位置 实例的 __dict__ 类的 __dict__
class Student:
    school = '深兰教育'      # 类属性

    def __init__(self, name, age):
        self.name = name      # 实例属性
        self.age = age        # 实例属性

stu1 = Student('张三', 18)
stu2 = Student('李四', 20)

# 访问实例属性
print(stu1.name)        # '张三'
print(stu2.name)        # '李四'

# 访问类属性(两种方式都可以)
print(Student.school)   # '深兰教育' —— 推荐
print(stu1.school)      # '深兰教育' —— 通过实例也能访问
print(stu2.school)      # '深兰教育'

# 修改类属性:必须通过类!
Student.school = '深兰大学'
print(stu1.school)      # '深兰大学'
print(stu2.school)      # '深兰大学'

# ⚠️ 陷阱:通过实例"修改"类属性
stu1.school = 'ShenLanEdu'    # 这实际上给 stu1 新增了一个实例属性!
print(stu1.school)            # 'ShenLanEdu' —— 实例属性
print(Student.school)         # '深兰大学' —— 类属性没变
print(stu2.school)            # '深兰大学' —— 其他实例不受影响

在这里插入图片描述

核心规则

  • 读取属性时:先找实例属性,找不到再找类属性
  • 赋值属性时:实例.属性 = 值 永远只修改/新增实例属性,不影响类属性

四、属性操作的内置函数

Python 提供了四个内置函数来动态操作属性:

class Student:
    school = '深兰教育'

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

stu = Student('张三', 18)

# 1. getattr() —— 获取属性值
print(getattr(stu, 'name'))              # '张三'
print(getattr(stu, 'height', 178))       # 178 —— 不存在时返回默认值
# print(getattr(stu, 'height'))          # AttributeError! 不存在且无默认值

# 2. setattr() —— 设置属性值(不存在则新增)
setattr(stu, 'age', 20)                  # 修改已有属性
setattr(stu, 'height', 178)              # 新增实例属性
print(stu.age, stu.height)               # 20, 178

# 3. hasattr() —— 判断属性是否存在
print(hasattr(stu, 'name'))              # True
print(hasattr(stu, 'height'))            # True
print(hasattr(stu, 'weight'))            # False

# 4. delattr() —— 删除属性
delattr(stu, 'age')                      # 删除实例属性
# print(stu.age)                         # AttributeError!
函数 作用 等价写法
getattr(obj, 'attr', default) 获取属性 obj.attr
setattr(obj, 'attr', value) 设置属性 obj.attr = value
hasattr(obj, 'attr') 判断属性是否存在
delattr(obj, 'attr') 删除属性 del obj.attr

这些函数在需要动态操作属性时非常有用(比如属性名来自字符串变量),日常开发中用 . 语法就够了。

五、实例方法、类方法、静态方法

类中可以定义三种方法:

class Student:
    school = '深兰教育'

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

    # 实例方法:第一个参数是 self(实例对象)
    def study(self, course):
        print(f'{self.name}在学习{course}课!')

    # 类方法:第一个参数是 cls(类对象),用 @classmethod 装饰
    @classmethod
    def get_school(cls):
        return cls.school

    # 静态方法:没有 self/cls 参数,用 @staticmethod 装饰
    @staticmethod
    def is_valid_age(age):
        return 0 < age < 150
5.1 三种方法的对比
类型 装饰器 第一个参数 能访问实例属性? 能访问类属性? 调用方式
实例方法 self 实例.方法()
类方法 @classmethod cls 类.方法()实例.方法()
静态方法 @staticmethod ✅(通过类名) 类.方法()实例.方法()
5.2 调用方式
stu = Student('张三')

# 实例方法:通常用实例调用
stu.study('Python')                     # ✓ 推荐
Student.study(stu, 'Python')            # 也可以,但需要手动传 self

# 类方法:类和实例都可以调用
print(Student.get_school())             # ✓ 推荐
print(stu.get_school())                 # 也可以

# 静态方法:类和实例都可以调用
print(Student.is_valid_age(25))         # ✓ 推荐
print(stu.is_valid_age(25))             # 也可以
5.3 什么时候用什么方法?
# 实例方法:需要访问实例属性时
class Student:
    def __init__(self, name):
        self.name = name
    def introduce(self):          # 实例方法——需要 self.name
        print(f'我是{self.name}')

# 类方法:操作类级别的数据,或作为"工厂方法"
class Student:
    count = 0
    def __init__(self, name):
        self.name = name
        Student.count += 1
    @classmethod
    def get_count(cls):           # 类方法——操作类属性
        return cls.count

# 静态方法:工具函数,和类相关但不需要访问实例或类数据
class Student:
    @staticmethod
    def validate_age(age):        # 静态方法——纯工具函数
        return 0 < age < 150

六、@property:把方法变成属性

@property 装饰器让一个方法可以像属性一样被访问(不需要加括号):

class Student:
    def __init__(self, name, math, english):
        self.name = name
        self.math = math
        self.english = english

    @property
    def average(self):
        """平均分——像属性一样访问,不需要加括号"""
        return (self.math + self.english) / 2

stu = Student('张三', 90, 85)
print(stu.average)     # 87.5 —— 不需要 stu.average()!
# stu.average = 100    # AttributeError! @property 默认只读

@property 的好处:

  • 代码更优雅——stu.averagestu.get_average() 更直观
  • 可以隐藏计算细节——调用者不需要知道 average 是计算出来的
  • 可以在不改变外部接口的情况下修改内部实现

七、动手练习

练习 1:创建你的第一个类

class Book:
    """图书类"""
    library = '市图书馆'    # 类属性

    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages
        self.is_borrowed = False    # 默认未借出

    def borrow(self):
        if self.is_borrowed:
            print(f'《{self.title}》已被借出')
        else:
            self.is_borrowed = True
            print(f'《{self.title}》借阅成功')

    def return_book(self):
        self.is_borrowed = False
        print(f'《{self.title}》已归还')

    @property
    def info(self):
        status = '已借出' if self.is_borrowed else '可借阅'
        return f'《{self.title}》| {self.author} | {self.pages}页 | {status}'

# 测试
book1 = Book('Python编程', 'Guido', 500)
book2 = Book('机器学习', '吴恩达', 350)

book1.borrow()
print(book1.info)
book2.borrow()
book1.return_book()
print(book1.info)

练习 2:属性操作练习

# 预测以下代码的输出
class Demo:
    x = 10
    def __init__(self):
        self.y = 20

d = Demo()
print(d.x)           # ?
d.x = 30
print(d.x)           # ?
print(Demo.x)        # ?
Demo.x = 40
print(d.x)           # ?
print(Demo.x)        # ?

小结

面向对象编程是 Python 中最核心的编程范式。本文覆盖了基础知识:

| 知识点 | 核心内容 |
|:—|:—|:—|
| 类 vs 对象 | 类是模板,对象是实例 |
| self | 代表实例对象本身,自动传入 |
| __init__ | 初始化方法,创建对象时自动调用 |
| 实例属性 | 每个实例独有,通过 self.xxx 定义 |
| 类属性 | 所有实例共享,在类体中定义 |
| 实例方法 | 第一个参数是 self,能访问实例数据 |
| 类方法 | @classmethod,第一个参数是 cls |
| 静态方法 | @staticmethod,没有 self/cls |
| @property | 把方法变成属性,调用时不用括号 |

下一篇文章,我们将深入面向对象的三大特性之一:封装——私有属性、私有方法、名称改写机制,以及如何用 ___ 保护你的数据。


本文是「Python从入门到数据分析」系列的第 9 篇,共 24 篇。关注我,不错过后续更新。

更多推荐