深入理解Python类和对象

Python的鸭子类型和多态

这个概念的名字来源于由James Whitcomb Riley提出的鸭子测试,“鸭子测试”可以这样表述:“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”

在鸭子类型中,关注的不是对象的类型本身,而是它是如何使用的。例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为”鸭子”的对象,并调用它的”走”和”叫”方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的”走”和”叫”方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。任何拥有这样的正确的”走”和”叫”方法的对象都可被函数接受的这种行为引出了以上表述,这种决定类型的方式因此得名。

调用不同的子类将会产生不同的行为,而无须明确知道这个子类实际上是什么,这是多态的重要应用场景。而在python中,因为鸭子类型(duck typing)使得其多态不是那么酷。

鸭子类型是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由”当前方法和属性的集合”决定。

所有的类都实现了一种方法,则这些类可以归为一种类。

抽象基类(abc模块)

首先明确一点,抽象基类是无法实例化的。Python类的特性是根据实现的魔法函数决定的,而不是继承什么类,注意和静态语言的区分。

静态语言和动态语言的区别

静态类型语言在编译时便已确定变量的类型,而动态类型语言的变量类型要到程序运行的时候,待变量被赋予某个值之后,才会具有某种类型。

静态类型语言的优点首先是在编译时就能发现类型不匹配的错误,编辑器可以帮助我们提前避免程序在运行期间有可能发生的一些错误。其次,如果在程序中明确地规定了数据类型,编译器还可以针对这些信息对程序进行一些优化工作,提高程序执行速度。

静态类型语言的缺点首先是迫使程序员依照强契约来编写程序,为每个变量规定数据类型,归根结底只是辅助我们编写可靠性高程序的一种手段,而不是编写程序的目的,毕竟大部分人编写程序的目的是为了完成需求交付生产。其次,类型的声明也会增加更多的代码,在程序编写过程中,这些细节会让程序员的精力从思考业务逻辑上分散开来。

动态类型语言的优点是编写的代码数量更少,看起来也更加简洁,程序员可以把精力更多地放在业务逻辑上面。虽然不区分类型在某些情况下会让程序变得难以理解,但整体而言,代码量越少,越专注于逻辑表达,对阅读程序是越有帮助的。
动态类型语言的缺点是无法保证变量的类型,从而在程序的运行期有可能发生跟类型相关的错误。

抽象基类定义(个人理解)

在基础的类当中我们设定好一些方法,所有的继承基类的类都要覆盖这些方法。

抽象基类的适用场景

场景一:判定某个类的类型

1
2
3
4
5
6
7
8
9
class Company(object):
def __init__(self, employee_list):
self.employee = employee_list

def __len__(self):
return len(self.employee)

com = Company(["bobby1","bobby2"])
print(hasattr(com, "__len__"))

如果我们不使用抽象基类,想判断类Company是否有长度属性__len__我们只能使用hasattr

我们可以使用抽象基类判定是否是有长度的类型。

1
2
from collections.abc import Sized
print(isinstance(com, Sized))

上面的这个print的输出是True,为什么呢?我们并没有继承Sized

我们可以看下Sized的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Sized(metaclass=ABCMeta):

__slots__ = ()

@abstractmethod
def __len__(self):
return 0

@classmethod
def __subclasshook__(cls, C):
if cls is Sized:
return _check_methods(C, "__len__")
return NotImplemented

我们看到在源码中实现了一个__subclasshook__用于检测是否包含__len__方法,如果包含则返回True。

这里我们可以进一步理解Python中的鸭子类型,只要实现了对象的方法,就是某种类型。

场景二:我们需要强制某个子类必须实现某些方法

实现了一个web框架,集成cache(redis, cache, memorychache)
需要设计一个抽象基类, 指定子类必须实现某些方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import abc
class CacheBase(metaclass=abc.ABCMeta):
@abc.abstractmethod
def get(self, key):
pass

@abc.abstractmethod
def set(self, key, value):
pass

class RedisCache(CacheBase):
def set(self, key, value):
pass

redis_cache = RedisCache()
redis_cache.set("key", "value")

我们创建了一个抽象基类CacheBase通过继承abc.ABCMeta然后在抽象基类中指定子类必须实现的两个方法,getset。当子类不实现这两个方法的时候回直接抛出异常。

isinstance和type的区别

我们首先定义两个类

1
2
3
4
5
6
7
class A:
pass

class B(A):
pass

b = B()

我们使用ininstance判断类的从属

1
2
3
4
print(isinstance(b, B))
print(isinstance(b, A))
True
True

然后我们使用type判断

1
2
3
4
print(type(b) is B)
print(type(b) is A)
True
False

第二个判断为False是因为type(b)的指向对象B,虽然B继承了A,但是A和B是两个不同的对象。

结论:我们尽量用ininstance判断类的类型。

类变量和实例变量

首先明确类变量和实例变量是两个独立的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A:
aa = 1
def __init__(self, x, y):
self.x = x
self.y = y

a = A(2,3)
# 修改类变量的值
A.aa = 11
# 动态赋值实例a属性100
a.aa = 100
print(a.x, a.y, a.aa)
print(A.aa)

b = A(3,5)
# 类变量是所有实例共享的
print(b.aa)

类属性和实例属性的查找顺序

优先明确什么是类属性:定义在类内部的变量或方法都是属性。

我们先看个简单的顺序查找

1
2
3
4
5
6
7
8
class A:
name = "A"

def __init__(self):
self.name = "obj"

a = A()
print(a.name)

当实例对象查找属性name的时候是由下往上查找的,首先查找实例内部是否存在,然后再查找类属性。

我们看下多继承情况下的MRO算法:

深度优先算法

上图是深度优先算法。

深度优先弊端

但是当时这种菱形继承的时候,就会出现问题。因为B和C都继承了D。如果C重写了D中的方法 是无法被覆盖的(执行到C)

广度优先解决深度优先菱形问题
广度优先解决深度优先菱形问题

广度优先弊端

我们看下上图的广度优先算法 也是有一定弊端的

但是当继承关系如上图所示s的时候,因为优先继承的是B,当在B查找不到,而D中存在对应属性的时候会被C覆盖掉。

Python3之后的查找算法更改为C3算法。

我们看下上图在Python3中的中的继承关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#新式类
class D:
pass

class E:
pass

class C(E):
pass

class B(D):
pass

class A(B, C):
pass

print(A.__mro__)
(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.D'>, <class '__main__.C'>, <class '__main__.E'>, <class 'object'>)

查找顺序是A-B-D-C-E。上述存在的问题就不会再存在了。

静态方法,实例方法,类方法

我们通过代码查看区别

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Date:
# 构造函数
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day

def tomorrow(self):
self.day += 1

@staticmethod
def parse_from_string(date_str):
year, month, day = tuple(date_str.split("-"))
# 硬编码 当改变Date类名的时候,这里就会报错
return Date(int(year), int(month), int(day))

@staticmethod
def valid_str(date_str):
year, month, day = tuple(date_str.split("-"))
if int(year) > 0 and (int(month) > 0 and int(month) <= 12) and (int(day) > 0 and int(day) <= 31):
return True
else:
return False

@classmethod
def from_string(cls, date_str):
year, month, day = tuple(date_str.split("-"))
# 这里使用软编码 cls 代表类
return cls(int(year), int(month), int(day))

def __str__(self):
return "{year}/{month}/{day}".format(year=self.year, month=self.month, day=self.day)


if __name__ == "__main__":
new_day = Date(2018, 6, 11)
new_day.tomorrow()
print(new_day)

date_str = "2018-6-11"
year, month, day = tuple(date_str.split("-"))
new_day = Date(int(year), int(month), int(day))
print(new_day)

# 用staticmethod完成初始化
new_day = Date.parse_from_string(date_str)
print(new_day)

# 用classmethod完成初始化
new_day = Date.from_string(date_str)
print(new_day)

print(Date.valid_str("2018-6-11"))

数据封装和私有属性

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
from datetime import date
class User:
def __init__(self, birthday):
self.__birthday = birthday

def get_age(self):
#返回年龄
return 2018 - self.__birthday.year

class Student(User):

def __init__(self, birthday):
self.__birthday = birthday


if __name__ == "__main__":
user = User(date(1990,2,1))
student = Student(date(1990,2,1))
print(user._User__birthday)
print(student._Student__birthday)
print(user.get_age())

1990-02-01
1990-02-01
28

Python的数据封装是指在属性前面增加__完成。

Python将私有属性伪装成_User__birthday (类名拼接)我们直接调用_User__birthday也是可以调用私有属性的。对于继承,同样适用。

Python对象的自省机制

自省就是通过一定的机制查询到对象的内部结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person:
"""

"""
name = "user"

class Student(Person):
def __init__(self, school_name):
self.school_name = school_name

if __name__ == "__main__":
user = Student("浙大")

# 通过__dict__查询属性
print(user.__dict__)
print(user.name)

# 输出
{'school_name': '浙大'}
user

这里__dict__获得的是对象的属性,而实例user的属性是{'school_name': '浙大'}并没有name,为何能打印输出呢?

这是因为name是类Person的属性,而Student继承它,根据属性查找顺序(根据继承关系,从下往上查找),我们就能获得name的值。

我们查看一下Person类的属性

1
2
3
print(Person.__dict__)
#输出
{'__module__': '__main__', '__doc__': '\n 人\n ', 'name': 'user', '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>}

我们看到里面的属性name

我们可以通过__dict__动态的给对象赋予属性。

1
2
3
4
5
6
user.__dict__["school_addr"] = "北京市"
print(user.__dict__)
print(user.school_addr)
# 输出
{'school_name': '浙大', 'school_addr': '北京市'}
北京市

我们也可以使用dir获得对象的属性有哪些,但是只有属性,没有属性值。

1
2
3
4
a = [1,2]
print(dir(a))
# 输出
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

Python中的super调用

我们看下代码

1
2
3
4
5
6
7
8
9
10
class A:
def __init__(self):
print("A")


class B(A):
def __init__(self):
print("B")
# 我们希望在实例化的内部调用父类的方法
super().__init__()

这里我们带着两个疑问看下面的代码

  1. 既然我们重写B的构造函数, 为什么还要去调用super?
  2. super到底执行顺序是什么样的?

问题一:当我们想要重用父类的一些属性的时候

下面我们自己定义一个线程类,通过继承

1
2
3
4
5
6
from threading import Thread

class MyThread(Thread):
def __init__(self, name, user):
self.user = user
super().__init__(name=name)

这时我们可以使用super去调用父类的初始化函数,是因为在Thread类的初始化方法里已经定义了name

1
2
3
# Thread的初始化方法
def __init__(self, group=None, target=None, name=None,
args=(), kwargs=None, *, daemon=None):

问题二:super并不是简单地调用父类

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
27
28
29
30
31
32
33
class A:
def __init__(self):
print("A")


class B(A):
def __init__(self):
print("B")
super().__init__()


class C(A):
def __init__(self):
print("C")
super().__init__()


class D(B, C):
def __init__(self):
print("D")
super().__init__()


if __name__ == "__main__":
d = D()
print(D.__mro__)

# 输出
D
B
C
A
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

从上面的输出我们可以看出super的调用顺序是指MRO算法的下一个类的构造函数。

django rest framework中对多继承使用的经验

在Python中并不支持多继承。推荐使用mixin混合模式。

mixin模式特点

  1. Mixin类功能单一
  2. 不和基类关联,可以和任意基类组合, 基类可以不和mixin关联就能初始化成功
  3. 在mixin中不要使用super这种用法

Python中的with语句

我们看看下面的代码输出

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
# try except finally
def exe_try():
try:
print("code started")
raise KeyError
return 1
except KeyError as e:
print("key error")
return 2
else:
print("other error")
return 3
finally:
print("finally")
return 4


if __name__ == "__main__":
result = exe_try()
print(result)

# 输出
code started
key error
finally
4

为什么return的是4而不是2呢?

这是因为在 try except finally语句中,存在return的语句会先将值压到栈中,最后只取栈顶数据。2在4之前压入栈,因此返回的数据是4。

下面我们看下上下文管理器(with语句),可以大大简化 try except finally 语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 上下文管理器协议
class Sample:
def __enter__(self):
print("enter")
# 获取资源
return self

def __exit__(self, exc_type, exc_val, exc_tb):
# 释放资源
print("exit")

def do_something(self):
print("doing something")

# 直接使用with语句
with Sample() as sample:
sample.do_something()
# 输出
enter
doing something
exit

我们定义了一个类,实现了两个魔法函数__enter__, __exit__,就实现了一个上下文管理器。

使用contextlib简化上下文管理器

我们先看下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import contextlib


@contextlib.contextmanager
def file_open(file_name):
# 首先执行yield 之前的
print("file open")
yield {}
# 最后执行 yield 之后的
print("file end")


with file_open("bobby.txt") as f_opened:
print("file processing")

# 输出
file open
file processing
file end

装饰器contextlib.contextmanager修饰的是一个生成器函数。然后将函数变成上下问管理器(利用了生成器的特性)。

知识就是财富
如果您觉得文章对您有帮助, 欢迎请我喝杯水!