Python魔法方法进阶

纸上得来终觉浅,绝知此事要躬行。

Python魔法方法进阶

在Python中,所有以双下划线包起来的方法,都统称为“魔术方法”。比如我们接触最多的init__。有些魔术方法,我们可能一辈子都不会再遇到了。而有些魔术方法,巧妙使用它可以构造出非常优美的代码,比如将复杂的逻辑封装成简单的API。介绍的顺序大概是:常见的先介绍,越少见的越靠后讲。

魔术方法 调用方式 解释
__new__(cls [,...]) instance=MyClass(args) 在创建实例的时候被调用
__init__(self [,...]) instance=MyClass(args) 在创建实例的时候被调用
__cmp__(self, other) self==other, self>other 在比较的时候调用
__pos__(self) +self 一元加运算符
__neg__(self) -self 一元减运算符
__invert__(self) ~self 取反运算符
__index__(self) x[self] 对象被作为索引使用的时候
__nonzero__(self) bool(self) 对象的布尔值
__getattr__(self, name) self.name 访问一个不存在的属性时
__setattr__(self, name, val) self.name=val 对一个属性赋值时
__delattr__(self, name) del self.name 删除一个属性时
__getattribute__(self, name) self.name 访问任何属性时
__getitem__(self, key) self[key] 使用索引访问元素时
__setitem__(self, key, val) self[key] = val 对某个索引值赋值时
__delitem__(self, key) del self[key] 删除某个索引值时
__iter__(self) for x in self 迭代时
__contains__(self, value) value [not] in self 使用 in 操作测试关系时
__concat__(self, value) self+other 连接两个对象时
__call__(self [,...]) self(args) “调用”对象时
__enter__(self) with self as x: with 语句环境管理
__exit__(self, exc, val, trace) with self as x: with 语句环境管理
__getstate__(self) pickle.dump(file, self) 序列化
__setstate__(self) data=pickle.load(file) 序列化

1. 构造函数

控制属性访问特点

魔法函数 解释说明
__new__ 构造函数
__init__ 初始化函数
__del__ 删除函数

函数作用

  • 用于创建对象时,初始化对象,也就是为对象成员变量赋初始值。

注意要点

  • __new__要早于__init__的执行
  • __new__是类方法而__init__是实例方法

示例说明

  • 构造方法并不负责创建实例,而是用来初始化实例变量
# __new__用户设置初始化类变量PAYLOAD的值
1. 这里的super函数让其余变量继承自父类,再对新变量赋值
class ExampleClass:
    def __new__(cls, *args, **kwargs):
        print('Create new instance...')
        instance = super().__new__(cls)
        instance.PAYLOAD = (args, kwargs)
        return instance

    def __init__(self, payload):
        print('Init instance...')
        self.payload = payload

    def __del__(self):
        print('Del instance...')
In [1]: from func import ExampleClass

In [2]: ec = ExampleClass([1, 2])
Create new instance...
Init instance...

In [3]: ec.PAYLOAD
Out[3]: (([1, 2],), {})

In [4]: ec.payload
Out[4]: [1, 2]

In [5]: del ec
Del instance...
  • 如果__new__函数不创建实例,__init__函数是不会执行的
# 这里并没有使用super函数,而是直接返回了object的__new__方法
In [1]: class ExampleClass:
   ...:     def __new__(cls, *args, **kwargs):
   ...:         print("call __new__")
   ...:         print(type(cls))
   ...:         return object.__new__(cls)
   ...:
   ...:     def __init__(self, x):
   ...:         print("call __init__")
   ...:         print(type(self))
   ...:         s1 = set(dir(self))
   ...:         self.x = x
   ...:         s2 = set(dir(self))
   ...:         print(s2-s1)
   ...:

In [2]: a = ExampleClass(5)
call __new__

call __init__

{'x'}
In [3]: class ExampleClass:
   ...:     def __new__(cls, *args, **kwargs):
   ...:         print("call __new__")
   ...:         print(type(cls))
   ...:
   ...:     def __init__(self, x):
   ...:         print("call __init__")
   ...:         print(type(self))
   ...:         s1 = set(dir(self))
   ...:         self.x = x
   ...:         s2 = set(dir(self))
   ...:         print(s2-s1)
   ...:

In [4]: a = ExampleClass(5)
call __new__

2. 对象可视化

  • 控制属性访问特点
魔法函数 解释说明
__repr__ 字符串(程序友好)
__str__ 字符串(用户友好)
__bytes__ 二进制
  • 代码示例说明
In [1]: class A:
   ....:     def __init__(self, name):
   ....:         self.name = name
   ....:     def __repr__(self):
   ....:         return self.name
   ....:     def __str__(self):
   ....:         return 'call __str__ name is {0}'.format(self.name)
   ....:     def __bytes__(self):
   ....:         return 'call _bytes__ name is {0}'.format(self.name).encode('utf-8')
   ....:

In [2]: cls = A('escape')

1. __repr__()
In [3]: cls
Out[3]: 

1. __str__()
In [4]: print(a)
call __str__ name is escape

1. __repr__()
In [5]: repr(a)
Out[5]: escape

1. __str__()
In [5]: str(a)
Out[5]: 'call __str__ name is escape'

1. __bytes__()
In [6]: bytes(a)
Out[6]: b'call _bytes__ name is escape'

3. 反射

可以使用代码获取对象本身的一些属性,如对象的字段、方法等,就算是反射。

  • 魔术方法特点
魔法函数 解释说明
xxx.__class__ 获取当前实例的类名
xxx.__doc__ 获取文档字符串
xxx.__dir__ 内建函数 dir 实际调用的函数
xxx.__dict__ 获取此实例持有的所有变量
xxx.__module__ 返回当前所在的模块
xxx.__name__ 获取类的名称,实例没有
  • 代码示例说明
In [34]: class ExcapleClass:
   ....:     X = 1
   ....:     Y = 2
   ....:     Z = 3
   ....:     def __init__(self, x, y, z):
   ....:         self.x = x
   ....:         self.y = y
   ....:         self.z = z
   ....:     def method(self):
   ....:         pass
   ....:

In [35]: ec = ExcapleClass(1,2,3)

In [36]: ec.__doc__

In [37]: ec.__dict__
Out[37]: {'x': 1, 'y': 2, 'z': 3}

In [38]: ec.__class__
Out[38]: __main__.Grok

In [39]: ec.__dir__
Out[39]: 

In [40]: ec.__dir__()
......

4. 比较运算符重载

  • 控制属性访问特点
魔法函数 解释说明
__lt__ 小于
__le__ 小于等于
__eq__ 等于
__nq__ 不等于
__gt__ 大于
__ge__ 大于等于
  • 代码示例说明
# 需要在对象定义的时候通过比较运算重载来实现比较的
In [1]: class Person:
   ....:     def __init__(self, age):
   ....:         self.age = age
   ....:     def __lt__(self, other):
   ....:         return self.age < other.age
   ....:     def __le__(self, other):
   ....:         return self.age <= other.age
   ....:     def __eq__(self, other):
   ....:         return self.age == other.age
   ....:     def __nq__(self, other):
   ....:         return self.age == other.age
   ....:     def __gt__(self, other):
   ....:         return self.age > other.age
   ....:     def __ge__(self, other):
   ....:         return self.age >= other.age
   ....:

In [2]: p1 = Person(18)

In [3]: p2 = Person(14)

In [4]: p1 > p2
Out[4]: True

In [5]: p1 < p2
Out[5]: False

5. 上下文管理器

with后面紧跟一个实例,这个实例具有enterexit方法。

Python魔法方法进阶

魔术方法特点

魔法函数 解释说明
__enter__ 进入with块后立即执行__enter__方法
__exit__ 退出with块后立即执行__exit__方法

方法特性说明

  • as子句用于把__enter__方法的返回值,赋值给一个变量
  • __exit__方法在报错的时候,返回三个参数的一个元组(exc_type, exc, traceback)
    • exc_type表示报错的类型
    • exc表示报错的信息
    • traceback表示堆栈信息

方法使用场景

  • 成对出现操作,并且必须确保两个都执行
  • 资源打开和关闭
  • 加锁和解锁
  • 进入代码块之前需要执行一些操作
  • 初始化
  • 权限判断
  • 离开代码块之后需要执行一些操作
  • 关闭资源连接
  • 解锁

代码示例说明

  • 这里我们引入的是一个简单的示例进行方式和演示
# 当socket出现问题,连接也是会正常关闭的,不用手动关闭连接

import socket

class SocketClient:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.conn = socket.socket()

    def __enter__(self):
        self.conn.connect((self.host, self.port))
        return self.conn

    1. 接收三个参数 __exit__(self, exc_type, exc, traceback)
    def __exit__(self, *args, **kwargs):
        self.conn.close()

with SocketClient('192.168.1.199', 8000) as conn:
    print(conn)
  • 相关的库contextlib可以帮助我们更好的使用上下文管理
import contextlib

@contextlib.contextmanager
def connect(host, port):
    conn = socket.socket()
    try:
        conn.connect((host, port))
        yield conn
    finally:
        conn.close()

with connect('192.168.1.199', 8000) as conn:
    print(conn)

6. 属性访问控制

使用字符串操作对象的属性,通过点操作符操作的都可以使用,如属性、方法等。

控制属性访问特点

魔法函数 解释说明
__getattr__ 在属性被访问而对象没有这样的属性时自动调用
__setattr__ 试图给属性赋值时自动调用
__delattr__ 试图删除属性时自动调用
__getattribute__ 在属性被访问时自动调用,且只适用于新式类;它和__getattr__的区别是无论属性是否存在,都要被调用;通过访问该方法无法获取时,再使用__getattr__获取,依旧无法获取则报错

特殊方法说明

特殊方法 解释说明
getattr 调用__getattribute__方法,通过字符串的方式访问类的属性,等价于a.num
setattr 实际调用__setattr__方法,通过属性名修改属性,等价于a.num=3
delattr 实际调用__delattr__方法,通过属性名删除属性,等价于del a.num
# getattr
getattr(...)
    getattr(object, name[, default]) -> value

    Get a named attribute from an object; getattr(x, 'y') is equivalent to x.y.
    When a default argument is given, it is returned when the attribute doesn't
    exist; without it, an exception is raised in that case.
# setattr
setattr(...)
    setattr(object, name, value)

    Set a named attribute on an object; setattr(x, 'y', v) is equivalent to
    ``x.y = v''.
# delattr
delattr(...)
    delattr(object, name)

    Delete a named attribute on an object; delattr(x, 'y') is equivalent to
    ``del x.y''.

代码示例说明

  • 对于getattr、setattr、delattr三个属性的初步了解。
class Attr:
    X = 1
    Y = 2

    def __init__(self, name):
        self.name = name

    def show_num(self):
        return self.name

    def __setattr__(self, name, value):
        print('settattr')

    def __getattr__(self, name):
        print('getattr')

    def __getattribute__(self, name):
        print('getattribute')

    def __delattr__(self, name):
        print('delattr')

1. 我们由此可以看出,外部调用都是我们自己定义的方法
In [1]: from magic import Attr

In [2]: a = Attr('escape')
getattribute
getattribute
settattr

In [3]: getattr(a, 'X')
getattribute

In [4]: setattr(a, 'name', 'attr')
settattr

In [5]: delattr(a, 'name')
delattr
  • getattr方法有第三个可选参数,用于方法不存在时的替补方案
class WorkerInterface:
    def method1(self):
        print(1)

    def method2(self):
        print(2)

    def method3(self):
        print(3)

class WorkerImpl:
    def method2(self):
        print('impl 2')

imterface = WorkerInterface()
impl = WorkerImpl()

getattr(impl, 'method1', imterface.method2)()        ==> 返回,2
getattr(impl, 'method2', imterface.method2)()        ==> 返回,impl2
  • 在调用__getattribute__的时候,最后返回的时候用的是object.__getattribute__而没有用self.__getattribute__。这是由于,如果使用了self那么无论使用了self中的什么属性,最后都会触发__getattribute__的调用,这样就陷入了一个死循环中了。
# 其中getattr方法调用的我们自定义的__getattribute__方法
1. 而其他的方法,则都使用object的方法,为了避免陷入死循环中
1. ...写法是Python3.6中的新用法,类似于pass这样的用法,可以等价使用

class User:
    ...

class Proxy:
    title = '代理'
    _data = User()

    def show_title(self):
        return self.title

    def __getattr__(self, name):
        print('use __getattr__')
        return getattr(self._data, name)

    def __setattr__(self, name, value):
        print('use __setattr__')
        return object.__setattr__(self._data, name, value)

    def __delattr__(self, name):
        print('use __delattr__')
        return object.__delattr__(self._data, name)

    def __getattribute__(self, name):
        if name in ('_data', 'title', 'show_title'):
            return object.__getattribute__(self, name)
        print('use __getattribute__: {}'.format(name))
        if name.startswith('b'):
            raise AttributeError
        return object.__getattribute__(self._data, name)
In [1]: from func import Proxy

In [2]: p = Proxy()

In [3]: p.title
Out[3]: '代理'

In [4]: p.show_title()
Out[4]: '代理'

In [5]: p.a = 1
use __setattr__

In [6]: p.a
use __getattribute__: a
Out[6]: 1

In [7]: p.b = 2
use __setattr__

In [8]: p.b
use __getattribute__: b
use __getattr__
Out[8]: 2

In [9]: p._data.b
Out[9]: 2

In [10]: del p.b
use __delattr__

In [11]: p.b
use __getattribute__: b
use __getattr__
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
 in ()
----> 1 p.b

~/Escape/MorePractise/func.py in __getattr__(self, name)
     12     def __getattr__(self, name):
     13         print('use __getattr__')
---> 14         return getattr(self._data, name)
     15
     16     def __setattr__(self, name, value):

AttributeError: 'User' object has no attribute 'b'

7. 容器方法

容器方法特点

魔法函数 解释说明
__getitem__ 得到给定键(key)的值
__setitem__ 设置给定键(key)的值
__delitem__ 删除给定键(key)的值
__slots__ 阻止在实例化类时为实例分配 dict 属性
__len__ 获得项的数目

slots方法的用处

  • 方法优点
    • <1> 减少内存使用、
    • <2> 限制对实例添加新的属性
  • 方法缺点
    • <1> 不可以被继承、
    • <2> 不可以动态添加新的属性
  • 方法用途
    • __slots__的作用是为了阻止在实例化类时为实例分配dict属性。默认情况下每个类都会有一个dict,这个dict维护了这个实例的所有属性,可以通过__dict__来访问。