【问题标题】:Can I restrict objects in Python3 so that only attributes that I make a setter for are allowed?我可以限制 Python3 中的对象,以便只允许我为其设置设置器的属性吗?
【发布时间】:2015-11-01 14:52:42
【问题描述】:

我有一个叫做节点的东西。 Definition 和 Theorem 都是一种节点,但只有 Definitions 应该被允许有plural 属性:

class Definition(Node):


    def __init__(self,dic):
        self.type = "definition"
        super(Definition, self).__init__(dic)
        self.plural = move_attribute(dic, {'plural', 'pl'}, strict=False)


    @property
    def plural(self):
        return self._plural

    @plural.setter
    def plural(self, new_plural):
        if new_plural is None:
            self._plural = None
        else:
            clean_plural = check_type_and_clean(new_plural, str)
            assert dunderscore_count(clean_plural)>=2
            self._plural = clean_plural


class Theorem(Node):


    def __init__(self, dic):
        self.type = "theorem"
        super().__init__(dic)
        self.proofs = move_attribute(dic, {'proofs', 'proof'}, strict=False)
        # theorems CANNOT have plurals:
        # if 'plural' in self:
        #   raise KeyError('Theorems cannot have plurals.')

如您所见,定义有plural.setter,但定理没有。但是,代码

theorem = Theorem(some input)
theorem.plural = "some plural"

运行良好并且不会引发错误。但我希望它引发错误。如您所见,我尝试在显示的代码底部手动检查复数,但这只是一个补丁。我想阻止任何未明确定义的属性的设置。这种事情的最佳做法是什么?


我正在寻找满足“chicken" requirement:”的答案:

我认为这不能解决我的问题。在你的两个解决方案中,我都可以 附加代码 t.chicken = 'hi'; print(t.chicken),它会打印 hi 没有错误。我不希望用户能够编造新的 像鸡一样的属性。

【问题讨论】:

标签: oop python-3.x inheritance


【解决方案1】:

简短的回答是“是的,你可以。”

接下来的问题是“为什么?” Python 的优势之一是非凡的活力,通过限制这种能力,您实际上会降低您的类的用处(但请参阅底部的编辑)。

但是,限制是有充分理由的,如果您确实选择走这条路,您将需要修改您的 __setattr__ 方法:

def __setattr__(self, name, value):
    if name not in ('my', 'attribute', 'names',):
        raise AttributeError('attribute %s not allowed' % name)
    else:
        super().__setattr__(name, value)

没有必要搞乱__getattr____getattribute__,因为它们不会返回不存在的属性。

这是您的代码,稍作修改——我将__setattr__ 方法添加到Node,并将_allowed_attributes 添加到DefinitionTheorem

class Node:

    def __setattr__(self, name, value):
        if name not in self._allowed_attributes:
            raise AttributeError('attribute %s does not and cannot exist' % name)
        super().__setattr__(name, value)


class Definition(Node):

    _allowed_attributes = '_plural', 'type'

    def __init__(self,dic):
        self.type = "definition"
        super().__init__(dic)
        self.plural = move_attribute(dic, {'plural', 'pl'}, strict=False)

    @property
    def plural(self):
        return self._plural

    @plural.setter
    def plural(self, new_plural):
        if new_plural is None:
            self._plural = None
        else:
            clean_plural = check_type_and_clean(new_plural, str)
            assert dunderscore_count(clean_plural)>=2
            self._plural = clean_plural


class Theorem(Node):

    _allowed_attributes = 'type', 'proofs'

    def __init__(self, dic):
        self.type = "theorem"
        super().__init__(dic)
        self.proofs = move_attribute(dic, {'proofs', 'proof'}, strict=False)

在使用中是这样的:

>>> theorem = Theorem(...)
>>> theorem.plural = 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in __setattr__
AttributeError: attribute plural does not and cannot exist

编辑

考虑到这一点,我认为一个很好的折衷方案是:

  • 使用元类在创建时检查类并动态构建_allowed_attributes 元组
  • 修改Node__setattr__ 以始终允许修改/创建具有至少一个前导_ 的属性

这为您提供了一些保护,防止拼写错误和创建您不想要的属性,同时仍然允许程序员根据自己的需要解决或增强类。

好的,新的元类看起来像:

class NodeMeta(type):

    def __new__(metacls, cls, bases, classdict):
        node_cls = super().__new__(metacls, cls, bases, classdict)
        allowed_attributes = []
        for base in (node_cls, ) + bases:
            for name, obj in base.__dict__.items():
                if isinstance(obj, property) and hasattr(obj, '__fset__'):
                    allowed_attributes.append(name)
        node_cls._allowed_attributes = tuple(allowed_attributes)
        return node_cls

Node 类有两个调整:包括 NodeMeta 元类和调整 __setattr__ 以仅阻止非下划线前导属性:

class Node(metaclass=NodeMeta):

    def __init__(self, dic):
        self._dic = dic

    def __setattr__(self, name, value):
        if not name[0] == '_' and name not in self._allowed_attributes:
            raise AttributeError('attribute %s does not and cannot exist' % name)
        super().__setattr__(name, value)

最后,Node 子类 TheoremDefinitiontype 属性移到类命名空间中,因此设置它们没有问题 - 作为旁注,type 不好名称,因为它也是一个内置函数——也许是node_type

class Definition(Node):

    type = "definition"

    ...

class Theorem(Node):

    type = "theorem"

    ...

最后一点:即使这种方法也不能避免有人实际添加或更改属性,因为object.__setattr__(theorum_instance, 'an_attr', 99) 仍然可以使用——或者(更简单)_allowed_attributes 可以修改;但是,如果有人要从事所有这些工作,他们希望知道自己在做什么……如果没有,他们就拥有所有的东西。 ;)

【讨论】:

  • 感谢您的宝贵时间。不幸的是,我最近太忙了,以至于我现在无法测试它。但我正在授予赏金,因为这看起来正是我想要的。谢谢!
  • @mareoraft: the accepted answer for the related question 表示object.__setattr__() 可用于解决限制__setattr__() 方法。
  • @mareoraft:更新答案:固定属性检查代码;为回应 J.F. Sebastian 的评论,在底部添加了简介。
【解决方案2】:

如果你真的想阻止所有其他动态属性。我假设有一个明确定义的时间窗口,您希望允许添加属性。

下面我允许它直到对象初始化完成。 (你可以用allow_dynamic_attribute变量控制它。

class A:
    def __init__(self):
        self.allow_dynamic_attribute = True
        self.abc = "hello"
        self._plural = None     # need to give default value
        # A.__setattr__ = types.MethodType(__setattr__, A)
        self.allow_dynamic_attribute = False

    def __setattr__(self, name, value):

        if hasattr(self, 'allow_dynamic_attribute'):
            if not self.allow_dynamic_attribute:

                if not hasattr(self, name):
                    raise Exception

        super().__setattr__(name, value)

    @property
    def plural(self):

        return self._plural

    @plural.setter
    def plural(self, new_plural):

        self._plural = new_plural

a = A()

print(a.abc)                    # fine
a.plural = "yes"                # fine
print(a.plural)                 # fine
a.dkk = "bed"                   # raise exception

或者这样可以更紧凑,我想不通 MethodType + super 是如何相处的。

import types
def __setattr__(self, name, value):
    if not hasattr(self, name):
        raise Exception
    else:
        super().__setattr__(name,value) # this doesn't work for reason I don't know

class A:
    def __init__(self):
        self.foo = "hello"
        # after this point, there's no more setattr for you
        A.__setattr__ = types.MethodType(__setattr__, A) 


a = A()

print(a.foo)                    # fine
a.bar = "bed"                   # raise exception

【讨论】:

    【解决方案3】:

    您可以在每次访问时检查该属性。

    class Theorem(Node):
        ...
       def __getattribute__(self, name):
           if name not in ["allowed", "attribute", "names"]:
               raise MyException("attribute "+name+" not allowed")
           else:
               return self.__dict__[name]
    
       def __setattr__(self, name, value):
           if name not in ["allowed", "attribute", "names"]:
               raise MyException("attribute "+name+" not allowed")
           else:
               self.__dict__[name] = value
    

    您可以动态构建允许的方法列表作为装饰器的副作用:

     allowed_attrs = []
     def allowed(f):
         allowed_attrs.append(f.__name__)
         return f
    

    您还需要手动添加非方法属性。

    【讨论】:

    • 我的一些属性(例如plural)有自己的设置器。上面的代码似乎完全绕过了这些设置器。
    • 这段代码的目的是为了说明一般的__getattribute__ & __setattribute__ 机制。您可以对其进行定制以包含设置器的功能。很脏,但老实说,我认为没有其他方法可以限制基本的 Python 功能,例如在运行时添加属性。
    • 覆盖__getattribute__ 几乎总是一个坏主意。你的意思是__getattr__ 吗?
    【解决方案4】:

    是的,您可以创建不能从类外部修改的私有成员。变量名应该以two underscores开头:

    class Test(object):
    
        def __init__(self, t):
            self.__t = t
    
    
        def __str__(self):
            return str(self.__t)
    
    t = Test(2)
    print(t) # prints 2
    t.__t = 3
    
    print(t) # prints 2
    

    也就是说,尝试像我们在 t.__t = 3 中那样访问这样的变量将不会引发异常。

    实现所需行为的另一种方法是使用函数。这种方法将需要使用函数符号“访问属性”,但如果这不打扰您,您可以得到您想要的。以下演示对值进行“硬编码”,但显然您可以让 Theorem() 接受一个参数并使用它来动态设置属性值。

    演示:

    # -*- coding: utf-8 -*-
    def Theorem():
        def f(attrib):
    
            def proofs():
                return ''
    
            def plural():
                return '◊◊◊◊◊◊◊◊'
    
            if attrib == 'proofs':
                return proofs()
            elif attrib == 'plural':
                return plural()
            else:
                raise ValueError("Attribute [{}] doesn't exist".format(attrib))
    
        return f
    
    t = Theorem()
    print(t('proofs'))
    
    print(t('plural'))        
    
    print(t('wait_for_error')) 
    

    输出

    
    ◊◊◊◊◊◊◊◊
    Traceback (most recent call last):
      File "/Users/alfasi/Desktop/1.py", line 40, in <module>
        print(t('wait_for_error'))       
      File "/Users/alfasi/Desktop/1.py", line 32, in f
        raise ValueError("Attribute [{}] doesn't exist".format(attrib))
    ValueError: Attribute [wait_for_error] doesn't exist
    

    【讨论】:

    • 我认为这不能解决我的问题。在您的两个解决方案中,我都可以附加代码t.chicken = 'hi'; print(t.chicken),它会打印hi 而不会出错。我不希望用户能够编造像chicken 这样的新属性。
    • @mareoraft 第二个解决方案需要功能访问。让用户执行t.chicken = 'hi' 是没有意义的,如果他们尝试执行t(chicken),则会引发错误。 (当然,如果需要,您可以添加设置器)。
    • 两个前导下划线并不能阻止属性修改,它只是需要一些额外的输入;并且它不会阻止新属性的创建。
    • @EthanFurman 那么你如何解释输出(剩余 2,即使在分配之后)?
    • 双前导下划线(最多有一个尾随下划线)会导致名称混乱。要从课堂外访问__t,您必须以_classname__t 访问它。 See here 了解更多详情。
    猜你喜欢
    • 2017-05-04
    • 2011-04-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-16
    • 2016-06-25
    相关资源
    最近更新 更多