最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

python - How to reference class static data in decorator method - Stack Overflow

programmeradmin3浏览0评论

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
  • this seems to work for me as is, on 3.13.0. what is happening for you? – ysth Commented Mar 17 at 20:48
  • 3 What is the goal of keeping the decorator definition and the decorated method as part of the same class? Because it can be done easily if they're separate (I'm sure you know) and it will also look cleaner imo. – Gei Stoyanov Commented Mar 17 at 21:01
  • 1 No, I'm not trying to do multiple dispatch at all. It is a common server pattern, where routing is defined to map incoming requests to methods that will handle those requests. – TrentP Commented Mar 17 at 21:04
  • 2 The goal of having the decorator method be part of the class is to encapsulate all the private methods and data of the class into the class. It should not be possible use the decorator to decorate methods in another class. Because that will add the methods to the global dict to name->method routing. The dict itself should also not be global, since only the Foo class should ever use it. – TrentP Commented Mar 17 at 21:10
  • 2 @TrentP ok, but putting handlers in the class doesn't mean that only Foo 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 access module.handlers they can access module.Foo.handlers – juanpa.arrivillaga Commented Mar 17 at 21:14
 |  Show 11 more comments

4 Answers 4

Reset to default 3

When 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

发布评论

评论列表(0)

  1. 暂无评论