Python魔法方法漫游指南:可调用和槽
2549 views, 2021/07/19 updated Go to Comments
鉴于前面几个章节难度较高,本章我们聊两个轻松点的魔法方法。
虽然轻松,但是一样很有用哦。
可调用对象
通常情况下,类的实例是不可调用的。
举个栗子:
class Foo: pass foo = Foo() foo() # 输出: # TypeError: 'Foo' object is not callable
但是如果类定义里实现了 __call__
协议,那么这个类就变成可调用对象(callable)了。
比如:
class Bar: def __call__(self): print('Hi there') bar = Bar() bar() # 输出: # Hi there
单纯从 bar()
你都无法得知它是函数还是类实例。所以在 Python 中一切皆对象,连函数也是对象,它和类的区分不是那么显著。
那把类变成可调用对象有什么用呢?
有的时候你的代码需要同时支持调用函数和类,那么就可以这样:
def foo(value=70): """待调用函数""" print(f'Score is: {value}') class Bar: """待调用类""" def __init__(self, value=80): self.value = value def __call__(self): print(f'Score is: {self.value}') def print_score(obj): """实际运行函数""" obj() print_score(foo) # Score is: 70 print_score(Bar()) # Score is: 80
另一种应用场景是类装饰器。因为装饰器要求其对象必须可调用:
import functools class Logit(): def __init__(self, name): self.name = name def __call__(self, func): @functools.wraps(func) def wrapper(*args, **kwargs): value = func(*args, **kwargs) print(f'{self.name} is calling: ' + func.__name__) return value return wrapper @Logit(name='Dusai') def a_func(): pass a_func() # 输出: # Dusai is calling: a_func
想深入了解装饰器原理的同学,可以看我旧文装饰器入门。
槽
Python 是一门动态语言。当我们从定义好的类创建了实例后,可以在程序运行过程中,继续给实例绑定任何属性和方法。突出一个灵活。
举个例子:
class Foo: def __init__(self): self.a = 10 foo = Foo() foo.b = 20 foo.c = 30 print(foo.b, foo.c) # 20 30
这也太灵活了。如果我不想要这种动态特性,或者要限制属性的范围呢?
这时候就可以用到 __slots__
属性了:
class Foo: __slots__ = ('a', 'b') def __init__(self): self.a = 10 foo = Foo() foo.b = 20 foo.c = 30 # 输出报错: # AttributeError: 'Foo' object has no attribute 'c'
这在多人协作开发的场景可能派上用场,你可以用代码明确告诉同伴,这个类只允许有这几个属性,不要再添加了。
__slots__
的另一个应用场景是优化内存、提高查询效率。
为了测试,定义两个类,并分别进行一万次实例化放到列表里:
class Foo(): def __init__(self): self.a = 'xyz' self.b = 100 self.c = True class Bar(): __slots__ = ('a', 'b', 'c') def __init__(self): self.a = 'xyz' self.b = 100 self.c = True foos = [Foo() for _ in range(10000)] bars = [Bar() for _ in range(10000)]
接着使用某些手段,查看内存占用情况:
Partition of a set of 30026 objects. Total size = 2259528 bytes. Index Count % Size % Cumulative % Kind (class / dict of class) 0 10000 33 1040000 46 1040000 46 dict of __main__.Foo 1 10000 33 560000 25 1600000 71 __main__.Bar 2 10000 33 480000 21 2080000 92 __main__.Foo ...
序号 0 和 2 均为 Foo
实例占用的内存,序号 1 为 Bar
实例占用。可以看到,使用了 __slots__
的 Bar
类,占用内存压缩到只剩惊人的 36.8%,而你付出的劳动仅仅只有一行代码。
原因就在于 Python 原本的属性命名空间 __dict__
占用内存较多,而 __slots__
显然在幕后禁止了 __dict__
的创建并优化了其性能。
查看内存可用
guppy3
库,使用方法见heapy。
除了内存得到优化,查询效率也有提升。
用下面的代码测试:(适当增加了实例数量)
import time # Foo 和 Bar 的定义略过... foos = [Foo() for _ in range(1000000)] bars = [Bar() for _ in range(1000000)] t1 = time.time() for item in foos: item.a item.b = 50 del item.c t2 = time.time() for item in bars: item.a item.b = 50 del item.c t3 = time.time() print(t2 - t1) # 0.20045256614685059 print(t3 - t2) # 0.11068224906921387
查询速度提升了 44.8%,相当不错。
需要指出的是,实际情况下很少有如此大量的实例化对象。是否真的需要用
__slots__
牺牲灵活性以优化效率,请谨慎考虑。
最后要注意的是,__slots__
对继承的子类是不起作用的。除非在子类中也定义 __slots__
,此时子类允许定义的属性就是自身的 __slots__
加上父类的 __slots__
。
本系列文章开源发布于 Github,传送门:Python魔法方法漫游指南
看完文章想吐槽?欢迎留言告诉我!