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魔法方法漫游指南

看完文章想吐槽?欢迎留言告诉我!




本文作者: 杜赛
发布时间: 2021年07月19日 - 11:36
最后更新: 2023年10月17日 - 11:37
转载请保留原文链接及作者