Zodiac Wang
  • Home
  • Categories
  • Tags
  • Archives

PythonCookbook笔记(8)-类与对象

Table of Contents

  • 1  第八章 类与对象
    • 1.1  8.1 改变对象的字符串显示
    • 1.2  8.2 自定义字符串的格式化
    • 1.3  8.3 让对象支持上下文管理协议
    • 1.4  8.4 创建大量对象时节省内存方法
    • 1.5  8.5 在类中封装属性名
    • 1.6  8.6 创建可管理的属性
    • 1.7  8.7 调用父类方法
    • 1.8  8.8 子类中扩展property
    • 1.9  8.9 创建新的类或实例属性

第八章 类与对象¶

本章主要关注点的是和类定义有关的常见编程模型。包括让对象支持常见的 Python 特性、特殊方法的使用、类封装技术、继承、内存管理以及有用的设计模式。

比较重要的小节:

  • 8.7 调用父类方法

8.1 改变对象的字符串显示¶

问题:你想改变对象实例的打印或显示输出,让它们更具可读性。

解决方案:要改变一个实例的字符串表示,可重新定义它的 str() 和 repr() 方法。

repr() 方法返回一个实例的代码表示形式,通常用来重新构造这个实例。 内置的 repr() 函数返回这个字符串,跟我们使用交互式解释器显示的值是一样的。 str()方法将实例转换为一个字符串,使用 str() 或 print() 函数会输出这个字符串。

!r 格式化代码指明输出使用 repr() 来代替默认的 str()(也可以使用 %r )

>>> p = Pair(3, 4)
>>> print('p is {0!r}'.format(p))
p is Pair(3, 4)
>>> print('p is {0}'.format(p))
p is (3, 4)

讨论:自定义 repr() 和 str() 通常是很好的习惯,因为它能简化调试和实例输出。repr() 生成的文本字符串标准做法是需要让 eval(repr(x)) == x 为真。 如果实在不能这样,应该创建一个有用的文本表示,并使用 < 和 > 括起来。比如:

>>> f = open('file.dat')
>>> f
<_io.TextIOWrapper name='file.dat' mode='r' encoding='UTF-8'>

注:eval() 函数用来执行一个字符串表达式,并返回表达式的值。如果 没有被定义str(),会用 repr() 来代替。

8.2 自定义字符串的格式化¶

问题:你想通过 format() 函数和字符串方法使得一个对象能支持自定义的格式化。

解决方案:为了自定义字符串的格式化,我们需要在类上面定义 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)

现在 Date 类的实例可以支持格式化操作了,如同下面这样:

>>> 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() 方法给 Python 的字符串格式化功能提供了一个钩子。

例如,参考下面来自 datetime 模块中的代码:

>>> from datetime import date
>>> d = date(2012, 12, 21)
>>> format(d)
'2012-12-21'
>>> format(d,'%A, %B %d, %Y')
'Friday, December 21, 2012'
>>> 'The end is {:%d %b %Y}. Goodbye'.format(d)
'The end is 21 Dec 2012. Goodbye'

8.3 让对象支持上下文管理协议¶

问题:你想让你的对象支持上下文管理协议(with语句)。

解决方案:为了让一个对象兼容 with 语句,需要实现 __enter__() 和__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, exc_ty, exc_val, tb):
        self.sock.close()
        self.sock = None

讨论:编写上下文管理器的主要原理是你的代码会放到 with 语句块中执行。 当出现 with 语句的时候,对象的 enter() 方法被触发, 它返回的值(如果有的话)会被赋值给 as声明的变量。然后,with 语句块里面的代码开始执行。 最后,exit() 方法被触发进行清理工作。

不管 with 代码块中发生什么,上面的控制流都会执行完,就算代码块中发生了异常也是一样的。 事实上,exit() 方法的第三个参数包含了异常类型、异常值和追溯信息(如果有的话)。 exit() 方法能自己决定怎样利用这个异常信息,或者忽略它并返回一个None值。 如果 exit() 返回 True ,那么异常会被清空,就好像什么都没发生一样, with 语句后面的程序继续在正常执行。

在需要管理一些资源比如文件、网络连接和锁的编程环境中,使用上下文管理器是很普遍的。 这些资源的一个主要特征是它们必须被手动的关闭或释放来确保程序的正确运行。以下是一个利用栈实现的可以嵌套使用 with 语句的实现。

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.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()
# Example use
from functools import partial

conn = LazyConnection(('www.python.org', 80))
with conn as s1:
    pass
with conn as s2:
    pass
# s1 and s2 are independent sockets

在 contextmanager 模块中有一个标准的上下文管理方案模板。

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

问题:你的程序要创建大量(可能上百万)的对象,导致占用很大的内存。

解决方案:对于主要是用来当成简单的数据结构的类而言,你可以通过给类添加 __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 看上去是一个很有用的特性,但还是得减少对它的使用冲动 Python 的很多特性都依赖于普通的基于字典的实现,定义了 slots 后的类不再支持一些普通类特性了,比如多继承。应该只在那些经常被使用到的用作数据结构的类上定义 slots (比如在程序中需要创建某个类的几百万个实例对象)。

关于 __slots__ 的一个常见误区是它可以作为一个封装工具来防止用户给实例增加新的属性。 尽管使用slots可以达到这样的目的,但是这个并不是它的初衷。 __slots__ 更多的是用来作为一个内存优化工具。

zw:这种时候我可能会转向 NumPy 库进行内存优化

8.5 在类中封装属性名¶

问题:你想封装类的实例上面的“私有”数据,但是Python语言并没有访问控制。

解决方案:Python 程序不依赖语言特性封装数据,而是通过遵循一定的属性和方法命名规约来达到这个效果。

第一个约定是任何以单下划线_开头的名字都应该是内部实现

使用下划线开头的约定同样适用于模块名和模块级别函数。 例如,如果你看到某个模块名以单下划线开头(比如_socket),那它就是内部实现。 类似的,模块级别函数比如 sys._getframe() 在使用的时候就得加倍小心了。

第二个约定是任何以双下划线_开头的名字会变成其他形式

使用双下划线开始会导致访问名称变成其他形式。私有名称 private 和 private_method 被重命名为 _Cprivate 和 _Cprivate_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

讨论:大多数情况下非公共名称以单下划线开头。但是,如果你清楚你的代码会涉及到子类, 并且有些内部属性应该在子类中隐藏起来,那么才考虑使用双下划线方案。

8.6 创建可管理的属性¶

问题:需要给某个实例 attribute 增加除访问与修改之外的其他处理逻辑,比如类型检查或合法性验证。

解决方案:自定义某个属性的一种简单方法是将它定义为一个 property

class Person:
    def __init__(self, first_name):
        self.first_name = first_name

    # Getter function
    @property
    def first_name(self):
        return self._first_name

    # Setter function
    @first_name.setter
    def first_name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._first_name = value

    # Deleter function (optional)
    @first_name.deleter
    def first_name(self):
        raise AttributeError("Can't delete attribute")

上述代码中有三个相关联的方法,这三个方法的名字都必须一样。 第一个方法是一个 getter 函数,它使得 first_name 成为一个属性。 其他两个方法给 first_name 属性添加了 setter 和 deleter 函数。 需要强调的是只有在 first_name 属性被创建后, 后面的两个装饰器 @first_name.setter 和 @first_name.deleter 才能被定义。property 的一个关键特征是它看上去跟普通的 attribute 没什么两样, 但是访问它的时候会自动触发 getter 、setter 和 deleter 方法。

>>> a = Person('Guido')
>>> a.first_name # Calls the getter
'Guido'
>>> a.first_name = 42 # Calls the setter
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "prop.py", line 14, in first_name
        raise TypeError('Expected a string')
TypeError: Expected a string
>>> del a.first_name
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
AttributeError: can`t delete attribute

在实现一个 property 的时候,底层数据(如果有的话)仍然需要存储在某个地方。 因此,在 get 和 set 方法中,会对 _first_name 属性的操作,这是实际数据存储的地方。 另外,在 __init__() 方法中设置了 self.first_name 而不是 self._first_name,因为在设置 self.first_name 的时候会自动调用 first_name.setter,创建 property 的目的就是在设置 attribute 的时候进行检查。

另外,还能在已存在的 get 和 set 方法基础上定义 property

class Person:
    def __init__(self, first_name):
        self.set_first_name(first_name)

    # Getter function
    def get_first_name(self):
        return self._first_name

    # Setter function
    def set_first_name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._first_name = value

    # Deleter function (optional)
    def del_first_name(self):
        raise AttributeError("Can't delete attribute")

    # Make a property from existing get/set methods
    name = property(get_first_name, set_first_name, del_first_name)

讨论:一个 property 属性其实就是一系列相关绑定方法的集合。property 本身的 fget、fset 和 fdel 属性就是类里面的普通方法。通常不会直接调用 fget 或者 fset,它们会在访问 property 的时候自动被触发。只有确实需要对 attribute 执行其他额外的操作的时候才应该使用到 property。Properties 还是一种定义动态计算 attribute 的方法,这种类型的 attributes 并不会被实际的存储,而是在需要的时候计算出来。

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

尽管 properties 可以实现优雅的编程接口,但有些时候你还是会想直接使用 getter 和 setter 函数。有时是基于兼容性的考虑。

8.7 调用父类方法¶

问题:你想在子类中调用父类的某个已经被覆盖的方法。

解决方案:为了调用父类(超类)的一个方法,可以使用 super() 函数。

super() 函数的一个常见用法是在 init() 方法中确保父类被正确的初始化了:

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

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

super() 的另外一个常见用法出现在覆盖 Python 特殊方法的代码中,比如:

class Proxy:
    def __init__(self, obj):
        self._obj = obj

    # Delegate attribute lookup to internal obj
    def __getattr__(self, name):
        return getattr(self._obj, name)

    # Delegate attribute assignment
    def __setattr__(self, name, value):
        if name.startswith('_'):
            super().__setattr__(name, value) # Call original __setattr__
        else:
            setattr(self._obj, name, value)

在上面代码中,setattr() 的实现包含一个名字检查。如果某个属性名以下划线开头,就通过 super() 调用原始的 __setattr__() ,否则的话就委派给内部的代理对象 self._obj 去处理。这看上去有点意思,因为就算没有显式的指明某个类的父类,super() 仍然可以有效的工作。

讨论:通过父类名调用父类方法的做法有时可以正常工作,但如果遇到多重继承,情况就会变得复杂,有可能父类方法会被多次执行,而通过 super() 进行调用则不会出现这样的问题。

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 列表并遵循如下三条准则:

子类会先于父类被检查
多个父类会根据它们在列表中的顺序被检查
如果对下一个类存在两个合法的选择,选择第一个父类


使用 super() 函数时,Python 会在 MRO 列表上继续搜索下一个类。只要每个重定义的方法统一使用 super() 并只调用它一次, 那么控制流最终会遍历完整个 MRO 列表,每个方法也只会被调用一次。

super() 有个令人吃惊的地方是它并不一定去查找某个类在MRO中下一个直接父类,可以在一个没有直接父类的类中使用它。例如,考虑如下这个类:

class A:
    def spam(self):
        print('A.spam')
        super().spam()

如果你试着直接使用这个类就会出错,但是,如果你使用多继承的话看看会发生什么:

>>> class B:
...     def spam(self):
...         print('B.spam')
...
>>> class C(A,B):
...     pass
...
>>> c = C()
>>> c.spam()
A.spam
B.spam

在定义混入类的时候这样使用 super() 是很普遍的。

然而,由于 super() 可能会调用不是你想要的方法,你应该遵循一些通用原则。

  • 首先,确保在继承体系中所有相同名字的方法拥有可兼容的参数签名(比如相同的参数个数和参数名称)。 这样可以确保 super() 调用一个非直接父类方法时不会出错。
  • 其次,最好确保最顶层的类提供了这个方法的实现,这样的话在MRO上面的查找链肯定可以找到某个确定的方法。

8.8 子类中扩展property¶

问题:在子类中扩展定义在父类中的 property 的功能

解决方案:如下父类定义了一个 property

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

    # Getter function
    @property
    def name(self):
        return self._name

    # Setter function
    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._name = value

    # Deleter function
    @name.deleter
    def name(self):
        raise AttributeError("Can't delete attribute")

扩展 name 属性

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)
>>> s = SubPerson('Guido')
Setting name to Guido
>>> s.name
Getting name
'Guido'
>>> s.name = 'Larry'
Setting name to Larry
>>> s.name = 42
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "example.py", line 16, in name
        raise TypeError('Expected a string')
TypeError: Expected a string
>>>

如果只想修改 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)

在 setter 函数中使用 super(SubPerson, SubPerson).name.__set__(self, value) 语句是为了委托给之前定义的 setter 方法,需要将控制权传递给之前定义的 name 属性的 set() 方法。 不过,获取这个方法的唯一途径是使用类变量而不是实例变量来访问。

如果只想定义一个方法,那么不能只重新定义 property

class SubPerson(Person):
    @property  # Doesn't work
    def name(self):
        print('Getting name')
        return super().name

运行会发现 setter 函数整个消失

>>> s = SubPerson('Guido')
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "example.py", line 5, in __init__
        self.name = name
AttributeError: can't set attribute
>>>

应该这样修改

class SubPerson(Person):
    @Person.name.getter
    def name(self):
        print('Getting name')
        return super().name

之前定义过的方法会被复制过来,而修改的方法会被替换

没办法使用更加通用的方式去替换硬编码的 Person 类名。 如果不知道到底是哪个基类定义了 property,只能重新定义所有 property 并使用 super() 来将控制权传递给前面的实现。

上面演示的第一种技术还可以被用来扩展一个描述器(8.9小节)

# A descriptor
class String:
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, cls):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        instance.__dict__[self.name] = value

# A class with a descriptor
class Person:
    name = String('name')

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

# Extending a descriptor with a 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)

8.9 创建新的类或实例属性¶

问题:创建一个新的拥有一些额外功能的实例属性类型,比如类型检查。

解决方案:如果需要创建一个全新的实例属性,可以通过一个描述器类的形式来定义它的功能

# Descriptor attribute for an integer type-checked attribute
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__() 这三个特殊的方法。这些方法接受一个实例作为输入,之后相应的操作实例底层的字典。

要使用描述器,需要把描述器作为属性放到类的定义中去。

class Point:
    x = Integer('x')
    y = Integer('y')

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

所有对描述器属性(比如x或y)的访问会被 __get__() 、__set__() 和 __delete__() 方法捕获到

>>> 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
>>>

作为输入,描述器的每一个方法会接受一个操作实例。为了实现请求操作,会相应的操作实例底层的字典(dict属性)。 描述器的 self.name 属性存储了在实例字典中被实际使用到的 key。

讨论:描述器可实现大部分Python类特性中的底层魔法, 包括 @classmethod 、@staticmethod 、@property ,甚至是 slots 特性。

描述器的一个比较困惑的地方是它只能在类级别被定义,而不能为每个实例单独定义

# Does NOT work
class Point:
    def __init__(self, x, y):
        self.x = Integer('x') # No! Must be a class variable
        self.y = Integer('y')
        self.x = x
        self.y = y

__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>
>>>

举个例子,涉及到类装饰器的基于描述器的代码

# 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

如果只想简单的自定义某个类的单个属性访问的话就不用去写描述器了。这种情况下使用 8.6 小节介绍的 property 技术会更加容易


  • « PythonCookbook笔记(7)- 函数
  • PythonCookbook笔记(9)-编程 »

Published

6 24, 2019

Category

posts

Tags

  • PythonCookbook 10

Contact

  • Zodiac Wang - A Fantastic Learner
  • Powered by Pelican. Theme: Elegant