使 class 中的函数只能在不调用 class 的情况下访问

Make function in class only accessible without calling the class

假设我有这个 class:

class A:
    def __init__(self, a):
        self.a = a
    @classmethod
    def foo(self):
        return 'hello world!'

我用的是@classmethod,这样可以不用调用class直接调用函数:

>>> A.foo()
'hello world!'
>>> 

但现在我想知道,因为我仍然可以通过调用 class:

来访问它
>>> A(1).foo()
'hello world!'
>>> 

如果从被调用的 class 中调用函数 foo,我是否可以让它引发错误?并且只让它被调用而不调用 class,比如 A.foo().

所以如果我这样做:

A(1).foo()

应该会报错。

解决方法是在 class 对象初始化时覆盖它的值,以确保它不会被 self.

调用
def raise_(exc):
    raise exc

class A:
    STRICTLY_CLASS_METHODS = [
        "foo",
    ]

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

        for method in self.STRICTLY_CLASS_METHODS:
            # Option 1: Using generator.throw() to raise exception. See https://www.python.org/dev/peps/pep-0342/#new-generator-method-throw-type-value-none-traceback-none
            # setattr(self, method, lambda *args, **kwargs: (_ for _ in ()).throw(AttributeError(method)))

            # Option 2: Using a function to raise exception
            setattr(self, method, lambda *args, **kwargs: raise_(AttributeError(method)))

    @classmethod
    def foo(cls):
        return 'hello world!'

    def bar(self):
        return 'hola mundo!', self.a

输出

>>> A.foo()
'hello world!'
>>> a = A(123)
>>> a.bar()
('hola mundo!', 123)
>>> a.foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 11, in <lambda>
  File "<stdin>", line 2, in raise_
AttributeError: foo
>>> a.bar()
('hola mundo!', 123)
>>> A(45).bar()
('hola mundo!', 45)
>>> A(6789).foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 11, in <lambda>
  File "<stdin>", line 2, in raise_
AttributeError: foo
>>> A.foo()
'hello world!'

如何查找/绑定 classmethodstaticmethod 以及实际上正常方法的功能是通过 descriptors 实现的。同样,可以在实例上定义 forbids lookup/binding 的描述符。

这种描述符的简单实现检查它是否通过实例查找并在这种情况下引发错误:

class NoInstanceMethod:
    """Descriptor to forbid that other descriptors can be looked up on an instance"""
    def __init__(self, descr, name=None):
        self.descr = descr
        self.name = name

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        # enforce the instance cannot look up the attribute at all
        if instance is not None:
            raise AttributeError(f"{type(instance).__name__!r} has no attribute {self.name!r}")
        # invoke any descriptor we are wrapping
        return self.descr.__get__(instance, owner)

这可以应用于其他描述符之上,以防止在实例上查找它们。值得注意的是,它可以与 classmethodstaticmethod 结合使用以防止在实例上使用它们:

class A:
    def __init__(self, a):
        self.a = a
    @NoInstanceMethod
    @classmethod
    def foo(cls):
        return 'hello world!'


A.foo()     # Stdout: hello world!
A(1).foo()  # AttributeError: 'A' object has no attribute 'foo'

上面的 NoInstanceMethod 是“幼稚的”,因为它不关心将 __get__ 以外的描述符调用传播到其包装的描述符。例如,可以传播 __set_name__ 调用以允许包装的描述符知道其名称。

由于描述符可以自由(不)实现任何描述符方法,这可以得到支持,但需要适当的错误处理。扩展 NoInstanceMethod 以支持实践中需要的任何描述符方法。