Is it possible to make the global variable handlers
a static class variable?
from typing import Callable, Self
# dispatch table with name to method mapping
handlers:dict[str, Callable[..., None]] = {}
class Foo:
# Mark method as a handler for a provided request name
@staticmethod
def handler(name: str) -> Callable[[Callable], Callable]:
def add_handler(func: Callable[..., None]) -> Callable:
handlers[name] = func # This line is the problem
return func
return add_handler
# This method will handle request "a"
@handler("a")
def handle_a(self) -> None:
pass
# Handle one request with provided name, using dispatch table to determine method to call
def handle(self, name: str) -> None:
handlers[name](self)
The goal is to have the decorator add the decorated method into a dict that is part of the class. The dict can then be used to dispatch methods via the name used in request messages it will receive.
The problem seems to be how to refer to class data inside a decorator.
Of course using self.handlers
won't work, as self
isn't defined. The decorator is called when the class is defined and there aren't any instances of the class created yet for self
to reference.
Using Foo.handlers
doesn't work either, as the class name isn't defined until after the class definition is finished.
If this wasn't a decorator, then handler()
could be defined as a @classmethod
, and then the class would be the first argument to the method. But it doesn't appear possible to make a decorator a class method.
An example of how this might be used, would be as a handler for a server requests, e.g. a websocket server:
from websockets.sync.server import serve
def wshandler(websocket):
f = Foo() # Create object to handle all requests
for msg in websocket:
decoded = json.loads(msg)
# Decoded message will have a field named 'type', which is a string indicating the request type.
Foo.handler(decoded['type'])
with serve(wshandler, "localhost", 8888) as server:
server.serve_forever()
Is it possible to make the global variable handlers
a static class variable?
from typing import Callable, Self
# dispatch table with name to method mapping
handlers:dict[str, Callable[..., None]] = {}
class Foo:
# Mark method as a handler for a provided request name
@staticmethod
def handler(name: str) -> Callable[[Callable], Callable]:
def add_handler(func: Callable[..., None]) -> Callable:
handlers[name] = func # This line is the problem
return func
return add_handler
# This method will handle request "a"
@handler("a")
def handle_a(self) -> None:
pass
# Handle one request with provided name, using dispatch table to determine method to call
def handle(self, name: str) -> None:
handlers[name](self)
The goal is to have the decorator add the decorated method into a dict that is part of the class. The dict can then be used to dispatch methods via the name used in request messages it will receive.
The problem seems to be how to refer to class data inside a decorator.
Of course using self.handlers
won't work, as self
isn't defined. The decorator is called when the class is defined and there aren't any instances of the class created yet for self
to reference.
Using Foo.handlers
doesn't work either, as the class name isn't defined until after the class definition is finished.
If this wasn't a decorator, then handler()
could be defined as a @classmethod
, and then the class would be the first argument to the method. But it doesn't appear possible to make a decorator a class method.
An example of how this might be used, would be as a handler for a server requests, e.g. a websocket server:
from websockets.sync.server import serve
def wshandler(websocket):
f = Foo() # Create object to handle all requests
for msg in websocket:
decoded = json.loads(msg)
# Decoded message will have a field named 'type', which is a string indicating the request type.
Foo.handler(decoded['type'])
with serve(wshandler, "localhost", 8888) as server:
server.serve_forever()
Share
Improve this question
edited Mar 17 at 21:25
TrentP
asked Mar 17 at 20:27
TrentPTrentP
4,74427 silver badges41 bronze badges
16
|
Show 11 more comments
4 Answers
Reset to default 3When handler
is called, the name handlers
will exist in the class
statement's namespace, but not yet be bound as a class attribute (as you've noticed). It also is not part of any scope that handler
will have access to, since the class
statement does not define any scope.
You can, however, inject a reference to the dict into the scope of handler
by using handlers
as the default value for a parameter you will otherwise never provide an argument for. Inside handle
, you can access handlers
as an ordinary class attribute.
(Note, too, that handler
does not need to be a static method, because you only use the regular function that will eventually be bound to a class attribute. You could even add del handler
to the end of the class
statement to prevent the attribute from being defined, because by that point you are done calling handler
.)
from typing import Callable, Self
# dispatch table with name to method mapping
class Foo:
handlers: dict[str, Callable[..., None]] = {}
def handler(name: str, _h=handlers) -> Callable[[Callable], Callable]:
def add_handler(func: Callable[..., None]) -> Callable:
_h[name] = func
return func
return add_handler
@handler("a")
def handle_a(self) -> None:
pass
def handle(self, name: str) -> None:
self.handlers[name](self)
I don't think the example provided demonstrates the motivation for doing this, but here's one way to do it if you really needed to:
from __future__ import annotations
import typing_extensions as t
if t.TYPE_CHECKING:
import collections.abc as cx
_HandlerMethod = t.TypeAliasType("_HandlerMethod", cx.Callable[..., None])
_HandlerMethodT = t.TypeVar("_HandlerMethodT", bound=_HandlerMethod)
class Foo:
handlers: t.ClassVar[dict[str, _HandlerMethod]] = {}
class handler:
_name: str
_handler_method: _HandlerMethod
def __init__(self, name: str, /) -> None:
self._name = name
def __call__(self, handler_method: _HandlerMethodT, /) -> _HandlerMethodT:
self._handler_method = handler_method
# Temporarily lie to the type-checker here - returning `self` is needed to
# activate `__set_name__` during class definition, which will in-turn
# replace the `handler` instance with the actual handler method.
return self # type: ignore[return-value]
def __set_name__(self, owner: type[Foo], method_name: str, /) -> None:
owner.handlers[self._name] = self._handler_method
setattr(owner, method_name, self._handler_method)
# This method will handle request "a"
@handler("a")
def handle_a(self) -> None:
pass
# Handle one request with provided name, using dispatch table to determine method to call
def handle(self, name: str) -> None:
self.handlers[name](self)
>>> Foo.handlers
{'a': <function Foo.handle_a at 0x7ff99c007ca0>}
I don't see any particular reason to put handler
as a member of Foo
here - I'm only doing this because it matches the API in your example snippet.
In case you don't intend to inherit Foo to some subclasses, the question - from the Functional Programming standpoint - would be: Do you actually need a class?
Given that Python classes are actually anyway never truly encapsulated (well as seen in the comments - this is not true - it is not really encapsulated even).
You could create a closure in Python. (A function which contains a local variable and generate inside it some other functions. These functions share the local variable inside the mother-function (close over those variables) - and only these generated functions can access these local variables.
If you place e.g. your dictionary into such a closure - this dicitonary is - even in Python - truly hidden (encapsulated) and you can modify or query the dictionary only using the daughter functions - closure functions.
The result is a simpler construct.
from typing import Callable, Dict
def make_handler_registry():
handlers: Dict[str, Callable[[], None]] = {}
def register(name: str) -> Callable[[Callable[[], None]], Callable[[], None]]:
def decorator(func: Callable[[], None]) -> Callable[[], None]:
handlers[name] = func
return func
return decorator
def handle(name: str) -> None:
if name in handlers:
handlers[name]()
else:
raise ValueError(f"No handler registered for '{name}'")
return register, handle
You can use this closure construct like this:
# Create a new registry
register, handle = make_handler_registry()
# Then, you can register different cases:
@register("a")
def handle_a():
print("Handling request 'a'")
@register("b")
def handle_b():
print("Handling request 'b'")
# And you can use/call the registered functions like this:
# Usage
handle("a") # Output: Handling request 'a'
handle("b") # Output: Handling request 'b'
handle("c") # Raises ValueError: No handler registered for 'c'
One could also generate a deregister decorator:
from typing import Callable, Dict
def make_handler_registry():
handlers: Dict[str, Callable[[], None]] = {}
def register(name: str) -> Callable[[Callable[[], None]], Callable[[], None]]:
def decorator(func: Callable[[], None]) -> Callable[[], None]:
handlers[name] = func
return func
return decorator
def deregister(name: str) -> Callable[[Callable[[], None]], Callable[[], None]]:
def decorator(func: Callable[[], None]) -> Callable[[], None]:
handlers.pop(name, None) # Remove handler if it exists
return func
return decorator
def handle(name: str) -> None:
if name in handlers:
handlers[name]()
else:
raise ValueError(f"No handler registered for '{name}'")
return register, deregister, handle
And the usage would be:
# Create a new registry
register, deregister, handle = make_handler_registry()
@register("a")
def handle_a():
print("Handling request 'a'")
@register("b")
def handle_b():
print("Handling request 'b'")
@deregister("a")
def remove_handle_a():
pass # This function doesn't need to do anything, just trigger deregistration
# Usage
handle("b") # Output: Handling request 'b'
handle("a") # Raises ValueError: No handler registered for 'a'
You don't need class methods or static methods - but can come along with just a simple closure (a function returning some functions which share certain objects which are truly encapsulated - can be accessed and modified only by them).
The class scope does not define a closure, so functions defined in a class body cannot access class variables through name lookups.
Excerpt from the Resolution of names section of the documentation:
The scope of names defined in a class block is limited to the class block; it does not extend to the code blocks of methods.
and also the Executing the class body section of the documentation:
However, even when the class definition occurs inside the function, methods defined inside the class still cannot see names defined at the class scope.
In this case, since all that the handler
decorator does is to add the decorated function to the handlers
dict under a given name, and since you won't actually be accessing the decorated function as a bound instance method, one simple implementation of the desired handler
decorator is with a call to functools.partial
to obtain a decorator function that sets a given name as the first argument to the dict's __setitem__
method with another partial
call:
from typing import Callable
from functools import partial
class Foo:
handlers: dict[str, Callable[..., None]] = {}
handler = partial(partial, handlers.__setitem__)
@handler("a")
def handle_a(self) -> None:
pass
def handle(self, name: str) -> None:
self.handlers[name](self)
Demo: https://ideone/WN167B
Foo
class should ever use it. – TrentP Commented Mar 17 at 21:10handlers
in the class doesn't mean that onlyFoo
would ever use it, indeed, it would be "safer" (from a python convention perspective) to simply name it with a single leading underscore,_handlers = {}
and then users of this module will know "don't touch this, this is an internal implementation detail", after all, if a user can accessmodule.handlers
they can accessmodule.Foo.handlers
– juanpa.arrivillaga Commented Mar 17 at 21:14