Python 面向对象知识总结

本文系统总结了 Python 面向对象编程的核心知识点,包括类与实例、静态方法与类方法、属性装饰器、父类调用以及上下文管理器等进阶技巧。

1. 面向对象

待解决

dataclass + __solts__的问题

__solts__ 使用slots一个不好的地方就是我们不能再给实例添加新的属性了,只能使用在 __slots__ 中定义的那些属性名

使用__solts__,python实例通过一个很小的固定大小的数组来构建,而不是为每个实例定义一个字典,这跟元组或列表很类似.Python的很多特性都依赖于普通的基于字典的实现。
另外,定义了slots后的类不再支持一些普通类特性了,比如多继承。

尽量不要使用 __solts__

github上相关问题

多加一个装饰器

基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Color():
r = 200
def __init__(self,r=250):
self.r = r
self.g = self.get_g()
self.b = self.get_b

def get_g(self):
g = 223
return g

@property
def get_b(self):
b = 222
return b

def __str__(self):
return f'{self.__class__.__name__}(class_r = {self.__class__.r}, r = {self.r}, g = {self.g}, b = {self.b})'

color = Color(255)
color1 = Color()
print(color) # Color(class_r = 200, r = 255, g = 223, b = 222)
print(color1) # Color(class_r = 200, r = 250, g = 223, b = 222)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Color():
r = 200
def __init__(self):
self.g = self.get_g()
self.b = self.get_b

def get_g(self):
g = 223
return g

@property
def get_b(self):
b = 222
return b

def __str__(self):
return f'{self.__class__.__name__}(class_r = {self.__class__.r}, r = {self.r}, g = {self.g}, b = {self.b})'

color = Color()
print(color) # Color(class_r = 200, r = 255, g = 223, b = 222)

使用第三方插件修改一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from attr import attrs, attrib  # dataclass 内置库可替代
'''
attrs 修饰符,可以自动实现__init__,__repr__,__eq__,__ne__,__it__,__le__,__gt__,
__ge__,__hash__,这几个方法
'''

@attrs
class Color():
r = attrib(type=int, default=100)
g = attrib(type=int, default=200)
b = attrib(type=int, default=222)


color = Color(255,254,253)
color1 = Color()
print(color) # Color(class_r = 200, r = 255, g = 223, b = 222)
print(color1) # Color(class_r = 200, r = 250, g = 223, b = 222)

声明和比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from attr import attrs, attrib  # dataclass内置库可替代

'''
attrs 修饰符,可以自动实现__init__,__repr__,__eq__,__ne__,__it__,__le__,__gt__,
__ge__,__hash__,这几个方法
'''

@attrs
class Point():
x = attrib()
y = attrib()

p1 = Point(1,2)
p2 = Point(y=3,x=4)
print(p1,p2) # Point(x=1, y=2) Point(x=4, y=3)

使用python3.7 的内置库

1
2
3
4
5
6
7
8
9
10
from dataclasses import dataclass

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

p1 = Point(1,2)
p2 = Point(y=3,x=4)
print(p1,p2) # Point(x=1, y=2) Point(x=4, y=3)
1
2
3
4
5
6
7
8
9
10
11
## 默认init,repr,eq 为True, 其余为False
@dataclass(init=1,repr=1,eq=1,order=1,unsafe_hash=0,frozen=0)
class Point():
x:int = 0
y:int = 0

p1 = Point(1,2)
p2 = Point(y=3,x=4)
p3 = Point(x=1,y=2)
print(p1>p2) # fasle
print(p1==p3) # True
1
2
3
4
5
6
7
8
9
10
11
from dataclasses import dataclass

@dataclass(frozen=1)
class Point():
x:int = 0
y:int = 0

p1 = Point(1,2)
print(p1)
## 当实例化一个frozen 对象时,任何企图修改对象属性的行为都会引发 FrozenInstanceError
##p1.x = 10 # dataclasses.FrozenInstanceError: cannot assign to field 'x'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from dataclasses import dataclass

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

def x_add_1(self):
self.x += 1

p1 = Point(1,2)
print(p1) # Point(x=1, y=2)
p1.x_add_1()
print(p1) # Point(x=2, y=2)

__init__方法在返回前会调用__post_init__

1
2
3
4
5
6
7
8
9
10
11
12
from dataclasses import dataclass

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

def __post_init__(self):
self.x += 1

p1 = Point(1,2)
print(p1) # Point(x=2, y=2)

有关继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from dataclasses import dataclass

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

def __post_init__(self):
self.x += 1

@dataclass
class ThirdPonit(Point):
z:int =0

ppp = ThirdPonit(1,3,4)
print(ppp) # ThirdPonit(x=2, y=3, z=4)

super

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

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

def __post_init__(self):
self.x += 1

@dataclass
class ThirdPonit(Point):
z:int =0

def __post_init__(self):
super().__post_init__() # 调用 父类的__post_init__()
self.x += 10

ppp = ThirdPonit(1,3,4)
print(ppp) # ThirdPonit(x=12, y=3, z=4)

复合初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import random
from dataclasses import dataclass

def get_random_marks():
return [random.randint(1,10) for _ in range(5)]

@dataclass
class Student:
marks:list = list

def __post_init__(self): # 额外的工作
self.marks = get_random_marks()

a = Student()
print(a) # Student(marks=[2, 9, 8, 8, 2])
print(a.marks) # [2, 9, 8, 8, 2]

修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import random
from dataclasses import dataclass, field

def get_random_marks():
return [random.randint(1,10) for _ in range(5)]

@dataclass
class Student:
marks:int = field(default_factory= get_random_marks) # int 不建议,建议使用数据类型
# default_factory :如果在创建对象时没赋值,则使用该方法初始化该字段 (必须是可以调用的无参数方法)
# 使用 get_random_marks 方法 初始化 marks


a = Student()
print(a) # Student(marks=[7, 2, 4, 7, 4])
print(a.marks) # [7, 2, 4, 7, 4]
print(type(a.marks)) # <class 'list'>
b = Student('b') # 传参则不影响
print(b) # Student(marks='b')
print(b.marks) # b
print(type(b.marks)) # <class 'str'>

filed 更多操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from dataclasses import dataclass, field

@dataclass(order=1)
class Number:
number:int = field(compare= False,repr= False) # number 不进行比较,不打印
val:int
verified:1 = field(default=0,init=0) # 默认为 1,不进行初始化

a = Number(3,4)
b = Number(4,3)
print(a < b) # False
print(a) # Number(val=4, verified=0)
print(b) # Number(val=3, verified=0)
b.verified = 1
print(b) # Number(val=3, verified=1)

2. @staticmethod和@classmethod

一般来说,要使用某个类的方法,需要先实例化一个对象再调用方法。

而使用 @staticmethod@classmethod,就可以不需要实例化,直接类名.方法名()来调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A:
bar = 1
def foo(self):
print 'foo'

@staticmethod
def static_foo():
print 'static_foo'
print A.bar

@classmethod
def class_foo(cls):
print 'class_foo'
print cls.bar
cls().foo()

A.static_foo()
A.class_foo()

如果在@staticmethod中要调用到这个类的一些属性方法,只能直接类名.属性名或类名.方法名。

而@classmethod因为持有cls参数,可以来调用类的属性,类的方法,实例化对象等,避免硬编码

这两个方法的用法是类似的,大多数情况下,classmethod也可以通过staticmethod代替,在通过类调用时,这两者对于调用者来说是不可区分的。

这两者的区别在于,classmethod增加了一个对实际调用类的引用,这带来了很多方便的地方:

  1. 方法可以判断出自己是通过基类被调用,还是通过某个子类被调用
  2. 通过子类调用时,方法可以返回子类的实例而非基类的实例
  3. 通过子类调用时,方法可以调用子类的其他classmethod

3. 有关于@property

有关于@property

1. 将方法改为属性

这种类型的attributes并不会被实际的存储,而是在需要的时候计算出来

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

def area(self):
return math.pi * self.radius ** 2

@property
def diameter(self):
return self.radius * 2

@property
def perimeter(self):
return 2 * math.pi * self.radius

c = Circle(3)
print(c.area()) # 方法调用
print(c.diameter) # 属性访问
print(c.perimeter)

2. 做限定

参考前文

3.


4. 调用父类方法

调用父类方法

super()

  1. 为了调用父类(超类)的一个方法,可以使用 super() 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A:
def spam(self):
print('A.spam')

class B(A):
def spam(self):
print('B.spam')
super().spam() # Call parent spam()

b = B()
b.spam()
'''
输出结果
B.spam 先调用 B 的 spam
A.spam 然后再调用 A 的 spam
'''
  1. super() 函数的一个常见用法是在 __init__() 方法中确保父类被正确的初始化了
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class A:
    def __init__(self):
    self.x = 0

    class B(A):
    def __init__(self):
    super().__init__()
    self.y = 1

    b = B()
    print(b.x,b.y) # 0,1

使用dataclasses改写

1
2
3
4
5
6
7
8
9
10
11
12
from dataclasses import dataclass

@dataclass
class A:
x:int = 0

@dataclass
class B(A):
y:int = 1

b = B()
print(b) # B(x=0, y=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
    class Father:
    def __init__(self, name):
    self.name = name
    print("init Father's name")


    class Mather:
    def __init__(self, age):
    self.age = age
    print("init Mather's age")


    class Son(Father, Mather):
    def __init__(self, name, age, sex):
    super().__init__(name) # 先继承 Father 的 name
    super(Father, self).__init__(age) # 继承后 再继承 Mother 的age
    self.sex = sex # 最后 实例化自己的 sex
    print("init Son's sex")


    if __name__ == "__main__":
    son = Son("Tom", 5, "Male")
    print(Son.__mro__) # (<class '__main__.Son'>, <class '__main__.Father'>, <class '__main__.Mather'>, <class 'object'>)
    print(son.name, son.age, son.sex)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from dataclasses import dataclass

@dataclass
class Father:
name:str
print("init Father's name")

@dataclass
class Mather:
age:int
print("init Mather's age")

@dataclass
class Son(Father, Mather):
sex:str
print("init Son's sex")


if __name__ == "__main__":
son = Son(name="Tom", age=5, sex="Male") # 这个会先实例化age,再name,再sex
print(Son.__mro__) # (<class '__main__.Son'>, <class '__main__.Father'>, <class '__main__.Mather'>, <class 'object'>)
print(son)

MRO列表

Python会在MRO列表上继续搜索下一个类。
只要每个重定义的方法统一使用 super() 并只调用它一次,
那么控制流最终会遍历完整个MRO列表,每个方法也只会被调用一次
super()有个令人吃惊的地方是它并不一定去查找某个类在MRO中下一个直接父类,
你甚至可以在一个没有直接父类的类中使用它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A:
def spam(self):
print('A.spam')
super().spam()
class B:
def spam(self):
print('B.spam')

class C(A,B):
pass

c = C()
print(C.__mro__) # (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)
c.spam()
'''
A.spam
B.spam
'''
1
2
3
4
5
6
7
8
9
10
class C(B,A):
pass

c = C()
print(C.__mro__) # (<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
c.spam()
'''
B.spam
'''
## 不会报错,只会执行 B下面的

关于super()

由于 super() 可能会调用不是你想要的方法,你应该遵循一些通用原则。
首先,确保在继承体系中所有相同名字的方法拥有可兼容的参数签名(比如相同的参数个数和参数名称)。
这样可以确保 super() 调用一个非直接父类方法时不会出错。
其次,最好确保最顶层的类提供了这个方法的实现,这样的话在MRO上面的查找链肯定可以找到某个确定的方法。

在Python社区中对于 super() 的使用有时候会引来一些争议。
尽管如此,如果一切顺利的话,你应该在你最新代码中使用它。
Raymond Hettinger为此写了一篇非常好的文章
Python’s super() Considered Super
通过大量的例子向我们解释了为什么 super() 是极好的。


5. 格式化字符串

实例的字符串显示

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

def __repr__(self):
return f'({self.x}, {self.y})'


def __str__(self):
return f'Pair({self.x=}, {self.y=})'

p = Pair(3, 4)
print(p)

1
2
3
4
5
6
7
8
from dataclasses import dataclass

@dataclass
class Pair:
x:int
y:int = 0 # y 默认值为0
p = Pair(3, 4)
print(p)

对象自定义格式化

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
_formats = {
'ymd': '{d.year}-{d.month}-{d.day}',
'mdy': '{d.month}/{d.day}/{d.year}',
'dmy': '{d.day}/{d.month}/{d.year}'
}


class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day

def __format__(self, code):
if code == '':
code = 'ymd'
fmt = _formats[code]
return fmt.format(d=self)


d = Date(2012, 12, 21)
print(d)
print(format(d, 'mdy'))
print('The date is {:ymd}'.format(d))
print('The date is {:mdy}'.format(d))

修改后

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 dataclasses import dataclass
_formats = {
'ymd': '{d.year}-{d.month}-{d.day}',
'mdy': '{d.month}/{d.day}/{d.year}',
'dmy': '{d.day}/{d.month}/{d.year}'
}

@dataclass
class Date:
year:int
month:int
day:int

def __format__(self, code):
if code == '':
code = 'ymd'
fmt = _formats[code]
return fmt.format(d=self)


a = Date(2012, 12, 21)
print(a)
print(format(a, 'mdy'))
print('The date is {:ymd}'.format(a))
print('The date is {:mdy}'.format(a))


6. 上下文管理器

上下文管理器

让对象支持上下文管理器

对象需要定义 __enter____exit__

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
from socket import socket, AF_INET, SOCK_STREAM


class LazyConnection:
def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
self.address = address
self.family = family
self.type = type
self.sock = None

def __enter__(self):
if self.sock is not None:
raise RuntimeError('Already connected')
self.sock = socket(self.family, self.type)
self.sock.connect(self.address)
return self.sock

def __exit__(self, exc_ty, exc_val, tb): # 异常类型,异常值和异常的trackback
self.sock.close()
self.sock = None
#return True 如果这里返回True 则代表不处理with中的异常

if __name__ == '__main__':
from functools import partial

conn = LazyConnection(('www.python.org', 80))
# Connection closed
with conn as s:
# conn.__enter__() executes: connection open
s.send(b'GET /index.html HTTP/1.0\r\n')
s.send(b'Host: www.python.org\r\n')
s.send(b'\r\n')
resp = b''.join(iter(partial(s.recv, 8192), b''))
print(resp)
# conn.__exit__() executes: connection closed

线程安全修改版

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
from socket import socket, AF_INET, SOCK_STREAM
import threading

class LazyConnection:
def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
self.address = address
self.family = AF_INET
self.type = SOCK_STREAM
self.local = threading.local()

def __enter__(self):
if hasattr(self.local, 'sock'):
raise RuntimeError('Already connected')
self.local.sock = socket(self.family, self.type)
self.local.sock.connect(self.address)
return self.local.sock

def __exit__(self, exc_ty, exc_val, tb):
self.local.sock.close()
del self.local.sock

from functools import partial
def test(conn):
with conn as s:
s.send(b'GET /index.html HTTP/1.0\r\n')
s.send(b'Host: www.huawei.com\r\n')
s.send(b'\r\n')
resp = b''.join(iter(partial(s.recv, 8192), b''))

print('Got {} bytes'.format(len(resp)))

if __name__ == '__main__':
conn = LazyConnection(('www.huawei.com', 80))

t1 = threading.Thread(target=test, args=(conn,))
t2 = threading.Thread(target=test, args=(conn,))
t1.start()
t2.start()
t1.join()
t2.join()

使用 contexlib 模块中的 @contextmanager装饰器

实现一个新的上下文管理器的最简单的方法

yield 之前的代码会在上下文管理器中作为 __enter__() 方法执行,
所有在 yield 之后的代码会作为 __exit__() 方法执行

先执行print('《', end=''),遇到yeild ,执行 print('挪威的森林',end='') , 最后执行 print('》', end='')

1
2
3
4
5
6
7
8
9
10
11
12
from contextlib import contextmanager
@contextmanager
def book_mark():
print('《', end='')
yield
print('》', end='')


with book_mark():
print('挪威的森林',end='')

## 《挪威的森林》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time
class timethis:
def __init__(self, label):
self.label = label

def __enter__(self):
self.start = time.time()
print(f'start = {self.start}')

def __exit__(self, exc_ty, exc_val, exc_tb):
end = time.time()
print(f'{end = }')
print('{}: {}'.format(self.label, end - self.start))

with timethis('counting'):
n = 10000000
while n > 0:
n -= 1
使用from contextlib import contextmanager
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import time
from contextlib import contextmanager

@contextmanager
def timethis(label):
start = time.time()
print(f'{start = }')
try:
yield
finally:
end = time.time()
print(f'{end = }')
print(f'{label} : {end - start}')

## Example use
with timethis('counting'):
n = 10000000
while n > 0:
n -= 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
    

from contextlib import contextmanager

@contextmanager
def list_transaction(orig_list):
working = list(orig_list)
yield working
orig_list[:] = working
# 任何对列表的修改只有当所有代码运行完成并且不出现异常的情况下才会生效

items = [1,2,3]
with list_transaction(items) as working:
working.append(4)
working.append(5)

print(items) # [1,2,3,4,5]

with list_transaction(items) as working:
working.append(6)
working.append(7) working是[1,2,3,4,5,6,7]
raise RuntimeError('oops') 执行不了orig_list[:] = working

print(items) ## 还是 [1,2,3,4,5]