本文系统总结了 Python 面向对象编程的核心知识点,包括类与实例、静态方法与类方法、属性装饰器、父类调用以及上下文管理器等进阶技巧。
1. 面向对象 __slots__ 使用slots一个不好的地方就是我们不能再给实例添加新的属性了,只能使用在 __slots__ 中定义的那些属性名
使用__slots__ ,python实例通过一个很小的固定大小的数组来构建,而不是为每个实例定义一个字典,这跟元组或列表很类似.Python的很多特性都依赖于普通的基于字典的实现。 多继承倒不是完全不能用,但有两个坑。一是两个基类都定义了非空 __slots__ 时会直接报 TypeError: multiple bases have instance lay-out conflict——两边各自规划了实例的内存布局,合不到一起;跟空 __slots__ = () 的类或者普通类混着继承都是合法的。二是一旦混进了没定义 __slots__ 的普通类,子类实例又会长出 __dict__,属性能随便加,省内存的初衷也就落空了。
尽量不要使用 __slots__
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 = 200, g = 223, b = 222)
这一版 __init__ 里没有 self.r = ...,实例上就没有 r 这个属性,self.r 于是顺着查到类属性上,读到了 200——和 self.__class__.r 是同一个值。上一版设了 self.r,实例属性把类属性遮住,读到的才是传进来的 255。
使用第三方插件修改一下 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(r=255, g=254, b=253) print(color1) # Color(r=100, g=200, b=222)
这个类只声明了 r/g/b 三个字段,__repr__ 是 attrs 自动生成的,输出里自然也就只有这三个。
声明和比较 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) # False 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) # 默认为 0,不进行初始化 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增加了一个对实际调用类的引用,这带来了很多方便的地方:
方法可以判断出自己是通过基类被调用,还是通过某个子类被调用
通过子类调用时,方法可以返回子类的实例而非基类的实例
通过子类调用时,方法可以调用子类的其他classmethod
3. 有关于@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. 做限定 参考前文
4. 调用父类方法 super()
为了调用父类(超类)的一个方法,可以使用 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 '''
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 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 36 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 如果这里返回真值,就表示异常已经被 __exit__ 处理掉了,不再向外抛 # 返回 False 或者什么都不返回(None),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=''),遇到yield ,执行 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]