Python魔法方法漫游指南:可调用和槽
2761 views, 2023/10/17 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魔法方法漫游指南
看完文章想吐槽?欢迎留言告诉我!