8.13 實(shí)現(xiàn)數(shù)據(jù)模型的類型約束

2018-02-24 15:26 更新

問題

你想定義某些在屬性賦值上面有限制的數(shù)據(jù)結(jié)構(gòu)。

解決方案

在這個(gè)問題中,你需要在對(duì)某些實(shí)例屬性賦值時(shí)進(jìn)行檢查。所以你要自定義屬性賦值函數(shù),這種情況下最好使用描述器。

下面的代碼使用描述器實(shí)現(xiàn)了一個(gè)系統(tǒng)類型和賦值驗(yàn)證框架:

# Base class. Uses a descriptor to set a value
class Descriptor:
    def __init__(self, name=None, **opts):
        self.name = name
        for key, value in opts.items():
            setattr(self, key, value)

    def __set__(self, instance, value):
        instance.__dict__[self.name] = value

# Descriptor for enforcing types
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)

# Descriptor for enforcing values
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, **opts)

    def __set__(self, instance, value):
        if len(value) >= self.size:
            raise ValueError('size must be < ' + str(self.size))
        super().__set__(instance, value)

這些類就是你要?jiǎng)?chuàng)建的數(shù)據(jù)模型或類型系統(tǒng)的基礎(chǔ)構(gòu)建模塊。下面就是我們實(shí)際定義的各種不同的數(shù)據(jù)類型:

class Integer(Typed):
    expected_type = int

class UnsignedInteger(Integer, Unsigned):
    pass

class Float(Typed):
    expected_type = float

class UnsignedFloat(Float, Unsigned):
    pass

class String(Typed):
    expected_type = str

class SizedString(String, MaxSized):
    pass

然后使用這些自定義數(shù)據(jù)類型,我們定義一個(gè)類:

class Stock:
    # Specify constraints
    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

然后測(cè)試這個(gè)類的屬性賦值約束,可發(fā)現(xiàn)對(duì)某些屬性的賦值違法了約束是不合法的:

>>> s.name
'ACME'
>>> s.shares = 75
>>> s.shares = -10
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "example.py", line 17, in __set__
        super().__set__(instance, value)
    File "example.py", line 23, in __set__
        raise ValueError('Expected >= 0')
ValueError: Expected >= 0
>>> s.price = 'a lot'
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "example.py", line 16, in __set__
        raise TypeError('expected ' + str(self.expected_type))
TypeError: expected <class 'float'>
>>> s.name = 'ABRACADABRA'
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "example.py", line 17, in __set__
        super().__set__(instance, value)
    File "example.py", line 35, in __set__
        raise ValueError('size must be < ' + str(self.size))
ValueError: size must be < 8
>>>

還有一些技術(shù)可以簡(jiǎn)化上面的代碼,其中一種是使用類裝飾器:

# Class decorator to apply constraints
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

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

另外一種方式是使用元類:

# A metaclass that applies checking
class checkedmeta(type):
    def __new__(cls, clsname, bases, methods):
        # Attach attribute names to the descriptors
        for key, value in methods.items():
            if isinstance(value, Descriptor):
                value.name = key
        return type.__new__(cls, clsname, bases, methods)

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

討論

本節(jié)使用了很多高級(jí)技術(shù),包括描述器、混入類、super() 的使用、類裝飾器和元類。不可能在這里一一詳細(xì)展開來講,但是可以在8.9、8.18、9.19小節(jié)找到更多例子。但是,我在這里還是要提一下幾個(gè)需要注意的點(diǎn)。

首先,在 Descriptor 基類中你會(huì)看到有個(gè) __set__() 方法,卻沒有相應(yīng)的 __get__() 方法。如果一個(gè)描述僅僅是從底層實(shí)例字典中獲取某個(gè)屬性值的話,那么沒必要去定義 __get__() 方法。

所有描述器類都是基于混入類來實(shí)現(xiàn)的。比如 UnsignedMaxSized 要跟其他繼承自 Typed 類混入。這里利用多繼承來實(shí)現(xiàn)相應(yīng)的功能。

混入類的一個(gè)比較難理解的地方是,調(diào)用 super() 函數(shù)時(shí),你并不知道究竟要調(diào)用哪個(gè)具體類。你需要跟其他類結(jié)合后才能正確的使用,也就是必須合作才能產(chǎn)生效果。

使用類裝飾器和元類通常可以簡(jiǎn)化代碼。上面兩個(gè)例子中你會(huì)發(fā)現(xiàn)你只需要輸入一次屬性名即可了。

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

# Metaclass
class Point(metaclass=checkedmeta):
    x = Integer()
    y = Integer()

所有方法中,類裝飾器方案應(yīng)該是最靈活和最高明的。首先,它并不依賴任何其他新的技術(shù),比如元類。其次,裝飾器可以很容易的添加或刪除。

最后,裝飾器還能作為混入類的替代技術(shù)來實(shí)現(xiàn)同樣的效果;

# 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

# Decorator for allowing sized values
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

# Specialized descriptors
@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

這種方式定義的類跟之前的效果一樣,而且執(zhí)行速度會(huì)更快。設(shè)置一個(gè)簡(jiǎn)單的類型屬性的值,裝飾器方式要比之前的混入類的方式幾乎快100%。現(xiàn)在你應(yīng)該慶幸自己讀完了本節(jié)全部?jī)?nèi)容了吧?^_^

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)