Người trang trí một phương thức thể hiện có thể truy cập vào lớp không?


108

Xin chào, tôi có một cái gì đó đại khái như sau. Về cơ bản, tôi cần truy cập lớp của một phương thức thể hiện từ một trình trang trí được sử dụng dựa trên phương thức thể hiện trong định nghĩa của nó.

def decorator(view):
    # do something that requires view's class
    print view.im_class
    return view

class ModelA(object):
    @decorator
    def a_method(self):
        # do some stuff
        pass

Mã nguyên trạng cung cấp:

AttributeError: 'function' object has no attribute 'im_class'

Tôi đã tìm thấy câu hỏi / câu trả lời tương tự - trình trang trí Python làm cho hàm quên rằng nó thuộc về một lớpNhận lớp trong trình trang trí Python - nhưng những câu hỏi này dựa trên một giải pháp lấy phiên bản tại thời điểm chạy bằng cách lấy tham số đầu tiên. Trong trường hợp của tôi, tôi sẽ gọi phương thức dựa trên thông tin thu thập được từ lớp của nó, vì vậy tôi không thể chờ một cuộc gọi đến.

Câu trả lời:


68

Nếu bạn đang sử dụng Python 2.6 trở lên, bạn có thể sử dụng trình trang trí lớp, có thể là một cái gì đó như thế này (cảnh báo: mã chưa được kiểm tra).

def class_decorator(cls):
   for name, method in cls.__dict__.iteritems():
        if hasattr(method, "use_class"):
            # do something with the method and class
            print name, cls
   return cls

def method_decorator(view):
    # mark the method as something that requires view's class
    view.use_class = True
    return view

@class_decorator
class ModelA(object):
    @method_decorator
    def a_method(self):
        # do some stuff
        pass

Trình trang trí phương thức đánh dấu phương thức là phương thức được quan tâm bằng cách thêm thuộc tính "use_class" - các hàm và phương thức cũng là các đối tượng, vì vậy bạn có thể đính kèm siêu dữ liệu bổ sung vào chúng.

Sau khi lớp đã được tạo, trình trang trí lớp đi qua tất cả các phương thức và thực hiện bất cứ điều gì cần thiết trên các phương thức đã được đánh dấu.

Nếu bạn muốn tất cả các phương thức bị ảnh hưởng thì bạn có thể loại bỏ trình trang trí phương thức và chỉ sử dụng trình trang trí lớp.


2
Cảm ơn tôi nghĩ đây là con đường để đi. Chỉ cần một dòng mã bổ sung cho bất kỳ lớp nào tôi muốn sử dụng trình trang trí này. Có lẽ tôi có thể sử dụng một siêu kính tùy chỉnh và thực hiện kiểm tra tương tự như vậy trong khi ... mới ?
Carl G

3
Bất kỳ ai đang cố gắng sử dụng điều này với staticmethod hoặc classmethod sẽ muốn đọc PEP này: python.org/dev/peps/pep-0232 Không chắc là có thể vì bạn không thể đặt một thuộc tính trên một phương thức class / static và tôi nghĩ rằng chúng ngấu nghiến lên bất kỳ thuộc tính chức năng tùy chỉnh nào khi chúng được áp dụng cho một chức năng.
Carl G

Chỉ những gì tôi đang tìm kiếm, cho ORM dựa trên DBM của tôi ... Cảm ơn, anh bạn.
Coyote 21

Bạn nên sử dụng inspect.getmro(cls)để xử lý tất cả các lớp cơ sở trong trình trang trí lớp để hỗ trợ kế thừa.
schlamar

1
oh, trên thực tế có vẻ như inspectđể giải cứu stackoverflow.com/a/1911287/202168
Anentropic

16

Kể từ python 3.6, bạn có thể sử dụng object.__set_name__để thực hiện điều này một cách rất đơn giản. Tài liệu nói rằng __set_name__"được gọi vào thời điểm chủ sở hữu lớp sở hữu được tạo". Đây là một ví dụ:

class class_decorator:
    def __init__(self, fn):
        self.fn = fn

    def __set_name__(self, owner, name):
        # do something with owner, i.e.
        print(f"decorating {self.fn} and using {owner}")
        self.fn.class_name = owner.__name__

        # then replace ourself with the original method
        setattr(owner, name, self.fn)

Lưu ý rằng nó được gọi vào lúc tạo lớp:

>>> class A:
...     @class_decorator
...     def hello(self, x=42):
...         return x
...
decorating <function A.hello at 0x7f9bedf66bf8> and using <class '__main__.A'>
>>> A.hello
<function __main__.A.hello(self, x=42)>
>>> A.hello.class_name
'A'
>>> a = A()
>>> a.hello()
42

Nếu bạn muốn biết thêm về cách các lớp được tạo và cụ thể là chính xác khi nào __set_name__được gọi, bạn có thể tham khảo tài liệu về "Tạo đối tượng lớp" .


1
Điều đó sẽ trông như thế nào khi sử dụng trình trang trí với các tham số? Ví dụ:@class_decorator('test', foo='bar')
luckydonald

2
@luckydonald Bạn có thể tiếp cận nó tương tự như trình trang trí bình thường lấy đối số . Chỉ cần códef decorator(*args, **kwds): class Descriptor: ...; return Descriptor
Matt Eding

Ồ, cảm ơn bạn rất nhiều. Tôi không biết về __set_name__mặc dù tôi đã sử dụng Python 3.6+ trong một thời gian dài.
kawing-chiu

Có một nhược điểm của phương pháp này: trình kiểm tra tĩnh hoàn toàn không hiểu điều này. Mypy sẽ nghĩ rằng đó hellokhông phải là một phương thức, mà thay vào đó là một đối tượng của kiểu class_decorator.
kawing-chiu

@ kawing-chiu Nếu không có gì khác hoạt động, bạn có thể sử dụng một if TYPE_CHECKINGđể xác định class_decoratornhư một người trang trí bình thường trả về loại chính xác.
tyrion

15

Như những người khác đã chỉ ra, lớp này chưa được tạo tại thời điểm trình trang trí được gọi. Tuy nhiên , có thể chú thích đối tượng hàm bằng các tham số trang trí, sau đó trang trí lại hàm trong __new__phương thức của siêu kính . Bạn sẽ cần truy cập __dict__trực tiếp vào thuộc tính của hàm , ít nhất đối với tôi, func.foo = 1dẫn đến lỗi AttributeError.


6
setattrnên được sử dụng thay vì truy cập__dict__
schlamar

7

Như Mark gợi ý:

  1. Bất kỳ trình trang trí nào được gọi là lớp TRƯỚC KHI được xây dựng, vì vậy người trang trí sẽ không biết.
  2. Chúng tôi có thể gắn thẻ các phương pháp này và thực hiện bất kỳ quá trình hậu kỳ cần thiết nào sau đó.
  3. Chúng tôi có hai tùy chọn để xử lý hậu kỳ: tự động ở cuối định nghĩa lớp hoặc một nơi nào đó trước khi ứng dụng chạy. Tôi thích lựa chọn thứ nhất bằng cách sử dụng lớp cơ sở, nhưng bạn cũng có thể làm theo cách tiếp cận thứ hai.

Mã này cho biết cách hoạt động của điều này bằng cách sử dụng xử lý hậu kỳ tự động:

def expose(**kw):
    "Note that using **kw you can tag the function with any parameters"
    def wrap(func):
        name = func.func_name
        assert not name.startswith('_'), "Only public methods can be exposed"

        meta = func.__meta__ = kw
        meta['exposed'] = True
        return func

    return wrap

class Exposable(object):
    "Base class to expose instance methods"
    _exposable_ = None  # Not necessary, just for pylint

    class __metaclass__(type):
        def __new__(cls, name, bases, state):
            methods = state['_exposed_'] = dict()

            # inherit bases exposed methods
            for base in bases:
                methods.update(getattr(base, '_exposed_', {}))

            for name, member in state.items():
                meta = getattr(member, '__meta__', None)
                if meta is not None:
                    print "Found", name, meta
                    methods[name] = member
            return type.__new__(cls, name, bases, state)

class Foo(Exposable):
    @expose(any='parameter will go', inside='__meta__ func attribute')
    def foo(self):
        pass

class Bar(Exposable):
    @expose(hide=True, help='the great bar function')
    def bar(self):
        pass

class Buzz(Bar):
    @expose(hello=False, msg='overriding bar function')
    def bar(self):
        pass

class Fizz(Foo):
    @expose(msg='adding a bar function')
    def bar(self):
        pass

print('-' * 20)
print("showing exposed methods")
print("Foo: %s" % Foo._exposed_)
print("Bar: %s" % Bar._exposed_)
print("Buzz: %s" % Buzz._exposed_)
print("Fizz: %s" % Fizz._exposed_)

print('-' * 20)
print('examine bar functions')
print("Bar.bar: %s" % Bar.bar.__meta__)
print("Buzz.bar: %s" % Buzz.bar.__meta__)
print("Fizz.bar: %s" % Fizz.bar.__meta__)

Sản lượng mang lại:

Found foo {'inside': '__meta__ func attribute', 'any': 'parameter will go', 'exposed': True}
Found bar {'hide': True, 'help': 'the great bar function', 'exposed': True}
Found bar {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Found bar {'msg': 'adding a bar function', 'exposed': True}
--------------------
showing exposed methods
Foo: {'foo': <function foo at 0x7f7da3abb398>}
Bar: {'bar': <function bar at 0x7f7da3abb140>}
Buzz: {'bar': <function bar at 0x7f7da3abb0c8>}
Fizz: {'foo': <function foo at 0x7f7da3abb398>, 'bar': <function bar at 0x7f7da3abb488>}
--------------------
examine bar functions
Bar.bar: {'hide': True, 'help': 'the great bar function', 'exposed': True}
Buzz.bar: {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Fizz.bar: {'msg': 'adding a bar function', 'exposed': True}

Lưu ý rằng trong ví dụ này:

  1. Chúng ta có thể chú thích bất kỳ hàm nào với bất kỳ tham số tùy ý nào.
  2. Mỗi lớp có các phương thức tiếp xúc riêng.
  3. Chúng tôi cũng có thể kế thừa các phương pháp tiếp xúc.
  4. có thể ghi đè lên khi cập nhật tính năng hiển thị.

Hi vọng điêu nay co ich


4

Như Ants đã chỉ ra, bạn không thể nhận được một tham chiếu đến lớp từ bên trong lớp. Tuy nhiên, nếu bạn quan tâm đến việc phân biệt giữa các lớp khác nhau (không thao tác với đối tượng kiểu lớp thực tế), bạn có thể chuyển một chuỗi cho mỗi lớp. Bạn cũng có thể chuyển bất kỳ tham số nào khác mà bạn thích cho trình trang trí bằng cách sử dụng trình trang trí kiểu lớp.

class Decorator(object):
    def __init__(self,decoratee_enclosing_class):
        self.decoratee_enclosing_class = decoratee_enclosing_class
    def __call__(self,original_func):
        def new_function(*args,**kwargs):
            print 'decorating function in ',self.decoratee_enclosing_class
            original_func(*args,**kwargs)
        return new_function


class Bar(object):
    @Decorator('Bar')
    def foo(self):
        print 'in foo'

class Baz(object):
    @Decorator('Baz')
    def foo(self):
        print 'in foo'

print 'before instantiating Bar()'
b = Bar()
print 'calling b.foo()'
b.foo()

Bản in:

before instantiating Bar()
calling b.foo()
decorating function in  Bar
in foo

Ngoài ra, hãy xem trang của Bruce Eckel về trang trí.


Cảm ơn vì đã xác nhận kết luận chán nản của tôi rằng điều này là không thể. Tôi cũng có thể sử dụng một chuỗi đủ điều kiện hoàn toàn cho mô-đun / lớp ('module.Class'), lưu trữ (các) chuỗi cho đến khi tất cả các lớp được tải đầy đủ, sau đó tự mình truy xuất các lớp bằng nhập. Đó có vẻ như là một cách không KHÔ để hoàn thành nhiệm vụ của tôi.
Carl G

Bạn không cần phải sử dụng một lớp cho loại trình trang trí này: cách tiếp cận thành ngữ là sử dụng một cấp bổ sung của các hàm lồng nhau bên trong hàm trang trí. Tuy nhiên, nếu bạn đi với các lớp, có thể tốt hơn là không sử dụng viết hoa trong tên lớp để làm cho bản thân trang trí trông "chuẩn", tức @decorator('Bar')là trái ngược với @Decorator('Bar').
Erik Kaplun

4

Những gì flask-class làm là tạo một bộ đệm tạm thời mà nó lưu trữ trên phương thức, sau đó nó sử dụng một thứ khác (thực tế là Flask sẽ đăng ký các lớp bằng một registerphương thức lớp) để thực sự kết thúc phương thức.

Bạn có thể sử dụng lại mẫu này, lần này bằng cách sử dụng siêu kính để bạn có thể bọc phương thức tại thời điểm nhập.

def route(rule, **options):
    """A decorator that is used to define custom routes for methods in
    FlaskView subclasses. The format is exactly the same as Flask's
    `@app.route` decorator.
    """

    def decorator(f):
        # Put the rule cache on the method itself instead of globally
        if not hasattr(f, '_rule_cache') or f._rule_cache is None:
            f._rule_cache = {f.__name__: [(rule, options)]}
        elif not f.__name__ in f._rule_cache:
            f._rule_cache[f.__name__] = [(rule, options)]
        else:
            f._rule_cache[f.__name__].append((rule, options))

        return f

    return decorator

Trên lớp thực tế (bạn có thể làm điều tương tự bằng cách sử dụng siêu kính):

@classmethod
def register(cls, app, route_base=None, subdomain=None, route_prefix=None,
             trailing_slash=None):

    for name, value in members:
        proxy = cls.make_proxy_method(name)
        route_name = cls.build_route_name(name)
        try:
            if hasattr(value, "_rule_cache") and name in value._rule_cache:
                for idx, cached_rule in enumerate(value._rule_cache[name]):
                    # wrap the method here

Nguồn: https://github.com/apiguy/flask-classy/blob/master/flask_classy.py


đó là một mô hình hữu ích, nhưng điều này không giải quyết vấn đề của một trang trí phương pháp có thể để tham khảo các tầng lớp phụ huynh của phương pháp này nó áp dụng cho
Anentropic

Tôi đã cập nhật câu trả lời của mình để rõ ràng hơn về cách điều này có thể hữu ích để truy cập vào lớp tại thời điểm nhập (tức là sử dụng siêu kính + bộ nhớ đệm tham số trang trí trên phương thức).
charlax

3

Vấn đề là khi decorator được gọi là lớp chưa tồn tại. Thử cái này:

def loud_decorator(func):
    print("Now decorating %s" % func)
    def decorated(*args, **kwargs):
        print("Now calling %s with %s,%s" % (func, args, kwargs))
        return func(*args, **kwargs)
    return decorated

class Foo(object):
    class __metaclass__(type):
        def __new__(cls, name, bases, dict_):
            print("Creating class %s%s with attributes %s" % (name, bases, dict_))
            return type.__new__(cls, name, bases, dict_)

    @loud_decorator
    def hello(self, msg):
        print("Hello %s" % msg)

Foo().hello()

Chương trình này sẽ xuất:

Now decorating <function hello at 0xb74d35dc>
Creating class Foo(<type 'object'>,) with attributes {'__module__': '__main__', '__metaclass__': <class '__main__.__metaclass__'>, 'hello': <function decorated at 0xb74d356c>}
Now calling <function hello at 0xb74d35dc> with (<__main__.Foo object at 0xb74ea1ac>, 'World'),{}
Hello World

Như bạn thấy, bạn sẽ phải tìm ra một cách khác để làm những gì bạn muốn.


khi người ta định nghĩa một hàm thì hàm đó chưa tồn tại, nhưng người ta có thể gọi một cách đệ quy hàm từ bên trong chính nó. Tôi đoán đây là một tính năng ngôn ngữ dành riêng cho các hàm và không có sẵn cho các lớp.
Carl G

DGGenuine: Hàm chỉ được gọi và do đó hàm tự truy cập vào chính nó, chỉ sau khi nó được tạo hoàn chỉnh. Trong trường hợp này, lớp không thể hoàn thành khi trình trang trí được gọi, vì lớp phải đợi kết quả của trình trang trí, kết quả này sẽ được lưu trữ dưới dạng một trong các thuộc tính của lớp.
u0b34a0f6ae

3

Đây là một ví dụ đơn giản:

def mod_bar(cls):
    # returns modified class

    def decorate(fcn):
        # returns decorated function

        def new_fcn(self):
            print self.start_str
            print fcn(self)
            print self.end_str

        return new_fcn

    cls.bar = decorate(cls.bar)
    return cls

@mod_bar
class Test(object):
    def __init__(self):
        self.start_str = "starting dec"
        self.end_str = "ending dec" 

    def bar(self):
        return "bar"

Đầu ra là:

>>> import Test
>>> a = Test()
>>> a.bar()
starting dec
bar
ending dec

1

Đây là một câu hỏi cũ nhưng đã gặp phải venusian. http://venusian.readthedocs.org/en/latest/

Nó dường như có khả năng trang trí các phương thức và cho phép bạn truy cập vào cả lớp và phương thức trong khi làm như vậy. Lưu ý rằng cách gọi tht setattr(ob, wrapped.__name__, decorated)không phải là cách sử dụng venusian điển hình và phần nào làm mất đi mục đích.

Dù bằng cách nào ... ví dụ dưới đây đã hoàn thành và sẽ chạy.

import sys
from functools import wraps
import venusian

def logged(wrapped):
    def callback(scanner, name, ob):
        @wraps(wrapped)
        def decorated(self, *args, **kwargs):
            print 'you called method', wrapped.__name__, 'on class', ob.__name__
            return wrapped(self, *args, **kwargs)
        print 'decorating', '%s.%s' % (ob.__name__, wrapped.__name__)
        setattr(ob, wrapped.__name__, decorated)
    venusian.attach(wrapped, callback)
    return wrapped

class Foo(object):
    @logged
    def bar(self):
        print 'bar'

scanner = venusian.Scanner()
scanner.scan(sys.modules[__name__])

if __name__ == '__main__':
    t = Foo()
    t.bar()

1

Hàm không biết liệu nó có phải là một phương thức tại điểm định nghĩa hay không, khi mã trang trí chạy. Chỉ khi nó được truy cập thông qua định danh class / instance, nó mới có thể biết class / instance của nó. Để khắc phục hạn chế này, bạn có thể trang trí bằng đối tượng mô tả để trì hoãn mã trang trí thực tế cho đến khi truy cập / thời gian gọi:

class decorated(object):
    def __init__(self, func, type_=None):
        self.func = func
        self.type = type_

    def __get__(self, obj, type_=None):
        func = self.func.__get__(obj, type_)
        print('accessed %s.%s' % (type_.__name__, func.__name__))
        return self.__class__(func, type_)

    def __call__(self, *args, **kwargs):
        name = '%s.%s' % (self.type.__name__, self.func.__name__)
        print('called %s with args=%s kwargs=%s' % (name, args, kwargs))
        return self.func(*args, **kwargs)

Điều này cho phép bạn trang trí các phương thức riêng lẻ (static | class):

class Foo(object):
    @decorated
    def foo(self, a, b):
        pass

    @decorated
    @staticmethod
    def bar(a, b):
        pass

    @decorated
    @classmethod
    def baz(cls, a, b):
        pass

class Bar(Foo):
    pass

Bây giờ bạn có thể sử dụng mã trang trí để xem xét nội tâm ...

>>> Foo.foo
accessed Foo.foo
>>> Foo.bar
accessed Foo.bar
>>> Foo.baz
accessed Foo.baz
>>> Bar.foo
accessed Bar.foo
>>> Bar.bar
accessed Bar.bar
>>> Bar.baz
accessed Bar.baz

... và để thay đổi hành vi chức năng:

>>> Foo().foo(1, 2)
accessed Foo.foo
called Foo.foo with args=(1, 2) kwargs={}
>>> Foo.bar(1, b='bcd')
accessed Foo.bar
called Foo.bar with args=(1,) kwargs={'b': 'bcd'}
>>> Bar.baz(a='abc', b='bcd')
accessed Bar.baz
called Bar.baz with args=() kwargs={'a': 'abc', 'b': 'bcd'}

Đáng buồn thay, cách tiếp cận này về mặt chức năng tương đương với câu trả lời không thể giải thích được của Will McCutchen . Cả câu trả lời này và câu trả lời đó đều có được lớp mong muốn tại thời gian gọi phương thức thay vì thời gian trang trí phương thức , như yêu cầu của câu hỏi ban đầu. Phương tiện hợp lý duy nhất để có được lớp này vào một thời điểm đủ sớm là xem xét bên trong tất cả các phương thức tại thời điểm xác định lớp (ví dụ: thông qua trình trang trí lớp hoặc siêu kính). </sigh>
Cecil Curry

1

Như các câu trả lời khác đã chỉ ra, decorator là một hàm-ish, bạn không thể truy cập lớp mà phương thức này thuộc về vì lớp này chưa được tạo. Tuy nhiên, bạn hoàn toàn có thể sử dụng decorator để "đánh dấu" hàm và sau đó sử dụng các kỹ thuật metaclass để xử lý phương thức sau đó, vì ở __new__giai đoạn này, lớp đã được tạo bởi metaclass của nó.

Đây là một ví dụ đơn giản:

Chúng tôi sử dụng @fieldđể đánh dấu phương thức là một trường đặc biệt và xử lý nó trong metaclass.

def field(fn):
    """Mark the method as an extra field"""
    fn.is_field = True
    return fn

class MetaEndpoint(type):
    def __new__(cls, name, bases, attrs):
        fields = {}
        for k, v in attrs.items():
            if inspect.isfunction(v) and getattr(k, "is_field", False):
                fields[k] = v
        for base in bases:
            if hasattr(base, "_fields"):
                fields.update(base._fields)
        attrs["_fields"] = fields

        return type.__new__(cls, name, bases, attrs)

class EndPoint(metaclass=MetaEndpoint):
    pass


# Usage

class MyEndPoint(EndPoint):
    @field
    def foo(self):
        return "bar"

e = MyEndPoint()
e._fields  # {"foo": ...}

Bạn có một lỗi đánh máy trong dòng này: if inspect.isfunction(v) and getattr(k, "is_field", False):nó nên được getattr(v, "is_field", False)thay thế.
EvilTosha

0

Bạn sẽ có quyền truy cập vào lớp của đối tượng mà phương thức đang được gọi trong phương thức được trang trí mà trình trang trí của bạn sẽ trả về. Như vậy:

def decorator(method):
    # do something that requires view's class
    def decorated(self, *args, **kwargs):
        print 'My class is %s' % self.__class__
        method(self, *args, **kwargs)
    return decorated

Sử dụng lớp ModelA của bạn, đây là những gì nó làm được:

>>> obj = ModelA()
>>> obj.a_method()
My class is <class '__main__.ModelA'>

1
Cảm ơn nhưng đây chính xác là giải pháp tôi đã tham khảo trong câu hỏi của mình mà không phù hợp với tôi. Tôi đang cố gắng triển khai một mẫu người quan sát bằng cách sử dụng bộ trang trí và tôi sẽ không bao giờ có thể gọi phương thức trong ngữ cảnh chính xác từ bộ điều phối quan sát của tôi nếu tôi không có lớp tại một số thời điểm trong khi thêm phương thức vào bộ điều phối quan sát. Lấy lớp khi gọi phương thức không giúp tôi gọi phương thức một cách chính xác ngay từ đầu.
Carl G

Whoa, xin lỗi vì sự lười biếng của tôi trong việc không đọc toàn bộ câu hỏi của bạn.
Will McCutchen
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.