《PythonCookbook》note-8 类与对象

第八章 类与对象

8.1 改变对象的字符串显示

重新定义类的 __str__() 和 __repr__() 方法。
__repr__() 返回实例的代码表示形式,内置的 repr() 函数返回这个字符串。
__str__() 方法把实例转换为字符串,str() 或 print() 会输出这个字符串。

class Pair:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return 'Pair({0.x!r}, {0.y!r})'.format(self)
    
    def __str__(self):
        return '({0.x!s}, {0.y!s})'.format(self)
>>> p = Pair(3, 4)
>>> p
Pair(3, 4)
>>> print(p)
(3, 4)

格式化代码 !r 指明了使用 __repr__() 代替默认的 __str__()。
__str__() 没有定义时会使用 __repr__() 方法代替。

8.2 自定义字符串的格式化

在类上面定义 __format__() 方法。

_format = {
    'ymd': '{d.year}-{d.month}-{d.day}',
    'mdy': '{d.month}/{d.day}/{d.year}',
    'dmy': '{d.day}/{d.month}/{d.year}',
	}

class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day
    
    def __format__(self, code):
        if code == '':
            code = 'ymd'
        fmt = _format[code]
        return fmt.format(d=self)
>>> d = Date(2019, 10, 1)
>>> format(d)
'2019-10-1'
>>> 'date: {:mdy}'.format(d)
'date: 10/1/2019'

__format__() 方法是字符串格式化的一个钩子函数。格式化代码的解析由类自己实现。

8.3 让对象支持上下文管理器

需要实现 __enter__() 和 __exit__() 方法。出现 with 语句时,__enter__() 方法被触发,返回值被赋给 as 声明的变量;结束时,__exit__() 被触发进行清理工作。

from socket import socket, AF_INET, SOCK_STREAM

class LazyConnection:
    def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
        self.address = address
        self.family = family
        self.type = type
        self.sock = None
    
    def __enter__(self):
        if self.sock is not None:
            raise RunTimeError('Already connected')
        self.sock = socket(self.family, self.type)
        self.sock.connect(self.address)
        return self.sock
    
    def __exit__(self):
        self.sock.close()
        self.sock = None

这个类表示的连接,建立和关闭都是在 with 自动完成。

from functols import partial

conn = LazyConnection(('www.python.org', 80))
with conn as s:
    s.send(b'GET /index.html HTTP/1.0\r\n')
    s.send(b'Host: ww.python.org\r\n')
    resp = b''.join(iter(partial(s.recv, 8192)))

上面的这个类一次只允许一个 socket 连接,不允许多个 with 语句嵌套使用。可以做如下修改:

class LazyConnection:
    def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
        self.address = address
        self.family = family
        self.type = type
        self.connections = []
    
    def __enter__(self):
        sock = socket(self.family, self.type)
        sock.connect(self.address)
        self.connections.append(sock)
        return sock
    
    def __exit__(self, exc_ty, exc_val, tb):
        self.connections.pop().close()

在需要管理资源(文件、网络连接、锁)的程序中,上下文管理器被普遍使用。

8.4 创建大量对象时节省内存的方法

对于当做加单数据结构的类,可以定义 __slots__ 属性来减少实例站的内存。定义这个属性后,类的实例会通过一个固定大小的数组构建,而不是每个实例定义一个字典。
定义 __slots__ 后,类的实例不能添加新的属性,类不再支持多继承。

class Date:
    __slots__ = ['year', 'month', 'day']
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

8.5 在类中封装属性名

私有数据使用命名规则进行规约。
单下划线开头的名字是内部实现;双下划綫开头的名字的访问名称是不同的形式。

class B:
    def __init__(self):
        self.__private = 0
    
    def __private_method(self):
        print('B')


class C(B):
    def __init__(self):
        super().__init__()
>>> c = C()
>>> c.__private
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'C' object has no attribute '__private'
>>> b = B()
>>> b.__private
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'B' object has no attribute '__private'
>>> 
>>> b._B__private
0
>>> >>> b._B__private_method()
B

通常,隐藏内部名称使用单下划线即可。但在这些内部属性需要对子类隐藏时,就要考虑使用双下划线。
有时定义的变量和保留关键字冲突了,可以用单下划线结尾。

8.6 创建可管理的属性

添加除了访问和修改之外的其它逻辑,可以将该属性定义为一个 property。

class Person:
    def __init__(self, first_name):
        self.first_name = first_name
    
    @property
    def first_name(self):
        return self._first_name
    
    @first_name.setter
    def first_name(self, value):
        if not isinstance(value, str):	# 设置类型检查
            raise TypeError('Expected a string.')
        self._first_name = value
    
    @first_name.deleter
    def first_name(self):
        raise AttrbuteError("Can't delete attrivute.")

使用装饰器时,三个方法的名字必须都一样。第一个方法是一个 getter 函数,使 first_name 成为属性,接下来的两个方法为 first_name 属性添加了 setter 和 deleter 函数。first_name 属性被创建后才能定义 后面的两个装饰器。
在访问 property 时会自动触发 getter、setter 和 deleter 方法。
上面的代码中,初始化为 first_name 赋值时也会进行类型检查。
也能在已经存在的 get、set 方法上定义 property:

class Person:
    def __init__(self, first_name):
        self.set_first_name(first_name)
    
    def get_first_name(self):
        return self._first_name
    
    def set_first_name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string.')
        self._first_name = value
    
    def del_first_name(self):
        raise AttributeError("Can't delete attribute.")
    
    name = property(get_first_name, set_first_name, del_first_name)

property 还能定义动态计算的属性,这种属性不会被存储,而是需要的时候才计算出来。

import math
class Circle:
    def __init__(self, radius):
        self.radius = radius
    
    @property
    def area(self):
        return math.pi * self.radius ** 2
    
    @property
    def diameter(self):
        return self.radius * 2
    
    @property
    def perimeter(self):
        return 2 * math.pi * self.radius

对圆的直径、周长、面积的访问都能通过属性访问。如果不这么做,就要在访问时混合使用属性访问和方法调用。

8.7 调用父类方法

在子类中调用被覆盖的父类方法:super() 函数。
在 __init__() 中使用,确保父类被正确地初始化:

class A:
    def __init__(self):
        self.x = 0

class B:
    def __init__(self):
        super().__init__()
        self.y = 1

在覆盖特殊方法时使用:

class Proxy:
    def __init__(self, obj):
        self._obj = obj
    
    def __getattr__(self, name):
        return getattr(self._obj, name)
    
    def __setattr__(self, name, value):
        if name.startswith('_'):
            # 下划线开头交给原生方法解决
            super().__setattr__(name, value)
        else:
            setattr(self._obj, name, value)

继承的时候,会使用 C3 算法构造一个方法解析顺序表,包含子类的所有基类。子类访问属性时,会在这个表上从左到右查找,直到找到匹配这个属性的类。
出现 super() 时,python 会在顺序表上从下一个开始往后搜索。菱形继承的时候,每个子类直接调用父类的方法,最顶层类的方法会调用多次,使用 super() 能使这些方法只被调用一次。

8.8 子类中扩展 property

父类定义了 property:

class Person:
    def __init__(self, name):
        self.name = name
    
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string.')
        self._name = value
    
    @name.deleter
    def name(self):
        raise AttributeError("Can't delete attribute.")

子类中扩展这个 property 全部方法:

class SubPerson(Person):
    @property
    def name(self):
        print('Getting name')
        return super().name
    
    @name.setter
    def name(self, value):
        print('Setting name to', value)
        super(SubPerson, SubPerson).name.__set__(self, value)
    
    @name.deleter
    def name(self):
        print('Deleting name')
        super(SubPerson, SubPerson).name.__delete__(self)

如果只扩展其中一个方法,使用 property 是不行的。

class SubPerson(Person):
    @Person.name.getter
    def name(self):
        print('Getting name')
        return super().name
class SubPerson(Person):
    @Person.name.setter
    def name(self, value):
        print('Setting name to', value)
        super(SubPerson, SubPerson).name.__set__(self, value)

8.9 创建新的类或实例属性

简单地对某个类的单个属性添加新的操作或功能,使用 property 即可。但想对大量使用的属性创建一个新的属性类型,可以创建一个描述器类。

class Integer:
    def __init__(self, name):
        self.name = name
    
    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.name]
     
    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise TypeError('Expected an int')
        instance.__dict__[self.name] = value
    
    def __delete__(self, instance):
        del instance.__dict__[self.name]

描述器是是实现了三个核心访问操作的类。三个访问方法接收一个实例作为输入,然后操作实例底层的字典。
使用描述器的时候,需要将它的实例作为类属性放到一个类的定义中。

class Point:
    x = Integer('x')
    y = Integer('y')
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

描述器的 name 属性是实例字典的 key。
描述器可以实现大部分 python 类特性中的底层魔法。定义描述器可以在底层捕获实例操作并完全自定义它们的行为。利用描述器可以实现很多高级功能。但要注意只能在类级别定义。
描述器通常是使用到装饰器和元类的大型框架的一个组件。

class Typed:
    def __init__(self, name, expected_type):
        self.name = name
        self.expected_type = expected_type
    
    def __get__(self, instannce, cls):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.name]
    
    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError('Expected' + str(self.expected_type))
        instance.__dict__[self.name] = value
    
    def __delete__(self, instance):
        del instance.__dict__[self.name]


def typeassert(**kwargs):
    def decorate(cls):
        for name, expected_type in kwargs.items():
            setattr(cls, name, Typed(name, expected_type))
        return cls
    return decorate


@typeassert(name=str, shares=int, price=float)
class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

8.10 使用延迟计算特性

想将一个只读属性定义为 property,且只在访问的时候才会计算结果,但是访问后,结果值会被保存,下次访问就不用计算了。
使用一个描述器类:

class lazyproperty:
    def __init__(self, func):
        self.func = func
    
    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            value = self.func(instance)
            setattr(instance, self.func.__name__, value)
            return value
import math
class Circle:
    def __init__(self, radius):
        self.radius = radius
    
    @lazyproperty
    def area(self):
        print('Computing area')
        return math.pi * self.radius ** 2
    
    @lazyproperty
    def permeter(self):
        print('Computing perimeter')
        return 2 * math.pi * self.radius

描述器只定义了一个 __get__() 方法,当访问的属性不在实例的底层字典时,才会触发 __get__() 方法。

>>> c.area
Computing area
28.274333882308138
>>> vars(c)
{'radius': 3, 'area': 28.274333882308138}
>>> c.area
28.274333882308138
>>> del c.area
>>> vars(c)
{'radius': 3}
>>> c.area
Computing area
28.274333882308138

这个方法的一个缺陷是计算出的值创建后可以被修改。
把 lazyproperty 改为下面的实现能不允许修改。但每次访问都必须使用到 getattr 函数,不如直接读取字典快。

def lazyproperty(func):
    # 不改名字,会无限递归
    name = '_lazy_' + func.__name__
    @property
    def lazy(self):
        if hasattr(self, name):
            return getattr(self, name)
        else:
            value = func(self)
            setattr(self, name, value)
            return value
    return lazy
>>> c = Circle(3)
>>> c.area
Computing area
28.274333882308138
>>> c.area = 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>> del c.area
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't delete attribute
>>> c.__dict__
{'radius': 3, '_lazy_area': 28.274333882308138}

8.11 简化数据结构的初始化

好多仅仅作为数据结构的类,不想写太多 __init__() 函数。
可以在一个基类里写一个公共的 __init__() 函数。

class Structure1:
    _fields = []
    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError('Expected {} arguments'.format(len(self._fields)))
        
        for name, value in zip(self._fields, args):
            setattr(self, name, value)

使用这个基类:

class Stock(Structure1):
    _fields = ['name', 'shares', 'price']


class Point(Structure1):
    _fields = ['x', 'y']


import math
class Circle(Structure1):
    _fields = ['radius']
    
    def area(self):
        return math.pi * self.radius ** 2

支持关键字参数的版本:

class Structure2:
    _fields = []
    def __init__(self, *args, **kwargs):
        if len(args) > len(self._fields):
            raise TypeError('Expected {} arguments'.format(len(self._fields)))
        
        for name, value in zip(self._fields, args):
            setattr(self, name, value)
        
        for name in self._fields[len(args):]:
            setattr(self, name, kwargs.pop(name))
        
        if kwargs:
            raise TypeError('Invalid argument(s): {}'.format(','.join(kwargs)))

把不在 _fields 中的名称加到属性中去。

class Structure3:
    _fields = []
    
    def __init__(self, *args, **kwargs):
        if len(args) != len(self._fields):
            raise TypeError('Expected {} arguments'.format(len(self._fields)))
        
        for name, value in zip(self._fields, args):
            setattr(self, name, value)
        
        extra_args = kwargs.keys() - self._fields
        for name in extra_args:
            setattr(self, name, kwargs.pop(name))
        
        if kwargs:
            raise TypeError('Duplicate values for {}'.format(','.join(kwargs)))

注意 extra_args 的作用,避免同一个属性多次赋值。这种实现和支持关键字参数的版本是冲突的。
使用 setattr 的方法设置属性值,也能直接更新实例的字典。但是在一个子类定义了 __slots__ 或使用 property 包装某个属性时(字典中的键不是这个属性名),直接访问实例字典就不起作用了。

8.12 定义接口或者抽象基类

定义抽象基类,能够检查子类实现了某些特定的方法。
用 abc 模块定义抽象基类。抽象基类不能被实例化,它的目的就是让别的类继承它并实现特定的抽象方法。

from abc import ABCMeta, abstractmethod

class IStream(metaclass=ABCMeta):
    @abstractmethod
    def read(self, maxbytes=-1):
        pass
    
    @abstractmethod
    def write(self, data):
        pass


class SocketStream(IStream):
    def read(self, maxbytes=-1):
        pass
    
    def write(self, data):
        pass

抽象基类主要用来检查某些类是否为特定类型、实现了某些特定方法。
除了继承外,还能通过注册让某个类实现抽象基类:

import os

IStream.register(io.IOBase)

f = open('test.txt')
isinstance(f, IStream)

@abstractmethod 还能注解静态方法、类方法和 properties,使用时,@abstractmethod 要紧靠函数定义。
标准库中有很多用到抽象基类的地方。collections 定义很多跟容器和迭代器有关的抽象基类,numbers 定义了跟数字对象有关的基类,io库定义了跟 I/O 操作相关的基类。

8.13 实现数据模型的类型约束

定义某些在值上有约束的数据结构。
使用描述器。

# 基类,使用描述器赋值
class Descriptor:
    def __init__(self, name=None, **opts):
        self.name = name
        for key, value in opt.items():
            setter(self, key, value)
    
    def __set__(self, instance, value):
        instance.__dict__[self.name] = value

# 限制类型的描述器
class Typed(Descriptor):
    expected_type = type(None)
    
    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError('expected' + str(self.expected_type))
        super().__set__(instance, value)

# 限制值的描述器
class Unsigned(Descriptor):
    def __set__(self, instance, value):
        if value < 0:
            raise ValueError('Expected >= 0')
        super().__set__(instance, value)


class MaxSized(Descriptor):
    def __init__(self, name=None, **opts):
        if 'size' not in opts:
            raise TypeError('missing size option')
        super().__init__(name, **opt)
    
    def __set__(self, instance, value):
        if len(value) >= self.size:
            raise ValueError('size must be <' + str(self.size))
        super().__set__(instance, value)

拿上面的基类定义数据类型:

class Interger(Typed):
    excepted_type = int


class UnsignedInterger(Interger, Unsigned):
    pass


class UnsignedFloat(Float, Unsigned):
    pass


class Float(Typed):
    expected_type = float


class UnsignedFloat(Float, Unsigned):
    pass


class String(Typed):
    expected_type = str


class SizedString(String, Maxsized):
    pass

使用这些数据类型:

class Stock:
    name = SizedString('name', size=8)
    shares = UnsignedInteger('shares')
    price = UnsignedFloat('price')
    
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

Stock 实例的属性有赋值约束。上述代码能使用一些技术简化。使用类装饰器:

def check_attributes(**kwargs):
    def decorate(cls):
        for key, value in kwargs.items():
            if isinstance(value, Descriptor):
                value.name = key
                setattr(cls, key, value)
            else:
                setattr(cls, key, value(key))
        return cls
    return decorate


@check_attributes(name=SizedString(size=8), 
    shares=UnsignedInterger, 
    price=UnsignedFloat)
class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

使用元类简化:

class checkedmeta(type):
    def __new__(cls, clsname, bases, methods):
        for key, value in methods.items():
            if isinstance(value, Descriptor):
                value.name = key
        return type.__new__(cls, clsname, bases, methods)
    

class Stock2(metaclass=checkedmeta):
    name = SizedString(size=8)
    shares = UnsignedInteger()
    price = UnsignedFloat()
    
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

Descriptor 基类只定义了 __set__() 方法。如果只从底层字典获取某个属性值的话,就没必要定义 __get__() 方法。
所有描述器类都是基于混入类实现的。Unsigned 和 Maxsized 要和继承自 Typed 类混入。
类装饰器简化代码的方案,不依赖其它新的技术,并且装饰器容易添加或删除。装饰器还能作为混入类的替代方案,且比混入类快几乎 1 倍。

def Typed(expected_type, cls=None):
    if cls is None:
        return lambda cls: Typed(expected_type, cls)
    super_set = cls.__set__
    
    def __set__(self, instance, value):
        if not isinstance(value, expected_type):
            raise TypeError('expected' + str(expected_type))
        super_set(self, instance, value)
    
    cls.__set__ = __set__
    return cls


def Unsigned(cls):
    super_set = cls.__set__
    
    def __set__(self, instance, value):
        if value < 0:
            raise ValueError('Excepted >= 0')
        super_set(self, instance, value)
    
    cls.__set__ = __set__
    return cls


def MaxSized(cls):
    super_init = cls.__init__
    
    def __init__(self, name=None, **opts):
        if 'size' not in opts:
            raise TypeError('missing size option')
        super_init(self, name, **opts)
    
    cls.__init__ = __init__
    
    super_set = cls.__set__
    
    def __set__(self, instance, value):
        if len(value) >= self.size:
            raise ValueError('size must be < ' + str(self.size))
        super_set(self, instance, value)
    
    cls.__set__ = __set__
    return cls


@Typed(int)
class Integer(Descriptor):
    pass


@Unsigned
class UnsignedInteger(Integer):
    pass


@Typed(float)
class Float(Descriptor):
    pass


@Unsigned
class UnsignedFloat(Float):
    pass


@Typed(str)
class String(Descriptor):
    pass


@MaxSized
class SizedString(String):
    pass

8.14 实现自定义容器

collections 定义了很多抽象基类,想自定义容器类的时候它们会很有用。
如果想让类支持迭代,就让这个类继承 collections.Iterable 即可。但是需要实现 collections.Iterable 的所有抽象方法。可以试着先实例化这个类,在错误提示中找到需要实现的方法。

import bisect
from collections import Sequence

class SortedItems(Sequence):
    def __init__(self, initial=None):
        self._items = sorted(initial) if initial is not None else []
    
    def __getitem__(self, index):
        return self._items[index]
    
    def __len__(self):
        return len(self._item)
    
    def add(self, item):
        bisect.insort(self._item, item)

bisect 可以保证元素插入后还保持顺序。
使用 collections 中的抽象基类能确保你自定义的容器实现了所有必要的方法,并且能简化类型检查。
collections 类会为常见容器操作提供默认的实现。因此只需要实现感兴趣的部分即可。

from collections import MutableSequence

class Items(MutableSequence):
    def __init__(self, initial=None):
        self._items = list(initial) if initial is not None else []
    
    def __getitem__(self, index):
        print('Getting:', index)
        return self._items[index]
    
    def __setitem__(self, index, value):
        print('Setting:', index, value)
        self._item[index] = value
    
    def __delitem__(self, index):
        print('Deleting:', index)
        del self._items[index]
    
    def insert(self, index, value):
        print('Inserting:', index, value)
        self._items.insert(index, value)
    
    def __len__(self):
        print('Len')
        return len(self._items)
>>> a = Items([1, 2, 3])
>>> len(a)
Len
3
>>> a.append(4)
Len
Inserting: 3 4
>>> a.append(2)
Len
Inserting: 4 2
>>> a.count(2)
Getting: 0
Getting: 1
Getting: 2
Getting: 3
Getting: 4
Getting: 5
2
>>> a.remove(3)
Getting: 0
Getting: 1
Getting: 2
Deleting: 2

8.15 属性的代理访问

某个属性的实例访问代理到另一个实例中去。目的是作为继承的一个替代方法或实现代理模式。

class A:
    def spam(self, x):
        pass
    
    def foo(self):
        pass


class B1:
    def __init__(self):
        self._a = A()
    
    def spam(self, x):
        return self._a.spam(x)
    
    def foo(self):
        return self._a.foo()
    
    def bar(self):
        pass

如果需要代理的方法很多,使用 __getattr__() 方法会更好些:

class B2:
    def __init__(self):
        self._a = A()
    
    def bar(self):
        pass
    
    def __getattr__(self, name):
        """__getattr__()方法在访问的属性不存在的时候才被调用"""
        return getattr(self._a, name)

实现代理模式:

# 代理类,包装其它对象,但是显示自身的公共属性
class Proxy:
    def __init__(self, obj):
        self._obj = obj
    
    def __getattr__(self, name):
        print('getattr', name)
        return getattr(self._obj, name)
    
    def __setattr__(self, name, value):
        if name.startswith('_'):
            super().__setattr__(name, value)
        else:
            print('setattr', name, value)
            setattr(self._obj, name, value)
    
    def __delattr__(self, name):
        if name.startswith('_'):
            super().__delattr__(name)
        else:
            print('delattr:', name)
            delattr(self._obj, name)

使用的时候,使用它包装其它的类:

class Spam:
    def __init__(self, x):
        self.x = x
    
    def bar(self, y):
        print('Spam.bar:', self.x, y)
>>> s = Spam(3)
>>> p = Proxy(s)
>>> p.x
getattr x
3
>>> p.bar(4)
getattr bar
Spam.bar: 3 4
>>> p.x = 6
setattr x 6

自定义属性访问方法,可以用不同方式自定义代理类的行为,如加入日志或只读访问等。
代理类有时候能作为继承的替代方法。

class A:
    def spam(self, x):
        pass
    def foo(self):
        print('A.foo')

class B(A):
    def spam(self, x):
        print('B.spam')
        super().spam(x)
    def bar(self):
        print('B.bar')

使用代理:

class A:
	def spam(self, x):
        print('A.spam', x)
    def foo(self):
        print('A.foo')

class B:
    def __init__(self):
        self._a = A()
    def spam(self, x):
        print('B.spam')
        self._a.spam(x)
    def bar(self):
        print('B.bar')
    def __getattr__(self, name):
        return getattr(self._a, name)

实现代理模式时,注意 __getattr__() 是一个后备方法,只有在属性不存在的时候才会调用,因此,如果代理类实例本身有这个属性的话,就不会触发这个方法。
__setattr__() 和 __delattr__() 需要额外的魔法来区分代理实例和被代理实例 _obj 的属性。通常约定只代理那些非下划线开头的属性。
__getattr__() 对某些双下划线开头和结尾的属性不适用。这些属性和方法需要一个个重定义。

8.16 在类中定义多个构造器

类除了使用 __init__() 之外,还有其它方式初始化它。
类方法的一个主要用途就是定义多个构造器,接收一个类作为第一个参数,用这个类创建并返回最终的实例。

import time

class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day
    
    @classmethod
    def today(cls):
        t = time.localtime()
        return cls(t.tm_year, t.tm_mon, t.tm_mday)

在继承时也能较好地工作。

8.17 创建不调用 init 方法的实例

__new__() 方法能创建未初始化的实例,这个实例不包含 __init__() 中创建的属性,需要手动初始化。
在反序列化对象或实现某个类方法构造函数时需要绕过 __init__() 方法来创建对象。

from time import localtime

class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day
    
    @classmethod
    def today(cls):
        d = cls.__new__(cls)
        t = localtime()
        d.year = t.tm_year
        d.month = t.tm_month
        d.day = t.tm_day
        return d

反序列化时,出现的字典对象想转换成 Date 类型实例,能使用上面的方法。
这种创建实例的方法最好不要直接访问实例字典,因为这个类使用了 __slots__、properties、descriptors 或其它技术的时候代码就失效了。使用 setattr() 方法会让代码更加通用。

8.18 利用 Mixins 扩展类功能

有很多有用的方法,想使用它们来扩展其它类的功能,但是这些类并没有任何的继承关系。

class LoggedMappingMixin:
    """为调试时产生的操作添加日志功能"""
    __slots__ = ()
    
    def __getitem__(self, key):
        print('Getting ' + str(key))
        return super().__getitem__(key)
    
    def __setitem__(self, key, value):
        print('Setting {} = {!r}'.format(key, value))
        return super().__setitem__(key, value)
    
    def __delitem__(self, key):
        print('Dleting ' + str(key))
        return super().__delitem__(key)


class SetOnceMappingMixin:
    """一个键只能赋值一次"""
    __slots__ = ()
    
    def __setitem__(self, key, value):	
        if key in self:
            raise KeyError(str(key), 'already set')
        return super().__setitem__(key, value)
    

class StringKeysMappingMixin:
    """键强制使用字符串类型"""
    __slots__ = ()
    
    def __setitem__(self, key, value):	
        if not isinstance(key, str):
            raise TypeError('keys must be strings')
        return super().__setitem__(key, value)

这些类单独使用没意义,要通过继承和其它对象混入使用:

class LoggedDict(LoggedMappingMixin, dict):
    pass

混入类不能直接被实例化,没有自己的状态信息和实例属性。
使用类装饰器是另一种实现混入类的方法,这时就不需要多继承了。

def LoggedMapping(cls):
    cls_getitem = cls.__getitem__
    cls_setitem = cls.__setitem__
    cls_delitem = cls.__delitem__
    
    def __getitem__(self, key):
        print('Getting', str(key))
        return cls_getitem(self, key)
    
    def __setitem__(self, key, value):
        print('Setting {} = {!r}'.format(key, value))
        return cls_setitem(self, key, value)
    
    def __delitem__(self, key):
        print('Deleting', str(key))
        return cls_delitem(self, key)
    
    cls.__getitem__ = __getitem__
    cls.__setitem__ = __setitem__
    cls.__delitem__ = __delitem__
    return cls

@LoggedMapping
class LoggedDict(dict):
    pass

8.19 实现状态对象或者状态机

想实现在不同状态下执行操作的对象,但是又不想要太多的条件判断语句。

class Connection:
	# 普通方法,好多个判断语句
    def __init__(self):
        self.state = 'CLOSED'
    
    def read(self):
        if self.state != 'OPEN':
            raise RuntimeError('Not open')
        print('reading')
    
    def write(self, data):
        if self.state != 'OPEN':
            raise RuntimeError('Not open')
        print('writing')
    
    def open(self):
        if self.state == 'OPEN':
            raise RuntimeError('Already open')
        self.state = 'OPEN'
    
    def close(self):
        if self.state == 'CLOSED':
            raise RuntimeError('Already closed')
        self.state = 'CLOSED'

太多的条件判断代码复杂且低效,每次执行操作都要先检查条件。更好的方法是为每个状态定义一个对象

class Connection1:
    def __init__(self):
        self.new_state(ClosedConnectionState)
    
    def new_state(self, newstate):
        self._state = newstate
    
    def read(self):
        return self._state.read(self)
    
    def write(self, data):
        return self._state.write(self, data)
    
    def open(self):
        return self._state.open(self)
    
    def close(self):
        return self._state.close(self)

# 连接状态基类
class ConnectionState:
    @staticmethod
    def read(conn):
        raise NotImplementedError()
    
    @staticmethod
    def write(conn, data):
        raise NotImplementedError()
    
    @staticmethod
    def open(conn):
        raise NotImplementedError()
    
    @staticmethod
    def close(conn):
        raise NotImplementedError()

# 不同状态的工具类
class ClosedConnectionState(ConnectionState):
    @staticmethod
    def read(conn):
        raise RuntimeError('Not open')
    
    @staticmethod
    def write(conn, data):
        raise RuntimeError('Not open')
    
    @staticmethod
    def open(conn):
        conn.new_state(OpenConnectionState)
    
    @staticmethod
    def close(conn):
        raise RuntimeError('Already closed')


class OpenConnectionState(ConnectionState):
    @staticmethod
    def read(conn):
        print('reading')
    
    @staticmethod
    def write(conn, data):
        print('writing')
    
    @staticmethod
    def open(conn):
        raise RuntimeError('Already open')
    
    @staticmethod
    def close(conn):
        conn.new_state(ClosedConnectionState)

每个状态对象只有静态方法,没有存储任何实例属性。所有的状态信息都在 Connection 实例中。基类中定义 NotImplementedError 是为了确保子类实现了相应的方法。

8.20 通过字符串调用对象方法

使用 getattr():

import math

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return 'Point({!r:},{!r:})'.format(self.x, self.y)
    
    def distance(self, x, y):
        return math.hypot(self.x - x, self.y - y)
>>> p = Point(4,5)
>>> p.distance(0,0)
6.4031242374328485
>>> d = getattr(p, 'distance')(0,0)
>>> d
6.4031242374328485

另一种方法是使用 operator.methodcaller()。该方法创建一个可调用对象,已有所需参数。调用的时候把实例对象传给它即可。

import operator
operator.methodcaller('distance', 0, 0)(p)

需要使用相同的参数多次调用某个方法时,operator.methodcaller 很有用:

points = [
    Point(1, 2),
    Point(3, 0),
    Point(10, -1),
    Point(-4, -8),
    ]
# 按到原点的距离排序
points.sort(key=operator.methodcaller('distance', 0, 0))

通过字符串调用方法通常出现在需要模拟 case 语句或实现访问者模式的时候。

8.21 实现访问者模式

需要处理大量不同对象组成的复杂数据结构,每一个对象都需要不同的处理。
一个表示数学表达式的程序可能需要定义下面这些类:

class Node:
    pass

class UnaryOperator(Node):
    def __init__(self, operand):
        self.operand = operand

class BinaryOperator(Node):
    def __init__(self, left, right):
        self.left = left
        self.right = right

class Add(BinaryOperator):
    pass

class Sub(BinaryOperator):
    pass

class Mul(BinaryOperator):
    pass

class Div(BinaryOperator):
    pass

class Negate(UnaryOperator):
    pass

class Number(Node):
    def __init__(self, value):
        self.value = value

对于表达式 1 + 2 * (3 - 4) / 5,需要构建如下嵌套结构:

t1 = Sub(Number(3), Number(4))
t2 = Mul(Number(2), t1)
t3 = Div(t2, Number(5))
t4 = Add(Number(1), t3)

这样做的话,对于每个表达式,每次都要重新定义一遍。访问者模式可以支持所有的数字和操作符。

class NodeVisitor:
    def visit(self, node):
        methname = 'visit_' + type(node).__name__
        meth = getattr(self, methname, Node)
        if meth is None:
            meth = self.generic_visit
        return meth(node)
    
    def generic_visit(self, node):
        raise RuntimeError('No {} method'.format('visit_' + type(node).__name__))


class Evaluator(NodeVisitor):
    def visit_Number(NodeVisitor):
        return node.value
    
    def visit_Add(self, node):
        return self.visit(node.left) + self.visit(node.right)
    
    def visit_Sub(self, node):
        return self.visit(node.left) - self.visit(node.right)
    
    def visit_Mul(self, node):
        return self.visit(node.left) * self.visit(node.right)
    
    def visit_Div(self, node):
        return self.visit(node.left) / self.visit(node.right)
    
    def visit_Negate(self, node):
        return -node.operand
e = Evaluator()
e.visit(t4)

定义一个类,在一个栈上面将一个表达式转换为多个操作;

class StackCode(NodeVisitor):
    def generate_code(self, node):
        self.instructions = []
        self.visit(node)
        return self.instructions
    
    def visit_Number(self, node):
        self.instructions.append(('PUSH', node.value))

    def binop(self, node, instruction): 
        self.visit(node.left)
        self.visit(node.right)
        self.instructions.append((instruction,))
    
    def visit_Add(self, node):
        self.binop(node, 'ADD')
    
    def visit_Sub(self, node):
        self.binop(node, 'SUB')
    
    def visit_Mul(self, node):
        self.binop(node, 'MUL')
    
    def visit_Div(self, node):
        self.binop(node, 'DIV')
    
    def unaryop(self, node, instruction):
        self.visit(node.oprand)
        self.instructions.append((instruction,))
    
    def visit_Negate(self, node):
        self.unaryop(node, 'NEG')

访问者模式的好处就是 getattr() 来获取相应的方法,并利用递归遍历所有节点。这种技术也是实现其他语言中的 switch 或 case 语句的方式。
用这种模式写一个请求分发的控制器:

class HTTPHandler:
    def handle(self, request):
        methname = 'do_' + request.request_method
        getattr(self, methname)(request)
    
    def do_GET(self, request):
        pass
    
    def do_POST(self, request):
        pass
    
    def do_HEAD(self, request):
        pass

访问者模式的缺点是严重依赖递归,数据结构嵌套层次太深可能会超过 Python 的递归深度限制。

8.22 不用递归实现访问者模式

巧妙使用生成器能在消除递归的同时保持访问者模式。

import types

class NodeVisitor:
    def visit(self, node):
        stack = [node]
        last_result = None
        while stack:
            try:
                last = stack[-1]
                if isinstance(last, types.GeneratorType):
                    stack.append(last.send(last_result))
                elif isinstance(last, Node):
                    stack.append(self._visit(stack.pop()))
                else:
                    last_result = stack.pop()
            except StopIteration:
                stack.pop()
        return last_result
    
    def _visit(self, node):
        methname = 'visit_' + type(node).__name__
        meth = getattr(self, methname, None)
        if meth is None:
            meth = self.generic_visit
        return meth(node)
    
    def generic_visit(self, node):
        raise RuntimeError('No {} method'.format('visit_' + type(node).__name__))

这个类能作为上一节中访问者模式的替代实现。
上一节中的 Evaluator 在处理过深的嵌套层次时会失效:

>>> a = Number(0)
>>> for i in range(1, 100000):
...     a = Add(a, Number(i))
...
>>> e = Evaluator()
>>> e.visit(a)
...
RecursionError: maximum recursion depth exceeded

改一下 Evaluator:

class Evaluator(NodeVisitor):
    def visit_Number(self, node):
        return node.value
    
    def visit_Add(self, node):
        yield (yield node.left) + (yield node.right)
    
    def visit_Sub(self, node):
        yield (yield node.left) - (yield node.right)
    
    def visit_Mul(self, node):
        yield (yield node.left) * (yield node.right)
    
    def visit_Div(self, node):
        yield (yield node.left) / (yield node.right)
    
    def visit_Negate(self, node):
        yield - (yield node.left)
>>> e = Evaluator()
>>> e.visit(a)
4999950000

碰到 yield 语句时,生成器会返回一个数据并暂时挂起,利用这点代替递归:

value = self.visit(node.left)
# 换成生成器
value = yield node.left

yield 语句会将 node.left 返回给 visit() 方法,然后 visit() 调用这个节点响应的 visit_Name()。

8.23 循环引用数据结构的内存管理

weakref 库中的弱引用。

import weakref

class Node:
    def __init__(self, value):
        self.value = value
        self._parent = None
        self.children = []
    
    def __repr__(self):
        return 'Node({!r:})'.format(self.value)
    
    @property
    def parent(self):
        return None if self._parent is None else self._parent()
    
    @parent.setter
    def parent(self, node):
        self._parent = weakref.ref(node)
    
    def add_child(self, child):
        self.children.append(child)
        child.parent = self

正常的垃圾回收机制是基于应用计数,一个对象的引用数变成 0 的时候才能被删除掉。循环引用时,每个对象的引用计数不会变成 0。

class Data:
    def __del__(self):
        print('Data.__del__')


class Node:
    def __init__(self):
        self.data = Data()
        self.parent = None
        self.children = []
    
    def add_child(self, child):
        self.children.append(child)
        child.parent = self
>>> a = Data()
>>> del a
Data.__del__
>>> a = Node()
>>> del a
Data.__del__
>>> a = Node()
>>> a.add_child(Node())
>>> del a
>>>

弱引用笑出了这个问题。弱引用本质上是对象的指针,不会增加这个对象的引用计数。访问弱引用所引用的对象,需要调用它,这个对象存在就返回,不存在返回 None。

>>> a = Node()
>>> b = weakref.ref(a)
>>> b
<weakref at 0x0000007A34E4F3B8; to 'Node' at 0x0000007A34E5A0B8>
>>> b()
<__main__.Node object at 0x0000007A34E5A0B8>
>>> del a
>>> print(b())
None

8.24 让类支持比较操作

想让类支持比较运算,但是又不想实现一大堆特殊方法。
装饰器 functools.total_ordering 能简化这个处理。使用它装饰一个类,这个类仅需要定义一个 __eq__() 方法,外加其它方法(__lt__(),__le__(),__gt__(),__ge__())中的一个,装饰器会自动填充好其它比较方法。

from functools import total_ordering

class Room:
    def __init__(self, name, length, width):
        self.name = name
        self.length = length
        self.width = width
        self.square_feet = self.length * self.width
    
@total_ordering
class House:
    def __init__(self, name, style):
        self.name = name
        self.style = style
        self.rooms = list()
    
    @property
    def living_space_footage(self):
        return sum(r.square_feet for r in self.rooms)
    
    def add_room(self, room):
        self.rooms.append(room)
    
    def __str__(self):
        return '{}: {} square foot {}'.format(self.name, self.living_space_footage, self.style)
    
    def __eq__(self, other):
        return self.living_space_footage == other.living_space_footage
    
    def __lt__(self, other):
        return self.living_space_footage < other.living_space_footage

8.25 创建缓存实例

同样的参数再次创建对象时,想返回它的缓存引用。希望相同参数创建的对象是单例的。
需要用一个和类分开的工厂函数:

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

import weakref
_spam_cache = weakref.WeakValueDictionary()
def get_spam(name):
    if name not in _spam_cache:
        s = Spam(name)
        _spam_cache[name] = s
    else:
        s = _spam_cache[name]
    return s

重新定义类的 __new__() 方法也能达到预期,但是每次都会调用 __init__() 函数,无论该实例是否已缓存。
WeakValueDictionary 的实例只会保存在其他地方还被使用的实例。
把工厂函数跟类放在一块,单独设一个缓存管理器,能更灵活地实现所需的效果。

import weakref

class CachedSpamManager:
    def __init__(self):
        self._cache = weakref.WeakValueDictionary()
    
    def get_spam(self, name):
        if name not in self._cache:
            s = Spam(name)
            self._cache[name] = s
        else:
            s = self._cache[name]
        return s
    
    def clear(self):
        self._cache.clear()


class Spam:
    manager = CachedSpamManager()
    def __init__(self, name):
        self.name = name
    
    def get_spam(name):
        return Spam.manager.get_spam(name)

为了防止用户直接使用类实例化,而不是用工厂方法,可以在实例初始化时抛出异常。缓存管理器使用另外的方法来创建实例。

class CachedSpamManager:
    def __init__(self):
        self._cache = weakref.WeakValueDictionary()
    
    def get_spam(self, name):
        if name not in self._cache:
            temp = Spam._new(name)
            self._cache[name] = temp
        else:
            temp = self._cache[name]
        return temp
    
    def clear(self):
        self._cache.clear()


class Spam:
    manager = CachedSpamManager()
    def __init__(self, *args, **kwargs):
        raise RuntimeError("Can't instantiate directly")
    
    @classmethod
    def _new(cls, name):
        self = cls.__new__(cls)
        self.name =name
        return self
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值