Python3 CookBook学习笔记 -- 类与对象
1. 改变对象的字符串显示
你想改变对象实例的打印或显示输出,让它们更具可读性。
需要我们重新定义它的 __str__()
和 __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 'Pair({0.x!s}, {0.y!s})'.format(self)
...
>>> pair=Pair('1', '2')
>>>
>>> pair
Pair('1', '2')
>>> print(pair)
Pair(1, 2)
-
!r
表示以__repr__()
方式表示。 -
!s
表示以__str__()
方式表示。
自定义 __repr__()
和 __str__()
通常是很好的习惯,因为它能简化调试和实例输出。
规则:
-
__repr__()
生成的文本字符串标准做法是需要让eval(repr(x)) == x
为真。 如果实在不能这样子做,应该创建一个有用的文本表示,并使用<
和>
括起来。
>>> f = open('file.dat')
>>> f
<_io.TextIOWrapper name='file.dat' mode='r' encoding='UTF-8'>
- 如果
__str__()
没有被定义,那么就会使用__repr__()
来代替输出。
2. 自定义字符串的格式化
你想通过 format()
函数和字符串方法使得一个对象能支持自定义的格式化。
>>> class Date:
... _formats={
... 'ymd': '{d.year}-{d.month}-{d.day}',
... 'mdy': '{d.month}/{d.day}/{d.year}',
... 'dmy': '{d.day}/{d.month}/{d.year}'
... }
...
... def __init__(self, year, month, day):
... self.year=year
... self.month=month
... self.day=day
...
... def __format__(self, format_spec):
... if format_spec is None or format_spec == '':
... format_spec='ymd'
... fmt = Date._formats[format_spec]
... return fmt.format(d=self)
...
>>> d=Date(2018,12,12)
>>> format(d)
'2018-12-12'
>>>
>>> format(d, 'mdy')
'12/12/2018'
>>>
>>> 'The date is {:ymd}'.format(d)
'The date is 2018-12-12'
__format__()
方法给 Python
的字符串格式化功能提供了一个钩子。
3. 让对象支持上下文管理协议
我们都知道 with
在文件处理中,提供了很好的上下文管理。
为了让一个对象兼容 with
语句,你需要实现 __enter__()
和 __exit__()
方法。
>>> class FileOperator:
... def __init__(self, path):
... self.path=path
...
... def __enter__(self):
... print('__enter__')
... self.file=open(self.path, 'rt')
... return self.file
...
... def __exit__(self, exc_type, exc_val, exc_tb):
... print('__exit__')
... self.file.close()
... self.file=None
...
>>> operator= FileOperator('/Users/faris/Desktop/splider.py')
>>>
>>> with operator as f:
... print(f)
...
__enter__
<_io.TextIOWrapper name='/Users/faris/Desktop/splider.py' mode='rt' encoding='UTF-8'>
__exit__
4. 在类中封装私有属性名
Python
语言并没有访问控制,而是通过遵循一定的属性和方法命名规约来达到这个效果。
-
任何以单下划线
_
开头的名字都应该是内部实现 -
任何双下划线
__
开头的名字都是为了防止被继承后覆盖。
class B:
def __init__(self):
self.__private = 0
def __private_method(self):
pass
def public_method(self):
pass
在前面的类B中,私有属性会被分别重命名为 _B__private
和 _B__private_method
。
class C(B):
def __init__(self):
super().__init__()
self.__private = 1 # Does not override B.__private
# Does not override B.__private_method()
def __private_method(self):
pass
而当 C
继承 B
时,C
中的 __private
和 __private_method
被重命名为 _C__private
和_C__private_method
,这个跟父类 B
中的名称是完全不同的。
规则:
你应该让你的非公共名称以单下划线开头。但是,如果你清楚你的代码会涉及到子类, 并且有些内部属性应该在子类中隐藏起来,那么才考虑使用双下划线方案。
有时候你定义的一个变量和某个保留关键字冲突,这时候可以使用单下划线作为后缀。这里我们并不使用单下划线前缀的原因是它避免误解它的使用初衷 (如果这里使用单下划线前缀的目的是为了防止命名冲突而不是指明这个属性是私有的)。 通过使用单下划线后缀可以解决这个问题。
5. 创建可管理的属性
我们可以将某个属性定义为一个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 AttributeError("can't delete attribute")
if __name__ == "__main__":
person = Person(1)
print(person.first_name)
比如动态计算:
import math
class Circle:
def __int__(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 * self.radius * math.pi
Python
不需要像 Java
那样的getter
、setter
方法。那样只会让代码很臃肿,违背了 Python
简洁的原则。
6. 调用父类方法
class A:
def __init__(self, name):
self.name = name
self._age = 1
self.__sex = 'female'
def get_name(self):
return self.name
class B(A):
def __init__(self, name):
super().__init__(name)
def age1(self):
return self._age
def name1(self):
return self.name
def sex1(self):
return super().__sex
b = B('faris')
print('b.name: ', b.name)
print('b.name1(): ', b.name1())
print('b.get_name(): ', b.get_name())
print('b._age: ', b._age)
print('b.age1(): ', b.age1())
print('b.sex1(): ', b.sex1())
print('A.__sex:', A.__sex)
那些会报错?
由于 name
、get_name
为 A
的公共属性和公共方法,因此会被继承到 B
上,可以随意使用。
b.name: faris
b.name1(): faris
b.get_name(): faris
由于_age
为A
的私有属性,Python
仅仅靠命名约束私有属性,但是不会强制约束,因此编译器会有警告,但是仍然可以被 B
正常使用。
b._age: 1
b.age1(): 1
由于 __sex
前面为双下划线,本来就是为了防止被继承后覆盖的,且在编译时被编译为 _A__sex
, 这样B
肯定无法调用。
>>> b.sex1()
AttributeError: 'super' object has no attribute '_B__sex'
>>> A.__sex
AttributeError: type object 'A' has no attribute '__sex'
初始化顺序:
class Base:
def __init__(self):
print('Base.__init__')
class A(Base):
def __init__(self):
super().__init__()
print('A.__init__')
class B(Base):
def __init__(self):
super().__init__()
print('B.__init__')
class C(A, B):
def __init__(self):
super().__init__()
print('C.__init__')
执行结果:
>>> c = C()
Base.__init__
B.__init__
A.__init__
C.__init__
>>>
我们会发现 Base.__init__
只被调用了 一次。Python
是如何实现继承关系的?
Python
会计算出一个所谓的方法解析顺序( MRO
)列表。 这个 MRO
列表就是一个简单的所有基类的线性顺序表。
>>> C.__mro__
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,
<class '__main__.Base'>, <class 'object'>)
>>>
为了实现继承,Python
会在 MRO
列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。
而这个 MRO
列表的构造是通过一个C3线性化算法来实现的。 我们不去深究这个算法的数学原理,它实际上就是合并所有父类的 MRO
列表并遵循如下三条准则:
- 子类会先于父类被检查
- 多个父类会根据它们在列表中的顺序被检查
- 如果对下一个类存在两个合法的选择,选择第一个父类
7. 定义接口或者抽象基类
你想定义一个接口或抽象类,并且通过执行类型检查来确保子类实现了某些特定的方法
使用 abc
模块可以很轻松的定义抽象基类:
from abc import ABCMeta, abstractmethod
class IStream(metaclass=ABCMeta):
@abstractmethod
def read(self, max_bytes=-1):
pass
@abstractmethod
def write(self, data):
pass
class SocketStream(IStream):
def read(self, max_bytes=-1):
pass
def write(self, data):
pass
@abstractmethod
还能注解 静态方法
、类方法
和 properties
。 你只需保证这个注解紧靠在函数定义前即可,这对Java
程序员来说是毁三观的定义:
from abc import ABCMeta, abstractmethod
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
@classmethod
def method3(cls):
print(cls)
@staticmethod
def method4():
print(A)
class B(A):
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 string')
@classmethod
def method1(cls):
print(cls)
@staticmethod
def method2():
print(B)
b = B('1')
b.name = 'faris'
print(b.name)
B.method1()
B.method2()
B.method3()
B.method4()
执行结果:
faris
<class '__main__.B'>
<class '__main__.B'>
<class '__main__.B'>
<class '__main__.A'>
我们可以看到,B
仍然可以调用 method3
和 method4
。但是method3
中打印的却是 B
类。
8. 属性的代理访问
代理是一种编程模式,它将某个操作转移给另外一个对象来实现。
这个对于 Java
程序员来说,应该已经习以为常了。
class Proxy:
def __init__(self, obj):
self._obj = obj
# Delegate attribute lookup to internal obj
def __getattr__(self, name):
print('getattr:', name)
return getattr(self._obj, name)
# Delegate attribute assignment
def __setattr__(self, name, value):
if name.startswith('_'):
super().__setattr__(name, value)
else:
print('setattr:', name, value)
setattr(self._obj, name, value)
# Delegate attribute deletion
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(4)
p = Proxy(s)
p.bar(3)
需要注意:
-
__getattr__()
实际是一个后备方法,只有在属性不存在时才会调用。 因此,如果代理类实例本身有这个属性的话,那么不会触发这个方法的。 -
__setattr__()
和__delattr__()
只代理那些不以下划线_
开头的属性(代理类只暴露被代理类的公共属性)。 -
__getattr__()
对于大部分以双下划线(__
)开始和结尾的属性并不适用。
和 Java
中的思路一样,我们会使用 代理
与 组合
来替代 继承
。
例如:我们实现一个列表,我们不会使用继承 :
>>> class ListLike:
... def __init__(self):
... self._items = []
...
... def __getattr__(self, name):
... print("method name:", name)
... return getattr(self._items, name)
>>> l=ListLike()
>>>
>>>
>>> l.insert(1,1,)
method name: insert
>>> l.sort()
method name: sort
>>> l.append(2)
method name: append
但是,当我们调用 len()
、元素查找
、以及 打印
则报错,或者没有调用__getattr__
。
原因在于上面需要注意的第三点:
__getattr__()
对于大部分以双下划线(__
)开始和结尾的属性并不适用
-
len()
需要对象实现__len__
-
元素查找
需要对象__getitem__
、__setitem__
、__delitem__
。 - 打印则需要实现
__str__
、__repr__
。
>>> class ListLike:
... def __init__(self):
... self._items = []
...
... def __getattr__(self, name):
... print("method name:", name)
... return getattr(self._items, name)
...
... def __repr__(self):
... return self._items.__repr__()
...
... def __str__(self):
... return self._items.__str__()
...
... def __len__(self):
... return len(self._items)
...
... def __setitem__(self, index, value):
... self._items[index] = value
...
... def __getitem__(self, index):
... return self._items[index]
...
... def __delitem__(self, index):
... del self._items[index]
9. 在类中定义多个构造器
你想实现一个类,除了使用 __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)
类方法的一个主要用途就是定义多个构造器。它接受一个 class 作为第一个参数(cls)。 你应该注意到了这个类被用来创建并返回最终的实例。在继承时也能工作的很好:
class NewDate(Date):
pass
c = Date.today() # Creates an instance of Date (cls=Date)
d = NewDate.today() # Creates an instance of NewDate (cls=NewDate)
10. 实现数据模型的类型约束
你想定义某些在属性赋值上面有限制的数据结构,
10.1 描述器类
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]
一个描述器就是一个实现了三个核心的属性访问操作(get
, set
, delete
)的类, 分别为 __get__()
、__set__()
和 __delete__()
这三个特殊的方法。 这些方法接受一个实例作为输入,之后相应的操作实例底层的字典。
所有对描述器属性的访问会被 __get__()
、__set__()
和 __delete__()
方法捕获到。
描述器只能在类级别被定义,而不能为每个实例单独定义。
class Point:
x = Integer('x')
y = Integer('y')
def __init__(self, x, y):
self.x = x
self.y = y
执行结果:
>>> p = Point(2, 3)
>>> p.x # Calls Point.x.__get__(p,Point)
2
>>> p.y = 5 # Calls Point.y.__set__(p, 5)
>>> p.x = 2.3 # Calls Point.x.__set__(p, 2.3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "descrip.py", line 12, in __set__
raise TypeError('Expected an int')
TypeError: Expected an int
>>>
__get__()
看上去有点复杂的原因归结于实例变量和类变量的不同。 如果一个描述器被当做一个类变量来访问,那么 instance
参数被设置成 None
>>> p = Point(2,3)
>>> p.x # Calls Point.x.__get__(p, Point)
2
>>> Point.x # Calls Point.x.__get__(None, Point)
<__main__.Integer object at 0x100671890>
>>>
10.2 类装饰器
我们发现描述器类需要在每一个类中定义类属性,会侵入代码。
而类装饰器则会更加的简洁与灵活
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
stock = Stock('1', 1, 1)
这样利用类装饰器将描述器隐藏在身后,进而展示出装饰器的灵活性。
10.3 元类
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 checked_meta(type):
def __new__(cls, clsname, bases, methods):
print(cls, clsname, bases, methods)
for key, value in methods.items():
if isinstance(value, Integer):
value.name = key
return type.__new__(cls, clsname, bases, methods)
class Stock2(metaclass=checked_meta):
shares = Integer('shares')
def __init__(self, shares):
self.shares = shares
s=Stock2(1)
有时我们需要通过多个描述器才能完成一个限制,这样我们可以通过多继承的方式来实现。
10.4 混入
class Descriptor:
def __init__(self, name, **opts):
self.name = name
for key, value in opts.items():
setattr(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 String(Typed):
expected_type = str
class Sized(Descriptor):
def __init__(self, name, **opts):
if 'min_size' not in opts and 'max_size' not in opts:
raise TypeError('missing min_size or max_size option')
super().__init__(name, **opts)
def __set__(self, instance, value):
length = len(value)
if self.min_size and self.min_size is not None and self.min_size > length:
raise ValueError('size must be > ' + str(self.min_size))
if self.max_size and self.max_size is not None and self.max_size < length:
raise ValueError('size must be < ' + str(self.max_size))
super().__set__(instance, value)
class SizedString(String, Sized):
pass
class Stock:
name = SizedString('name', min_size=8, max_size=10)
def __init__(self, name):
self.name = name
s=Stock('a')
混入类的一个比较难理解的地方是,调用 super()
函数时,你并不知道究竟要调用哪个具体类。 你需要跟其他类结合后才能正确的使用,也就是必须合作才能产生效果。
类装饰器方案应该是最灵活和最高明的。
-
首先,它并不依赖任何其他新的技术,比如元类。
-
其次,装饰器可以很容易的添加或删除。
-
最后,也是最重要的,装饰器是最快的。
11. 利用Mixins
扩展类功能
想扩展映射对象,给它们添加日志功能。
11.1 混入类
class LoggedMappingMixin:
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
11.2 装饰器
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
l=LoggedDict()
l['x'] = 1