查看全课大纲(第 32 / 51 节)
  1. 1.Python 是什么?为什么值得学?
  2. 2.如何为 Python 编程准备一个“全家桶”环境?
  3. 3.Python 有哪几种执行方法?
  4. 4.行与缩进到底有什么用?
  5. 5.注释有什么用,怎么写?
  6. 6.变量:如何给数据起个好名字?
  7. 7.运算符的优先级和常见坑点是什么?
  8. 8.Python 中,数字和布尔值究竟有哪几种?
  9. 9.输入函数 `input()` 和文件读取 `open()` 怎么用?
  10. 10.如何将程序的结果展示给用户或保存下来?
  11. 11.CSV 文件怎么读怎么写才方便?
  12. 12.列表是什么,如何创建和访问它?
  13. 13.列表的修改操作,有哪些方法?
  14. 14.列表切片为什么能取到部分元素?排序和查找又该怎么用?
  15. 15.判断语句:如何让程序根据条件做不同的事?
  16. 16.for 循环如何遍历序列中的元素?
  17. 17.while 循环什么时候才会停?
  18. 18.while 循环的结束条件,你写对了吗?
  19. 19.循环中途想提前结束,怎么办?
  20. 20.`break` 和 `continue` 到底有什么区别?
  21. 21.列表推导式如何让代码更简洁高效?
  22. 22.字符串怎么定义、拼接和操作?
  23. 23.元组:为什么定义单个元素时要加逗号?
  24. 24.集合(set)怎么用?如何快速去重和做集合运算?
  25. 25.字典是什么,为什么比列表更方便?
  26. 26.字典的增删改查与遍历,如何避免 KeyError?
  27. 27.函数里的变量,为什么有的能用有的不能用?
  28. 28.在函数内部修改全局变量,为什么有时不生效?
  29. 29.函数里能修改全局变量吗?返回值怎么用?
  30. 30.匿名函数:如何用 lambda 快速定义简单函数?
  31. 31.面向对象编程是什么,为什么需要它?
  32. 32.类中的方法和属性到底怎么定义和使用?
  33. 33.如何控制对对象内部属性和方法的访问?
  34. 34.异常发生时,程序会怎样继续执行?
  35. 35.如何组织代码:包和模块
  36. 36.`if __name__ == '__main__'` 到底有什么用?
  37. 37.如何用 IDE 的 Debug 功能排查程序错误?
  38. 38.Matplotlib 绘图的第一步该做什么?
  39. 39.Figure 是什么,如何设置图形尺寸?
  40. 40.如何自定义 Matplotlib 的坐标轴?
  41. 41.如何为图表添加图例,让不同曲线一目了然?
  42. 42.饼图怎么画才能直观展示数据占比?
  43. 43.如何用 Matplotlib 绘制散点图?
  44. 44.柱状图怎么画?
  45. 45.如何使用 Matplotlib 绘制等高线图?
  46. 46.如何在 Matplotlib 中绘制子图?
  47. 47.zip 函数如何把多个列表“打包”在一起?
  48. 48.如何给可迭代对象加上索引?
  49. 49.random 模块如何生成随机数?
  50. 50.math 模块怎么用?
  51. 51.Counter 如何快速统计元素出现次数?

类中的方法和属性到底怎么定义和使用?

约 14 分钟

小象实战讲义 · Python 编程从入门到实践

在上一节我们初步了解了面向对象编程的概念。本节将深入类的内部,学习如何为类定义方法(行为)和属性(状态)。你将学会如何创建具有特定属性和行为的对象,并理解对象初始化、实例属性与类属性的区别。

💡 核心导读

  • 类的基本定义:使用 class 关键字定义类,类名采用大驼峰命名法。
  • 方法的定义与 self 参数:类中的方法第一个参数必须是 self,它指向当前对象实例。
  • 构造方法 __init__:在对象创建时自动调用,用于初始化对象的属性。
  • 实例属性与类属性:实例属性属于具体对象,类属性属于类本身,被所有实例共享。
  • 属性的访问与修改:掌握通过对象和类名访问、修改属性的正确方式。

定义类与基本方法

类是对象的蓝图。在 Python 中,使用 class 关键字定义类,类名通常采用大驼峰命名法(每个单词首字母大写,无下划线)。

关键点:类中定义的方法,第一个参数必须是 self。self 是一个指向当前对象实例的引用。当通过某个对象调用方法时,Python 会自动将该对象作为 self 参数传入。因此,在定义方法时,即使方法不需要其他参数,也必须写上 self。

定义好类之后,就可以创建对象(称为实例化)并调用其方法。一个类可以创建多个对象,每个对象都是独立的。可以用内置函数 id() 观察对象在内存中的地址:两个对象的地址不同(具体数值每次运行可能不同,所以下面只比较它们是否相同)。

class Dog:
    def eat(self):
        print("小狗正在啃骨头...")
    def drink(self):
        print("小狗正在喝水")

# 创建一个小狗对象,变量名为 wang_cai
wang_cai = Dog()
# 调用对象的方法
wang_cai.eat()
wang_cai.drink()

# 再创建一个对象:阿福
a_fu = Dog()
wang_cai.eat()
a_fu.drink()

# 用 id() 查看对象的内存地址:两个对象的地址不同
print("两个对象的 id 是否相同:", id(wang_cai) == id(a_fu))
小狗正在啃骨头...
小狗正在喝水
小狗正在啃骨头...
小狗正在喝水
两个对象的 id 是否相同: False

构造方法与实例属性

我们希望每只小狗创建时就有自己的名字、品种等信息。这需要在对象创建时就进行初始化,使用构造方法 __init__。

__init__ 方法在对象被创建时自动调用。我们可以在其中为对象设置初始状态,即定义实例属性。

class Dog:
    # 构造方法,在创建对象时自动调用
    def __init__(self, gender, variety, name):
        # 使用 self.属性名 = 值 来定义实例属性
        self.gender = gender   # 性别
        self.variety = variety # 品种
        self.name = name       # 名字

    def get_property(self):
        # 在类内部的其他方法中,通过 self 访问实例属性
        print("性别:{}, 品种:{}, 名字:{}".format(self.gender, self.variety, self.name))

# 创建对象时,必须传入 __init__ 方法中定义的参数(self 除外)
wangcai = Dog("雄性", "金毛", "旺财")
wangcai.get_property()

yuanbao = Dog("雌性", "哈士奇", "来福")
yuanbao.get_property()
性别:雄性, 品种:金毛, 名字:旺财
性别:雌性, 品种:哈士奇, 名字:来福

对象创建过程理解:

  1. 执行 Dog(...) 时,Python 在内存中为新的 Dog 对象申请空间。
  2. 自动调用该对象的 __init__ 方法,并将我们传入的参数(如 "雄性", "金毛", "旺财")传递给它。
  3. 在 __init__ 方法内部,通过 self.xxx = ... 将数据存储为对象的属性。
  4. 初始化完成后,新对象的引用(内存地址)被返回,并赋值给变量(如 wangcai)。

如果不定义 __init__ 方法,Python 会提供一个默认的、什么都不做的 __init__ 方法。

实例属性与类属性

属性分为两种:实例属性和类属性。

  • 实例属性:属于具体的对象。通过 self.属性名 = 值 在 __init__ 或其他方法中定义。每个对象的实例属性互不影响。
  • 类属性:属于类本身。在类内部、方法外部直接定义。所有该类的对象共享同一个类属性。
class Dog:
    # 类属性,在类内部、方法外部定义
    weight = 10  # 所有狗的默认体重

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

# 创建两个对象
wang_cai = Dog("旺财")
yuan_bao = Dog("元宝")

# 访问类属性:通过类名或对象名
print("类属性 Dog.weight: {}".format(Dog.weight))
print("旺财访问 weight: {}".format(wang_cai.weight))
print("元宝访问 weight: {}".format(yuan_bao.weight))

# 修改类属性:通过类名修改,会影响到所有对象
Dog.weight = 20
print("修改类属性后:")
print("旺财的 weight: {}".format(wang_cai.weight))
print("元宝的 weight: {}".format(yuan_bao.weight))
print("Dog类的 weight: {}".format(Dog.weight))
类属性 Dog.weight: 10
旺财访问 weight: 10
元宝访问 weight: 10
修改类属性后:
旺财的 weight: 20
元宝的 weight: 20
Dog类的 weight: 20

修改类属性:应通过类名进行修改,这会影响到所有对象。

一个重要的坑:如果通过对象去给一个与类属性同名的属性赋值,并不会修改类属性,而是会为该对象创建一个新的实例属性,从而“屏蔽”掉类属性。

class Dog:
    weight = 20  # 类属性

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

wang_cai = Dog("旺财")
yuan_bao = Dog("元宝")

# 通过对象给 weight 赋值
wang_cai.weight = 30  # 这为旺财创建了一个实例属性 weight=30
print("通过旺财对象赋值后:")
print("旺财的 weight: {}".format(wang_cai.weight))  # 访问的是旺财自己的实例属性 30
print("元宝的 weight: {}".format(yuan_bao.weight))  # 访问的仍是类属性 20
print("Dog类的 weight: {}".format(Dog.weight))      # 类属性本身没有变,还是 20
通过旺财对象赋值后:
旺财的 weight: 30
元宝的 weight: 20
Dog类的 weight: 20

何时使用类属性?当某个属性是整个类所有对象共有的、不常改变的,并且你希望通过修改它来影响所有对象时,才使用类属性。

📝 动手练一练

  1. 创建一个 Student 类,要求:
    • 构造方法 __init__ 接收两个参数:name(姓名)和 student_id(学号),并将它们保存为实例属性。
    • 定义一个类属性 school,初始值为 "小象中学"。
    • 定义一个方法 introduce,打印 "我是{school}的{name},学号是{student_id}。"。
👉 点击查看参考答案

思路:按照题目要求,在类定义中正确放置类属性和实例属性,在方法中通过 self 访问实例属性,通过 类名.类属性 访问类属性。

class Student:
    # 类属性
    school = "小象中学"

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

    def introduce(self):
        # 访问类属性推荐使用类名,更清晰
        print("我是{}的{},学号是{}。".format(Student.school, self.name, self.student_id))

# 测试
stu1 = Student("张三", "2024001")
stu1.introduce()

# 修改类属性
Student.school = "小象第一中学"
stu2 = Student("李四", "2024002")
stu2.introduce()
stu1.introduce()  # stu1的自我介绍也会使用新的学校名
我是小象中学的张三,学号是2024001。
我是小象第一中学的李四,学号是2024002。
我是小象第一中学的张三,学号是2024001。

常见错误

错误写法现象或报错原因正确写法
def eat():
(在类中定义方法)
调用时 TypeError: Dog.eat() takes 0 positional arguments but 1 was given类的方法在调用时,Python 会自动传入调用者对象作为第一个参数。定义时缺少 self 参数来接收它。def eat(self):
wangcai = Dog调用方法时报错 TypeError: Dog.eat() missing 1 required positional argument: 'self'Dog 是类本身,Dog() 才是创建对象。直接 Dog 赋值只是将类这个“模板”赋给了变量,没有创建实际对象。wangcai = Dog()
wangcai = Dog("旺财")
(但 __init__ 定义为 def __init__(self):)
TypeError: Dog.__init__() takes 1 positional argument but 2 were given创建对象时传入的参数个数,必须与 __init__ 方法(除 self 外)定义的参数个数一致。修改 __init__ 定义以接收参数,或创建时不传参。
通过对象修改类属性:
obj.class_attr = new_value
其他对象访问该类属性时值未变。这实际上是为 obj 创建了一个同名的实例属性,覆盖了对类属性的访问,并未修改类属性本身。通过类名修改:
ClassName.class_attr = new_value

小结

  1. 使用 class ClassName: 定义类,类名采用大驼峰命名法。
  2. 类中的方法第一个参数必须是 self,它代表调用该方法的对象实例。
  3. 构造方法 __init__ 在对象创建时自动调用,用于初始化对象的实例属性(通过 self.attr = value 定义)。
  4. 实例属性属于具体对象,不同对象间独立。类属性在类内部方法外定义,属于类,被所有实例共享。
  5. 访问或修改类属性应通过类名(ClassName.attr)。通过对象给类属性同名变量赋值会创建新的实例属性,而非修改类属性。

📋 行动清单

  • 将 Dog 类的 __init__ 方法多加一个 age 参数,并修改 get_property 方法将其打印出来。
  • 在 Student 类的练习中,尝试通过 stu1.school = "某校" 赋值,然后分别打印 stu1.school, stu2.school, Student.school,观察结果,理解实例属性对类属性的覆盖。
  • 思考:如果有一个属性,希望每个 Dog 对象都有一份独立的副本,且创建时就有初始值,应该把它定义成实例属性还是类属性?

—— 小象教研组

配套资料 · 直接下载

课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。

  • 第4章代码包:面向对象基础(含1 个 Notebook)
    ZIP · 2KB
    下载