首页 > 解决方案 > 是否可以创建一个在修改时实现其属性的对象?

问题描述

在这个例子中,应该怎么做才能print(left_hand.number_of_fingers)返回4而不是返回5

class Hand:
    def __init__(self, fingers:list):
        self.fingers = fingers
        self.number_of_fingers = len(fingers)

left_hand = Hand(["thumb", "index", "middle", "ring", "pinkie"])
left_hand.fingers.pop()
print(left_hand.number_of_fingers) # I want this to actualize and be 4, not 5

我找到了一个解决方案@property

class Hand:
    def __init__(self, fingers:list):
        self.fingers = fingers

    @property
    def number_of_fingers(self):
        return len(self.fingers)

但是由于计算能力问题我不满意,如果计算number_of_fingers成本很高,我们只想在fingers修改时计算它,而不是每次用户要求属性时计算number_of_fingers

现在我找到了一个不太优雅的解决方案来解决计算能力的问题:

class Hand:
    def __init__(self, fingers:list):
        self.fingers = fingers
        self.old_fingers = fingers
        self.number_of_fingers = len(fingers)

    def get_number_of_fingers(self):
        if self.fingers != self.old_fingers:
            self.old_fingers = self.fingers
            self.number_of_fingers = len(self.fingers)
        return self.number_of_fingers

标签: pythonclassattributes

解决方案


问题是你的Hand类中的底层列表,即self.fingers,没有被充分封装,因此任何用户都可以修改它,例如通过调用left_hand.fingers.pop()甚至分配一个新列表。因此,您不能假设它在调用之间没有被修改number_of_fingers,因此您别无选择,只能计算它在该调用中的长度。

解决方案是控制您班级的客户可以做什么和不能做什么。最简单的方法是使用name mangling。也就是说,您在属性名称前加上两个前导下划线字符。这使得您的班级的客户很难(尽管并非不可能)从班级外部访问这些属性(我们假设您的用户不是故意恶意的)。因此我们现在必须提供一个pop方法:

class Hand:
    def __init__(self, fingers:list):
        self.__fingers = fingers
        self.__number_of_fingers = len(fingers)

    def pop(self):
        assert(self.__fingers)
        self.__number_of_fingers -= 1
        return self.__fingers.pop()

    @property
    def number_of_fingers(self):
        return self.__number_of_fingers

left_hand = Hand(["thumb", "index", "middle", "ring", "pinkie"])
print(left_hand.pop())
print(left_hand.number_of_fingers)

印刷:

pinkie
4

并不是建议您实际上执行以下操作,但是如果您愿意,可以通过创建特殊的类装饰器来获得更详细的信息@Private@Public这会将您的类包装在一个新类中并检查对您的属性的访问,以确保您没有访问这些属性定义为私有。您可以使用@Private装饰器来定义那些私有的属性/方法(其他一切都被认为是公共的),或者使用@Public装饰器来定义那些公共的属性/方法(其他一切都被认为是私有的),但不能同时使用两者。您通常会使用前导单下划线命名您的私有属性,这是告诉用户属性/方法将被视为私有的约定。

这更多地意味着捕获对私有属性的无意访问。如果您使用-O Python 标志执行代码,则不会进行运行时检查。

def accessControl(failIf):
    def onDecorator(aClass):
        if not __debug__:
            return aClass
        else:
            class onInstance:
                def __init__(self, *args, **kargs):
                    self.__wrapped = aClass(*args, **kargs)

                def __getattr__(self, attr):
                    if failIf(attr):
                        raise TypeError('private attribute fetch: ' + attr)
                    else:
                        return getattr(self.__wrapped, attr)

                def __setattr__(self, attr, value):
                    if attr == '_onInstance__wrapped':
                        self.__dict__[attr] = value
                    elif failIf(attr):
                        raise TypeError('private attribute change: ' + attr)
                    else:
                        setattr(self.__wrapped, attr, value)
            return onInstance
    return onDecorator

def Private(*attributes):
    return accessControl(failIf=(lambda attr: attr in attributes))

def Public(*attributes):
    return accessControl(failIf=(lambda attr: attr not in attributes))

@Private('_fingers', '_number_of_fingers')
class Hand:

    def __init__(self, fingers:list):
        self._fingers = fingers
        self._number_of_fingers = len(fingers)

    def pop(self):
        assert(self._fingers)
        self._number_of_fingers -= 1
        return self._fingers.pop()

    @property
    def number_of_fingers(self):
        return self._number_of_fingers


left_hand = Hand(["thumb", "index", "middle", "ring", "pinkie"])
print(left_hand.pop())
print(left_hand.number_of_fingers)
# Thsis will throw an exception:
print(left_hand._fingers)

印刷:

pinkie
4
Traceback (most recent call last):
  File "C:\Booboo\test\test.py", line 50, in <module>
    print(left_hand._fingers)
  File "C:\Booboo\test\test.py", line 9, in __getattr__
    raise TypeError('private attribute fetch: ' + attr)
TypeError: private attribute fetch: _fingers

更新

这是 OP 使用缓存的方法:

class Hand:
    def __init__(self, fingers:list):
        self._cache = {}
        self.fingers = fingers

    def get_number_of_fingers(self):
        fingers = tuple(self.fingers) # can be key of a dictionary
        fingers_length = self._cache.get(fingers)
        if fingers_length:
            print(self.fingers, 'in cache')
            return fingers_length
        fingers_length = len(fingers)
        self._cache[fingers] = fingers_length
        return fingers_length

left_hand_fingers = ["thumb", "index", "middle", "ring", "pinkie"]
right_hand_fingers = ["thumb", "middle", "ring", "pinkie"]

hand = Hand(left_hand_fingers)
print(hand.get_number_of_fingers())
hand.fingers = right_hand_fingers
print(hand.get_number_of_fingers())
hand.fingers = left_hand_fingers
print(hand.get_number_of_fingers())
hand.fingers = right_hand_fingers
print(hand.get_number_of_fingers())
hand.fingers = left_hand_fingers
print(hand.get_number_of_fingers())

印刷:

5
4
['thumb', 'index', 'middle', 'ring', 'pinkie'] in cache
5
['thumb', 'middle', 'ring', 'pinkie'] in cache
4
['thumb', 'index', 'middle', 'ring', 'pinkie'] in cache
5

推荐阅读