写给前端的 Python 教程九 - 类

72 阅读14分钟

你好,我是hockor,今天咱们开始学习下 Python 中关于类的内容,我们会从最基础的类定义入手,逐步深入到属性、方法、继承、高级特性,并简要探讨两者的底层实现原理。

面向对象编程(Object-Oriented Programming, OOP)是一种强大的编程范式,它通过封装、继承和多态等核心概念,帮助开发者构建模块化、可维护和可扩展的软件系统。

从 JavaScript 的角度来看,class 关键字是ES6引入的语法糖,所以它的底层仍然基于原型继承,从本质上讲,JS的类仍然是函数,而类中定义的方法则被放置在构造函数的 prototype 对象上。

而Python 的类是类型(type)的实例,所有类(包括内置类)都是通过元类(默认是 type)动态创建的。类的定义会直接生成一个类对象。

python
class MyClass:
    pass

print(type(MyClass))  # <class 'type'>,说明类是 `type` 的实例
print(isinstance(MyClass, type))  # True

python 类定义不是函数的语法糖,而是一个独立的语言结构,类体中的代码会在定义时执行,生成类属性和方法。

类定义初识

JS demo

JavaScript使用class关键字来定义一个类。类体内部可以包含构造函数(constructor)、实例方法、getter/setter、静态方法/字段以及私有字段。


class PersonJS {
  constructor(name, age) {
    this.name = name; // 实例属性
    this.age = age;
    console.log(`JS Person ${this.name} created.`);
  }
  introduce() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}
const john = new PersonJS("John", 30); // 调用构造函数
john.introduce();

class StudentJS extends PersonJS {
  constructor(name, age, studentId) {
    super(name, age); // 必须先调用 super 方法,然后再使用 this
    this.studentId = studentId;
    console.log(`JS Student ${this.name} (ID: ${this.studentId}) created.`);
  }
}
const alice = new StudentJS("Alice", 20, "S12345");
alice.introduce();

JS中的构造函数constructor是JavaScript类中的一个特殊方法,用于创建和初始化通过new关键字创建的对象实例 。

  • 一个类中只能有一个名为constructor的方法,否则会抛出SyntaxError。它不能是getter、setter、async或generator方法 。  

  • 当使用new操作符调用类时,constructor方法会自动执行。它主要用于设置实例的初始状态,通常通过this.propertyName = value的方式来定义实例属性 。  

  • 在派生类(使用extends关键字继承的类)中,如果定义了constructor,则必须在访问this关键字之前显式调用super()来调用父类构造函数 。这是JavaScript的严格要求,如果违反,将导致 ReferenceError。

  • 如果未提供自定义构造函数,JavaScript会提供一个默认构造函数:基类(没有extends)的默认构造函数是空的constructor() {};派生类的默认构造函数则会调用super(...args) 。

Python demo

Python也使用class关键字来定义类,后跟类名和一个冒号。类体内部通常定义__init__方法(初始化器)、其他实例方法、类方法(@classmethod)、静态方法(@staticmethod)以及类属性(作为静态属性)。

# 定义一个名为 PersonPython 的基类
class PersonPython:
    # 类的构造函数,初始化实例时自动调用
    def __init__(self, name, age):
        # 定义实例属性 name 和 age
        self.name = name  # 实例属性:姓名
        self.age = age    # 实例属性:年龄
        # 打印创建信息
        print(f"Python Person {self.name} created.")
    
    # 定义一个实例方法
    def introduce(self):
        print(f"Hello, my name is {self.name} and I am {self.age} years old.")

# 创建 PersonPython 类的实例
jane = PersonPython("Jane", 25)  # 调用 __init__ 方法,传入 name 和 age 参数
jane.introduce()  # 调用实例方法 introduce

# 定义一个继承自 PersonPython 的子类 StudentPython
class StudentPython(PersonPython):
    # 子类的构造函数,扩展了父类的功能
    def __init__(self, name, age, student_id):
        # 推荐做法:首先调用父类的构造函数
        super().__init__(name, age)  # 使用 super() 调用父类的 __init__ 方法
        # 定义子类特有的实例属性 student_id
        self.student_id = student_id
        print(f"Python Student {self.name} (ID: {self.student_id}) created.")

# 创建 StudentPython 类的实例
alice_py = StudentPython("Alice", 20, "S54321")  # 调用子类的 __init__ 方法
alice_py.introduce()  # 调用继承自父类的 introduce 方法

Python中的初始化方法是__init__,__init__方法是Python中的一个特殊方法,通常被称为“构造器”或“初始化器” 。它的主要任务是在对象被创建后,初始化(赋值)类的数据成员。它在对象实例化时自动调用 。  

  • __init__的第一个参数必须是self,它是一个约定俗成的名称,指向当前正在创建的对象实例 。通过 self.attribute_name = value来定义实例属性 。值得注意的是,在Python中,__new__方法才是真正负责创建对象实例的“构造函数”,而__init__仅仅是初始化对象状态的“初始化器” 。这种两阶段的对象创建过程是Python数据模型的一个独特之处 。  

  • 在继承中,子类的__init__方法通常会通过super().__init__(*args, **kwargs)来调用父类的__init__方法,以确保父类中定义的属性得到正确初始化 。Python并不强制 super().__init__的调用顺序(不像JS),但为了避免属性未初始化的问题,强烈建议在子类__init__的开头调用父类构造函数 。也可以使用 ParentClass.__init__(self, *args, **kwargs)这种更显式的方式调用

对比表格

特性JavaScript (constructor)Python (init)
作用创建并初始化对象实例初始化对象实例的状态
关键字/方法名constructorinit (特殊方法/Dunder Method)
自动调用是(通过 new)是(通过对象实例化)
第一个参数无(this 隐式绑定)self (显式绑定到实例)
多构造函数不允许(SyntaxError)不允许(但可通过工厂方法或默认参数模拟)
派生类中调用父类必须在访问 this 前调用 super()建议使用 super().init(),但调用顺序更灵活
返回值基类可返回任意值(非对象被忽略),派生类必须返回对象或 undefined必须返回 None (隐式)

高级特性

静态成员

JavaScript使用static关键字定义静态方法和静态字段。静态成员属于类本身,而不是类的实例。它们通常用于工具函数(如用于创建或克隆对象的功能)、缓存或固定配置数据,这些数据不需要在每个实例中复制 。静态成员通过类名直接访问,例如ClassName.staticMethod()或ClassName.staticField

class CalculatorJS {
  // 静态属性 - 圆周率
  static PI = 3.14159;
  
  // 静态方法 - 两数相加
  static add(a, b) {
    return a + b;
  }
}

// 调用静态属性
console.log(CalculatorJS.PI); // 输出: 3.14159

// 调用静态方法
console.log(CalculatorJS.add(5, 3)); // 输出: 8

// 注意:静态成员只能通过类访问
// const calc = new CalculatorJS();
// console.log(calc.PI); // 输出: undefined (实例无法访问静态成员)

Python对静态成员的处理方式与JavaScript有所不同:

  • 静态方法 (@staticmethod): 使用@staticmethod装饰器定义。静态方法不接收隐式的第一个参数(self或cls),它们不访问或修改类或实例的状态 。它们是逻辑上属于类但独立于实例的工具函数,例如一个不依赖于任何特定对象状态的数学函数 。  

  • 静态属性(类属性): Python没有专门的@staticfield装饰器。其等价物是类属性,即定义在类体中、方法之外的变量 。这些类属性在所有实例之间共享,充当静态属性。所有实例都可以访问这些属性,并且对类属性的修改会影响所有实例。

class CalculatorPython:
    # 类属性(相当于静态属性)
    PI = 3.14159  
    
    # 静态方法 - 两数相加
    @staticmethod
    def add(a, b):
        return a + b

# 访问类属性(静态属性)
print(CalculatorPython.PI)  # 输出: 3.14159

# 调用静态方法
print(CalculatorPython.add(10, 20))  # 输出: 30

# 注意:虽然可以通过实例访问,但建议直接使用类名访问
# calc = CalculatorPython()
# print(calc.PI)  # 可以访问但不推荐,应该使用 CalculatorPython.PI

Getter 与 Setter

JavaScript使用get和set关键字来定义getter和setter。它们允许开发者像访问普通属性一样访问方法,从而提供对属性的受控访问 。Getter方法用于获取属性值,而setter方法用于设置或修改属性值。Getter方法不接受参数,setter方法必须接受一个参数 。这些访问器属性被定义在类的原型prototype上


class TemperatureJS {
  // 私有字段(存储实际温度值)
  #celsius = 0;
  
  // 构造函数
  constructor(celsius) {
    this.celsius = celsius; // 通过setter设置初始值
  }
  
  // celsius的getter方法
  get celsius() {
    console.log("获取摄氏温度...");
    return this.#celsius;
  }
  
  // celsius的setter方法
  set celsius(value) {
    console.log("设置摄氏温度...");
    if (value < -273.15) {
      throw new Error("温度不能低于绝对零度(-273.15°C)");
    }
    this.#celsius = value;
  }
  
  // fahrenheit的getter方法(计算华氏温度)
  get fahrenheit() {
    return (this.#celsius * 9) / 5 + 32;
  }
  
  // fahrenheit的setter方法(转换为摄氏温度存储)
  set fahrenheit(value) {
    this.celsius = (value - 32) * 5 / 9; // 通过celsius的setter设置值
  }
}

// 使用示例
const tempJS = new TemperatureJS(25); // 创建实例,初始温度25°C
console.log(tempJS.celsius); // 获取并打印摄氏温度
tempJS.fahrenheit = 68; // 设置华氏温度68°F(会自动转换为20°C)
console.log(tempJS.celsius); // 打印转换后的摄氏温度

// 错误示例(会抛出异常)
// tempJS.celsius = -300; // 尝试设置无效温度

在Python中实现getter和setter有多种方式,其中最“Pythonic”且推荐的方式是使用@property装饰器 。  

  • 传统方式: 可以通过定义独立的get_attribute()和set_attribute()方法来实现,但这不如@property优雅 。  

  • @property装饰器: Python提供@property装饰器,它允许将方法转换为属性,从而以更简洁的方式实现getter和setter 。 @property用于标记getter方法,而 @<property_name>.setter则用于标记对应的setter方法 。这种方式使得外部代码可以像访问普通属性一样访问这些方法,同时在内部执行验证、计算或其他逻辑 。通常,内部存储的实际值会使用单下划线_前缀(例如_celsius),以遵循保护成员的约定。

class TemperaturePython:
    def __init__(self, celsius=0):
        """
        初始化温度实例
        :param celsius: 初始摄氏温度,默认为0
        """
        self._celsius = celsius  # 受保护的内部变量(约定俗成)
    
    @property
    def celsius(self):
        """摄氏温度属性(获取)"""
        print("正在获取摄氏温度...")
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        """摄氏温度属性(设置)"""
        print("正在设置摄氏温度...")
        if value < -273.15:
            raise ValueError("温度不能低于绝对零度(-273.15°C)")
        self._celsius = value
    
    @property
    def fahrenheit(self):
        return (self.celsius * 9) / 5 + 32  # 通过摄氏温度getter计算
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        self.celsius = (value - 32) * 5 / 9  # 通过摄氏温度setter设置

# 使用示例
if __name__ == "__main__":
    temp_py = TemperaturePython(25)  # 创建实例,初始温度25°C
    print(temp_py.celsius)          # 获取并打印摄氏温度
    temp_py.fahrenheit = 68        # 设置华氏温度68°F(自动转换为20°C)
    print(temp_py.celsius)         # 打印转换后的摄氏温度
    
    # 错误示例(会抛出异常)
    # temp_py.celsius = -300       # 尝试设置无效温度

继承与多态

在 ES6 中,我们可以使用extends关键字来实现类的继承,子类可以继承父类的属性和方法 ,但是由于使用原型链继承(Prototypal Inheritance),所以在 JS 中是不支持多继承的,但是Python能支持多种继承方式,包括单继承、多重继承(一个类可以继承自多个父类)和多级继承(一个类继承自另一个已继承的类)。

我们先来看一个最简答的继承 demo


class AnimalPython:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        print(f"{self.name} makes a noise.")

class DogPython(AnimalPython): # 继承自 AnimalPython
    def __init__(self, name, breed):
        super().__init__(name) # 调用父类的初始化方法
        self.breed = breed # 设置子类特有的breed属性
    
    def bark(self):
        print(f"{self.name} ({self.breed}) barks loudly!")

my_dog_py = DogPython("Max", "Labrador")
my_dog_py.speak() # 来自AnimalPython类的speak方法
my_dog_py.bark() # 来自DogPython类的bark方法

多继承的 demo


# 基类1 - 鸟类
class Bird:
    def __init__(self, name):
        self.name = name
    
    def fly(self):
        print(f"{self.name} 正在飞翔")
    
    def speak(self):
        print(f"{self.name} 发出鸟叫声")

# 基类2 - 马类
class Horse:
    def __init__(self, name, speed):
        self.name = name
        self.speed = speed
    
    def run(self):
        print(f"{self.name}{self.speed} km/h 的速度奔跑")
    
    def speak(self):
        print(f"{self.name} 发出马嘶声")

# 子类 - 飞马类 (同时继承Bird和Horse)
class Pegasus(Bird, Horse):
    def __init__(self, name, speed):
        # 由于两个父类都有name属性,我们只需要初始化一次
        Horse.__init__(self, name, speed)  # 显式调用Horse的初始化
        Bird.__init__(self, name)          # 显式调用Bird的初始化
    
    def magical_ability(self):
        print(f"{self.name} 展示了它的魔法能力!")
    
    # 重写speak方法解决多继承的冲突
    def speak(self):
        print(f"{self.name} 发出神奇的鸣叫")
        # 如果想调用特定父类的方法
        Horse.speak(self)  # 显式调用Horse的speak方法

# 创建飞马实例
pegasus = Pegasus("天马流星", 60)

# 调用从不同父类继承的方法
pegasus.fly()       # 来自Bird类
pegasus.run()       # 来自Horse类

# 调用子类自己的方法
pegasus.magical_ability()

# 调用被重写的方法
pegasus.speak()

# 查看方法解析顺序(MRO)
print(Pegasus.__mro__)

打印结果

天马流星 正在飞翔
天马流星 以 60 km/h 的速度奔跑
天马流星 展示了它的魔法能力!
天马流星 发出神奇的鸣叫
天马流星 发出马嘶声
(<class '__main__.Pegasus'>, <class '__main__.Bird'>, <class '__main__.Horse'>, <class 'object'>)

方法重写

JavaScript和Python都支持方法重写,其核心机制相似。Python提供了两种调用父类方法的方式super()和ClassName.method()。

# 定义基础形状类
class ShapePython:
    def draw(self):
        """绘制基础形状"""
        print("Drawing a generic shape.")  # 打印绘制基础形状的信息
    
# 定义圆形类,继承自ShapePython
class CirclePython(ShapePython):
    def draw(self):
        """绘制圆形(重写父类方法)"""
        super().draw()  # 推荐方式:调用父类的draw方法
        # ShapePython.draw(self)  # 替代方式:显式调用父类方法(不推荐)
        print("Drawing a circle with specific details.")  # 打印绘制圆形的详细信息

# 创建圆形实例
my_circle_py = CirclePython()
# 调用绘制方法
my_circle_py.draw()

底层机制简析

作为前端,我们都知道JavaScript是一种基于原型的语言。每个对象都有一个内部链接到另一个对象,称为其原型(prototype),这个原型对象也有自己的原型,如此往复,形成了原型链。  

当访问对象的属性或方法时,JavaScript会首先在对象本身查找。如果找不到,它会沿着原型链向上查找,直到找到属性或到达链的末端(null)。 

ES6的class关键字是原型继承的语法糖 。它只是提供了一种更清晰、更结构化的方式来定义构造函数、方法和继承,使其看起来更像传统的类继承模型。然而,它的底层机制仍然是原型链。例如,类中定义的方法实际上是添加到类的 prototype 属性上的,而实例则通过__proto__(或Object.getPrototypeOf())链接到这个原型对象 。  

而Python的面向对象系统建立在其强大的数据模型之上,其中“一切皆对象” 。这意味着数字、字符串、函数,甚至类本身,都是对象。每个对象都具有身份、类型和值 。  

Python的类行为由其数据模型和一系列特殊方法(通常以双下划线开头和结尾,如__init__, __str__, __len__等,这些方法允许类与内置操作(如len()函数、+运算符)进行交互,从而实现多态行为和自定义对象行为。

在Python中,类本身也是对象,它们是由**元类(metaclass)**创建的 。默认的元类是内置的type。当你定义一个类时,实际上是调用type()来创建这个类对象。元类可以被视为“创建类的类”,它们允许开发者在类被创建时修改其行为,例如自动添加方法、修改属性或强制执行编码标准 。元类是Python中非常高级且强大的概念,通常只在需要高度定制类创建行为时使用 。  

JavaScript将原型性质隐藏在class语法背后,而Python则暴露了一个更明确的对象创建层次结构:对象是类的实例,而类又是元类(默认为type)的实例。这种层次结构是理解Python对象模型深度的关键。对于JavaScript开发者而言,这解释了Python对象模型背后的原理,以及为什么__init__和__new__等特殊方法会存在。

大白话来说就是:

JavaScript 的类:

  • 表面看着简单,用 class 语法糖包装了复杂的原型继承,就像把发动机盖焊死了的车,你只知道怎么开,但看不到内部怎么运作

  • 没有 Python 那种层层递进的创建关系

Python 的类:

  • 把创建对象的"流水线"明明白白展示给你看:对象是类造出来的 → 类是元类(type)造出来的,就像乐高:积木(对象)←模具(类)←注塑机(元类)

  • 能看到 new(造对象) 和 init(初始化对象) 两个步骤,还能自己改造"注塑机"(元类编程),造出特殊定制的类

ok,以上就是我们这一节的内容,我们下一节再见~