Python类与对象学习心得-10:简化数据结构的初始化 __init__()

你写了很多仅仅用作数据结构的类,这时定义大量的 __init__() 函数变成了一件很烦人的工作。为了简化,我们可以在一个基类中写一个公用的 __init__() 函数:

import math

class Structure1:
    # Class variable that specifies expected fields
    _fields = []

    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError('Expected {} arguments'.format(len(self._fields)))
        # Set the instance attributes by setattr 内置函数
        for name, value in zip(self._fields, args):
            setattr(self, name, value)

然后使你的类继承自这个基类:

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

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

class Circle(Structure1):
    _fields = ['radius']

    def area(self):
        return math.pi * self.radius ** 2

使用这些类的示例:

>>> s = Stock('ACME', 50, 91.1)
>>> p = Point(2, 3)
>>> c = Circle(4.5)
>>> s2 = Stock('ACME', 50)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "structure.py", line 6, in __init__
        raise TypeError('Expected {} arguments'.format(len(self._fields)))
TypeError: Expected 3 arguments

如果还想支持关键字参数,你可以有几种设计选择。其中一种选择是只将 _fields 中指定的属性名对应的关键字参数设置为实例属性,额外的关键字参数则报错:

class Structure2:
    _fields = []

    def __init__(self, *args, **kwargs):
        if len(args) > len(self._fields):
            raise TypeError('Expected {} arguments'.format(len(self._fields)))
        
        # Set all of the positional arguments
        for name, value in zip(self._fields, args):
            setattr(self, name, value)
        
        # Set the remaining keyword arguments
        for name in self._fields[len(args):]:
            setattr(self, name, kwargs.pop(name))
        
        # Check for any remaining unknown arguments
        if kwargs:
            raise TypeError('Invalid argument(s): {}'.format(','.join(kwargs)))

# Example use
if __name__ == '__main__':
    class Stock(Structure2):
        _fields = ['name', 'shares', 'price']
    
    s1 = Stock('ACME', 50, 91.1)
    s2 = Stock('ACME', 50, price=91.1)
    s3 = Stock('ACME', shares=50, price=91.1)
    # s3 = Stock('ACME', shares=50, price=91.1, aa=1)

另一种可能的选择,是把关键字参数作为一种添加新实例属性的方式( _fields 中未指定的属性):

class Structure3:
    # Class variable that specifies expected fields
    _fields = []

    def __init__(self, *args, **kwargs):
        if len(args) != len(self._fields): # 此时所有在 _fields 中指定的字段必须以位置参数的形式提供字段值
            raise TypeError('Expected {} arguments'.format(len(self._fields)))

        # Set the arguments
        for name, value in zip(self._fields, args):
            setattr(self, name, value)

        # Set the additional arguments (if any)
        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)))

# Example use
if __name__ == '__main__':
    class Stock(Structure3):
        _fields = ['name', 'shares', 'price']

    s1 = Stock('ACME', 50, 91.1)
    s2 = Stock('ACME', 50, 91.1, date='8/2/2012')

当你需要使用大量很小的数据结构类的时候,相比手工一个个定义 __init__() 方法,使用这种方式可以大大简化代码,否则你得像下面这样写代码:

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

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

class Circle:
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return math.pi * self.radius ** 2

在上面的优化实现中有一点值得注意,就是使用了 setattr() 内置函数来设置实例属性值,你可能不想用这种方式,而是想直接更新实例字典(这样似乎更加高效),就像下面这样:

class Structure:
    # Class variable that specifies expected fields
    _fields= []

    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError('Expected {} arguments'.format(len(self._fields)))

        # Set the arguments (alternate)
        self.__dict__.update(zip(self._fields, args))

尽管这也可以正常工作(而且确实更高效),但是当定义子类的时候问题就来了。当一个子类定义了 __slots__ 或者通过特性(property)或描述符(descriptor)来包装某个属性时,直接访问实例字典就不起作用了。我们上面使用 setattr() 内置函数会显得更通用些,因为它兼容了子类的各种情况。

这种方法唯一不好的地方就是对某些 IDE 而言,在显示帮助函数时可能不太友好(无法显示出每个类 __init__() 方法的具体参数)。比如:

>>> help(Stock)
Help on class Stock in module __main__:
class Stock(Structure)
...
| Methods inherited from Structure:
|
| __init__(self, *args, **kwargs)
|
...
>>>

很多这类问题都可以通过在 __init__() 中附着或强加一个类型签名来解决。稍后的文章会详细讲到。

当然,通过一个辅助函数和所谓的"frame hack"也可以自动初始化实例变量。例如:

import sys

def init_fromlocals(self):
    locs = sys._getframe(1).f_locals
    for k, v in locs.items():
        if k != 'self':
            setattr(self, k, v)

class Stock:
    def __init__(self, name, shares, price):
        init_fromlocals(self)

在这种实现方案中,init_fromlocals() 函数通过调用 sys._getframe() 来“偷窥”调用它的方法的局部变量(返回一个字典)。如果 __init__() 方法中一开始就调用了 init_fromlocals() 函数,那么它的局部变量就等同于传入的参数(self, name, shares, price),因此可以很容易地设置好同名的实例属性。尽管这种方案避免了前面提到的在 IDE 中获取正确的调用签名的问题,但它比我们提供的实现方案慢了至少 50%,而且涉及到更为复杂的幕后魔法(调用 sys 的内部方法)。如果你的代码不需要这种额外的能力,那么更简单的方案会更加适用。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值