Home > Software design >  Make function in class only accessible without calling the class
Make function in class only accessible without calling the class

Time:09-21

Let's say I have this class:

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

I use @classmethod, so that I can directly call the function without calling the class:

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

But now I am wondering, since I still can access it with calling the class:

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

Would I be able to make it that it would raise an error if the function foo is called from a called class. And only let it to be called without calling the class, like A.foo().

So if I do:

A(1).foo()

It should give an error.

CodePudding user response:

The functionality of how classmethod, staticmethod and in fact normal methods are lookedup / bound is implemented via descriptors. Similarly, one can define a descriptor that forbids lookup/binding on an instance.

A naive implementation of such a descriptor checks whether it is looked up via an instance and raises an error in this case:

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"{instance} has no attribute {self.name!r}")
        # invoke any descriptor we are wrapping
        return self.descr.__get__(instance, owner)

This can be applied on top of other descriptors to prevent them from being looked up on an instance. Prominently, it can be combined with classmethod or staticmethod to prevent using them on an instance:

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


A.foo()     # Stdout: hello world!
A(1).foo()  # AttributeError: <__main__.A object at 0x115469c70> has no attribute 'foo'

The above NoInstanceMethod is "naive" in that it does not take care of propagating descriptor calls other than __get__ to its wrapped descriptor. For example, one could propagate __set_name__ calls to allow the wrapped descriptor to know its name.

Since descriptors are free to (not) implement any of the descriptor methods, this can be supported but needs appropriate error handling. Extend the NoInstanceMethod to support whatever descriptor methods are needed in practice.

CodePudding user response:

A workaround is to override its value upon initialization of a class object to make sure it wouldn't be called from self.

class A:
    STRICTLY_CLASS_METHODS = [
        "foo",
    ]

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

        for method in self.STRICTLY_CLASS_METHODS:
            delattr(self, method)

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

try:
    print(A.foo())
    print(A(1).foo())
except AttributeError as error:
    print(f"Exception occurred: AttributeError {error}")

Output

hello world!
Exception occurred: AttributeError foo
  • Related