第八章 类与对象
- 改变对象的字符串显示
- 自定义字符串的格式化
- 创建大量对象时节省内存方法
- 类的"私有"变量
- 8.7和8.8
- 创建新的类或实例属性
- 使用延迟计算属性
- 简化数据结构的初始化
- 定义接口或者抽象基类
- 实现数据模型的类型约束
- 实现自定义容器
- 属性的代理访问
- 在类中定义多个构造器
- 创建不调用init方法的实例
- 利用Mixins扩展类功能
- 实现状态对象或者状态机
- 通过字符串调用对象方法
- 实现访问者模式
- 循环引用数据结构的内存管理
- 让类支持比较操作
- 创建缓存实例
改变对象的字符串显示
__repr__()
方法返回一个实例的代码表示形式,通常用来重新构造这个实例。 内置的 repr()
函数返回这个字符串,跟我们使用交互式解释器显示的值是一样的。 __str__()
方法将实例转换为一个字符串,使用 str()
或 print()
函数会输出这个字符串。
__repr__()
生成的文本字符串标准做法是需要让 eval(repr(x)) == x
为真。 如果实在不能这样子做,应该创建一个有用的文本表示,并使用 < 和 > 括起来。
如果 __str__()
没有被定义,那么就会使用 __repr__()
来代替输出。
自定义字符串的格式化
为了自定义字符串的格式化,我们需要在类上面定义 __format__()
方法。例如:
_formats = {
'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 = _formats[code]
return fmt.format(d=self)
>>> d = Date(2012, 12, 21)
>>> format(d)
'2012-12-21'
>>> format(d, 'mdy')
'12/21/2012'
>>> 'The date is {:ymd}'.format(d)
'The date is 2012-12-21'
>>> 'The date is {:mdy}'.format(d)
'The date is 12/21/2012'
>>>
需要注意的是,默认的 format_spec
是一个空字符串,它通常和调用 str(value)
的结果相同。
调用 format(value
, format_spec)
会转换成 type(value).__format__(value, format_spec)
,所以实例字典中的 __format__()
方法将不会调用。
创建大量对象时节省内存方法
对于主要是用来当成简单的数据结构的类而言,你可以通过给类添加 __slots__
属性来极大的减少实例所占的内存。比如:
class Date:
__slots__ = ['year', 'month', 'day']
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
当你定义 __slots__
后,Python就会为实例使用一种更加紧凑的内部表示。 实例通过一个很小的固定大小的数组来构建,而不是为每个实例定义一个字典,这跟元组或列表很类似。 在 __slots__
中列出的属性名在内部被映射到这个数组的指定小标上。 使用slots一个不好的地方就是我们不能再给实例添加新的属性了,只能使用在 __slots__
中定义的那些属性名。
关于 __slots__
的一个常见误区是它可以作为一个封装工具来防止用户给实例增加新的属性。 尽管使用slots可以达到这样的目的,但是这个并不是它的初衷。 __slots__
更多的是用来作为一个内存优化工具。另外,定义了slots后的类不再支持一些普通类特性了,比如多继承。 大多数情况下,你应该只在那些经常被使用到的用作数据结构的类上定义slots (比如在程序中需要创建某个类的几百万个实例对象)。
类的"私有"变量
Python程序员不去依赖语言特性去封装数据,约定上任何以单下划线_开头的名字都应该是内部实现。Python并不会真的阻止别人访问内部名称。但是如果你这么做肯定是不好的,可能会导致脆弱的代码。 同时还要注意到,使用下划线开头的约定同样适用于模块名和模块级别函数。
你还可能会遇到在类定义中使用两个下划线(__)开头的命名。比如:
class B:
def __init__(self):
self.__private = 0
def __private_method(self):
pass
def public_method(self):
pass
self.__private_method()
使用双下划线开始会导致访问名称变成其他形式。 比如,在前面的类B中,私有属性会被分别重命名为 _B__private 和 _B__private_method 。 这时候你可能会问这样重命名的目的是什么,答案就是继承——这种属性通过继承是无法被覆盖的。
8.7和8.8
这两个小节可厉害了,专门写了(搬运)文章记下来。
创建新的类或实例属性
这一节接着讲描述器和property,就不水了,最后讲到如果你只是想简单的自定义某个类的单个属性访问的话就不用去写描述器了,直接用property会方便。但是如果你需要很多逻辑相似的property呢,一个一个写应该超级痛苦吧,那就改描述器上场了。
最后给了个类型检测的demo可以参考下,类装饰器很秀:
# Descriptor for a type-checked attribute
class Typed:
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
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, self.expected_type):
raise TypeError('Expected ' + str(self.expected_type))
instance.__dict__[self.name] = value
def __delete__(self, instance):
del instance.__dict__[self.name]
# Class decorator that applies it to selected attributes
def typeassert(**kwargs):
def decorate(cls):
for name, expected_type in kwargs.items():
# Attach a Typed descriptor to the class
setattr(cls, name, Typed(name, expected_type))
return cls
return decorate
# Example use
@typeassert(name=str, shares=int, price=float)
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
使用延迟计算属性
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 perimeter(self):
print('Computing perimeter')
return 2 * math.pi * self.radius
>>> c = Circle(4.0)
>>> c.radius
4.0
>>> c.area
Computing area
50.26548245743669
>>> c.area
50.26548245743669
>>> c.perimeter
Computing perimeter
25.132741228718345
>>> c.perimeter
25.132741228718345
这里第一次查找属性,会调用非数据描述器的__get__函数,从而第一次调用 func 计算出结果并通过 setattr 设置了同名的实例属性,这样一来第二次查找属性的时候就直接找到了实例属性,不会进入到描述器。所以说如果你更改了radius的值,这些属性也不会重新计算,看起来很有用但是有坑。
简化数据结构的初始化
暂时看不出有啥用,可能大工程里会用到?
定义接口或者抽象基类
使用 abc 模块可以很轻松的定义抽象基类:
from abc import ABCMeta, abstractmethod
class IStream(metaclass=ABCMeta):
@abstractmethod
def read(self, maxbytes=-1):
pass
@abstractmethod
def write(self, data):
pass
抽象类的一个特点是它不能直接被实例化,比如你想像下面这样做是不行的:
a = IStream() # TypeError: Can't instantiate abstract class
# IStream with abstract methods read, write
抽象类的目的就是让别的类继承它并实现特定的抽象方法:
class SocketStream(IStream):
def read(self, maxbytes=-1):
pass
def write(self, data):
pass
@abstractmethod 还能注解静态方法、类方法和 properties 。 你只需保证这个注解紧靠在函数定义前即可:
class A(metaclass=ABCMeta):
@property
@abstractmethod
def name(self):
pass
@name.setter
@abstractmethod
def name(self, value):
pass
@classmethod
@abstractmethod
def method1(cls):
pass
@staticmethod
@abstractmethod
def method2():
pass
此外,注册虚拟子类的方式在抽象基类上调用register方法,注册的类就会变成抽象基类的虚拟子类,而且issubclass和isinstance等函数都能识别,但是注册的类并不会从抽象基类中继承任何方法。
from abc import *
class A(metaclass=ABCMeta):
@abstractmethod
def func1(self):
pass
@A.register
class B:
def func2(self):
pass
# 或者
A.register(B)
虚拟子类不会出现在__mro__属性里:
b=B()
print(isinstance(b, A))
# True
print(B.mro())
# [<class '__main__.B'>, <class 'object'>]
尽管ABCs可以让我们很方便的做类型检查,但是我们在代码中最好不要过多的使用它。 因为Python的本质是一门动态编程语言,其目的就是给你更多灵活性, 强制类型检查或让你代码变得更复杂,这样做无异于舍本求末。
实现数据模型的类型约束
这里的demo 是直接把约束判断放在了 __set__ 函数里面,以便于多继承;而 Python文档的demo 则是把约束判断定义成了一个抽象基类的抽象方法,结构比较明了但是不适合继承。
文章下面使用类装饰器或元类来简化代码,其实可以直接在描述器的里添加__set_name__函数,这是新特性:
def __set_name__(self, cls, name):
self.name = name
最后的装饰器作为混入类的替代技术;
# Decorator for applying type checking
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
# Decorator for unsigned values
def Unsigned(cls):
super_set = cls.__set__
def __set__(self, instance, value):
if value < 0:
raise ValueError('Expected >= 0')
super_set(self, instance, value)
cls.__set__ = __set__
return cls
# Specialized descriptors
@Typed(int)
class Integer(Descriptor):
pass
@Unsigned
class UnsignedInteger(Integer):
pass
大概就是手动拓展了__set__函数,而不是通过继承,听说速度会快100%,看起来很牛的样子。
实现自定义容器
内置模块collections里定义了很多抽象基类,当你想自定义容器类的时候它们会非常有用。
属性的代理访问
感觉没什么讲究,需要注意的是What is the relationship between __getattr__ and getattr?
,前者是定义了找不到属性时的行为,后者是得到属性。
还有一点需要注意的是,__getattr__() 对于大部分以双下划线(__)开始和结尾的属性并不适用。 比如,考虑如下的类:
class ListLike:
"""__getattr__对于双下划线开始和结尾的方法是不能用的,需要一个个去重定义"""
def __init__(self):
self._items = []
def __getattr__(self, name):
return getattr(self._items, name)
如果是创建一个ListLike对象,会发现它支持普通的列表方法,如append()和insert(), 但是却不支持len()、元素查找等。例如:
>>> a = ListLike()
>>> a.append(2)
>>> a.insert(0, 1)
>>> a.sort()
>>> len(a)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'ListLike' has no len()
>>> a[0]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'ListLike' object does not support indexing
为了让它支持这些方法,你必须手动的实现这些方法代理:
class ListLike:
"""__getattr__对于双下划线开始和结尾的方法是不能用的,需要一个个去重定义"""
def __init__(self):
self._items = []
def __getattr__(self, name):
return getattr(self._items, name)
# Added special methods to support certain list operations
def __len__(self):
return len(self._items)
def __getitem__(self, index):
return self._items[index]
def __setitem__(self, index, value):
self._items[index] = value
def __delitem__(self, index):
del self._items[index]
在类中定义多个构造器
没办法像其他语言那样直接对构造器重载,但可以用类方法达到类似的效果:
import time
class Date:
"""方法一:使用类方法"""
# Primary constructor
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
# Alternate constructor
@classmethod
def today(cls):
t = time.localtime()
return cls(t.tm_year, t.tm_mon, t.tm_mday)
创建不调用init方法的实例
用__new__方法,具体用处没看出。
利用Mixins扩展类功能
假设你想扩展映射对象,给它们添加日志、唯一性设置、类型检查等等功能。下面是一些混入类:
class LoggedMappingMixin:
"""
Add logging to get/set/delete operations for debugging.
"""
__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('Deleting ' + str(key))
return super().__delitem__(key)
class LoggedDict(LoggedMappingMixin, dict):
pass
d = LoggedDict()
d['x'] = 23
print(d['x'])
del d['x']
大概就是拓展一些方法,写多继承的时候顺序要注意下。
混入类不能直接被实例化使用。 其次,混入类没有自己的状态信息,也就是说它们并没有定义 __init__()
方法,并且没有实例属性。 这也是为什么我们在上面明确定义了 __slots__
= ()
。
使用类装饰器也可以实现:
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
实现状态对象或者状态机
为了避免出现太多的判断语句,将每个状态抽取出来定义成一个类,每个状态对象都只有静态方法,通过静态方法实现不同状态下的行为,并没有存储任何的实例属性数据。 代码太长不贴。
通过字符串调用对象方法
用 getattr()
或者 operator.methodcaller
:
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(2, 3)
d = getattr(p, 'distance')(0, 0) # Calls p.distance(0, 0)
import operator
operator.methodcaller('distance', 0, 0)(p)
实际上methodcaller也等价于:
def methodcaller(name, /, *args, **kwargs):
def caller(obj):
return getattr(obj, name)(*args, **kwargs)
return caller
实现访问者模式
TODO
循环引用数据结构的内存管理
一个简单的循环引用数据结构例子就是一个树形结构,双亲节点有指针指向孩子节点,孩子节点又返回来指向双亲节点。 这种情况下,可以考虑使用 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 that manages the parent as a weak-reference
@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
弱引用消除了引用循环的这个问题,本质来讲,弱引用就是一个对象指针,它不会增加它的引用计数。为了访问弱引用所引用的对象,你可以像函数一样去调用它即可。如果那个对象还存在就会返回它,否则就返回一个None。 由于原始对象的引用计数没有增加,那么就可以去删除它了。
让类支持比较操作
你不想去一个一个实现那些特殊方法,觉得有点烦人。
装饰器 functools.total_ordering
就是用来简化这个处理的。 使用它来装饰一个类,你只需定义一个 __eq__()
方法, 外加其他方法(__lt__
, __le__
, __gt__
, or __ge__
)中的一个即可。 然后装饰器会自动为你填充其它比较方法。
创建缓存实例
用人话讲就是你希望相同参数创造的对象是单例的(注意和单例模式有区别)。
在很多库中都有实际的例子,比如 logging
模块,使用相同的名称创建的 logger
实例永远只有一个。例如:
>>> import logging
>>> a = logging.getLogger('foo')
>>> b = logging.getLogger('bar')
>>> a is b
False
>>> c = logging.getLogger('foo')
>>> a is c
True
为了达到这样的效果,你需要使用一个和类本身分开的工厂函数(或者装饰器),例如:
# The class in question
class Spam:
def __init__(self, name):
self.name = name
# Caching support
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__()
每次都会被调用,不管这个实例是否被缓存了,所以达咩。
这里注意用到了 weakref.WeakValueDictionary
而不是普通的字典(把学到了打在公屏上),为的是弱引用计数,对于垃圾回收来讲是很有帮助的。当我们保持实例缓存时,你可能只想在程序中使用到它们时才保存。 一个 WeakValueDictionary
实例只会保存那些在其它地方还在被使用的实例。 否则的话,只要实例不再被使用了,它就从字典中被移除了。观察下下面的测试结果:
>>> a = get_spam('foo')
>>> b = get_spam('bar')
>>> c = get_spam('foo')
>>> list(_spam_cache)
['foo', 'bar']
>>> del a
>>> del c
>>> list(_spam_cache)
['bar']
>>> del b
>>> list(_spam_cache)
[]
这样我们就不用手动得去维护这个字典了,非常牛!
对于大部分程序而已,这里代码已经够用了。不过还是有一些更高级的实现值得了解下。
首先是这里使用到了一个全局变量,并且工厂函数跟类放在一块。我们可以通过将缓存代码放到一个单独的缓存管理器中:
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)
这样的话代码更清晰,并且也更灵活,我们可以增加更多的缓存管理机制,只需要替代manager即可。
还有一点就是,我们暴露了类的实例化给用户,用户很容易去直接实例化这个类,而不是使用工厂方法。
有几种方式可以防止用户这样做,第一个是将类的名字修改为以下划线(_)开头,提示用户别直接调用它。 第二种就是让这个类的 __init__()
方法抛出一个异常,让它不能被初始化:
class Spam:
def __init__(self, *args, **kwargs):
raise RuntimeError("Can't instantiate directly")
# Alternate constructor
@classmethod
def _new(cls, name):
self = cls.__new__(cls)
self.name = name
然后修改缓存管理器代码,使用 Spam._new()
来创建实例,而不是直接调用 Spam()
构造函数:
# ------------------------最后的修正方案------------------------
class CachedSpamManager2:
def __init__(self):
self._cache = weakref.WeakValueDictionary()
def get_spam(self, name):
if name not in self._cache:
temp = Spam3._new(name) # Modified creation
self._cache[name] = temp
else:
temp = self._cache[name]
return temp
def clear(self):
self._cache.clear()
class Spam3:
def __init__(self, *args, **kwargs):
raise RuntimeError("Can't instantiate directly")
# Alternate constructor
@classmethod
def _new(cls, name):
self = cls.__new__(cls)
self.name = name
return self