Day 22 - 构造函数 init 详解

什么是 init 方法?

__init__ 是 Python 类中的一个特殊方法,也被称为"构造函数"或"初始化方法"。当我们使用 类名() 创建对象时,__init__ 方法会被自动调用。它的主要作用是对新创建的对象进行初始化设置,比如给对象的属性赋初值、建立对象之间的关联关系等。

__init__ 这个名字来源于 “initialize”(初始化)这个词。在 Python 中,以双下划线 __ 开头和结尾的方法称为"魔术方法"(Magic Methods)或"特殊方法"(Special Methods),它们有特殊的用途,会在特定情况下被 Python 自动调用。

init 的基本语法

class 类名:
    def __init__(self, 参数1, 参数2, ...):
        # 初始化代码
        self.属性1 = 参数1
        self.属性2 = 参数2

__init__ 方法的第一个参数永远是 self(指向正在创建的对象),后面的参数是你在创建对象时需要传入的值。

class Person:
    def __init__(self, name, age, gender):
        self.name = name
        self.age = age
        self.gender = gender
        print(f"新对象创建:{self.name}")

# 创建对象时传入参数
person1 = Person("张三", 25, "男")
person2 = Person("李四", 30, "女")

print(f"{person1.name}{person1.age} 岁的 {person1.gender}生")
print(f"{person2.name}{person2.age} 岁的 {person2.gender}生")

输出:

新对象创建:张三 新对象创建:李四 张三 是 25 岁的 男生 李四 是 30 岁的 女生

为什么需要 init

__init__ 方法的主要价值在于确保每个对象在创建后都处于一个有效的初始状态。没有 __init__ 的对象,其属性是未定义的,尝试访问这些属性会引发 AttributeError。通过在 __init__ 中定义属性并赋予初始值,我们可以保证对象的属性在任何时候都是可访问的。

考虑一个实际场景:假设我们需要创建一个表示"学生"的类。没有 __init__ 时,我们不得不这样做:

class Student:
    pass  # 空类

# 创建对象
s = Student()
s.name = "张三"  # 手动添加属性
s.score = 95     # 手动添加属性

这种方式虽然可行,但存在很多问题:忘记设置某个属性、属性名称拼写错误、不同对象可能缺少不同的属性等。通过使用 __init__,我们强制要求在创建对象时提供必要的初始值,并且明确了对象应该具有哪些属性。

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

# 创建对象时必须提供所有必要参数
s = Student("张三", 95)  # 正确:所有属性都被初始化
print(s.name, s.score)

# s2 = Student()        # 错误:缺少必要参数
# s3 = Student("李四")   # 错误:缺少 score 参数

默认参数值

与普通函数一样,__init__ 方法的参数也可以有默认值。这允许我们创建对象时提供部分参数,使用默认值填充其他参数。

class Rectangle:
    def __init__(self, width=10, height=5):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height

# 使用默认值
r1 = Rectangle()
print(f"r1 宽:{r1.width},高:{r1.height},面积:{r1.area()}")  
# r1 宽:10,高:5,面积:50

# 只提供一个参数
r2 = Rectangle(20)
print(f"r2 宽:{r2.width},高:{r2.height},面积:{r2.area()}")
# r2 宽:20,高:5,面积:100

# 提供两个参数
r3 = Rectangle(8, 12)
print(f"r3 宽:{r3.width},高:{r3.height},面积:{r3.area()}")
# r3 宽:8,高:12,面积:96

None 作为默认值的妙用

在某些情况下,我们需要创建一个可能暂时没有实际值的属性。这时可以使用 None 作为默认值,并在后续代码中根据需要赋值。

class Book:
    def __init__(self, title, author, price=None, publisher=None):
        self.title = title
        self.author = author
        self.price = price        # 价格可能还不知道
        self.publisher = publisher  # 出版社可能还不知道
    
    def publish(self, publisher, price):
        """出版图书,设置出版社和价格"""
        self.publisher = publisher
        self.price = price
    
    def display(self):
        """显示图书信息"""
        print(f"书名:《{self.title}》")
        print(f"作者:{self.author}")
        if self.publisher:
            print(f"出版社:{self.publisher}")
        if self.price:
            print(f"价格:{self.price}元")

# 创建图书时,价格和出版社暂未确定
book = Book("时间简史", "史蒂芬·霍金")
book.display()
print("--- 出版后 ---")
book.publish("湖南科技出版社", 68)
book.display()

可变默认参数的陷阱

这是一个 Python 初学者经常犯的错误:不要使用可变对象(如列表、字典)作为默认参数。默认值只在函数定义时计算一次,如果修改了可变默认值,所有后续调用都会使用同一个被修改的对象。

# 错误的方式:使用了可变默认参数
class BuggyStack:
    def __init__(self, items=[]):  # 危险!items 会在所有实例间共享
        self.items = items
    
    def push(self, item):
        self.items.append(item)
    
    def pop(self):
        return self.items.pop()

# 测试
stack1 = BuggyStack()
stack1.push(1)
print(f"stack1.items: {stack1.items}")  # [1]

stack2 = BuggyStack()
print(f"stack2.items: {stack2.items}")  # [1] -- 这是个 BUG!应该是 []

# 正确的方式:使用 None 作为默认值
class CorrectStack:
    def __init__(self, items=None):
        self.items = items if items is not None else []
    
    def push(self, item):
        self.items.append(item)
    
    def pop(self):
        return self.items.pop()

# 测试
stack3 = CorrectStack()
stack3.push(1)
print(f"stack3.items: {stack3.items}")  # [1]

stack4 = CorrectStack()
print(f"stack4.items: {stack4.items}")  # [] -- 正确!

实例属性与 init 的关系

__init__ 方法是设置实例属性的主要场所,但实例属性并不一定要在 __init__ 中定义。你可以在其他方法中创建实例属性,也可以在对象创建后动态添加属性。

class Example:
    def __init__(self, value):
        self.value = value  # 在 __init__ 中定义
        self.double = value * 2  # 也可以在 __init__ 中计算得到
    
    def add_method(self, amount):
        # 在其他方法中创建新的实例属性
        self.added = amount
        self.total = self.value + amount

obj = Example(10)
print(obj.value)    # 10
print(obj.double)   # 20

obj.add_method(5)
print(obj.added)     # 5
print(obj.total)     # 15

# 动态添加属性(不推荐,但可行)
obj.new_attr = "动态添加的属性"
print(obj.new_attr)  # 动态添加的属性

虽然可以动态添加属性,但为了代码的可读性和可维护性,建议将所有实例属性都在 __init__ 中定义。这样做的好处是:所有对象都会有一致的属性结构,代码审查时能一眼看出类的所有状态。

多态与 init

__init__ 方法在面向对象的多态特性中扮演重要角色。虽然 Python 本身是动态类型语言,但通过合理设计 __init__ 方法和类层次结构,可以实现统一接口的效果。

class Shape:
    def __init__(self, color="黑色"):
        self.color = color
    
    def area(self):
        raise NotImplementedError("子类必须实现 area 方法")
    
    def describe(self):
        return f"一个{self.color}的形状"

class Circle(Shape):
    def __init__(self, radius, color="红色"):
        super().__init__(color)  # 调用父类的 __init__
        self.radius = radius
    
    def area(self):
        return 3.14159 * self.radius ** 2

class Rectangle(Shape):
    def __init__(self, width, height, color="蓝色"):
        super().__init__(color)
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height

# 多态:不同类型的对象可以用相同的接口
shapes = [Circle(5), Rectangle(4, 6), Circle(3)]

for shape in shapes:
    print(f"{shape.describe()},面积:{shape.area():.2f}")

initnew 的区别

Python 中还有另一个魔术方法 __new__,它负责创建对象,而 __init__ 负责初始化对象。在大多数情况下,你只需要重写 __new__ 来实现单例模式或其他特殊的对象创建逻辑,而让 __init__ 处理初始化。

class MyClass:
    def __new__(cls, *args, **kwargs):
        print("__new__ 被调用,创建对象")
        instance = super().__new__(cls)  # 调用父类的 __new__
        return instance
    
    def __init__(self, value):
        print("__init__ 被调用,初始化对象")
        self.value = value

# 创建对象
obj = MyClass(100)
print(f"obj.value = {obj.value}")

输出:

__new__ 被调用,创建对象 __init__ 被调用,初始化对象 obj.value = 100

链式调用与 init

一个高级技巧是在 __init__ 中返回 None(或者更准确地说,__init__ 不应该返回任何东西)。如果你在 __init__ 中返回非 None 值,Python 会抛出 TypeError。这是因为 __init__ 的设计初衷是初始化对象,而不是创建对象。

class Test:
    def __init__(self):
        self.data = []
        # return []  # 错误!__init__ 不能返回值

验证与类型检查

__init__ 中进行参数验证是一个好的实践。这确保了对象始终处于有效状态。

class BankAccount:
    def __init__(self, owner, balance=0):
        # 类型检查
        if not isinstance(owner, str):
            raise TypeError("owner 必须是字符串")
        if not isinstance(balance, (int, float)):
            raise TypeError("balance 必须是数字")
        if balance < 0:
            raise ValueError("balance 不能为负数")
        
        self.owner = owner
        self.balance = balance
    
    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("存款金额必须为正数")
        self.balance += amount
    
    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("取款金额必须为正数")
        if amount > self.balance:
            raise ValueError("余额不足")
        self.balance -= amount

# 测试验证
try:
    account = BankAccount("张三", -100)  # 抛出 ValueError
except ValueError as e:
    print(f"错误:{e}")

try:
    account = BankAccount("李四", 100)
    account.withdraw(200)  # 余额不足
except ValueError as e:
    print(f"错误:{e}")

属性类型标注(Type Hints)

Python 3.5+ 引入了类型标注(Type Hints)功能,在 __init__ 中使用类型标注可以让代码更加清晰,也能配合 IDE 提供更好的自动补全和类型检查。

from typing import Optional, List

class Student:
    def __init__(self, name: str, age: int, grades: Optional[List[int]] = None):
        self.name: str = name
        self.age: int = age
        self.grades: List[int] = grades if grades is not None else []
    
    def add_grade(self, grade: int) -> None:
        if 0 <= grade <= 100:
            self.grades.append(grade)
        else:
            raise ValueError("成绩必须在 0-100 之间")
    
    def average(self) -> float:
        if not self.grades:
            return 0.0
        return sum(self.grades) / len(self.grades)

# 使用类型标注
s = Student("王五", 18)
s.add_grade(95)
s.add_grade(88)
s.add_grade(92)
print(f"{s.name} 的平均分:{s.average():.2f}")

练习题

练习 1:创建日期类

class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day
    
    def is_valid(self):
        """简单的日期有效性检查"""
        if month < 1 or month > 12:
            return False
        if day < 1 or day > 31:
            return False
        return True
    
    def display(self):
        """以 YYYY-MM-DD 格式显示"""
        return f"{self.year:04d}-{self.month:02d}-{self.day:02d}"

# 创建日期对象
date1 = Date(2024, 1, 15)
date2 = Date(2024, 12, 25)

print(date1.display())  # 2024-01-15
print(date2.display())  # 2024-12-25

练习 2:创建带验证的分数类

class Fraction:
    def __init__(self, numerator, denominator=1):
        if denominator == 0:
            raise ValueError("分母不能为零")
        self.numerator = numerator
        self.denominator = denominator
        self._simplify()
    
    def _simplify(self):
        """约分"""
        from math import gcd
        g = gcd(abs(self.numerator), abs(self.denominator))
        self.numerator //= g
        self.denominator //= g
        if self.denominator < 0:
            self.numerator = -self.numerator
            self.denominator = -self.denominator
    
    def __str__(self):
        if self.denominator == 1:
            return str(self.numerator)
        return f"{self.numerator}/{self.denominator}"

# 测试
f1 = Fraction(4, 8)
f2 = Fraction(6, -9)
f3 = Fraction(5)

print(f1)  # 1/2
print(f2)  # -2/3
print(f3)  # 5

练习 3:创建简单的联系人簿

class Contact:
    def __init__(self, name, phone, email=None, address=None):
        self.name = name
        self.phone = phone
        self.email = email
        self.address = address
    
    def display(self):
        print(f"姓名:{self.name}")
        print(f"电话:{self.phone}")
        if self.email:
            print(f"邮箱:{self.email}")
        if self.address:
            print(f"地址:{self.address}")

class AddressBook:
    def __init__(self):
        self.contacts = []
    
    def add_contact(self, contact):
        self.contacts.append(contact)
    
    def search(self, name):
        for c in self.contacts:
            if name in c.name:
                c.display()
                print("-" * 20)

# 使用
ab = AddressBook()
ab.add_contact(Contact("张三", "13800138000", "zhang@example.com"))
ab.add_contact(Contact("李四", "13900139000", address="北京市朝阳区"))
ab.add_contact(Contact("张五", "13700137000"))

print("搜索'张'的结果:")
ab.search("张")

总结

__init__ 方法是 Python 面向对象编程中最核心的概念之一:

  1. __init__ 在对象创建后自动调用,用于初始化对象的状态
  2. 第一个参数必须是 self,指向正在创建的对象
  3. 可以使用默认参数来提供可选参数
  4. 避免使用可变对象作为默认参数,使用 None 代替
  5. 应该在 __init__ 中定义所有实例属性,保证对象的完整性
  6. 可以进行参数验证,确保对象状态的有效性
  7. 配合类型标注使用,可以让代码更加清晰和易于维护

掌握 __init__ 的使用是编写高质量 Python 类的基础。在后续的学习中,我们将继续探索 __str____del__ 等其他魔术方法,以及继承、多态等高级面向对象概念。