tongsiying

阅读|运动|自律

0%

第23篇:面向对象编程进阶

学习目标

掌握Python面向对象进阶知识及应用

一、类的成员

类的成员分为三大类:字段、属性和方法。

类的成员

所有成员中,只有实例字段的内容保存在对象中,即:根据此类创建了多少对象,在内存中就有多少个实例字段。而其他的成员,则都是保存在类中,即:无论对象的多少,在内存中只创建一份。

1. 字段(attribute)

类中有两种类型的变量,类变量(class variable)实例变量(instance variable)。由于类中声明的变量称为字段,故也称类变量也称类字段(普通字段),实例变量为实例字段或成员字段。类字段和实例字段在定义和使用中有所区别,而最本质的区别是内存中保存的位置不同:类字段属于,实例字段属于对象

看一下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Province:
# 类字段
country = '中国'
def __init__(self, name):
# 实例字段
self.name = name

# 直接访问实例字段
obj = Province('山西省')
print(obj.name) # 山西省
print(obj.country) # 中国
# 直接访问类字段
print(Province.country) # 中国
obj2 = Province('河南省')
print(obj2.name) # 河南省
print(obj2.country) # 中国

由上述代码可以看出【实例字段需要通过对象来访问】【类字段通过类访问】。在看一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Rectangle():
# 类字段
recs = []
nums = 0

def __init__(self, w, h):
self.width = w
self.height = h
# 每次实例对象初始化改变类字段
self.recs.append((self.width, self.len))
self.nums += 1

r1 = Rectangle(10, 24)
r2 = Rectangle(20, 40)
r3 = Rectangle(100, 200)

print(Rectangle.recs) # [(10, 24), (20, 40), (100, 200)]
print(Rectangle.nums) # 3

本例中,我们子在类Rectangle中添加了一个叫recs的类变量,它是在__init__方法之外定义的。因为Python只有在创建对象时才调用__init__方法,而我们希望能够使用类对像(不会调用__init__方法)访问类变量。

接下来,我们创建了3个Rectangle对象。每创建一个Rectangle对象,__init__方法中的代码就会向recs列表中添加一个由新对象宽度和长度组成的元组。这样,每当新创一个Rectangle对象时,就会被自动添加到recs列表。通过使用类变量,即可在不使用全局变量的情况下,做到了在类创建的不同实例之间共享数据

在使用上可以看出实例字段和类字段的归属是不同的。其在内容的存储方式类似如下图:

类字段和实例字段的存储

由上图可知:类字段在内存中只保存一份实例字段在每个对象中都要保存一份

应用场景: 通过类创建对象时,如果每个对象都具有相同的字段,那么就使用类字段。

2. 方法

方法包括:实例方法、静态方法和类方法,三种方法在内存中都归属于类,区别在于调用方式不同。

  • 实例方法:由对象调用;至少一个self参数;执行实例方法时,自动将调用该方法的对象赋值给self
  • 类方法:由调用; 至少一个cls参数;执行类方法时,自动将调用该方法的复制给cls
  • 静态方法:由调用;无默认参数。

下面来看一个示例

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 Foo:
def __init__(self, name):
self.name = name

def ord_func(self):
""" 定义实例方法,至少有一个self参数 """
# print(self.name)
print('实例方法')

@classmethod
def class_func(cls):
""" 定义类方法,至少有一个cls参数 """
print('类方法')

@staticmethod
def static_func():
""" 定义静态方法 ,无默认参数"""
print('静态方法')

# 调用实例方法
f = Foo()
f.ord_func()
# 调用类方法
Foo.class_func()
# 调用静态方法
Foo.static_func()

相同点:对于所有的方法而言,均属于类(非对象)中,所以,在内存中也只保存一份。

不同点:方法调用者不同、调用方法时自动传入的参数不同。

定义一个三角形类,通过传入三条边的长度来构造三角形,并提供计算周长和面积的方法。计算周长和面积肯定是三角形对象的方法,这一点毫无疑问。但是在创建三角形对象时,传入的三条边长未必能构造出三角形,为此我们可以先写一个方法来验证给定的三条边长是否可以构成三角形,这种方法很显然就不是实例方法,因为在调用这个方法时三角形对象还没有创建出来。我们可以把这类方法设计为静态方法或类方法,也就是说这类方法不是发送给三角形对象的消息,而是发送给三角形类的消息,代码如下所示。

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
class Triangle(object):
"""三角形类"""

def __init__(self, a, b, c):
"""初始化方法"""
self.a = a
self.b = b
self.c = c

@staticmethod
def is_valid(a, b, c):
"""判断三条边长能否构成三角形(静态方法)"""
return a + b > c and b + c > a and a + c > b

# @classmethod
# def is_valid(cls, a, b, c):
# """判断三条边长能否构成三角形(类方法)"""
# return a + b > c and b + c > a and a + c > b

def perimeter(self):
"""计算周长"""
return self.a + self.b + self.c

def area(self):
"""计算面积"""
p = self.perimeter() / 2
return (p * (p - self.a) * (p - self.b) * (p - self.c)) ** 0.5

上面的代码使用staticmethod装饰器声明了is_valid方法是Triangle类的静态方法,如果要声明类方法,可以使用classmethod装饰器。可以直接使用类名.方法名的方式来调用静态方法和类方法,二者的区别在于,类方法的第一个参数是类对象本身,而静态方法则没有这个参数。简单的总结一下,实例方法、类方法、静态方法都可以通过类名.方法名的方式来调用,区别在于方法的第一个参数到底是普通对象还是类对象,还是没有接受消息的对象。静态方法通常也可以直接写成一个独立的函数,因为它并没有跟特定的对象绑定。

3. 属性(property)

如果你已经了解Python类中的方法,那么属性就非常简单了,因为Python中的属性其实是实例方法的变种。

3.1 属性的基本使用

使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ############### 定义 ###############
class Foo:
def func(self):
pass

# 定义属性
@property
def prop(self):
pass

# ############### 调用 ###############
foo_obj = Foo()
foo_obj.func()
foo_obj.prop #调用属性

由属性的定义和调用要注意一下几点:

  • 定义时,在实例方法的基础上添加 @property 装饰器;
  • 定义时,属性仅有一个self参数
  • 调用时,无需括号
    方法:`foo_obj.func()`
    属性:`foo_obj.prop`

注意:属性存在意义是访问属性时可以制造出和访问字段完全相同的假象属性由方法变种而来,如果Python中没有属性,方法完全可以代替其功能。

实例:对于主机列表页面,每次请求不可能把数据库中的所有内容都显示到页面上,而是通过分页的功能局部显示,所以在向数据库中请求数据时就要显示的指定获取从第m条到第n条的所有数据(即:limit m,n),这个分页的功能包括:根据用户请求的当前页和总数据条数计算出 m 和 n,根据m 和 n 去数据库中请求数据 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ############### 定义 ###############
class Pager:
def __init__(self, current_page):
# 用户当前请求的页码(第一页、第二页...)
self.current_page = current_page
# 每页默认显示10条数据
self.per_items = 10

@property
def start(self):
val = (self.current_page - 1) * self.per_items
return val

@property
def end(self):
val = self.current_page * self.per_items
return val

# ############### 调用 ###############
p = Pager(1)
print(p.start) 就是起始值,即:m
print(p.end) 就是结束值,即:n

从上述可见,Python的属性的功能是:属性内部进行一系列的逻辑计算,最终将计算结果返回。

3.2 属性的两种定义方式

属性的定义有两种方式:

  • 装饰器 即:在方法上应用装饰器
  • 类字段 即:在类中定义值为property对象的类字段

装饰器方式:在类的实例方法上应用@property装饰器

Python中的类有经典类和新式类,新式类的属性比经典类的属性丰富。( 如果类继object,那么该类是新式类,Python3默认都是新式类 )。新式类,具有三种@property装饰器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ############### 定义 ###############
class Goods(object):
@property
def price(self):
print('@property')

@price.setter
def price(self, value):
print('@price.setter')

@price.deleter
def price(self):
print('@price.deleter')

# ############### 调用 ###############
obj = Goods()
print(obj.price) # 自动执行 @property 修饰的 price 方法,并获取方法的返回值
obj.price = 123 # 自动执行 @price.setter 修饰的 price 方法,并将 123 赋值给方法的参数
del obj.price # 自动执行 @price.deleter 修饰的 price 方法

由于新式类中具有三种访问方式,我们可以根据他们几个属性的访问特点,分别将三个方法定义为对同一个属性:获取、修改、删除。

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
class Goods(object):
def __init__(self):
# 原价
self.original_price = 100
# 折扣
self.discount = 0.8

@property
def price(self):
# 实际价格 = 原价 * 折扣
new_price = self.original_price * self.discount
return new_price

@price.setter
def price(self, value):
self.original_price = value

@price.deltter
def price(self, value):
del self.original_price

obj = Goods()
print(obj.price) # 获取商品价格
obj.price = 200 # 修改商品原价
del obj.price # 删除商品原价

类字段方式,创建值为property对象的类字段

当使用类字段的方式创建属性时,经典类和新式类无区别

使用示例:

1
2
3
4
5
6
7
8
class Foo:
def get_bar(self):
return 'zhangyafei'
BAR = property(get_bar)

obj = Foo()
reuslt = obj.BAR # 自动调用get_bar方法,并获取方法的返回值
print(reuslt)

property的构造方法中有个四个参数

  • 第一个参数是方法名,调用 对象.属性 时自动触发执行方法
  • 第二个参数是方法名,调用 对象.属性 = XXX 时自动触发执行方法
  • 第三个参数是方法名,调用 del 对象.属性 时自动触发执行方法
  • 第四个参数是字符串,调用 对象.属性.__doc__ ,此参数是该属性的描述信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Foo
def get_bar(self):
return 'zhangyafei'

# *必须两个参数
def set_bar(self, value):
return 'set value' + value

def del_bar(self):
print('del zhangyafei')

BAR = property(get_bar, set_bar, del_bar, 'description...')

obj = Foo()

print(obj.BAR) # 自动调用第一个参数中定义的方法:get_bar
obj.BAR = "wangxiaoer" # 自动调用第二个参数中定义的方法:set_bar方法,并将“wangxiaoer”当作参数传入
del Foo.BAR # 自动调用第三个参数中定义的方法:del_bar方法
obj.BAE.__doc__ # 自动获取第四个参数中设置的值:description...

由于类字段方式创建属性具有三种访问方式,我们可以根据他们几个属性的访问特点,分别将三个方法定义为对同一个属性:获取、修改、删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Goods(object):
def __init__(self):
# 原价
self.original_price = 100
# 折扣
self.discount = 0.8

def get_price(self):
# 实际价格 = 原价 * 折扣
new_price = self.original_price * self.discount
return new_price

def set_price(self, value):
self.original_price = value

def del_price(self, value):
del self.original_price

PRICE = property(get_price, set_price, del_price, '价格属性描述...')

obj = Goods()
print(obj.PRICE) # 获取商品价格
obj.PRICE = 200 # 修改商品原价
del obj.PRICE # 删除商品原价

注意:Python WEB框架 Django 的视图中 request.POST就是使用的类字段的方式创建的属性。

  • Django源码
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
54
55
56
57
58
59
60
class WSGIRequest(http.HttpRequest):
def __init__(self, environ):
script_name = get_script_name(environ)
path_info = get_path_info(environ)
if not path_info:
# Sometimes PATH_INFO exists, but is empty (e.g. accessing
# the SCRIPT_NAME URL without a trailing slash). We really need to
# operate as if they'd requested '/'. Not amazingly nice to force
# the path like this, but should be harmless.
path_info = '/'
self.environ = environ
self.path_info = path_info
self.path = '%s/%s' % (script_name.rstrip('/'), path_info.lstrip('/'))
self.META = environ
self.META['PATH_INFO'] = path_info
self.META['SCRIPT_NAME'] = script_name
self.method = environ['REQUEST_METHOD'].upper()
_, content_params = cgi.parse_header(environ.get('CONTENT_TYPE', ''))
if 'charset' in content_params:
try:
codecs.lookup(content_params['charset'])
except LookupError:
pass
else:
self.encoding = content_params['charset']
self._post_parse_error = False
try:
content_length = int(environ.get('CONTENT_LENGTH'))
except (ValueError, TypeError):
content_length = 0
self._stream = LimitedStream(self.environ['wsgi.input'], content_length)
self._read_started = False
self.resolver_match = None
...

# ############### 看这里看这里 ###############
def _get_post(self):
if not hasattr(self, '_post'):
self._load_post_and_files()
return self._post

# ############### 看这里看这里 ###############
def _set_post(self, post):
self._post = post

@cached_property
def COOKIES(self):
raw_cookie = get_str_from_wsgi(self.environ, 'HTTP_COOKIE', '')
return http.parse_cookie(raw_cookie)

def _get_files(self):
if not hasattr(self, '_files'):
self._load_post_and_files()
return self._files

# ############### 看这里看这里 ###############
POST = property(_get_post, _set_post)

FILES = property(_get_files)
REQUEST = property(_get_request)

所以,定义属性共有两种方式,分别是【装饰器】和【类字段】,而【装饰器】方式针对经典类和新式类又有所不同。

3.3 字段和属性的有什么区别?

看一个例子:

1
2
3
4
5
6
7
8
9
10
class Circle(object):
def __init__(self, radius):
self.radius = radius

@property
def diameter(self):
return self.radius * 2
@diameter.setter
def diameter(self, new_diameter):
self.radius = new_diameter / 2

例子中,我定义一个圆圈类,圆圈(circle)嘛,肯定有直径(diameter)有半径(radius)的,我可以简单的设置他们为attribute, 比如:

1
2
3
4
class Circle(object):
def __init__(self, radius,diameter):
self.radius = radius
self.diameter = diameter

这样写没有问题,我们实例化的时候,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Circle(object):
def __init__(self, radius,diameter):
self.radius = radius
self.diameter = diameter


my_circle = Circle(2,4)

print('radius is {}'.format(my_circle.radius))
print('diameter is {}'.format(my_circle.diameter))


#change the radius into 6
my_circle.radius = 6

print('radius is {}'.format(my_circle.radius))
print('diameter is {}'.format(my_circle.diameter))

运行结果

1
2
3
4
radius is 2
diameter is 4
radius is 6
diameter is 4

发现问题了没有?一开始的时候,我们让半径等于2,直径等于4,那是因为我们知道对圆圈来说啊,这是肯定的事情。但是实际写代码过程中,我可能要去更改我的半径,比如说,让半径等于6,当然我在让半径等于6的时候,直径怎么还是4?因为代码不知道直径是半径的2倍,当然我可以暴力的把直径改为12,但我更希望,程序自己能在我把半径变成6的时候自动把直径变成12,或者当我需要改直径为10的时候,程序能把半径自动的变成5. 如果有这个需求的时候,@property就上场了,如同我一开始给出的例子这样,试试下面这个代码:

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
class Circle(object):
def __init__(self, radius):
self.radius = radius

@property
def diameter(self):
return self.radius * 2
@diameter.setter
def diameter(self, new_diameter):
self.radius = new_diameter / 2


my_circle = Circle(2)

print('radius is {}'.format(my_circle.radius))
print('diameter is {}'.format(my_circle.diameter))


#change the radius into 6
my_circle.radius = 6

print('radius is {}'.format(my_circle.radius))
print('diameter is {}'.format(my_circle.diameter))

#change the diameter into 6
my_circle.diameter = 6

print('radius is {}'.format(my_circle.radius))
print('diameter is {}'.format(my_circle.diameter))

运行结果

1
2
3
4
5
6
adius is 2
diameter is 4
radius is 6
diameter is 12
radius is 3.0
diameter is 6.0

其实property是一个有点函数意思的attribute,两者虽然字面上一致,或者说翻译成中文意思上基本没差别,但使用上是完全俩个不同的东东。如果实在理解不了,可以先跳过去,等用到或者碰到的时候,跑一下代码就明白了。

二、可见性

在很多面向对象编程语言中,对象的属性通常会被设置为私有(private)或受保护(protected)的成员,简单的说就是不允许直接访问这些属性;对象的方法通常都是公开的(public),因为公开的方法是对象能够接受的消息,也是对象暴露给外界的调用接口,这就是所谓的访问可见性。在Python中,可以通过给对象属性名添加前缀下划线的方式来说明属性的访问可见性。例如,可以用__name表示一个私有属性,_name表示一个受保护属性,代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Student:

def __init__(self, name, age):
self.__name = name
self.__age = age

def study(self, course_name):
print(f'{self.__name}正在学习{course_name}.')


stu = Student('王大锤', 20)
stu.study('Python程序设计')
print(stu.__name)

上面代码的最后一行会引发AttributeError(属性错误)异常,异常消息为:'Student' object has no attribute '__name'。由此可见,以__开头的属性__name是私有的,在类的外面无法直接访问,但是类里面的study方法中可以通过self.__name访问该属性。

特殊变量命名规则

  • 1、_xx 以单下划线开头的表示的是protected类型的变量。即保护类型只能允许其本身与子类进行访问。若内部变量标示,如: 当使用“from M import”时,不会将以一个下划线开头的对象引入 。

  • 2、__xx双下划线的表示的是私有类型的变量。只能允许这个类本身进行访问,连子类也不可以用于命名一个类属性(类变量),调用时名字被改变(在类FooBar内部,__boo变成_FooBar__boo,如self._FooBar__boo

  • 3、__xx__定义的是特殊方法。用户控制的命名空间内的变量或是属性,如__init__ , __import__或是__file__ 。只有当文档有说明时使用,不要自己定义这类变量。 (就是说这些是python内部定义的变量名)

需要提醒大家的是,Python并没有从语法上严格保证私有属性的私密性,它只是给私有的属性和方法换了一个名字来阻挠对它们的访问,事实上如果你知道更换名字的规则仍然可以访问到它们。当变量被标记为私有后,在变量的前端插入类名,再类名前添加一个下划线,即形成了_ClassName__变量名

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
"""私有成员只能类中访问,外部不能直接访问,但若要访问,可以强制访:_类名__var, 或者在内部提供一个接口供外部访问"""

class Obj(object):
def __init__(self, name, age):
self.name = name
self.__age = age

def get_age(self):
# print(self.__age)
return self.__age


obj = Obj('张亚飞',23)
print(obj.name) # 张亚飞
# print(obj.__age) # AttributeError: 'Obj' object has no attribute '__age'
print(obj.get_age()) # 23
print(obj._Obj__age) # 23

"""私有字段只能在当前类中访问,其子类也不能访问"""
class Obj2(Obj):
def print_age(self):
print(self.__age)


obj2 = Obj2('张亚飞',23)
obj2.print_age() # AttributeError: 'Obj2' object has no attribute '_Obj2__age'

我们可以对上面的代码稍作修改就可以访问到私有的。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Student:

def __init__(self, name, age):
self.__name = name
self.__age = age

def study(self, course_name):
print(f'{self.__name}正在学习{course_name}.')


stu = Student('张亚飞', 20)
stu.study('Python程序设计') # 张亚飞正在学习Python程序设计
print(stu._Student__name, stu._Student__age) # 张亚飞 20

Python中做出这样的设定是基于一句名言:“We are all consenting adults here”(大家都是成年人)。Python语言的设计者认为程序员要为自己的行为负责,而不是由Python语言本身来严格限制访问可见性,而大多数的程序员都认为开放比封闭要好,把对象的属性私有化并不是必须的东西。

三、动态语言特性

Python是一门动态语言,维基百科对动态语言的解释是:“在运行时可以改变其结构的语言,例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。动态语言非常灵活,目前流行的Python和JavaScript都是动态语言,除此之外如PHP、Ruby等也都属于动态语言,而C、C++等语言则不属于动态语言”。

1. 动态添加属性

  • 添加实例属性

在Python中,我们可以动态为对象添加属性,这是Python作为动态类型语言的一项特权,代码如下所示。需要提醒大家的是,对象的方法其实本质上也是对象的属性,如果给对象发送一个无法接收的消息,引发的异常仍然是AttributeError

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


stu = Student('张亚飞', 20)
# 为Student对象动态添加sex属性
stu.sex = '男'
print(stu.sex) # 男
stu2 = Student('王小明', 16)
print(stu2.sex) # AttributeError

由以上代码可知,Student类有两个属性:nameage。通过[对象名.属性名]给类对象stu动态添加了对象属性sex,而Student的另一个类对象stu2却不能调用这个属性。结论:通过对象名添加的对象属性,只有这个对象能使用。

  • 添加类属性
1
2
3
Student.score = 100
print(stu.score) # 100
print(stu2.score) # 100

由以上代码可知,通过[类名.属性名]给类Obj动态添加了类属性scoreStudent的类对象stustu2都能调用这个属性。注:通过类名添加的类属性,这个类的所有对象都能使用。

2.动态添加方法

类中有三种方法,实例方法,静态方法和类方法,三种方法的区别如下:

  • 实例方法:需要绑定要一个对象上,第一个参数默认使用self,会把对象作为第一个参数传递进来。
  • 静态方法:使用装饰器@staticmethod进行定义,类和对象都可以调用,不需要默认参数。
  • 类方法:使用装饰器@classmethod进行定义,类和对象都可以调用,第一个参数默认使用cls,会把类作为第一个参数传递进来。

首先定义一个类

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

class Obj(object):
def __init__(self):
self.name = '张亚飞'


def set_score(self, score):
self.score = score


@staticmethod
def static_func():
print('static_func')


@classmethod
def class_func(cls):
print('class_method')
  • 动态添加实例方法

    1
    2
    3
    4
    5
    6
    7
    # 动态添加实例方法
    obj = Obj()
    obj.set_score = MethodType(set_score, obj)
    obj.set_score(99)
    print(obj.score) # 99
    obj2 = Obj()
    obj2.set_score(99) # AttributeError: 'Obj' object has no attribute 'set_score'

    由以上代码可知,通过[types.MethodType(方法名, 对象名)]给类对象obj动态添加了实例方法set_score(),同理,Obj的另一个类对象obj2不能调用这个方法。注:通过对象名添加的对象方法,只有这个对象能使用

  • 动态添加静态方法

    1
    2
    3
    4
    5
    # 添加静态方法
    Obj.static_func = static_func
    Obj.static_func()
    obj.static_func()
    obj2.static_func()

    由以上代码可知,通过[类名.静态方法名]给类Obj动态添加了静态方法static_func()Obj类的Obj对象和obj2对象都能调用这个方法。注:通过类名添加的静态方法,这个类及这个类的所有对象都能使用。

  • 动态添加类方法

    1
    2
    3
    4
    5
    # 添加类方法
    Obj.class_func = class_func
    Obj.class_func()
    obj.class_func()
    obj2.class_func()

    由以上代码可知,通过[类名.类方法名]给类Obj动态添加了类方法class_func()Obj类的obj对象和obj2对象都能调用这个方法。注:通过类名添加的类方法,这个类及这个类的所有对象都能使用。

3. 限制添加属性和方法

通过以上内容,我们知道了如何动态的添加属性和方法。但是,如果我们想要限制class的属性该怎么办?例如:只允许Obj类的实例化对象添加nameage属性。为了达到这个目的,Python允许在定义class的时候,定义一个特殊变量__slots__来限制该class能添加的属性。

1
2
3
4
5
6
7
8
9
10
11
class Obj(object):
__slots__ = ('name', 'age')


obj = Obj()
obj.name = 'zhangyafei'
obj.age = 23
obj.score = 99 # AttributeError: 'Obj' object has no attribute 'score'
Obj.score = 100
print(obj.score) # 100
obj.score = 99 # AttributeError: 'Obj' object attribute 'score' is read-only

通过以上代码可知,__slots__Obj类的动态添加没有限制,而Obj类对象obj不能再动态添加对象属性和方法。

对于__slot__有以下几个需要注意的地方:

  • __slots__只对类对象进行限制,不对类进行限制
  • __slots__不仅限制类对象的属性,还限制类对象的方法
  • __slots__仅对当前类起作用,对继承的子类不起作用
  • 在子类中定义__slots__,子类允许定义的属性就是自身的__slots__加上父类的__slots__

四、继承和多态

1. 继承

1.1 单继承

在程序中,继承描述的是事物之间的所属关系,例如猫和狗都属于动物,程序中便可以描述为猫和狗继承自动物;同理,波斯猫和巴厘猫都继承自猫,而沙皮狗和斑点狗都继承足够,如下图所示:

面向对象继承关系

面向对象的编程语言支持在已有类的基础上创建新类,从而减少重复代码的编写。提供继承信息的类叫做父类(超类、基类),得到继承信息的类叫做子类(派生类、衍生类)。

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
class Animal:
def eat(self):
print("-----吃------")
def drink(self):
print("-----喝------")
def sleep(self):
print("-----睡------")
def run(self):
print("-----跑------")

class Dog(Animal):

def bark(self):
print("---汪汪叫----")

class Xiaotq(Dog):
def fly(self):
print("-----飞-----")

def bark(self):
print("---狂叫-----")
#第一种被重写的父类的方法
#Dog.bark(self)
#第二种
super().bark()

class Cat(Animal):
def catch(self):
print("----抓老鼠---")

xiaotq = Xiaotq()
xiaotq.fly()
xiaotq.bark()
xiaotq.eat()

wangcai = Dog()
wangcai.eat()
wangcai.bark()

tom = Cat()
tom.eat()
tom.catch()

继承的语法是在定义类的时候,在类名后的圆括号中指定当前类的父类。如果定义一个类的时候没有指定它的父类是谁,那么默认的父类是object类。object类是Python中的顶级类,这也就意味着所有的类都是它的子类,要么直接继承它,要么间接继承它。Python语言允许多重继承,也就是说一个类可以有一个或多个父类,关于多重继承的问题我们在后面会有更为详细的讨论。在子类的初始化方法中,我们可以通过super().__init__()来调用父类初始化方法,super函数是Python内置函数中专门为获取当前对象的父类对象而设计的。从上面的代码可以看出,子类除了可以通过继承得到父类提供的属性和方法外,还可以定义自己特有的属性和方法,所以子类比父类拥有的更多的能力。在实际开发中,我们经常会用子类对象去替换掉一个父类对象,这是面向对象编程中一个常见的行为,也叫做“里氏替换原则”(Liskov Substitution Principle)。所以,对于面向对象的继承来说,其实就是将多个类共有的方法提取到父类中,子类仅需继承父类而不必一一实现每个方法。注:除了子类和父类的称谓,你可能看到过派生类基类** ,他们与子类和父类只是叫法不同而已。

私有变量和私有方法在继承中的表现

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
class A:
def __init__(self):
self.num1 = 100
self.__num2 = 200

def test1(self):
print("----test1----")

def __test2(self):
print("-----test2----")

def test3(self):
self.__test2()
print(self.__num2)

class B(A):
def test4(self):
self.__test2()
print(self.__num2)

b = B()
b.test1()
#.test2() # 私有方法并不会被继承
print(b.num1)
#rint(b.__num2)
b.test3()
#.test4()

方法重写

子类继承父类的方法后,还可以对方法进行重写(重新实现该方法),子类中一个方法与父类中一个方法名相同,但功能不同,调用子类该方法时会执行该子类中重写的这个方法。不同的子类可以对父类的同一个方法给出不同的实现版本,这样的方法在程序运行时就会表现出多态行为(调用相同的方法,做了不同的事情)。

1
2
3
4
5
6
7
8
9
10
11
12
class ShenXianBase(Base):
def fight(self):
print("神仙始祖们在天地边界打架。。。。")

class ShenXian(ShenXianBase):
"""神仙类"""

def fight(self):
print("神仙在打架...")

shenxian = ShenXian()
shenxian.fight() # 神仙在打架...

1.2 多继承

一个子类继承多个父类的情况叫做多继承

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
class Base(object):
def fight(self):
print("动物在打架....")

class ShenXianBase(Base):
def fight(self):
print("神仙始祖们在天地边界打架。。。。")

class ShenXian(ShenXianBase):
"""神仙类"""
def fly(self):
print("神仙都会飞...")

def fight(self):
print("神仙在打架...")


class MonkeyBase(Base):
pass
# def fight(self):
# print("猿猴在打架。。。")


class Monkey(MonkeyBase):
def eat_peach(self):
print("猴子都喜欢吃桃子...")

# def fight(self):
# print("猴子在打架...")


class MonkeyKing(Monkey,ShenXian):

def play_goden_stick(self):
print("孙悟空玩金箍棒...")

m = MonkeyKing()
m.play_goden_stick() # 孙悟空玩金箍棒...
m.fly() # 神仙都会飞...
m.eat_peach() # 猴子都喜欢吃桃子...

m.fight() # 神仙在打架...

由以上可知,当子类继承多个父类时,会继承其父类的方法。

那么多继承的属性和方法的查找顺序是怎么样呢?答:广度优先遍历

多继承

注:Python2中不声明object默认是经典类,深度优先,python3不管写不写object都是新式类,广度优先。

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 Base(object):
def test(self):
print("----Base")


class A(Base):
pass


class B(Base):
def test(self):
print("-----B")


class C(A, B):
pass


c = C()
c.test() # ----B

print(C.__mro__) # 继承顺序: 广度优先
"""
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Base'>, <class 'object'>)
"""

2. 多态

多态是指一类事物有多种形态,比如动物类,可以有猫,狗,猪等等。(一个抽象类有多个子类,因而多态的概念依赖于继承)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import abc
class Animal(metaclass=abc.ABCMeta): #同一类事物:动物
@abc.abstractmethod
def talk(self):
pass

class Cat(Animal): #动物的形态之一:猫
def talk(self):
print('say miaomiao')

class Dog(Animal): #动物的形态之二:狗
def talk(self):
print('say wangwang')

class Pig(Animal): #动物的形态之三:猪
def talk(self):
print('say aoao')

多态性是指具有不同功能的函数可以使用相同的函数名,这样就可以用一个函数名调用不同内容的函数。在面向对象方法中一般是这样表述多态性:向不同的对象发送同一条消息,不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个对象可以用自己的方式去响应共同的消息。所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。

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
import abc
class Animal(metaclass=abc.ABCMeta): #同一类事物:动物
@abc.abstractmethod
def talk(self):
pass

class Cat(Animal): #动物的形态之一:猫
def talk(self):
print('say miaomiao')

class Dog(Animal): #动物的形态之二:狗
def talk(self):
print('say wangwang')

class Pig(Animal): #动物的形态之三:猪
def talk(self):
print('say aoao')

c = Cat()
d = Dog()
p = Pig()

def func(obj):
obj.talk()

func(c) # say miaomiao
func(d) # say wangwang
func(p) # say aoao

综上可以说,多态性是 : 一个接口,多种实现

多态性的好处:

  • 增加了程序的灵活性,以不变应万变,不论对象千变万化,使用者都是同一种形式去调用,如func(obj)
  • 增加了程序的可扩展性,通过继承animal类创建了一个新的类,使用者无需更改自己的代码,还是用func(obj)去调用

鸭子模型

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

鸭子类型是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由”当前方法和属性的集合”决定。这个概念的名字来源于由James Whitcomb Riley提出的鸭子测试,“鸭子测试”可以这样表述:“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”

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

鸭子类型通常得益于不测试方法和函数中参数的类型,而是依赖文档、清晰的代码和测试来确保正确使用。

Duck typing 这个概念来源于美国印第安纳州的诗人詹姆斯·惠特科姆·莱利(James Whitcomb Riley,1849- 1916)的诗句:”When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.”

先上代码,也是来源于网上很经典的案例:

1
2
3
4
5
6
7
8
9
10
11
class Duck():
def walk(self):
print('I walk like a duck')
def swim(self):
print('i swim like a duck')

class Person():
def walk(self):
  print('this one walk like a duck')
def swim(self):
  print('this man swim like a duck')

可以很明显的看出,Person类拥有跟Duck类一样的方法,当有一个函数调用Duck类,并利用到了两个方法walk()和swim()。我们传入Person类也一样可以运行,函数并不会检查对象的类型是不是Duck,只要他拥有walk()和swim()方法,就可以正确的被调用。

再举例,如果一个对象实现了__getitem__方法,那python的解释器就会把它当做一个collection,就可以在这个对象上使用切片,获取子项等方法;如果一个对象实现了__iter____next__方法,python就会认为它是一个iterator,就可以在这个对象上通过循环来获取各个子项。

最后通过一个支付的例子感受一下多态:

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
# 接口:若干抽象方法的集合
# 作用:限制实现接口的类必须按照接口给定的调用方式实现这些方法;对高层模块隐藏了类的内部实现

from abc import ABCMeta, abstractmethod


# 接口 abstract class
class PayMent(metaclass=ABCMeta):
@abstractmethod # 定义抽象方法的关键字
def pay(self, money):
pass
# def pay(self, money):
# raise NotImplementedError('must implement pay methods')


class AliPay(PayMent):
# 子类继承接口,必须实现接口中定义的抽象方法,否则不能实例化对象
def pay(self, money):
print(f'支付宝支付{money}元')


class WechatPay(PayMent):
def pay(self, money):
print(f'微信支付{money}元')


class ApplePay(PayMent):
def pay(self, money):
print(f'苹果支付{money}元')


def finish_pay(p, money):
p.pay(money)


p1 = AliPay()
p2 = WechatPay()
p3 = ApplePay()
finish_pay(p1, 100)
finish_pay(p2, 200)
finish_pay(p3, 300)

五、创建类的三种方式

  • 普通方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Obj(object):
    x = 123

    def func(self):
    return 666


    Obj1 = type('Obj1',(object,),{'x':123,'func':lambda self:666})

    obj = Obj()
    obj1 = Obj1()

    print(obj.x, obj.func())
    print(obj1.x,obj1.func())
  • 自定义type

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class MyType(type):
    pass


    class Obj(object, metaclass=MyType):
    x = 123

    def func(self):
    return 666

    Obj2 = MyType('Obj2',(object,),{'x':123,'func': lambda self:666})

    # 注意:metaclass的作用是制定当前类由谁创建, 默认是由type创建
  • metaclass:指定类由哪个type创建?(type泛指继承type的所有类)

    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
    54
    55
    56
    57
    58
    59
    60
    61
    class MyType(type):
    def __init__(self, *args, **kwargs):
    print('MyType的__init__')
    super(MyType, self).__init__(*args, **kwargs)

    def __call__(cls, *args, **kwargs):
    print('MyType的__call__')
    obj3 = cls.__new__(cls)
    cls.__init__(obj3)
    return obj


    class Obj3(object, metaclass=MyType):
    x = 123

    def __init__(self):
    print('Obj3的__init__')

    def __new__(cls, *args, **kwargs):
    print('Obj3的__new__')
    return object.__new__(cls)

    def func(self):
    return 666


    # print(Obj3) # MyType的__init__ <class '__main__.Obj3'>
    obj3 = Obj3() # MyType的__init__ MyType的__call__ Obj3的__new__ Obj3的__init__
    # obj3 = Obj3()
    # Obj3是类
    # Obj3是MyType的一个对象
    """
    1. 创建类时,先执行metaclass(默认为type)的__init__方法
    2. 类在实例化时, 执行metaclass(默认为type)的__call__方法,__call__方法的返回值就是实例化的对象
    __call__内部调用:
    - 类.__new__方法:创建对象
    _ 类.__init__方法:对象的初始化
    """


    class MyType(type):
    def __init__(self, *args, **kwargs):
    print('mytype__init__')
    super(MyType, self).__init__(*args,**kwargs)


    # class Base(object, metaclass=MyType):
    # pass

    # class Obj4(Base):
    # pass

    def with_metaclass(arg):
    Base = MyType('Base',(arg,),{})
    # class Base(arg, metaclass=MyType):
    # pass
    return Base


    class Obj4(with_metaclass(object)):
    pass

六、常用的魔法方法

Python中所有的类,均继承自一个叫Object的父类。Object类中定义了中众多以双下划线开头和结尾的方法,它们在面向对象的Python的处处皆是。它们是一些可以让你对类添加“魔法”的特殊方法。 它们经常是两个下划线包围来命名的(比如 __init____repr__ )。下面代码中列出了常用的魔法方法。

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
class Obj(object):
"""限制对象添加属性"""
__slots__ = ['storage', 'stack_func', 'num', 'name']

def __init__(self):
""" 创建对象的时候若new返回一个对象会执行此方法 类名()"""
object.__setattr__(self, 'storage', {})
print('__init__')

def __new__(cls, *args, **kwargs):
"""创建对象的时候会执行,返回一个对象
应用:单例/rest framework序列化
"""
print('__new__')
return super(Obj, cls).__new__(cls, *args, **kwargs)

def __call__(self):
""" 对象()会执行
应用:flask源码请求入口,django请求入口(WSGIHandler.__call__)
"""
print('__call__')

def __hash__(self):
""" hash()会执行 """
return id(self)

def __str__(self):
"""
调用对象会执行此函数
:return: string_obj 返回一个字符串对象
"""
return '__str__'

def __repr__(self):
"""
转化为机器可以解释的语言
case 1: repr(object)时会执行此函数
case 2: 交互模式下打印对象会执行此函数
:return: 用于对象信息的显示
"""
return '__repr__'

def __getattr__(self, item):
"""当访问不存在的属性时会调用"""
return '__getattr__'

def __setattr__(self, key, value):
"""给对象设置属性的时候会调用"""
# self.key = value #容易出现循环调用
print('__setattr__')
if key == 'num':
object.__setattr__(self, key, value - 100)
else:
object.__setattr__(self, key, value)

def __delattr__(self, item):
"""删除属性的时候会调用"""
print('__delattr__')
object.__delattr__(self, item)

def __getattribute__(self, item):
"""访问任何属性的时候都会调用此方法"""
print('__getattribute__')
return super(Obj, self).__getattribute__(item)

def __del__(self):
"""对象的生命周期执行结束之后执行"""
print('__del__')

def __setitem__(self, key, value):
"""obj[key] = value时会调用此方法"""
print('__setitem__')
self.storage[key] = value

def __getitem__(self, key):
"""obj[key]会调用此方法"""
return self.storage.get(key, None)

def __delitem__(self, key):
"""del obj[key]调用"""
print('__delitem__')
del self.storage[key]

def __add__(self, other):
return '__add__'

def __sub__(self, other):
return '__sub__'

def __mul__(self, other):
return '__mul'

def __floordiv__(self, other):
return '__floatdiv__'

def __mod__(self, other):
return '__mod__'

def __divmod__(self, other):
return '__divmod__'

def __pow__(self, power, modulo=None):
return '__pow__'


obj = Obj() # __new__ __init__
print(obj) # __str__
obj() # __call__
print(Obj.__mro__) # (<class '__main__.Obj'>, <class 'object'>)
obj.name = '__dict__'
print(obj.__dict__)
# print(Obj.__dict__)
print(repr(obj)) # __repr__
print(obj.world) # __getattribute__ __getattr__
obj.num = 200 # __setattr__
print(obj.num) # __getattribute__, 100
del obj.num # __delattr__
print(obj.storage) # {}
obj['name'] = '张亚飞' # __setitem__
print(obj.storage) # __getattrbute__ __getattrbute__ {'name':'张亚飞'}
print(obj['name']) # __getattrbute__ 张亚飞
del obj['name'] # __delitem__
print(obj['name']) # __getitem__, __getitem__, None
print(obj + 7)
print(obj - 1)
print(obj * 1)
print(obj // 1)
print(obj % 3)
print(obj.__divmod__(3))
print(obj.__pow__(2))

objx = Obj()
print(hash(obj))
print(hash(objx))
# __del__

"""
这里我们想让__setattr__执行默认行为,也就是将value赋值给name,和object对象中的同样方法,做类似的操作。
但是这里我们不调用父类__setattr__的方法来实现,做这样的尝试得到的结果就是,超过循环调用深度,报错。因为
这里在执行初始化方法self.world = world的时候,就会调用__setattr__方法,而这里的__setattr__方法里面的
self.name = value又会调用自身。所以造成了循环调用。所以使用该魔法方法的时候要特别注意。
"""


class Friends(object):
def __init__(self):
self.name = 'zhang'
self.age = 23

def func(self):
print('__func__')


class Xiaoming(Friends):
score = 99

def __init__(self):
super(Xiaoming, self).__init__()
self.run = 200


if __name__ == '__main__':
# 一些内置数据类型没有__dict__属性
ll = []
dic = {}
num = 3
# print(ll.__dict__) # AttributeError: 'list' object has no attribute '__dict__'
# print(dic.__dict__)
# print(num.__dict__)

# 类的__dict__和对象的__dict__的区别
f = Friends() # 创建实例
print(f.__dict__)
f.message = 'hello world'
f.func = lambda x:x
print(f.__dict__)
print(Friends.__dict__)

# 继承关系的__dict__
xiaoming = Xiaoming()
print(xiaoming.__dict__)
print(Xiaoming.__dict__)
"""
1. 一些内置数据类型没有__dict__
2. 实例的__dict__存有与实例相关的实例变量和函数.
类的__dict__则是和实例共享的变量,函数(方法,类属性).注意,类的__dict__并不包含其父类的属性.
3. 对象也有自己的__dict__属性, 存储self.xxx 信息,父子类对象公用__dict__
"""


class BAR(object):
def __init__(self, cls):
self.cls = cls


class NEW_OBJ(object):

def __new__(cls, *args, **kwargs):
# return super(NEW_OBJ, cls).__new__(cls, *args, **kwargs) # <__main__.NEW_OBJ object at 0x000000D445061CF8>
# return 123 # 123
# return BAR # <class '__main__.BAR'>
# return BAR() # <__main__.BAR object at 0x000000AD77141C50>
return BAR(cls) # <__main__.BAR object at 0x0000003BFFA31D68>


obj = NEW_OBJ()
print(obj)
"""new方法的返回值决定对象到底是什么"""


class Obj4():
def __enter__(self):
print('__enter__')

def __exit__(self, exc_type, exc_val, exc_tb):
print('__exit__')


with Obj4():
print('执行中')

# __enter__
# 执行中
# __exit__
"""with 对象默认执行__enter__方法,执行结束执行__exit__方法"""


class Obj5(object):
pass


setattr(Obj5,'send', lambda x:1)
obj5 = Obj5()
print(obj5.send())


""" 对象可以被for循环 """
class Obj(object):
def __iter__(self):
# return iter([1,2,3])
yield 1
yield 2
yield 3


obj = Obj()

for item in obj:
print(item)

七、简单的总结

Python中拥有字段、方法和属性三种成员,属性看起来像字段,但实际上是方法的变种,有时我们描述的时候不会严格区分字段和属性,大多数情况下将字段、变量、属性统一称为属性。同时,Python为了保护私有变量、私有方法不被用户随意访问,制定了变量名或方法名前加下划线的规则来对用户进行约束。Python是动态语言,Python中的对象可以动态的添加属性。在面向对象的世界中,一切皆为对象,我们定义的类也是对象,所以类也可以接收消息,对应的方法是类方法或静态方法。通过继承,我们可以从已有的类创建新类,实现对已有类代码的复用,多态是面向对象非常重要的特性。

赞赏一下吧~