python魔术修炼手册

前言

又到了愉快的周末了,最近在看同事写的一段python业务代码,深感其对python语言理解之深刻。受其启发,故而本次围绕python当中的魔术方法进行一些探讨。

本篇的讨论将会结合实际代码,根据本人经验结合对应代码使用场景的方式进行说明和列举

更新

  1. 补充了算数计算相关模式方法,东西太多了…下周接着补充~(20200920/周日/晴)
  2. 这一熬就熬到了国庆节,终于又有空更新博客了,继续来唠一唠python魔术方法,补充了自定义容器和上下文管理相关。(20201002/周五/晴)

来自语言的魔术

也许你已经听过无数次这样的论调:python中一切皆对象
那么对于魔术方法来说,他们即是面向对象的python的一切。我们在探索一切皆对象的python时,经常会看到他们的身影。正是这一道道魔法施加,构成了python语言灵活的根本。

从结构上来讲,他们是被双下划线所包围起来的方法,形如__xxx__(),有些我们基本不会用到(当然本篇也不会详细去讨论),但是对于一些我们常见的魔术方法,通过巧妙的运用,我们可以写出非常优美的代码。

听到这儿是不是有点心动了呢,那么接下来就一起看看吧~

python实例构建与初始化

new :构建类实例时首先执行

简单说明一下类的构建过程:
元类(metaclass) 可以通过方法 __metaclass__创造了类(class),而类(class)通过方法 __new__ 创造了实例(instance)

__metaclass__这里不会在进行深入的解释,有兴趣的可自行查阅

说句实话,__new__方法实际使用的还真不多(至少比__metaclass__多~),不过有一个场景,你应该会使用到——单例模式,我们在这里就通过单例模式来看看__new__方法是如何起作用的。

class Single(object):
    _instance = None
    def __new__(cls, *args, **kw):
        if cls._instance is None:
            cls._instance = object.__new__(cls, *args, **kw)
        return cls._instance
    def __init__(self):
        pass

__new__的第一个参数是这个类本身(cls),它本身的职责是创建类的实例。那既然已经写出来了,我们当然可以对他构建实例的行为进行干预。在单例模式场景下,我们加了如下逻辑:

  1. 首先给这个类添加了一个成员变量_instance
  2. 在实例化类时,首先对类变量_instance进行判断
  3. 如果_instanceNone,则正常执行类的实例化过程,并将其赋值给_instance(注意:此时_instance是属于类本身的)
  4. 当再次实例化该类时,由于之前类成员_instance已经实例化过了,此时_instance不为None,代码将直接把_instance返回

init :通过它对类实例进行初始化

这个应该都熟悉了,我们通过一些操作对类实例进行初始化

class Demo(object):
    def __init__(self, var1, var2):
    	self.var1 = var1
    	self.var2 = var2

我们在实例化类时传入的任何参数都会给到__init__方法,比如:

demo = Demo(var1, var2)

上述操作在实例初始化期间进行了一些赋值,当然如果需要,也可以在里面加上更加复杂的逻辑

属性操作

实例化也完了,接下来该干点什么呢,把我们的类拉出来秀一秀~为此,我们需要再给实例上点“ buff ”~

getattr :当访问一个实例中不存在的属性时会调用它

是的,当有人想要在你的实例中访问一个不存在的属性时,python为你提供了一个方法进行应对,这就是__getattr__,话不多说,直接看例子

class Demo(object):
    def __init__(self, var1, var2):
        self.var1 = var1
        self.var2 = var2

    def __getattr__(self, item):
        return f'属性 {item} 不存在!'

if __name__ == '__main__':
    demo = Demo(1, 2)
    print(f'var1 的值是 {demo.var1}')
    print(demo.var3)  # var3是一个不存在的属性
    print(getattr(demo, 'var3'))  # 另一种调用方式

执行一下,看看结果

var1 的值是 1
属性 var3 不存在!
属性 var3 不存在!

哦吼,结果显而易见了,基本的使用就是这样,那么什么场景下我们会使用到__getattr__呢,一般来说可以用于:

  1. 对普通拼写错误的获取和重定向
  2. 对获取一些不建议的属性时给出相对应的警告

然而 python是一个自由度非常高的语言,知道了用法,我们可以根据自己的需要将代码写的更加富有表现力,这就需要读者有一定的经验和天马行空的想象力了~接下来所有魔术方法皆是如此。

getattribute:访问存在属性时添加额外行为

class Demo(object):
    def __init__(self, var1, var2):
        self.var1 = var1
        self.var2 = var2

    def __getattribute__(self, item):
        print(f'{item} 被获取了!')
        return super(Demo, self).__getattribute__(item)

if __name__ == '__main__':
    demo = Demo(1, 2)
    _ = demo.var1
    _ = demo.var2

运行一下看看

var1 被获取了!
var2 被获取了!

对比__getattr__,在访问对象中存在的属性时__getattribute__会生效。

setattr :给对象属性赋值

当我们给对象属性赋值时,就会调用到__setattr__方法了,相对于__getattr____setattr__对属性存在与否不做判断,所以你的赋值行为总是会成功的~那么下面直接看代码进行理解

class Demo(object):
    def __init__(self, var1, var2):
        self.var1 = var1
        self.var2 = var2

    def __setattr__(self, key, value):
        print(f'{self.__class__.__name__} 中设置了属性 {key}, 值为 {value}')
        self.__dict__[key] = value

    def __getattr__(self, item):
        return f'属性 {item} 不存在!'

if __name__ == '__main__':
    demo = Demo(1, 2)
    demo.var3 = 3

运行一下,查看结果

Demo 中设置了属性 var1, 值为 1
Demo 中设置了属性 var2, 值为 2
Demo 中设置了属性 var3, 值为 3

OK,因为所有的赋值动作都会调用__setattr__,所以代码中不单单是新属性var3,连同var1var2在赋值时也一起调用了__setattr__

无限递归赋值

上述赋值代码当中,我们通过demo.var3 = 3var3进行了赋值,这触发了__setattr__方法,那么我们如果在__setattr__里面执行demo.var3 = 3的操作,代码将会如何执行呢?

直接看代码

count = 0  # 这里使用全局变量,避免计数时也陷入赋值递归的情况

class Demo(object):

    def __init__(self):
        pass

    def __setattr__(self, key, value):
        global count
        if count == 100:
            print('终止!')
            return
        print(f'{self.__class__.__name__} 中第 {count} 次为 {key} 进行赋值')
        count += 1
        self.var3 = 3

    def __getattr__(self, item):
        return f'属性 {item} 不存在!'


if __name__ == '__main__':
    demo = Demo()
    demo.var3 = 3

运行下看看

Demo 中第 0 次为 var3 进行赋值
Demo 中第 1 次为 var3 进行赋值
Demo 中第 2 次为 var3 进行赋值
Demo 中第 3 次为 var3 进行赋值
...
Demo 中第 97 次为 var3 进行赋值
Demo 中第 98 次为 var3 进行赋值
Demo 中第 99 次为 var3 进行赋值
终止!

当执行demo.var3 = 3时,调用了__setattr__,而__setattr__内部执行到demo.var3 = 3时,又会触发__setattr__本身。

教科书式的递归~

del:当对象被执行垃圾回收时调用它

__del__平时用的不咋多,简单说明一下, 它定义的是当一个对象进行垃圾回收时候的行为。但是不要轻易认为使用del attr就会调用掉它(你可能只是删除了一个引用),注意是垃圾回收的时候,那么什么时候会进行垃圾回收呢

  1. 调用gc.collect(),需要先导入gc模块。
  2. 当gc模块的计数器达到阀值的时候。
  3. 程序退出的时候

更多垃圾回收可以单独搜索 python垃圾回收 进行更多了解,总儿言之,当这个对象在删除时需要进行更多清洁工作的时候用处会比较大。

del attr时,对象中被调用的方法为__delattr__,下面来看一个简单的例子

class Demo(object):
    def __init__(self, var1, var2):
        self.var1 = var1
        self.var2 = var2

    def __getattr__(self, item):
        print(f'{item} 属性不存在')

    def __getattribute__(self, item):
        print(f'{item} 被获取了!')
        return super(Demo, self).__getattribute__(item)
    
    def __delattr__(self, item):
        print(f'{item} 被删除了!')
        super(Demo, self).__delattr__(item)

if __name__ == '__main__':
    demo = Demo(1, 2)
    del demo.var1
    _ = demo.var1

运行结果

var1 被删除了!
var1 被获取了!
var1 属性不存在

因为var1实例创建时就已经存在,故而即使后面删除var1,运行时依旧会将其当作已存在的成员变量调用,发现被删除之后继而继续执行__getattr__方法

实现上下文管理

上下文管理在python中是一个经常用到的功能,基本语法就是

with .. as ...:
	# do sth

最常见的就是打开文件了

with open('../text.txt') as txt:
	txt.readlines()

with声明的代码段中,可以嵌入式的对with后面的对象做一些额外的操作,主要是在with后面的语句执行完之后with下方代码块执行完之后进行。对应的函数分别是
__enter_____exit__,那我们直接通过一段初始化FTP的代码看看这两个方法是如何工作的。

from ftplib import FTP, error_perm

class FtpClient(object):

    def __init__(self, host='xx.xxx.xx.xxx', username='xxx', passwd='xxx', target_dir='/xxx'):
        self.ftp = FTP()  # 实例化FTP
        self.host = host
        self.username = username
        self.passwd = passwd
        self.target_dir = target_dir

    def __enter__(self):
        self.ftp.set_debuglevel(2)  # 设置日志等级
        self.ftp.connect(self.host)  # 连接
        self.ftp.login(self.username, self.passwd)  # 登陆
        self.ftp.cwd(self.target_dir)  # cd目录操作
        return self.ftp  # 返回了init中的初始化的ftp对象

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.ftp.close()  # 关闭ftp连接

    @staticmethod
    def download_file(filename, remote_abs_dir='/xxx'):
        with FtpClient(target_dir=remote_abs_dir) as ftp:
            with open('/'.join([settings.LOCAL_DEP_FILE_PATH, filename]), 'wb') as f:
                ftp.retrbinary(f'RETR {filename}', f.write)

    @staticmethod
    def upload_file(filename, data, remote_abs_dir='/xxx'):
        with FtpClient(target_dir=remote_abs_dir) as ftp:
            ftp.storbinary(f'STOR {filename}', data

if __name__ == '__main__':
	 with FtpClient() as ftp:
	     try:
	         ftp.mkd('/temp')  # 在ftp目录中新建一个temp文件夹
	     except error_perm:
	         pass

正常情况下 FTP 实例化之后还需要进行连接、登陆,操作完FTP之后需要对连接进行释放,这一套完整的流程,我们就可以放到__enter_____exit__中完成,避免在使用过程中手动调用。

比较你的对象

自定义各种比较行为:>, < , == , !=等等

直接看代码比任何解释都来的直接

class Demo(object):

    def __init__(self):
        self.val = 100  # 定义了一个专门用来与其他对象做比较的值

    def __eq__(self, other):
        """
        当对象进行 == 比较时会调用它
        """
        return self.val == other

    def __ne__(self, other):
        """
        当对象进行 != 比较时会调用它
        """
        return self.val != other

    def __lt__(self, other):
        """
        当对象进行 < 比较时会调用它
        """
        return self.val < other

    def __gt__(self, other):
        """
        当对象进行 > 比较时会调用它
        """
        return self.val > other

if __name__ == '__main__':
    demo = Demo()
    print(f'demo.val的值为:{demo.val}')
    for res in [
        demo == 100,
        demo > 100,
        demo < 100,
        demo != 100
    ]:
        print(f'比较结果是:{res}')

运行结果

demo.val的值为:100

比较结果是:True
比较结果是:False
比较结果是:False
比较结果是:False

我们还可以通过__cmp__来定制一个通用的比较方法,但是并不建议这么做,并且在python3中也已经将该方法废弃了。

自定义对象算数运算行为:+,-,*,/ 等等

继续上代码

class Demo(object):

    def __init__(self):
        self.val = 1  # 定义了一个专门用来做算数运算的成员变量

    def __add__(self, other):
        """
        使用 + 号运算
        """
        self.val += other
        return self.val

    def __sub__(self, other):
        """
        使用 - 号运算
        """
        self.val -= other
        return self.val

    def __mul__(self, other):
        """
        使用 * 号运算
        """
        self.val *= other
        return self.val

    def __truediv__(self, other):
        """
        使用 / 号运算
        """
        self.val /= other
        return self.val

    def __floordiv__(self, other):
        """
        使用 //(地板除) 号运算
        """
        self.val //= other
        return self.val

    def __abs__(self):
        """
        取绝对值
        """
        self.val = abs(self.val)
        return self.val

    def __neg__(self):
        """
        一元 - 号运算
        """
        self.val = -self.val
        return self.val

    def __pos__(self):
        """
        一元 + 号运算
        """
        self.val = +self.val
        return self.val

    def __invert__(self):
        """
        按位取反
        """
        self.val = ~int(self.val)
        return self.val

	# 还有一些增量方法比如__iadd__, __isub__之类的,太多了,以后再补充

if __name__ == '__main__':
    demo = Demo()
    for res in [
        f'demo.val值是: {demo.val}',
        f'{demo.val} + 1 结果是:{demo + 1}',
        f'{demo.val} - 1 结果是:{demo - 1}',
        f'{demo.val} * 100 结果是:{demo * 100}',
        f'{demo.val} / 2 结果是:{demo / 2}',
        f'{demo.val} // 30 结果是:{demo // 30}',
        f'{demo.val} 取绝对值 结果是:{abs(demo)}',
        f'{demo.val} 一元运算 - 结果是:{-demo}',
        f'{demo.val} 一元运算 + 结果是:{+demo}',
        f'{demo.val} 一元运算 按位取反(~) 结果是:{~demo}',
    ]:
        print(res)

运行结果

demo.val值是: 1
1 + 1 结果是:2
2 - 1 结果是:1
1 * 100 结果是:100
100 / 2 结果是:50.0
50.0 // 30 结果是:1.0
1.0 取绝对值 结果是:1.0
1.0 一元运算 - 结果是:-1.0
-1.0 一元运算 + 结果是:-1.0
-1.0 一元运算 按位取反(~) 结果是:0

python中定义了一系列针对不同运算方法,我们按照计算逻辑定义基本规则,也可以天马行空的进行一些自定义配置,python就是这么自由,比如我们就是__add__写一个简单的根据配方做点心饮料的功能

from hashlib import md5


class Cup:

    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = object.__new__(cls)
            cls._instance.material = []
        return cls._instance

    def __init__(self, kind=2):
        self.material = []
        self.kind = kind
        self.menu = {
            'f16c77c42af790de9f94543de009bbc9': '冰激凌',
            'cda4ae0ddf151f2361c97a86534e4f49': '红茶拿铁',
            'c607fc43d89287ae17adf8d9c9ee5aa7': '旺旺碎冰冰'
        }

    def __add__(self, other):
        self.material.append(other)
        if len(self.material) >= self.kind:
            return self.menu.get(md5(bytes(''.join(sorted(self.material)), encoding='utf-8')).hexdigest(), '未知的配方')
        return self


if __name__ == '__main__':
    print(f"做出了 {Cup(4) + '奶油' + '面粉' + '冰块' + '糖'} !")
    print(f"做出了 {Cup(3) + '冰块' + '饮料' + '色素'} !")
    print(f"做出了 {Cup(2) + '红茶' + '拿铁'} !")

运行结果

做出了 冰激凌 !
做出了 旺旺碎冰冰 !
做出了 红茶拿铁 !

只要脑洞大,代码玩出花~(工程代码还是建议在保证性能的基础上,越简洁易读越好)

描述你的对象

当(被描述的)类实例作为其他类实例的成员被调用时,将会调用被描述类的__get__方法,有点绕,直接看代码

class Demo:

    def __init__(self, var1, var2):
        self.var1 = var1
        self.var2 = var2

    def __get__(self, instance, owner):
        print(f'{owner.__name__} 调用我了!')
        return self.var1

    def __set__(self, instance, value):
        instance.use_var1 = value
        print(f'instance.use_var1 修改后为: {instance.use_var1}')

    def __delete__(self, instance):
        print('delete 被调用了~')
        del instance.use_var1

class UseDemo:
    demo_ = Demo(1, 2)

    def __init__(self):
        self.use_var1 = 1


if __name__ == '__main__':
    demo = Demo(1, 2)
    print(f'demo: {demo}')
    use_demo = UseDemo()
    print(f'use_demo.demo: {use_demo.demo_}')
    use_demo.demo_ = 2
    del use_demo.demo_

运行一下看看

demo: <__main__.Demo object at 0x10bbca0f0>
UseDemo 调用我了!
use_demo.demo: 1
instance.use_var1 修改后为: 2
delete 被调用了~

同样是展示实例化之后的demo,在UseDemo中实例化Demo后调用时,返回是Demovar1的值,这是因为__get__起作用了。__set____delete__同理

这种描述器方法的一个典型的使用场景是用不同的单位表示同一个数值,或者表示某个数据的附加属性。

但是呢,还是那句话,基本方法是这样,但是最好不要让他成为局限你使用方式的枷锁。

构建自定义容器对象

这里的容器,指的就是python中常见的数据结构对象,比如大家都熟悉的列表(list)、字典(dict)、元组(tuple)这些。

这里白嫖了一段代码简单修改了一下并附加了注释,用来定义容器的几个魔术方法都已经涉及到了。


class Functionalist:
    """
    实现了内置类型list的功能,并丰富了其他一些方法:
    """

    def __init__(self, values=None):
        if not values:
            self.values = []
        else:
            self.values = values

    def __len__(self):
        """
        用来展示容器的长度,必须实现
        """
        return len(self.values)

    def __getitem__(self, key):
        """
        执行self[key]时将会调用该方法,必须实现
        """
        return self.values[key]

    def __setitem__(self, key, value):
        """
        执行 self[key] = value 时调用该方法
        """
        self.values[key] = value

    def __delitem__(self, key):
        """
        执行 del self[key] 时调用该方法
        """
        del self.values[key]

    def __iter__(self):
        """
        执行 for..in.. 或者 iter(self) 时将会使用到该方法
        该方法要求返回一个迭代器
        """
        print('__iter__ 被调用了!')
        return iter(self.values)

    def __reversed__(self):
        """
        当该对象被内建函数reversed()调用时调用该方法
        """
        return Functionalist(reversed(self.values))

    def __contains__(self, item):
        """
        当执行 in 操作时将会调用该函数
        """
        if item in self.values:
            print(f'{item} 存在于当前实例中')
            return
        print(f'{item} 不存在于当前实例中')

    def __missing__(self, key):
        """
        字典类型操作将会调用该函数
        e.g. 当 self[key] 不存在时将会调用
        """
        print(f'找不到key: {key}')

    def append(self, value):
        self.values.append(value)


if __name__ == '__main__':
    """demo"""
    func_list = Functionalist([1, 2, 3, 4, 5])
    print(f'func_list[1:] 结果为 {func_list[1:]}')
    print(f'len(func_list) 结果为 {len(func_list)}')
    func_list[0] = 2
    print(f'func_list[0] = 2 then func_list[0] 结果为: {func_list[0]}')
    del func_list[1]
    print(f'after del func_list[1] 结果为 {func_list[1]}')
    print(f'reversed(func_list) 结果为 {[el for el in reversed(func_list)]}')
    bool(2 in func_list)

运行输出

func_list[1:] 结果为 [2, 3, 4, 5]
len(func_list) 结果为 5
func_list[0] = 2 then func_list[0] 结果为: 2
after del func_list[1] 结果为 3
__iter__ 被调用了!
reversed(func_list) 结果为 [5, 4, 3, 2]
2 存在于当前实例中

值得一提的时,该实例在进行切片调用时(e.g. func_list[1:]) ,会将切片语句[1:] 翻译为[slice(1, None, None)],这两者可以认为是等价的

利用missing实现字典结构自动创建

一个挺实用的功能,巧妙的利用了__missing__函数实现当引用一个未定义的key时自动创建数据或者字典。

class AutoVivification(dict):

    def __missing__(self, key):
        """
        字典类型操作将会调用该函数
        e.g. 当 self[key] 不存在时将会调用
        """
        self[key] = type(self)()
        return self[key]


if __name__ == '__main__':
    ins = AutoVivification()
    ins['super']['man'] = 'clack'
    print(ins)

运行

{'super': {'man': 'clack'}}

结语

魔术方法这东西,用好了,可以让代码非常的优雅,但是,不当的使用也会让后面的人摸不着头脑,如果坚持要使用,请把注释加好~

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值