python笔记-面向对象编程

面向对象

面向对象这一个概念来源于自然界

自然界编程
personperson 类
身高/年龄/体重类的属性
某一个人(Peter)类的实例
人可以做什么类的方法

面向对象的程序设计把计算机程序视为一组对象的集合,每个对象都可以接受其他对象发过来的消息,并处理这些消息.

计算机程序的执行就是一系列消息在各个对象之间传递.

在python中,所有数据类型都是对象.

自定义对象数据类型就是面向对象中的类(Class).

面向对象与面向过程的区别

比如要展示一个学生的成绩:

面向过程

1
2
3
4
5
6
# 学生的成绩数据可以用dict表示
std1 = { 'name': 'Michael', 'score': 98 }
std2 = { 'name': 'Bob', 'score': 81 }
# 处理学生成绩则通过函数实现
def print_score(std):
print(f"{std["name"]}: {std["score"]}")

面向对象

我们首先思考的不是程序执行的流程,而是Student这种数据类型应该被视作一个对象,这个对象拥有namescore这两个属性(Property).

如果要打印一个学生的成绩,我们首先要创建出这个学生对应的对象,然后给对象发一个print_score的消息,让对象自己把成绩打印出来.

1
2
3
4
5
6
7
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score

def print_score(self):
print(f"{self.name}: {self.score}")

上面说的给对象发消息,就是调用对象对应的关联函数(方法Method)

1
2
3
4
bart = Student('Bart Simpson', 59)
lisa = Student('Lisa Simpson', 87)
bart.print_score()
lisa.print_score()

面向对象的抽象程度比函数高,一个Class既包含数据也包含操作数据的方法.

类和实例

是抽象的模板,实例是具体的对象.

定义类

通过关键字class定义一个类

1
2
class Student(object):
pass

class 后面跟类名,类名通常使用驼峰命名.
(object)表示父类(从哪个类继承下来),如果没有合适的继承类就使用object类,这是所有类最终都会继承的类.

(object)表示继承根类,如果是继承根类,可以直接忽略不写.

创建实例

类名+()创建实例

1
2
3
4
5
6
7
bart = Student()
bart
# 变量bart指向一个Student实例对象,后面的0x10a67a590是内存地址,每个object地址都不一样
> <__main__.Student object at 0x10a67a590>
Student
# Student本身则是一个类
> <class '__main__.Student'>

给实例绑定属性

1
bart.name = 'Bart Simpson'

初始化属性

由于类起到模板的作用,在创建类时,我们可以把一些我们认为必须绑定的属性强制填写进去,通过__init__方法.

这个特殊方法有时也叫初始化属性.

1
2
3
4
5
6
7
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score

def print_score(self):
print(f"{self.name}: {self.score}")

init()

__init__() 方法是Python中类的特殊方法之一,用于在创建类的实例时进行初始化操作。它是一个构造方法,负责在对象被创建时设置对象的初始状态。当你创建一个类的实例时,Python会自动调用该类的 __init__() 方法,如果该方法在类中被定义的话。这允许你在对象创建时执行任何必要的初始化工作。__init__()方法在实例创建时自动调用,用于执行初始化任务.

1
2
3
4
5
6
7
8
9
class MyClass:
def __init__(self, param1, param2):
self.param1 = param1
self.param2 = param2
print("Object initialized with parameters:", param1, param2)

# __init__()在创建实例时就会执行
>>> my_object = MyClass("value1", "value2")
Object initialized with parameters: value1 value2

__init__方法第一个参数永远是self,表示创建的实例本身.这样在__init__方法内部就可以把各种属性绑定到self,self指向创建的实例本身.

有了__init__,在创建实例时,就不能传空参数,必须传入跟__init__匹配的参数,self不需要传,python解释器会自己把实例变量传进去.

1
2
3
4
5
>>> bart = Student('Bart Simpson', 59)
>>> bart.name
'Bart Simpson'
>>> bart.score
59

在类中定义的函数(方法),第一个参数永远是self,调用时不用传递self,除此以外,和普通函数没有区别.

数据封装

比如上面的Student类,每个实例都拥有name,score这些数据.

Student实例本身就拥有这些数据,要访问这些数据,没有必要通过外部函数读取,可以直接在Student类的内部定义访问数据的函数.

这个就叫数据封装,数据和逻辑被“封装”起来了,调用很容易,但却不用知道内部实现的细节.

封装数据的函数与类本身关联起来,称作类的方法.

实例绑定数据

Python运行实例绑定新的数据,即使类没有定义的数据.

1
2
3
4
5
6
bart = Student('Bart Simpson', 59)
bart.score
> 59
bart.age = 8
bart.age
> 8

访问限制

上面的例子中,外部的代码可以自由地修改一个实例的name,score属性:

1
2
3
4
5
6
bart = Student('Bart Simpson', 59)
bart.score
> 59
bart.score = 99
bart.score
> 99

如果想让内部属性不被外部访问,在属性名前加__,代表私有变量(private),外部就不能访问.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Student(object):
def __init__(self, name, score):
self.__name = name
self.__score = score

def print_score(self):
print('%s: %s' % (self.__name, self.__score))

>>> bart = Student('Bart Simpson', 59)
>>> bart.__score
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'

这样外部代码就不能随意修改对象内部的状态,通过访问限制的保护,代码更健壮.

如果外部代码要获取内部属性,可以给student类增加get_nameget_score等方法.

1
2
3
4
5
6
7
8
class Student(object):
...

def get_name(self):
return self.__name

def get_score(self):
return self.__score

如果又要允许外部代码修改内部属性呢?Best practise是额外写一个方法去修改,因为在方法中,我们可以对参数做检查,避免传入无效参数,代码更健壮.

1
2
3
4
5
6
7
8
class Student(object):
...

def set_score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError('bad score')

小结

  • __xxx__双下划线开头和结尾的变量是特殊变量,特殊变量可以直接访问,不是private.

  • _xxx单下划线开头的变量意思是虽然我可以被外部访问,但是,请把我视为私有变量,不要随意访问.

  • __xxx双下划线开头的变量是私有变量,但是它实际上是通过python解释器,把__xxx改成了__Student__xxx,实际上还是可以访问的.所以Python并没有强制的访问控制手段,一切全靠自觉.

注意:

1
2
3
4
5
6
7
8
>>> bart = Student('Bart Simpson', 59)
>>> bart.get_name()
'Bart Simpson'
>>> bart.__name = 'New Name' # 设置__name变量!
>>> bart.__name
'New Name'
>>> bart.get_name()
'Bart Simpson'

表面上好像是访问了__name并赋了新值,但实际上是创建了一个__name的新属性,因为原来的__name属性已经被python解释器改为了__Student__name.

继承

1
2
3
4
5
6
7
8
9
class Animal(object):
def run(self):
print('Animal is running...')

class Dog(Animal):
pass

class Cat(Animal):
pass

DogCat继承自Animal,所以Animal是Dog和Cat的父类,后者则是前者的子类.

  • 子类将获得父类的所有方法
  • 子类可以自己增加或者重写方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
dog = Dog()
dog.run()
> Animal is running...

class Dog(Animal):

def run(self):
print('Dog is running...')

def eat(self):
print('Eating meat...')

dog.run()
> Dog is running...

当子类和父类都有同样的方法时,我们说子类覆盖父类的方法,也叫重写.

继承可以一级一级地继承下去,object是所有类的根类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
                ┌───────────────┐
│ object │
└───────────────┘

┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Animal │ │ Plant │
└─────────────┘ └─────────────┘
│ │
┌─────┴──────┐ ┌─────┴──────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Dog │ │ Cat │ │ Tree │ │ Flower │
└─────────┘ └─────────┘ └─────────┘ └─────────┘

多态

首先我们需要理解:定义一个class,实际上是定义了一个新的数据类型,它跟python自身的list, str, dict 等数据类型一样.

1
2
3
4
5
6
7
8
9
10
a = list()
b = Animal()
c = Dog()
# 判断一个变量是否为某个类型用isinstance()
>>> isinstance(a, list)
True
>>> isinstance(b, Animal)
True
>>> isinstance(c, Dog)
True

然后你会发现,c也是Animal类型

1
2
>>> isinstance(c, Animal)
True

所以,继承关系中,如果一个实例的数据类型是某个子类,那么它也同时属于父类.当然,反过来是不行的.

一个实例同时属于多个数据类,这就叫多态.

举个例子说明多态的好处:

定义一个run_twice函数,接受Animal类型的参数

1
2
3
def run_twice(Animal):
Animal.run()
Animal.run()

此时,因为Dog(),Cat()也是属于Animal,所以可以直接传入:

1
2
3
>>> run_twice(Dog())
Dog is running...
Dog is running...

也可以再定义一个Tortoise类,同样继承于Animal

1
2
3
class Tortoise(Animal):
def run(self):
print('Tortoise is running slowly...')

同样可以直接传入并调用

1
2
3
>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...

多态的好处就是每新增一个子类,任何依赖其父类作为参数的函数或方法都可以不加修改地正常运行.

以上面的例子来说,不管我们传入的是Dog,Cat,Tortoise还是别的任意Animal子类,run_twice都能不加修改地正常运行.

对于一个变量,我们只需知道它是Animal类型,无需确切知道它的子类,就能放心调用run()方法.

调用方只管调用,不管细节,这就是著名的开闭原则:

  • 对扩展开放: 允许新增Animal子类
  • 对修改封闭: 不需要修改依赖Animal类型的run_twice函数

鸭子类型

有别于静态语言(Java),对于动态语言(python)来说,它其实并不要求严格的继承关系, 一个对象只要开起来像鸭子,走起路来像鸭子,就可以被看作鸭子.

这就是动态语言的鸭子类型:

如果需要传入Animal类型.传入的对象并不一定要Animal或它的子类,只要传入的对象又run()方法就行.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def make_it_walk(duck):
duck.walk() # 只要传入的对象有walk方法,就能执行,不需要管到底传入哪个class

class Duck:
def walk(self):
print("This duck is walking")

class Person:
def walk(self):
print("This person is walking")

duck = Duck()
person = Person()

make_it_walk(duck) # 输出: This duck is walking
make_it_walk(person) # 输出: This person is walking

获取对象信息

当我们拿到一个对象的引用时,如何知道这个对象是什么类型?有那些方法呢?

type()

用于判断对象类型

1
2
3
4
5
6
7
8
9
10
11
12
# 基本类型
>>> type(123)
<class 'int'>
>>> type('str')
<class 'str'>
>>> type(None)
<type(None) 'NoneType'>
# 函数或类
>>> type(abs)
<class 'builtin_function_or_method'>
>>> type(a)
<class '__main__.Animal'>

type()函数返回对应的Class类型,可以通过if语句比较

1
2
3
4
5
6
7
8
9
10
>>> type(123)==type(456)
True
>>> type(123)==int
True
>>> type('abc')==type('123')
True
>>> type('abc')==str
True
>>> type('abc')==type(123)
False

判断是否为函数,可以使用types模块定义的常量

1
2
3
4
5
6
7
8
9
10
11
12
import types
def fn():
pass

type(fn) == types.FunctionType
True
type(abs) == types.BuiltinFunctionType
True
type(lambda x: x) == types.LambdaType
True
type((x for x in range(10))) == types.GeneratorType
True

isinstance()

判断class的类型,和继承关系可以使用isinstance()

还是上面的例子,如果继承关系是:

1
object -> Animal -> Dog -> Husky
1
2
3
4
5
6
7
8
9
10
11
12
# 首先创建实例
a = Animal()
d = Dog()
h = Husky()

# 判断
isinstance(h, Husky)
True
isinstance(h, Dog)
True
isinstance(h, Animal)
True

能用type判断的基本类型也可以用isinstance判断

1
2
isinstance('a', str)
True

还可以判断是否为某些类型中的一种

1
2
isinstance([1, 2, 3], (list, tuple))
True

优先使用isinstance判断类型,可以将指定类型及其子类一网打尽

dir()

获取一个对象的所有属性方法, 返回一个包含字符串的list.

1
2
dir('ABC')
['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']

类似__xxx__的属性和方法再python中都是有特殊用途的,比如__len__方法返回长度.

如果你调用len()函数获取一个对象的函数,实际上,在len()函数内部是去调用该对象的__len__()方法

1
2
3
# 如下两段代码等价
len('ABC')
'ABC'.__len__()

如果是我们自己写的类,如果也想调用len(myObj),就要自己写一个__len__()方法

1
2
3
4
5
6
7
class MyDog(object):
def __len__(self):
return 100

>>> d = MyDog()
>>> len(d)
100

其余的都是普通的属性或方法.

操作对象属性

getattr()获取对象属性

setattr()设置对象属性

hasattr()判断对象属性是否存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class MyObject(object):
def __init__(self):
self.x = 9
def power(self):
return self.x * self.x

obj = MyObject()

>>> obj.x
9
>>> hasattr(obj, 'y')
False
>>> setattr(obj, 'y', 19)
>>> hasattr(obj, 'y')
True
>>> getattr(obj, 'y')
19

# 获取不存在的属性会报错
>>> getattr(obj, 'z')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'z'
# 也可以设定不存在时候返回的值
>>> getattr(obj, 'z', 404)
404

也可以获取对象的方法,在python中,对象的方法也同样属于对象的属性

1
2
3
4
5
6
7
>>> hasattr(obj, 'power')
True
>>> getattr(obj, 'power')
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn = getattr(obj, 'power')
>>> fn() # 等价于obj.power()
81

小结

一般来说,如果我们只有在不知道对象信息的时候,才会去获取, 如果可以直接写obj.x就不要写getattr(obj, 's').正确的用法例子如下:

1
2
3
4
5
6
7
"""
假设我们希望从文件流fp中读取图像,我们首先要判断该fp对象是否存在read方法,如果存在,则该对象是一个流,如果不存在,则无法读取
"""
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None

python是动态语言,根据鸭子类型,有read()方法,也不代表它就是一个文件流,但只要read()方法返回的是有效的图像数据,就不影响读取图像的功能.

实例属性和类属性

python是动态语言,根据类创建的实例可以任意绑定属性.

1
2
3
4
5
6
class Student(object):
def __init__(self, name):
self.name = name # 通过self变量绑定

s = Student('Bob')
s.score = 90 # 通过实例变量绑定

上面通过两种方式定义的是实例属性

下面定义的是类属性

1
2
class Student(object):
name = 'Student'

两个属性的调用有所不同

1
2
3
4
5
6
7
8
9
10
11
12
13
class Student(object):
name = 'Student'

>>> s = Student()
>>> print(s.name) # 打印实例属性, 因为实例没有name属性,所以会打印类属性
Student
>>> print(Student.name) # 打印类属性
Student
>>> s.name = 'Peter' # 绑定实例属性name
>>> print(s.name) # 打印实例属性
Peter
>>> print(Student.name) # 打印类属性
Student

实例属性优先级比类属性高,如果定义了相同名字的实例属性于类属性,将会优先打印实例属性.

所以千万不要用相同的名字.


python笔记-面向对象编程
http://example.com/2024/01/16/python-oop/
作者
Peter Pan
发布于
2024年1月16日
许可协议