面向对象(OOP:Object Oriented Programming)6 -- 魔术方法

接魔术方法5

13.3.9 @contextlib.contextmanager装饰器

对于单值生成器函数,即只yield 1个值时,可使用@contextmanager装饰器,实现上下文管理,将yield返回值与as子句后面的变量绑定,并执行完yield后面的函数语句。
代码示例:

import contextlib


@contextlib.contextmanager  # 不使用此装饰器时,不会执行exit函数语句
def foo():
    print('enter')
    yield 123
    print('exit')  # 异常情况,不能保证exit语句执行,使用try...finally语句


def foo1():
    print('enter')
    try:
        yield 123
    finally:  # 通过try...finally语句,保证异常场景,exit语句也能执行
        print('exit')  


@contextlib.contextmanager
def foo2():
    for i in range(3):  # yield不止1个值时,将抛RuntimeError: generator didn't stop异常
        yield i


if __name__ == '__main__':
    # next(foo())
    with foo() as f:
        print(f)

    with foo2() as f:
        print(f)

小结: 对于业务逻辑较为复杂的场景,直接使用__enter____exit__方法更可靠。

13.3.10 @functools.total_ordering装饰器

比较运算符<、>、<=、>=如果每一个都在类中实现太麻烦了,通过@total_ordering装饰器,只需要在类中实现<、>、<=、>=中的任意一个,即可进行实例的相关比较。

import functools


@functools.total_ordering
class A:
    def __init__(self, x):
        self.x = x

    def __eq__(self, other):
        return self.x == other.x

    def __gt__(self, other):
        return self.x > other.x


if __name__ == '__main__':
    a1 = A(3)
    a2 = A(4)
    a3 = A(3)
    print(a1 == a2)
    print(a1 > a2)
    print(a1 <= a2)
    print(a1 == a3)

小结:从执行示例可以看出:注释__eq__方法后,a1==a2返回值为false,与预期不符。这与前述去重吻合:当没有给出__eq__时,判断例是否相等,默认比较内存中的id。所以==必须单独实现,否则比较结果不准确

13.3.11 反射 :__getattr__、__setattr__、__hasattr__

概念:
运行时:指程序被加载到内存中执行的时候。区别于编译时。
反射:reflection,指运行时获取类型定义信息。一个对象能够在运行时,像照镜子一样,反射出其类型信息就是反射。简单的说,在python中,能够通过一个对象,找出其type、class、attribute或method的能力,称为反射
例如:下述Point类的实例p,通过反射能力,在p.__dict__中找到自己的attribute,并且修改、增加自己的attribute。
通过__dict__获取、修改属性不优雅,python提供了内建函数:

方法意义说明
getattr(object, name[, default])通过name返回 object的属性值。属性不存在返回default,如果没有给出default,抛AttributeError。注意,name必须为字符串类型;
getattr搜索顺序,遵从mro搜索顺序,先从自己的__dict__中找,然后找class的…
setattr(object, name, value)object的属性存在则覆盖,不存在新增。注意:object为类则新增类属性,为实例则新增实例性。
动态增加属性,未绑定。增加到实例,则在实例的__dict__中;增加到类,则在类的__dict__中
hasattr(object, name)判断对象是否有这个名字的属性name必须为字符串类型
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y


if __name__ == '__main__':
    p = Point(2, 3)
    # 运用对象的反射能力,在`p.__dict__`中找到自己的attribute,并且修改、增加自己的attribute
    print(p.__dict__)
    p.__dict__['x'] = 3
    p.z = 12
    print(p.__dict__)
    # 通过反射能力,在`p.__dict__`中找到自己的attribute,并且修改、增加自己的attribute
    setattr(Point, 't', 14)
    print(p.__dict__)
    print(getattr(p, 't'))
    if not hasattr(p, 'lab'):
        setattr(p, 'lab', lambda x: print(x))
        setattr(Point, 'lab1', lambda self: print('lab1'))
    print(p.__dict__)
    print(Point.__dict__)
    p.lab('lab')
    p.lab1()

执行结果:

{‘x’: 2, ‘y’: 3}
{‘x’: 3, ‘y’: 3, ‘z’: 12}
{‘x’: 3, ‘y’: 3, ‘z’: 12}
14
{‘x’: 3, ‘y’: 3, ‘z’: 12, ‘lab’: <function at 0x000001D3B1129430>}
{‘module’: ‘main’, ‘init’: <function Point.init at 0x000001D3B1129160>, ‘dict’: <attribute ‘dict’ of ‘Point’ objects>, ‘weakref’: <attribute ‘weakref’ of ‘Point’ objects>, ‘doc’: None, ‘t’: 14, ‘lab1’: <function at 0x000001D3B1129940>}
lab
lab1

小结:动态增加属性的方式,和装饰器修饰一个类、Mixin方式的差异在于,Mixin和装饰器在编译时就决定了;而动态增、删属性的方式是运用反射能力,运行时改变类或实例的属性,更灵活。

练习: 通过getattr/setattr/hasattr改造命令分发器

class Dispatcher:
    def reg(self, cmd: str, fn):
        if not hasattr(self, cmd):
            setattr(self.__class__, cmd, fn)
        else:
            raise Exception('Exist')

    def run(self):
        while True:
            cmd = input('enter:')
            if cmd == 'q':
                return
            getattr(self, cmd, self.default_func)()

    @classmethod
    def default_func(cls):
        print('default')


if __name__ == '__main__':
    dis = Dispatcher()
    dis.reg('cmd1', lambda self: print('cmd1'))
    dis.reg('cmd2', lambda self: print('cmd2'))
    dis.run()

反射相关的魔术方法:__getattr__、__setattr__、__hasattr__

方法意义
__getattr__当通过搜索实例、实例的类及祖先类查找不到属性时,就会调用此方法;如果没有这个方法,就会抛AttributeError异常
__setattr__通过obj.x=100方式增加、修改实例的属性都要调用__setattr__,包括初始化函数中的实例属性赋值
__delattr__通过实例来删除属性时调用此方法,可以阻止通过实例删除属性的操作。但是通过类依然可以删除属性
__getattribute__实例所有的属性调用都从这个方法开始,它阻止了属性的查找;该方法应该返回一个值或者抛出AttributeError.
如果return值,则作为属性的查找结果;如果抛出AttributeError,则会直接调用__getattr__方法,表示没有找到属性。
除非明确知道__getattribute__用来做什么,否则不要使用此方法

示例1:

class Base:
    n = 0


class Point(Base):
    z = 6

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __getattr__(self, item):
        return 'missing {}'.format(item)

    def __setattr__(self, key, value):
        print('setattr')
        # self.__dict__[key] = value


if __name__ == '__main__':
    p1 = Point(1, 2)
    print(p1.x)
    print(p1.z)
    print(p1.n)
    print(p1.t)  # missing t

执行结果:

setattr
setattr
missing x
6
0
missing t

小结:

  • 示例中给出了__setattr__方法,因此在类初始化时,设置实例属性调用了此方法,但是__setattr__中没有完成实例__dict__的操作,所以实例__dict__没有属性x、y;通过p1.x访问实例属性x,才调用了 __getattr__
  • 因此:__setattr__方法,可以拦截对实例属性的增加、修改操作;如果给出了这个方法,要想增加、修改实例属性生效,必须在方法中实现__dict__操作。

示例2:

class P:
    Z = 4

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __delattr__(self, item):
        print('can not delete {}'.format(item))


if __name__ == '__main__':
    p2 = P(4, 5)
    del p2.x
    p2.z = 13
    del p2.z
    del p2.Z
    print(p2.__dict__)
    print(P.__dict__)
    del P.Z
    print(P.__dict__)

执行结果:

can not delete x
can not delete z
can not delete Z
{‘x’: 4, ‘y’: 5, ‘z’: 13}
{‘module’: ‘main’, ‘Z’: 4, …}
{‘module’: ‘main’, ‘init’: <function P.init at 0x000002349845DAF0>, …}

小结: __delattr__方法,可以阻止通过实例删除属性的操作。但是通过类依然可以删除属性。

示例3:

class Base:
    n = 0

    
class C(Base):
    C = 8

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __getattr__(self, item):
        return 'missing {}'.format(item)

    def __getattribute__(self, item):
        # raise AttributeError
        return 'getattribute:{}'.format(item)


if __name__ == '__main__':
    c = C(3, 4)
    print(c.__dict__)
    print(c.x)
    print(c.c)
    print(c.n)
    print(C.__dict__)
    print(C.C)

执行结果:

getattribute:dict
getattribute:x
getattribute:c
getattribute:n
{‘module’: ‘main’, ‘C’: 8, ‘init’: <function C.init at 0x00000177F7A1DCA0>, ‘getattr’: <function C.getattr at 0x00000177F7A1DD30>, ‘getattribute’: <function C.getattribute at 0x00000177F7A1DDC0>, ‘doc’: None}
8

小结:

  • 实例的所有属性访问,第一个都会调用__getattribute__方法,它阻止了属性的查找;该方法应该返回一个值或者抛出AttributeError.如果return值,则作为属性的查找结果;如果抛出AttributeError,则会直接调用__getattr__方法,表示没有找到属性。
  • 除非明确知道__getattribute__用来做什么,否则不要使用此方法

实例反射总结: 属性的查找顺序:实例调用__getattribute__() -> instance.__dict__ -> instance.__class__.__dict__ -> 继承的祖先类的__dict__ ->调用__getattr__()

13.3.12 描述器:__get__、__set__、__delete__

描述器: 当类的定义中实现了__get__、__set__、__delete__三个魔术方法中的任意一个时,那么这个类就是一个描述器。

  • 当仅实现了__get__,称为非数据描述器non data descriptor;
  • 当同时实现了__get__ + __set___delete__就是数据描述器data descriptor 。常见的数据描述器:property。

owner属主: 如果一个类的类属性包含描述器,那么这个类称为owner属主。

1. 描述器通过__get__方法,对类的实例读取类属性的控制:

__get___(self, instance, owner)方法:

  • instance:属主的实例,当通过类B的实例b读取属性x时,解释器自动传入b和B
  • owner:属主,当通过类B读取属性x时,instance为None,只传入B

示例:

class A:
    AA = 'aa'

    def __init__(self):
        print('A.init')
        self.a1 = 'a1'

    def __get__(self, instance, owner):
        print('A:__get__', self, instance, owner)
        return self  # 通常return self


class B:
    x = A()

    def __init__(self):
        print('B.init')
        # self.x = 100
        self.y = A()

        
if __name__ == '__main__':
    print(B.x)
    print(B.x.AA)  # 通过类B读取类属性x,因为x是一个描述器,触发__get__
    print('~~~~~~~~~~')
    b = B()
    print(b.x.AA)  # 通过类B的实例b读取类属性x,因为x是一个描述器,触发__get__
    print('~~~~~~~~~~')
    print(b.y.a1)  # 通过实例属性访问类A的实例的属性,不会触发__get__

小结:

  • 当类A的实例x是类B的属性时,如果类A中给出了__get__方法,则对类B属性x的读取(不管是通过B的实例或B),或者进一步通过属性x访问类A的属性,都会触发__get__方法;所以一般__get__方法返回self。
  • 当类A的实例是类B的实例的属性时,通过类B的实例属性访问类A的实例的属性的时候,不会触发__get__方法

2. 数据描述器通过__set__或者__delete__方法,对类的实例修改类属性的控制:

示例:

class C:
    CC = 'cc'

    def __init__(self):
        print('C.init')
        self.c1 = 'c1'

    def __get__(self, instance, owner):
        print('C:__get__', self, instance, owner)
        return self  # 通常return self

    def __set__(self, instance, value):
        print('C:__set__', self, instance, value)


class D:
    x = C()

    def __init__(self):
        print('D.init')
        self.x = 100
        self.y = 123


if __name__ == '__main__':
    print('-----------')
    print(D.x)
    d = D()
    print(d.__dict__)  # 查看实例d的__dict__是否有属性x
    print(D.__dict__)
    print('~~~~~~~~~~~~~~')
    print(d.x)
    d.x = 100    # 尝试为实例d增加属性x
    print(d.x)   # 查看‘赋值即定义’是否可以增加实例属性x。从结果可以看出,由于类属性x是数据描述器,由于受数据描述器拦截,无法给实例d的__dict__写入x,即增加x属性。
    print(d.__dict__)
    print('~~~~~~~~~~~~~')

小结

  • 当类D中存在一个标识符为x的类属性,且这个属性x为数据描述器时,则这个类属性x在类D的__dict__的优先级高于类D的实例__dict__的优先级;即通过类D的实例d进行访问、修改属性x操作时(d.x或d.x=100),优先访问D.__dict__
  • 所以,一但类属性x为数据描述器,则实例b只能操作类属性x,无标识符为x的实例属性;而操作类属性x又受描述器控制,才会有d.x=100时触发了数据描述器的__set__方法,有点像运算符重载d.x=100 -> d.x.__set__(d, 100)
  • 本质上:数据描述器d.x=100 -> d.__dict__.get(x) -> d.__dict__没有x,继续找类的__dict_ ->d.__class__.__dict__[x] -> 找到了,但是x是描述器,类似运算符重载 -> d.__class__.__dict__[x].__set__(),走不到写d.__dict__的操作;d.x也是一样,从自己的__dict__中找不到x,就找类的,而类属性x又是一个描述器,进而触发了__get__.

描述器总结: python的方法几乎都实现为非数据描述器(例如staticmethod、classmethod),因此实例可以重新定义一个标识符与类属性一样的实例属性,从而允许单个实例可以获得与同一类的其他实例不同的行为。property函数实现为一个数据描述器,因此被property装饰的类属性z,实例无法定义一个同为标识符z的实例属性。
示例:

class E:
    
    @classmethod
    def foo(cls):
        pass
    
    @staticmethod
    def bar():
        pass
    
    @property
    def z(self):
        return 2
    
    def __init__(self):
        self.foo = 100  # foo和、bar方法都为非数据描述器,所以可以直接赋值修改
        self.bar = 123
        self.z = 'z'  # z方法为数据描述器,不能在实例中替换

练习:实现classmethod和staticmethod

import functools


class ClassMethod:
    def __init__(self, fn):
        print(fn)
        self._fn = fn

    def __get__(self, instance, owner):
        print(self, instance, owner)
        # return self._fn(owner)
        return functools.partial(self._fn, owner)


class StaticMethod:
    def __init__(self, fn):
        print(fn)
        self._fn = fn

    def __get__(self, instance, owner):
        print(self, instance, owner)
        return self._fn


class A:

    @StaticMethod  
    def foo():
        print('static')

    @ClassMethod  # foo = ClassMethod(foo) -> 新的foo是类ClassMethod的实例, 而ClassMethod非数据描述器,故通过A.foo读取类属性foo时,触发调用__get__,而__get__返回的是固定了参数cls的新的foo。所有最后可以直接foo()执行函数。
    def bar(cls):
        print(cls.__name__)


if __name__ == '__main__':
    f = A.foo
    print(f)
    f()
    b = A.bar
    print(b)
    b()

业务运用: 检查实例的参数类型

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一个两个四个三

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值