Python 中文教程

类

方法要显式写 self,字段不用声明,没有访问修饰符。dataclass 和生成器是最值得掌握的两个工具。

  • 第 8 章 · 对象、异常与文件
  • 28 分钟
  • 8 组对比

定义类

__init__ 是构造函数,self 就是 this,但必须写在参数列表第一位。创建对象不用 new。

类和实例

Python
class Dog:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name}: 汪"

d = Dog("Rex")
print(d.speak())  # => Rex: 汪
print(d.name)     # => Rex
class Dog {
    private final String name;

    Dog(String name) {
        this.name = name;
    }

    String speak() {
        return name + ": 汪";
    }
}

var d = new Dog("Rex");
System.out.println(d.speak());  // => Rex: 汪
对比项PythonJava
构造函数__init__和类同名
当前对象self,必须写在参数里this,隐式
访问成员必须写 self.可以省略 this.
字段声明不用声明,赋值时创建必须声明
创建对象不用 new必须写 new

忘记写 self 是最常见的错误,def speak(): 调用时会报参数个数不对。

类变量和实例变量

写在类体里的变量属于类本身,所有实例共享,相当于 static 字段。写在 self 上的才属于实例。

类级成员

Python
class Dog:
    count = 0                # 类变量

    def __init__(self, name):
        self.name = name     # 实例变量
        Dog.count += 1

    @classmethod
    def total(cls):
        return cls.count

    @staticmethod
    def is_adult(age):
        return age >= 2

Dog("Rex")
Dog("Max")
print(Dog.total())      # => 2
print(Dog.is_adult(3))  # => True
class Dog {
    static int count = 0;
    final String name;

    Dog(String name) {
        this.name = name;
        count++;
    }

    static int total() {
        return count;
    }
}

new Dog("Rex");
new Dog("Max");
System.out.println(Dog.total());  // => 2
对比项PythonJava
类变量写在类体里static 字段
类方法@classmethod,可被子类覆盖static 方法,不能覆盖
静态方法@staticmethodstatic 方法
共享范围相同所有实例共享

别把列表这样的可变对象写成类变量,否则所有实例共用一份,这和可变默认参数是同一类错误。

通过实例赋值 d.count = 5 不会修改类变量,而是在实例上新建一个同名属性,把类变量遮住。要改类变量就写 Dog.count = 5。

继承

类名后的括号里写父类。所有方法默认都可以被覆盖,不需要 open,也不需要 override 关键字。

继承和方法覆盖

Python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        raise NotImplementedError

class Cat(Animal):
    def __init__(self, name, lives=9):
        super().__init__(name)
        self.lives = lives

    def speak(self):
        return f"{self.name}: 喵"

c = Cat("Tom")
print(c.speak())              # => Tom: 喵
print(isinstance(c, Animal))  # => True
abstract class Animal {
    final String name;
    Animal(String name) { this.name = name; }
    abstract String speak();
}

class Cat extends Animal {
    Cat(String name) { super(name); }

    @Override
    String speak() { return name + ": 喵"; }
}

Animal c = new Cat("Tom");
System.out.println(c.speak());  // => Tom: 喵
System.out.println(c instanceof Animal);  // => true
对比项PythonJava
语法class Cat(Animal):class Cat extends Animal
能否被继承默认可以默认可以
覆盖方法直接重写@Override 可选
调用父类构造super().__init__()super()
抽象类abc.ABC + @abstractmethodabstract
类型判断isinstance(c, Animal)c instanceof Animal
多态相同按实际类型调用方法

方法覆盖时可以加 typing.override(3.12 新增),让类型检查器帮忙核对。

Python 没有 interface 关键字。只要对象有需要的方法就能用,这叫鸭子类型。想让类型检查器验证,就用 typing.Protocol,实现类不需要显式声明继承关系。

接口的替代方式

Python
from typing import Protocol

class Greeter(Protocol):
    def greet(self) -> str: ...

class En:           # 没有写继承关系
    def greet(self) -> str:
        return "Hello"

def run(g: Greeter):
    print(g.greet())

run(En())           # => Hello
interface Greeter {
    String greet();
}

class En implements Greeter {
    public String greet() { return "Hello"; }
}

static void run(Greeter g) {
    System.out.println(g.greet());
}

run(new En());  // => Hello
对比项PythonJava
定义接口Protocol 或鸭子类型interface
声明实现不需要implements
检查时机类型检查工具,运行时不查编译时
调用方依赖相同只依赖方法,不依赖具体类

小型项目直接靠鸭子类型就够了。大型项目用 Protocol 或 ABC 把约定写明。

多重继承和 MRO选读

Python 支持多重继承:class Duck(Swimmer, Walker): ...。方法按 MRO(方法解析顺序)查找,可以用 Duck.__mro__ 查看。super() 也沿着 MRO 走,而不只是找直接父类。实际项目主要用它写 Mixin,给类附加一组方法,用途接近 Java 的默认接口方法。

Python
class Swimmer:
    def move(self):
        return "游"

class Walker:
    def move(self):
        return "走"

class Duck(Swimmer, Walker):
    pass

print(Duck().move())  # => 游
print([c.__name__ for c in Duck.__mro__])  # => ['Duck', 'Swimmer', 'Walker', 'object']

访问控制

Python 没有 private。单下划线是「内部使用」的约定,双下划线会触发名称改写,用来避免子类意外覆盖。需要受控访问时,用 @property 把方法变成属性。

私有字段和属性

Python
class Account:
    def __init__(self, balance):
        self._balance = balance

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("余额不能为负")
        self._balance = value

a = Account(100)
a.balance = 50
print(a.balance)  # => 50
class Account {
    private int balance;

    Account(int balance) {
        this.balance = balance;
    }

    int getBalance() { return balance; }

    void setBalance(int value) {
        if (value < 0) throw new
            IllegalArgumentException("余额不能为负");
        balance = value;
    }
}

var a = new Account(100);
a.setBalance(50);
System.out.println(a.getBalance());  // => 50
对比项PythonJava
私有字段_name 是约定,不强制private
访问器@propertygetXxx() 方法
调用方写法a.balancea.getBalance()
何时加校验需要时再改,调用方不用变通常提前写好访问器

双下划线 __x 会触发名称改写,用来避免子类意外覆盖,并不是真正的私有。

特殊方法

双下划线包围的方法由解释器在特定语法下自动调用。实现它们就能让自定义类支持 print、==、+、len()、for 等语法。

特殊方法对照
PythonJava说明
__str__toString()print 和 str() 时调用
__repr__无调试用的字符串,交互模式显示的就是它
__eq__equals()== 时调用
__hash__hashCode()重写 __eq__ 时要一起处理
__lt__compareTo()< 时调用,用于排序
__add__无+ 时调用
__len__size()len() 时调用
__getitem__get(i)obj[i] 时调用
__iter__iterator()for 循环时调用
__call__函数式接口让对象能像函数一样调用
__enter__ / __exit__AutoCloseablewith 时调用
运行时查看和修改对象选读

下面这些内置函数相当于 Java 的反射,用起来简单得多。

Python
class User:
    def __init__(self, name):
        self.name = name

u = User("Ann")
print(type(u).__name__)          # => User
print(hasattr(u, "name"))        # => True
print(getattr(u, "age", 0))      # => 0
setattr(u, "age", 30)
print(u.__dict__)                # => {'name': 'Ann', 'age': 30}
print(issubclass(User, object))  # => True

obj.method 不加括号时得到绑定方法,可以存起来稍后调用。eval 和 exec 能执行字符串形式的代码,不要用在外部输入上。

dataclass

只保存数据的类用 dataclass,自动生成 __init__、__repr__ 和 __eq__。这是日常写类最常用的方式。

数据类

Python
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int = 0

p = Point(3)
print(p)              # => Point(x=3, y=0)
print(p == Point(3))  # => True
record Point(int x, int y) {
    Point(int x) { this(x, 0); }
}

var p = new Point(3);
System.out.println(p);  // => Point[x=3, y=0]
System.out.println(p.equals(new Point(3)));  // => true
对比项PythonJava
语法@dataclassrecord
可变性默认可变,frozen=True 后不可变不可变
字段默认值y: int = 0要写额外的构造函数
复制并修改dataclasses.replace(p, x=1)手写
自动生成相同构造函数、相等比较、字符串表示

类型注解在这里是必须写的,dataclass 靠它识别字段。

枚举

枚举要继承 enum.Enum。成员写成类变量,每个成员都有 name 和 value。

枚举

Python
from enum import Enum

class Color(Enum):
    RED = "红"
    GREEN = "绿"

c = Color.RED
print(c.name, c.value)  # => RED 红
print(Color("绿"))      # => Color.GREEN
print(c is Color.RED)   # => True
print(len(Color))       # => 2
enum Color {
    RED("红"), GREEN("绿");

    final String label;

    Color(String label) { this.label = label; }
}

var c = Color.RED;
System.out.println(c.name() + " " + c.label);  // => RED 红
System.out.println(c == Color.RED);  // => true
对比项PythonJava
定义继承 Enumenum 关键字
附带的值value,一个成员一个值构造函数,可以有多个字段
按值查找Color("绿")手写
按名查找Color["RED"]Color.valueOf("RED")
比较用 is用 ==
成员是单例相同每个成员只有一个实例

值不重要时用 enum.auto() 自动编号。match 里比较枚举要写 case Color.RED:。

迭代器和生成器

for 循环依靠迭代器协议:先用 iter(obj) 取得迭代器,再不断调用 next(),取完时抛 StopIteration。自己写迭代器很少见,生成器函数更简单。函数体里有 yield 时,调用它会得到一个惰性序列。

生成器

Python
def countdown(n):
    while n > 0:
        yield n
        n -= 1

print(list(countdown(3)))  # => [3, 2, 1]

for x in countdown(2):
    print(x)               # 2 1

squares = (x * x for x in range(4))
print(sum(squares))        # => 14
var list = Stream
    .iterate(3, n -> n > 0, n -> n - 1)
    .toList();
System.out.println(list);  // => [3, 2, 1]

var total = IntStream.range(0, 4)
    .map(x -> x * x).sum();
System.out.println(total);  // => 14
对比项PythonJava
定义方式函数里写 yieldStream.iterate 或手写 Iterator
惰性推导(x * x for x in xs)stream().map()
重复遍历只能遍历一次只能遍历一次
求值时机相同用到时才计算

生成器不占内存,适合处理大数据或无限序列。需要重复使用时,用 list() 转成列表。

自己写迭代器选读

实现 __iter__ 和 __next__ 就能用于 for,对应 Java 的 Iterable 和 Iterator。大多数情况下用生成器更简单。

Python
class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current == 0:
            raise StopIteration
        self.current -= 1
        return self.current + 1

print(list(Countdown(3)))  # => [3, 2, 1]

本章要点

  • self 必须显式写,__init__ 是构造函数,字段在里面赋值时创建
  • 类体里的变量是类变量,相当于 static
  • 没有 interface 和 private,靠鸭子类型和下划线约定
  • @property 让字段和访问器的调用方式相同,所以不用提前写 getter
  • 数据类用 @dataclass,frozen=True 对应 Java 的 record
  • 特殊方法让自定义类支持内置语法
  • 枚举继承 Enum,每个成员有 name 和 value
  • 生成器函数用 yield 写惰性序列,对应 Kotlin 的 sequence

官方原文:类