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