Python从设计之初就已经是一门面向对象的语言, 正因为如此, 在Python中创建一个类和对象是很容易的. 本章节我们将详细介绍Python的面向对象编程.
如果你以前没有接触过面向对象的编程语言, 那你可能需要先了解一些面向对象语言的一些基本特征, 在头脑里头形成一个基本的面向对象的概念, 这样有助于你更容易的学习Python的面向对象编程.
面向对象程序设计概述
面向对象程序设计(Object-Oriented Programming, OOP)是一种编程范式.
OOP把对象作为程序的基本单元, 一个对象包含了数据和操作数据的函数.
面向过程的程序设计把计算机程序视为一系列的命令集合, 即一组函数的顺序执行. 为了简化程序设计, 面向过程把函数继续切分为子函数, 即把大块函数通过切割成小块函数来降低系统的复杂度.
而面向对象的程序设计把计算机程序视为一组对象的集合, 而每个对象都可以接收其他对象发过来的消息, 并处理这些消息, 计算机程序的执行就是一系列消息在各个对象之间传递.
在Python中, 所有数据类型都可以视为对象, 当然也可以自定义对象. 自定义的对象数据类型就是面向对象中的类(Class)的概念.
这里举一个例子来说明面向过程和面向对象在程序流程上的不同之处:
假设我们要处理学生的成绩表, 为了表示一个学生的成绩.
面向过程的程序可以用一个dict表示:
student1 = {'name': 'Alice', 'age': 20, 'grades': 90}
student2 = {'name': 'Bob', 'age': 21, 'grades': 85}
而处理学生成绩可以通过函数实现, 比如打印学生的成绩:
def print_grades(student):
print(f"{student['age']}岁的{student['name']}的成绩是{student['grades']}")
print_grades(student1)
print_grades(student2)
如果采用面向对象的程序设计思想, 我们首先思考的不是程序的执行流程, 而是Student这种数据类型应该被视为一个对象, 这个对象拥有age, name和score这些属性(Property).
如果要打印一个学生的成绩, 首先必须创建出这个学生对应的对象, 然后给对象发一个print_score
消息, 让对象自己把自己的数据打印出来.
class Student:
def __init__(self, name, age, grades):
self.name = name
self.age = age
self.grades = grades
def print_grades(self):
print(f"{self.age}岁的{self.name}的成绩是{self.grades}")
然后我们可以创建两个Student对象, 并给它们发print_grades
消息, 给对象发消息实际上就是调用对象对应的关联函数, 我们称之为对象的方法(Method).
student1 = Student('Alice', 20, 90)
student2 = Student('Bob', 21, 85)
student1.print_grades()
student2.print_grades()
这样一来, 程序的执行流程变成了:
- 创建一个Student对象
- 给这个对象发一个
print_grades
消息 - 这个对象自己把自己的数据打印出来
面向对象的设计思想是从自然界中来的, 因为在自然界中, 类(Class)和实例(Instance)的概念是很自然的.
Class是一种抽象概念, 比如我们定义的Class——Student, 是指学生这个概念,而实例(Instance)则是一个个具体的Student, 比如Alice和Bob是两个具体的Student.
所以面向对象的设计思想是抽象出Class, 根据Class创建Instance.
对象
对象(Object)是面向对象程序设计的基本单元, 它是由数据和操作数据的函数组成的.
通常将对象分为两部分, 即静态部分和动态部分.
静态部分被称为对象的属性(Attribute), 它是对象拥有的状态和特征.
动态部分被称为对象的行为(Behavior), 它是对象可以响应的消息, 它决定了对象能做什么, 以及在何时做这些事情.
在Pytho中, 一切都是对象.
类
类(Class)是面向对象程序设计的基本单元, 它定义了对象的静态部分和动态部分.
类是封装对象的属性和行为的载体, 即具有相同属性和行为的对象可以归为一类.
面向对象程序设计的特点
面向对象程序设计有三个特点: 封装性(Encapsulation)、继承性(Inheritance)、多态性(Polymorphism).
封装性
封装是面向对象程序设计的核心思想, 将对象的属性和行为被封装在一个对象内部, 外部代码无法直接访问对象的内部数据和方法, 只有通过对象提供的接口才能访问对象.
而将对象的属性和行为封装在一起的载体是类.
继承性
继承性是面向对象程序设计的重要特征, 它允许创建新的类, 继承已有的类, 并扩展已有的类的功能.
继承性是通过让一个类派生自另一个类来实现的, 新类称为子类(Subclass), 被继承的类称为基类或父类(Baseclass), 子类可以获得基类的所有属性和方法, 并可以根据需要添加新的属性和方法.
子类的实例都是基类的实例, 但反过来不成立. 就好像平行四边形是特殊的四边形, 但不能说四边形是平行四边形.
多态性
多态性是面向对象程序设计的重要特征, 它允许不同类的对象对同一消息作出不同的响应.
将父类对象应用于子类的特征就是多态. 子类在继承父类时, 可以重写父类的方法, 这样就可以让子类表现出不同的行为.
多态性是通过方法重写(Override)和方法重载(Overload)实现的.
面向对象技术简介
- 类(Class): 用来描述具有相同的属性和方法的对象的集合. 它定义了该集合中每个对象所共有的属性和方法. 对象是类的实例.
- 方法: 类中定义的函数.
- 类变量: 类变量在整个实例化的对象中是公用的. 类变量定义在类中且在函数体之外. 类变量通常不作为实例变量使用.
- 数据成员: 类变量或者实例变量用于处理类及其实例对象的相关的数据.
- 方法重写: 如果从父类继承的方法不能满足子类的需求, 可以对其进行改写, 这个过程叫方法的覆盖(override), 也称为方法的重写.
- 局部变量: 定义在方法中的变量, 只作用于当前实例的类.
- 实例变量:在类的声明中,属性是用变量来表示的,这种变量就称为实例变量, 实例变量就是一个用 self 修饰的变量.
- 继承: 即一个派生类(derived class)继承基类(base class)的字段和方法. 继承也允许把一个派生类的对象作为一个基类对象对待. 例如有这样一个设计: 一个Dog类型的对象派生自Animal类, 这是模拟”是一个(is-a)”关系, 即Dog是一个Animal.
- 实例化: 创建一个类的实例, 类的具体对象.
- 对象: 通过类定义的数据结构实例. 对象包括两个数据成员(类变量和实例变量)和方法.
类的定义和使用
在Python中, 类表示具有相同属性和方法的对象的集合, 它定义了该集合中每个对象所共有的属性和方法. 在使用类时, 需要先定义类, 然后创建类的实例, 通过类的实例就可以访问类的属性和方法.
定义类
在Python中, 定义类使用class
关键字, 后面跟着类的名称, 然后在缩进块中定义类的属性和方法.
class ClassName:
'''类的帮助信息''' # 类文档字符串
statment # 类体
ClassName
: 类名, 遵循变量命名规则."类的帮助信息"
: 类文档字符串, 用于描述类的功能. 定义该字符串后在创建类的对象时, 输入类名和左括号后, 会显示该信息.statement
: 类体, 主要由类变量或类成员, 方法和属性等定义语句组成.
创建类的实例
定义完类后, 并没有创建一个实例, 只是相当于创建了一个设计图, 那么如何创建类的实例呢?
class语句本身并不创建该类的实例, 因此在类定义完成后, 可以通过实例化来创建类的实例.
实例化的语法如下:
instanceName = ClassName(parameterlist)
instanceName
: 实例名, 遵循变量命名规则.ClassName
: 类名.parameterlist
: 实例化参数, 用于初始化实例的属性. 仅有__init
方法没有被创建或者__init
方法只有一个self参数时, 可以省略该参数.
例如:
class Person:
def __init__(self, name, age): # 定义__init__方法
self.name = name
self.age = age
def say_hello(self):
print(f"Hello, my name is {self.name} and I am {self.age} years old.")
p1 = Person("Alice", 20) # 实例化一个Person对象
p1.say_hello()
在上面的例子中, 定义了一个Person类, 它有两个属性: name和age, 以及一个方法say_hello.
然后通过实例化创建了一个Person类的实例p1, 并调用了say_hello方法, 输出了”Hello, my name is Alice and I am 20 years old.”
创建_ init _方法
在定义类时, 通常会定义一个__init__
方法, 该方法是一个特殊的方法(构造方法), 每当创建一个类的实例时, 该方法就会自动调用.
__init__
方法的作用是初始化类的实例, 它可以接收参数, 并将参数的值赋给对象的属性.
__init__
必须包含一个self参数, 并且必须是第一个参数, 它代表的是类的实例本身, 用于访问类的属性和方法
在方法被调用时, 自动传递一个实例作为第一个参数, 因此在__init__
方法中,如果只有一个参数, 在创建实例时, 无需指定该实际参数.
创建类的成员并访问
类的成员主要由实例方法与数据成员组成, 在创建了类的成员后, 可以通过实例来访问这些成员.
创建实例方法并访问
实例方法是指在类中定义的函数, 该函数是一种在类的实例上运行的函数, 它可以访问类的属性和方法.
和__init__
方法一样, 实例方法也必须包含一个self
参数, 并且必须是第一个参数.
实例方法的创建方式如下:
def method_name(self, parameterlist):
'''方法的帮助信息''' # 方法文档字符串
block # 方法体
method_name
: 用于指定方法名, 一般使用小写字母开头;self
: 必要参数, 表示类的实例本身, 但是起名可以用任意的名字(但一般习惯用self
);parameterlist
: 实例方法的参数列表, 用于接收外部数据并处理, 各个数据用逗号分隔;"方法的帮助信息"
: 方法文档字符串, 用于描述方法的功能, 定义该字符串后在调用方法时, 输入方法名和左括号后, 会显示该信息;block
: 方法体, 用于实现具体的功能, 包括对参数的处理, 以及对类的属性的操作.
实例方法和Python函数的区别: 函数实现某个独立的功能, 而实例方法则实现了类的一个行为, 是类的一部分.
在创建实例方法后, 可以通过类的实例名称和.
来访问该方法. 具体的语法格式如下:
instanceName.method_name(parametervalu)
instanceName
: 实例名;method_name
: 方法名;parametervalu
: 方法指定对应的实际参数, 用于传递给方法的数据, 其值的个数和类型必须与方法定义时一致.
例如:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def say_hello(self):
print(f"Hello, my name is {self.name} and I am {self.age} years old.")
p1 = Person("Alice", 20)
p1.say_hello()
在上面的例子中, 定义了一个Person类, 它有两个属性: name和age, 以及一个方法say_hello.
然后通过实例化创建了一个Person类的实例p1, 并调用了say_hello方法, 输出了”Hello, my name is Alice and I am 20 years old.”
创建数据成员并访问
数据成员是指在类中定义的变量, 即属性.
根据定义位置, 数据成员可以分为类属性和实例属性.
类属性
类属性指的是定义在类中, 并且是在函数体外的属性. 类属性可以在类的所有实例之间共享值. 即类属性在所有实例化的对象中是公用的.
类属性可以通过类名称或者实例名称来访问.
类属性的创建方式如下:
ClassName.attribute_name = value
ClassName
: 类名;attribute_name
: 属性名, 遵循变量命名规则;value
: 属性值, 可以是任意类型的值.
例如:
class Person:
name = "person" # 类属性
def __init__(self, name, age):
self.name = name
self.age = age
p1 = Person("Alice", 20)
p2 = Person("Bob", 21)
print(p1.name) # 输出: "Alice"
print(p2.name) # 输出: "Bob"
print(Person.name) # 输出: "person"
在上面的例子中, 定义了一个Person类, 它有两个属性: name和age, 以及一个方法say_hello.
然后通过实例化创建了两个Person类的实例p1和p2, 并访问了name属性, 输出了”Alice”和”Bob”.
实例属性
实例属性指的是定义在类的实例中, 并且是在函数体内的属性. 实例属性只能在类的实例中访问.
实例属性只能通过实例名称来访问.
实例属性的创建方式如下:
self.attribute_name = value
self
: 实例名, 代表类的实例本身;attribute_name
: 属性名, 遵循变量命名规则;value
: 属性值, 可以是任意类型的值.
例如:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
p1 = Person("Alice", 20)
p2 = Person("Bob", 21)
p1.gender = "female" # 创建实例属性
p2.gender = "male"
print(p1.gender) # 输出: "female"
print(p2.gender) # 输出: "male"
在上面的例子中, 定义了一个Person类, 它有两个属性: name和age, 以及一个方法say_hello.
然后通过实例化创建了两个Person类的实例p1和p2, 并创建了实例属性gender, 并访问了gender属性, 输出了”female”和”male”.
访问限制
为了保证类内部的某些属性和方法只能被内部代码访问, 并不允许外部代码直接访问, 这时可以使用访问限制.
可以在属性或方法名前面加上两个下划线__
(__foo
)或者首尾都加上两个下划线__
(__foo__
)来实现访问限制. 这样的属性或方法只能在类的内部访问, 外部代码不能直接访问.
__foo__
: 双下划线开头和结尾的属性或方法, 一般用于特殊方法, 如__init__
和__str__
.__foo
: 单下划线开头的属性或方法, 一般用于私有方法, 只允许定义该方法的类本身对其进行访问, 而不允许通过类实例来访问, 但是可以通过类的实例名.类名__foo
来访问
类的专有方法
类有一些特殊的函数, 它们会在特殊的情况下被调用.
__init__
: 构造函数, 在生成对象时调用__del__
: 析构函数, 释放对象时使用__repr__
: 打印, 转换__setitem__
: 按照索引赋值__getitem__
: 按照索引获取值__len__
: 获得长度__cmp__
: 比较运算__call__
: 函数调用__add__
: 加运算__sub__
: 减运算__mul__
: 乘运算__div__
: 除运算__mod__
: 求余运算__pow__
: 乘方
运算符重载
运算符重载(operator overloading)是面向对象编程中常用的技术, 它允许类的实例支持一些运算符, 使得实例可以像操作数一样进行运算.
运算符重载的语法如下:
class ClassName:
def __operator__(self, other):
'''运算符的帮助信息''' # 运算符文档字符串
block # 运算符体
ClassName
: 类名;__operator__
: 运算符, 如__add__
等;self
: 必要参数, 表示类的实例本身;other
: 运算对象, 用于运算的对象;"运算符的帮助信息"
: 运算符的帮助信息, 用于描述运算符的功能, 定义该字符串后在调用运算符时, 输入运算符和左括号后, 会显示该信息;block
: 运算符体, 用于实现具体的运算功能, 包括对参数的处理, 以及对类的属性的操作.
例如:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __str__(self):
return f"({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v3 = v1 + v2
print(v3) # 输出: "(4, 6)"
在上面的例子中, 定义了一个Vector类, 它有两个属性: x和y, 以及两个运算符: __add__
和__str__
.
然后通过实例化创建了两个Vector类的实例v1和v2, 并调用了+
运算符, 输出了(4, 6)
.
属性
本节的属性(property)与前面介绍的类属性和实例属性不同.
创建用于计算的属性
在Python中, 可以通过@property
装饰器将一个方法转换为属性, 以实现用于计算的属性.
将方法转换为属性后, 可以通过方法名来访问方法, 而无需再加上小括号.
通过@property
装饰器将方法转换为属性的语法如下:
@property
def attribute_name(self):
'''属性的帮助信息''' # 属性文档字符串
block # 属性体
attribute_name
: 属性名, 遵循变量命名规则;self
: 必要参数, 表示类的实例本身;"属性的帮助信息"
: 属性文档字符串, 用于描述属性的功能, 定义该字符串后在访问属性时, 输入属性名和点号后, 会显示该信息;block
: 属性体, 一般以return
语句结尾, 用于返回计算结果.
例如定义一个矩形类, 其中包含两个属性: 长和宽, 并定义了计算面积的属性:
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
@property #将方法area转换为属性
def area(self):
return self.length * self.width
r1 = Rectangle(5, 10)
print(r1.area) # 输出: 50
继承
继承是面向对象程序设计的重要特征, 它允许创建新的类, 它们继承了基类的属性和方法, 并可以根据需要添加新的属性和方法.
继承语法
在程序设计中实现继承, 表示这个类拥有它继承的类的所有公有成员或者受保护成员. 在面向对象编程中, 被继承的类称为基类(base class), 继承的类称为派生类(derived class).
继承不仅可以实现代码的重用, 还可以通过继承理顺类与类的之间的关系.
在Python中, 可以在类定义语句中, 类名右侧使用一对小括号()
括起来的基类名, 来实现继承.
class DerivedClassName(BaseClassNamelist):
'''派生类的帮助信息''' # 类文档字符串
statement # 类体
DerivedClassName
: 派生类名, 遵循变量命名规则.BaseClassNamelist
: 基类名, 即被继承的类. 可以有多个基类, 用逗号分隔.如果不指定基类, 则默认继承自Python对象的根类object
类."派生类的帮助信息"
: 派生类的帮助信息, 用于描述派生类的功能.statement
: 派生类体, 包含派生类的类变量, 方法和属性等定义语句组成.
例如创建水果基类及其派生类:
class Fruit:
def __init__(self, name):
self.name = name
def describe(self):
print(f" {self.name} is a fruit.")
class Apple(Fruit):
def __init__(self, name):
super().__init__(name)
class Banana(Fruit):
def __init__(self, name):
super().__init__(name)
a1 = Apple("apple")
a1.describe() # 输出: "apple is a fruit."
b1 = Banana("banana")
b1.describe() # 输出: "banana is a fruit."
在上面的例子中, 定义了水果基类Fruit, 它有一个方法describe, 并定义了两个派生类Apple和Banana, 它们都继承了Fruit类, 并且可以使用Fruit类的describe方法.
通过继承, 我们可以创建新的类, 它们继承了基类的属性和方法, 并可以根据需要添加新的属性和方法.
方法重写
基类的成员都会被派生类继承, 当基类的某个方法不完全适用于派生类时, 可以在派生类中重新定义这个方法, 这就是方法重写(override).
还是刚才水果例子, 我们定义一个Fruit类的一个颜色color属性, 并在Apple和Banana类中重新定义这个属性, 这样就实现了颜色的不同.
class Fruit:
def __init__(self, name):
self.name = name
self.color = "unknown"
def describe(self):
print(f" {self.name} is a fruit with color {self.color}.")
class Apple(Fruit):
def __init__(self, name):
super().__init__(name)
self.color = "red"
class Banana(Fruit):
def __init__(self, name):
super().__init__(name)
self.color = "yellow"
a1 = Apple("apple")
a1.describe() # 输出: "apple is a fruit with color red."
b1 = Banana("banana")
b1.describe() # 输出: "banana is a fruit with color yellow."
在上面的例子中, 定义了Fruit类, 它有一个color属性, 并定义了describe方法, 这个方法打印出水果的名字和颜色.
然后定义了两个派生类Apple和Banana, 它们都继承了Fruit类, 并重新定义了color属性, 这样就实现了颜色的不同.
通过方法重写, 我们可以根据需要, 调整基类的方法, 使之更适合派生类.
派生类调用基类的构造方法
在派生类中, 不会自带基类的构造方法, 所以需要手动调用基类的构造方法.
在派生类中, 如果需要调用基类的构造方法, 则需要使用super()
函数.
即在派生类构造方法的第一行, 加上super().__init__(args)
语句, 其中args
是基类构造方法的参数.
如果没有调用而直接使用基类属性或方法, 则会报错.
例如:
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
#super().__init__(name)
'''
注释后, 错误: AttributeError: 'Dog' object has no attribute 'name'
'''
self.breed = breed
d1 = Dog("Buddy", "Labrador")
print(d1.name) # 输出: "Buddy"
print(d1.breed) # 输出: "Labrador"
多态
多态(polymorphism)是面向对象编程中一个重要的概念, 它允许不同类的对象对同一消息作出不同的响应.
多态是指允许不同类的对象对同一消息作出不同的响应. 多态是指允许一个变量或表达式的类型和实际类型不同, 并能根据实际类型调用相应的方法.
在Python中, 多态是通过方法重写和继承实现的.
这些前面都已经介绍过了, 这里再总结一下:
- 继承: 继承是面向对象编程中一个重要的特征, 它允许创建新的类, 它们继承了基类的属性和方法, 并可以根据需要添加新的属性和方法.
- 方法重写: 基类的成员都会被派生类继承, 当基类的某个方法不完全适用于派生类时, 可以在派生类中重新定义这个方法, 这就是方法重写(override).
面向对象高级编程
使用slots限制属性
在Python中, 所有类都可以动态地添加属性, 这使得类的定义非常灵活.
正常情况下, 当我们定义了一个class, 创建了一个class的实例后, 我们可以给该实例绑定任何属性和方法, 这就是动态语言的灵活性. 先定义class
class Student(object)
pass
尝试给实例绑定属性
s = Student()
s.name = 'Alice'
print(s.name)
也可以给实例绑定方法
def set_age(self, age):
self.age = age
s.set_age = types.MethodType(set_age, s)
s.set_age(20)
print(s.age)
但是给一个实例绑定的方法, 对其他实例是不起作用的.
为了给所有实例都绑定方法, 可以给class绑定方法, 给class绑定方法后,所有实例均可调用.
def set_score(self, score):
self.score = score
Student.set_score = set_score
s1 = Student()
s1.set_score(80)
print(s1.score)
s2 = Student()
s2.set_score(90)
print(s2.score)
但是如果我们想要限制实例可以绑定的属性, 我们可以使用slots来实现.
slots是一个特殊的变量, 它限制了该class实例能添加的属性.
class Student(object):
__slots__ = ('name', 'age')
s = Student()
s.name = 'Alice'
s.age = 20
print(s.name)
print(s.age)
s.score = 80 # AttributeError: 'Student' object has no attribute'score'
在上面的例子中, 限制了实例可以绑定的属性为name和age, 其他属性不能绑定.
用__slots__
要注意, __slots__
定义的属性仅对当前类实例起作用, 对继承的子类是不起作用的.
除非在子类中也定义__slots__
, 这样子类实例允许定义的属性就是自身的__slots__
加上父类的__slots__
.
使用@property装饰器为属性添加安全保护机制
在Python中, 默认情况下, 创建的类属性或者实例属性是可以在类体外进行修改的, 如果想要限制它在类体外被修改, 可以设为私有, 但是这时候在类体外就无法读取它的值, 这时可以通过访问限制来实现安全保护.
即使用@property
实现只读属性.
例如:
class Person:
def __init__(self, name, age):
self.__name = name
self.__age = age
@property
def name(self):
return self.__name
@property
def age(self):
return self.__age
p1 = Person("Alice", 20)
print(p1.name)
p1.name = "Bob"
print(p1.name)
这个时候就会报错: AttributeError: can’t set attribute
要特别注意属性的方法名不要和实例变量重名, 会造成递归调用,导致栈溢出报错.
多继承
Python支持多继承, 即一个派生类可以同时继承多个基类.
在类定义语句中, 基类名之间用逗号分隔, 即可实现多继承.
需要注意圆括号中父类的顺序, 若是父类中有相同的方法名, 而在子类使用时未指定,Python从左至右搜索 即方法在子类中未找到时, 从左到右查找父类中是否包含方法.
#类定义
class people:
#定义基本属性
name = ''
age = 0
#定义私有属性,私有属性在类外部无法直接进行访问
__weight = 0
#定义构造方法
def __init__(self,n,a,w):
self.name = n
self.age = a
self.__weight = w
def speak(self):
print("%s 说: 我 %d 岁。" %(self.name,self.age))
#单继承示例
class student(people):
grade = ''
def __init__(self,n,a,w,g):
#调用父类的构函
people.__init__(self,n,a,w)
self.grade = g
#覆写父类的方法
def speak(self):
print("%s 说: 我 %d 岁了,我在读 %d 年级"%(self.name,self.age,self.grade))
#另一个类,多继承之前的准备
class speaker():
topic = ''
name = ''
def __init__(self,n,t):
self.name = n
self.topic = t
def speak(self):
print("我叫 %s,我是一个演说家,我演讲的主题是 %s"%(self.name,self.topic))
#多继承
class sample(speaker,student):
a =''
def __init__(self,n,a,w,g,t):
student.__init__(self,n,a,w,g)
speaker.__init__(self,n,t)
test = sample("Tim",25,80,4,"Python")
test.speak() #方法名同,默认调用的是在括号中参数位置排前父类的方法
输出结果:
我叫 Tim,我是一个演说家,我演讲的主题是 Python
定制类
还记得前面说的特殊方法吗? 定制类就是通过这些特殊方法, 来定制类的行为.
class有很多特殊方法, 如__init__
, __str__
, __iter__
, __getitem__
等, 这些方法都是可以定制类的行为的.
_ str_()
我们先定义一个Student类, 打印一个实例:
class Student(object):
def __init__(self, name):
self.name = name
print(Student('Michael'))
输出:
<__main__.Student object at 0x000002D67FF408F0>
打印出一大坨, 不好看.
怎么才能打印得好看呢? 只需要定义好__str__()
方法, 返回一个好看的字符串就可以了:
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Student object (name: %s)' % self.name
print(Student('Michael'))
输出:
Student object (name: Michael)
这样打印出来的实例, 不但好看, 而且容易看出实例内部重要的数据.
但是细心的朋友会发现直接敲变量不用print, 打印出来的实例还是不好看:
s = Student('Michael')
print(s)
输出:
<__main__.Student object at 0x00000205ADFF8890>
这是因为直接显示变量调用的不是__str__()
, 而是__repr__()
, 两者的区别是__str__()
返回用户看到的字符串, 而__repr__()
返回程序开发者看到的字符串, 也就是说, ___repr__()
是为调试服务的.
解决办法是再定义一个__repr__()
. 但是通常__str__()
和__repr__()
代码都是一样的, 所以有个偷懒的写法:
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Student object (name=%s)' % self.name
__repr__ = __str__
_ iter _()
如果一个类想被用于for ... in
循环, 类似list或tuple那样, 就必须实现一个__iter__()
方法, 该方法返回一个迭代对象, 然后,Python的for循环就会不断调用该迭代对象的__next__()
方法拿到循环的下一个值, 直到遇到StopIteration错误时退出循环.
我们以斐波那契数列为例, 写一个Fib类, 可以作用于for循环:
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1 # 初始化两个计数器a, b
def __iter__(self):
return self # 实例本身就是迭代对象, 故返回自己
def __next__(self):
self.a, self.b = self.b, self.a + self.b # 计算下一个值
if self.a > 10000: # 退出循环的条件
raise StopIteration()
return self.a # 返回下一个值
现在, 试试把Fib实例作用于for循环:
for n in Fib():
print(n)
输出:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765
_ getitem _()
Fib实例虽然能作用于for循环, 看起来和list有点像, 但是把它当成list来使用还是不行, 比如取第5个元素:
print(Fib()[5])
'''
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'Fib' object does not support indexing
'''
要表现得像list那样按照下标取出元素, 需要实现__getitem__()
方法:
class Fib(object):
class Fib(object):
def __getitem__(self, n):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
现在, 试试取第5个元素:
“`python
print(Fib()[5])
输出:
55
但是list有个神奇的切片方法:
python
list(range(100))[5:10]
对于Fib却报错. 原因是`__getitem__()`传入的参数可能是一个int, 也可能是一个切片对象slice, 所以要做判断:
python
class Fib(object):
def getitem(self, n):
if isinstance(n, int): # n是索引
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
if isinstance(n, slice): # n是切片
start = n.start
stop = n.stop
if start is None:
start = 0
a, b = 1, 1
L = []
for x in range(stop):
if x >= start:
L.append(a)
a, b = b, a + b
return L
现在试试Fib的切片:
python
f = Fib()
print(f[0:5]) # 输出: [1, 1, 2, 3, 5]
print(f[:10]) # 输出: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
但是没有对step参数作处理:
python
print(f[:10:2]) # 输出: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
也没有对负数作处理, 所以,要正确实现一个__getitem__()还是有很多工作要做的.
此外,如果把对象看成dict,` __getitem__()`的参数也可能是一个可以作key的object, 例如str.
与之对应的是`__setitem__()`方法, 把对象视作list或dict来对集合赋值. 最后还有一个`__delitem__()`方法, 用于删除某个元素.
总之, 通过上面的方法, 我们自己定义的类表现得和Python自带的list, tuple, dict没什么区别, 这完全归功于动态语言的"鸭子类型", 不需要强制继承某个接口.
#### __ getattr __()
正常情况下, 当我们调用类的方法或属性时, 如果不存在就会报错. 比如定义Student类:
python
class Student(object):
def init(self):
self.name = ‘Michael’
调用name属性没问题, 但是调用不存在的score属性, 就有问题了:
python
s = Student()
print(s.name) # 输出: Michael
print(s.score)
”’
Traceback (most recent call last):
…
AttributeError: ‘Student’ object has no attribute ‘score’
”’
错误信息很清楚地告诉我们, 没有找到score这个attribute.
要避免这个错误, 除了可以加上一个score属性外, Python还有另一个机制, 那就是写一个`__getattr__()`方法, 动态返回一个属性. 修改如下:
python
class Student(object):
def init(self):
self.name = ‘Michael’
def __getattr__(self, attr):
if attr=='score':
return 99
当调用不存在的属性时, 比如score, Python解释器会试图调用`__getattr__(self, 'score')`来尝试获得属性, 这样我们就有机会返回score的值:
python
s = Student()
print(s.name) # 输出: Michael
print(s.score) # 输出: 99
返回函数也是完全可以的:
python
class Student(object):
def getattr(self, attr):
if attr==’age’:
return lambda: 25
只是调用方式要变为:
python
s = Student()
print(s.age()) # 输出: 25
注意, 只有在没有找到属性的情况下, 才调用`__getattr__`, 已有的属性比如name, 不会在`__getattr__`中查找.
此外, 注意到任意调用如`s.abc`都会返回`None`, 这是因为我们定义的`__getattr__`默认返回就是None. 要让class只响应特定的几个属性, 我们就要按照约定, 抛出AttributeError的错误:
python
class Student(object):
def getattr(self, attr):
if attr==’age’:
return lambda: 25
raise AttributeError(‘\’Student\’ object has no attribute \’%s\” % attr)
“`
这实际上可以把一个类的所有属性和方法调用全部动态化处理了, 不需要任何特殊手段.
_ call _()
一个对象实例可以有自己的属性和方法, 当我们调用实例方法时, 我们用instance.method()
来调用. 能不能直接在实例本身上调用呢? 在Python中, 答案是肯定的.
任何类, 只需要定义一个__call__()
方法, 就可以直接对实例进行调用. 请看示例:
class Student(object):
def __init__(self, name):
self.name = name
def __call__(self):
print('My name is %s.' % self.name)
调用方式如下:
s = Student('Michael')
s() # 输出: My name is Michael.
#self参数不要传入
__call__()
还可以定义参数. 对实例进行直接调用就好比对一个函数进行调用一样, 所以你完全可以把对象看成函数, 把函数看成对象, 因为这两者之间本来就没啥根本的区别.
如果你把对象看成函数, 那么函数本身其实也可以在运行期动态创建出来, 因为类的实例都是运行期创建出来的, 这么一来, 我们就模糊了对象和函数的界限.
那么, 怎么判断一个变量是对象还是函数呢?其实更多的时候, 我们需要判断一个对象是否可以被调用, 能被调用的对象就是一个Callable对象, 比如函数和我们上面定义的带有__call__()
的类实例:
callable(Student())
# 输出: True
callable(max)
# 输出: True
callable([1, 2, 3])
# 输出: False
callable(None)
# 输出: False
callable('str')
# 输出: False
通过callable()函数, 我们就可以判断一个对象是否是”可调用”对象.
注意, 并不是所有对象都可以被调用, 比如int, str, bool, 虽然可以被调用, 但并不是一个”可调用”对象.
最后, 再看一个例子:
class Student(object):
def __init__(self, name):
self.name = name
def __call__(self, *args, **kwargs):
print('My name is %s. And I received %s and %s.' % (self.name, args, kwargs))
调用方式如下:
s = Student('Michael')
s(1, 2, 3, x=4, y=5)
# 输出: My name is Michael. And I received (1, 2, 3) and {'x': 4, 'y': 5}.
通过__call__()
方法, 我们就把一个类的实例变成了一个可调用对象, 这在某些情况下非常有用.
使用枚举类
枚举类(Enum class)是Python 3.4引入的新特性, 它提供了一种更高级的枚举类型, 可以更方便地使用常量. 枚举类可以自动赋予成员的属性值, 并提供一些方法来访问这些值.
当我们需要定义常量时, 一个办法是用大写变量通过整数来定义, 例如月份:
JAN = 1
FEB = 2
MAR = 3
...
NOV = 11
DEC = 12
好处是简单, 缺点是类型是int, 并且仍然是变量.
更好的方法是为这样的枚举类型定义一个class类型, 然后, 每个常量都是class的一个唯一实例. Python提供了Enum类来实现这个功能:
from enum import Enum
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
这样我们就获得了Month类型的枚举类, 可以直接使用Month.Jan来引用一个常量, 或者枚举它的所有成员:
for name, member in Month.__members__.items():
print(name, '=>', member, ',', member.value)
value属性则是自动赋给成员的int常量, 默认从1开始计数.
如果需要更精确地控制枚举类型, 可以从Enum派生出自定义类:
from enum import Enum, unique
@unique
class Weekday(Enum):
Sun = 0 # Sun的value被设定为0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6
@unique装饰器可以帮助我们检查保证没有重复值.
访问这些枚举类型可以有若干种方法:
day1 = Weekday.Mon
print(day1) # 输出: Weekday.Mon
print(Weekday.Tue) # 输出: Weekday.Tue
print(Weekday['Tue']) # 输出: Weekday.Tue
print(Weekday.Tue.value) # 输出: 2
print(day1 == Weekday.Mon) # 输出: True
print(day1 == Weekday.Tue) # 输出: False
print(Weekday(1)) # 输出: Weekday.Mon
print(day1 == Weekday(1)) # 输出: True
Weekday(7) # 无法运行, 输出: ValueError: 7 is not a valid Weekday
for name, member in Weekday.__members__.items():
print(name, '=>', member)
'''输出
Sun => Weekday.Sun
Mon => Weekday.Mon
Tue => Weekday.Tue
Wed => Weekday.Wed
Thu => Weekday.Thu
Fri => Weekday.Fri
Sat => Weekday.Sat
'''
可见, 既可以用成员名称引用枚举常量, 又可以直接根据value的值获得枚举常量.
元类
元类(metaclass)是创建类的方式, 它可以控制类的创建过程, 并最终创建出类对象.
在Python中, 所有类都是对象, 类也是对象, 所以类可以被看做是对象, 也可以通过type()函数创建出类对象.
type()
动态语言和静态语言最大的不同, 就是函数和类的定义, 不是编译时定义的, 而是运行时动态创建的.
比方说我们要定义一个Hello的class, 就写一个hello.py模块:
class Hello(object):
def hello(self, name='world'):
print('Hello, %s.' % name)
当Python解释器载入hello模块时, 就会依次执行该模块的所有语句, 执行结果就是动态创建出一个Hello的class对象, 测试如下:
>>> import hello
>>> h = hello.Hello()
>>> h.hello()
Hello, world.
>>>print(type(Hello))
<class 'type'>
>>>print(type(h))
<class 'hello.Hello'>
type()函数可以查看一个类型或变量的类型, Hello是一个class, 它的类型就是type, 而h是一个实例, 它的类型就是class Hello.
我们说class的定义是运行时动态创建的, 而创建class的方法就是使用type()
函数.
type()函数既可以返回一个对象的类型, 又可以创建出新的类型, 比如, 我们可以通过type()
函数创建出Hello类, 而无需通过class Hello(object)…的定义:
def fn(self, name='world'): # 先定义函数
print('Hello, %s.' % name)
Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class
h = Hello()
h.hello() # 输出: Hello, world.
print(type(Hello)) # 输出: <class 'type'>
<class 'type'>
print(type(h)) # 输出: <class '__main__.Hello'>
要创建一个class对象,type()
函数依次传入3个参数:
- class的名称;
- 继承的父类集合, 注意Python支持多重继承, 如果只有一个父类, 别忘了tuple的单元素写法;
- class的方法名称与函数绑定, 这里我们把函数fn绑定到方法名hello上.
通过type()
函数创建的类和直接写class是完全一样的, 因为Python解释器遇到class定义时, 仅仅是扫描一下class定义的语法, 然后调用type()
函数创建出class.
正常情况下, 我们都用class Xxx…来定义类, 但是type()
函数也允许我们动态创建出类来, 也就是说动态语言本身支持运行期动态创建类, 这和静态语言有非常大的不同, 要在静态语言运行期创建类, 必须构造源代码字符串再调用编译器, 或者借助一些工具生成字节码实现, 本质上都是动态编译, 会非常复杂.
metaclass
除了使用type()动态创建类以外, 要控制类的创建行为, 还可以使用metaclass.
metaclass, 直译为元类, 简单的解释就是:
当我们定义了类以后, 就可以根据这个类创建出实例, 所以: 先定义类, 然后创建实例.
但是如果我们想创建出类呢? 那就必须根据metaclass创建出类, 所以: 先定义metaclass, 然后创建类.
连接起来就是: 先定义metaclass, 就可以创建类, 最后创建实例.
所以metaclass允许你创建类或者修改类. 换句话说你可以把类看成是metaclass创建出来的”实例”.
metaclass是Python面向对象里最难理解, 也是最难使用的魔术代码. 正常情况下, 你不会碰到需要使用metaclass的情况.
这里我们以最简单的元类为例, 先看看如何定义一个最简单的元类:
class ListMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(cls, name, bases, attrs)
ListMetaclass是一个metaclass, 它继承自type, 所以我们可以把它看成type的一个子类.
ListMetaclass实现了自己的__new__()
方法, 这个方法在创建类时被调用. 我们可以看到, 我们在这里添加了一个add()
方法, 这个方法可以给类的实例动态添加属性.
我们定义了一个ListMetaclass, 然后我们就可以用它来定义类:
class MyList(list, metaclass=ListMetaclass):
pass
这里我们定义了一个MyList类, 它继承自list, 并且指定了metaclass为ListMetaclass.
当我们创建MyList实例时, 就会自动调用ListMetaclass.new()方法, 并传入参数:
- 当前准备创建的类的名称;
- 父类集合;
- 类的方法名称与函数绑定。
ListMetaclass的__new__()
方法在创建类时, 动态添加了一个add()
方法, 这个方法可以给类的实例动态添加属性.
测试如下:
>>> m = MyList()
>>> m.add(1)
>>> m.add(2)
>>> m.add(3)
>>> print(m)
[1, 2, 3]
可以看到, 我们成功地给MyList实例添加了3个元素, 并且打印出了元素列表.
总结
- 动态语言和静态语言最大的不同, 就是函数和类的定义, 不是编译时定义的, 而是运行时动态创建的.
- 要创建一个类, 首先必须定义一个metaclass, 然后调用
metaclass.__new__()
方法创建出类. - metaclass可以控制类的创建行为, 包括创建出实例, 也可以创建出类.
当然了一般也不会用到metaclass, 除非你对类的创建过程非常了解.