Hướng dẫn python decorator performance - hiệu suất trang trí python

Hiệu suất chi phí khi áp dụng các nhà trang trí vào các phương pháp

Đây là bài viết thứ mười trong loạt bài đăng trên blog của tôi về các nhà trang trí Python và cách tôi tin rằng chúng thường được thực hiện kém. Nó tiếp nối từ bài viết trước có tiêu đề Hiệu suất trên đầu của việc sử dụng các nhà trang trí, với bài viết đầu tiên trong loạt bài là cách bạn thực hiện bộ trang trí Python của bạn là sai.

Trong bài trước, tôi bắt đầu xem xét ý nghĩa hiệu suất của việc sử dụng các nhà trang trí. Trong bài đăng đó, tôi đã bắt đầu bằng cách nhìn vào các chi phí khi áp dụng một bộ trang trí vào một chức năng bình thường, so sánh một trình trang trí được thực hiện như một chức năng đóng cửa cho việc triển khai trang trí mạnh mẽ hơn là chủ đề của loạt bài này.

Đối với mô hình Model năm 2012, các thử nghiệm mang lại cho một cuộc gọi chức năng thẳng:

10000000 loops, best of 3: 0.132 usec per loop

Khi sử dụng một bộ trang trí được thực hiện như một chức năng đóng, kết quả là:

1000000 loops, best of 3: 0.326 usec per loop

Và cuối cùng với nhà máy trang trí được mô tả trong loạt bài viết trên blog này:

1000000 loops, best of 3: 0.771 usec per loop

Con số cuối cùng này dựa trên việc thực hiện Python thuần túy. Tuy nhiên, khi proxy và trình bao bọc chức năng đối tượng được triển khai như một tiện ích mở rộng C, có thể đưa nó xuống:

1000000 loops, best of 3: 0.382 usec per loop

Kết quả này không khác nhiều so với khi sử dụng một bộ trang trí được thực hiện như một chức năng đóng cửa.

Bây giờ những gì khi các nhà trang trí được áp dụng cho các phương pháp của một lớp?

Chi phí phải liên kết các chức năng

Vấn đề với việc áp dụng các nhà trang trí vào các phương pháp của một lớp là nếu bạn định tôn vinh mô hình thực thi Python, người trang trí cần được thực hiện như một mô tả và liên kết chính xác các phương thức với một trường hợp lớp hoặc lớp khi được truy cập. Trong bộ trang trí được mô tả trong loạt bài viết này, chúng tôi thực sự đã sử dụng cơ chế đó để có thể xác định khi nào một người trang trí được áp dụng cho một hàm bình thường, phương pháp thể hiện hoặc phương pháp lớp.

Mặc dù quá trình ràng buộc này đảm bảo hoạt động chính xác, nhưng nó có thêm chi phí chi phí về những gì một nhà trang trí được thực hiện như một chức năng đóng, điều này không thực hiện bất kỳ nỗ lực nào để bảo tồn mô hình thực thi Python, sẽ làm.

Để xem những bước bổ sung nào xảy ra, một lần nữa chúng ta có thể sử dụng cơ chế móc hồ sơ Python để theo dõi việc thực hiện cuộc gọi của chức năng được trang trí của chúng ta. Trong trường hợp này, việc thực hiện một phương thức thể hiện.

Trước tiên, hãy kiểm tra lại những gì chúng tôi sẽ nhận được cho một người trang trí được thực hiện như một sự đóng cửa chức năng.

def my_function_wrapper(wrapped):
    def _my_function_wrapper(*args, **kwargs):
        return wrapped(*args, **kwargs)
    return _my_function_wrapper 

class Class(object):
    @my_function_wrapper
    def method(self):
        pass

instance = Class()

import sys

def tracer(frame, event, arg):
    print(frame.f_code.co_name, event)

sys.setprofile(tracer)

instance.method()

Kết quả trong việc chạy điều này thực sự giống như khi trang trí một chức năng bình thường.

_my_function_wrapper call
    method call
    method return
_my_function_wrapper return

Do đó, chúng ta nên hy vọng rằng chi phí sẽ không khác biệt đáng kể khi chúng ta thực hiện các bài kiểm tra thời gian thực tế.

Bây giờ khi sử dụng nhà máy trang trí của chúng tôi. Để cung cấp bối cảnh lần này, chúng tôi cần trình bày công thức hoàn chỉnh để thực hiện.

class object_proxy(object):

    def __init__(self, wrapped):
        self.wrapped = wrapped
        try:
            self.__name__ = wrapped.__name__
        except AttributeError:
            pass

    @property
    def __class__(self):
        return self.wrapped.__class__

    def __getattr__(self, name):
        return getattr(self.wrapped, name)

class bound_function_wrapper(object_proxy):

    def __init__(self, wrapped, instance, wrapper, binding, parent):
        super(bound_function_wrapper, self).__init__(wrapped)
        self.instance = instance
        self.wrapper = wrapper
        self.binding = binding
        self.parent = parent

    def __call__(self, *args, **kwargs):
        if self.binding == 'function':
            if self.instance is None:
                instance, args = args[0], args[1:]
                wrapped = functools.partial(self.wrapped, instance)
                return self.wrapper(wrapped, instance, args, kwargs)
            else:
                return self.wrapper(self.wrapped, self.instance, args, kwargs)
        else:
            instance = getattr(self.wrapped, '__self__', None)
            return self.wrapper(self.wrapped, instance, args, kwargs)

    def __get__(self, instance, owner):
        if self.instance is None and self.binding == 'function':
            descriptor = self.parent.wrapped.__get__(instance, owner)
            return bound_function_wrapper(descriptor, instance, self.wrapper,
                    self.binding, self.parent)
        return self 

class function_wrapper(object_proxy):

    def __init__(self, wrapped, wrapper):
        super(function_wrapper, self).__init__(wrapped)
        self.wrapper = wrapper
        if isinstance(wrapped, classmethod):
            self.binding = 'classmethod'
        elif isinstance(wrapped, staticmethod):
            self.binding = 'staticmethod'
        else:
            self.binding = 'function' 

    def __get__(self, instance, owner):
        wrapped = self.wrapped.__get__(instance, owner)
        return bound_function_wrapper(wrapped, instance, self.wrapper,
                self.binding, self) 

    def __call__(self, *args, **kwargs):
        return self.wrapper(self.wrapped, None, args, kwargs) 

def decorator(wrapper):
    def _wrapper(wrapped, instance, args, kwargs):
        def _execute(wrapped):
            if instance is None:
                return function_wrapper(wrapped, wrapper)
            elif inspect.isclass(instance):
                return function_wrapper(wrapped,
                        wrapper.__get__(None, instance))
            else:
                return function_wrapper(wrapped,
                        wrapper.__get__(instance, type(instance)))
        return _execute(*args, **kwargs)
    return function_wrapper(wrapper, _wrapper)

Với việc thực hiện trang trí của chúng tôi hiện đang là:

@decorator
def my_function_wrapper(wrapped, instance, args, kwargs):
    return wrapped(*args, **kwargs)

Kết quả chúng tôi nhận được khi thực hiện phương pháp thể hiện được trang trí của lớp là:

('__get__', 'call') # function_wrapper
    ('__init__', 'call') # bound_function_wrapper
        ('__init__', 'call') # object_proxy
        ('__init__', 'return')
    ('__init__', 'return')
('__get__', 'return')

('__call__', 'call') # bound_function_wrapper
    ('my_function_wrapper', 'call')
        ('method', 'call')
        ('method', 'return')
    ('my_function_wrapper', 'return')
('__call__', 'return')

Có thể thấy, do sự ràng buộc của phương pháp với thể hiện của lớp xảy ra trong

1000000 loops, best of 3: 0.326 usec per loop
4, nhiều hơn nữa hiện đang xảy ra. Do đó, chi phí có thể được dự kiến ​​sẽ được nhiều hơn đáng kể.

Thời gian thực hiện cuộc gọi phương thức

Như trước đây, thay vì sử dụng việc thực hiện ở trên, việc triển khai thực tế từ thư viện Wrapt sẽ lại được sử dụng.

Lần này, bài kiểm tra của chúng tôi được chạy như:

$ python -m timeit -s 'import benchmarks; c=benchmarks.Class()' 'c.method()'

Đối với trường hợp không có người trang trí được sử dụng trên phương thức thể hiện, kết quả là:

1000000 loops, best of 3: 0.326 usec per loop
0

Điều này nhiều hơn một chút so với trường hợp gọi hàm bình thường do liên kết của hàm với thể hiện đang xảy ra.

Tiếp theo là sử dụng bộ trang trí được thực hiện như một chức năng đóng cửa. Đối với điều này, chúng tôi nhận được:

1000000 loops, best of 3: 0.382 usec per loop

Một lần nữa, phần nào nhiều hơn trường hợp không được trang trí, nhưng không phải là nhiều hơn so với khi người trang trí được thực hiện như một chức năng đóng được áp dụng cho một chức năng bình thường. Chi phí của bộ trang trí này khi được áp dụng cho một hàm bình thường so với phương thức thể hiện không khác biệt đáng kể.

Bây giờ đối với trường hợp nhà máy trang trí và trình bao bọc chức năng của chúng tôi, người tôn vinh mô hình thực thi Python, bằng cách đảm bảo rằng việc ràng buộc hàm với thể hiện của lớp được thực hiện chính xác.

Đầu tiên là nơi sử dụng triển khai Python thuần túy.

1000000 loops, best of 3: 0.326 usec per loop
2

Ouch. So với khi sử dụng đóng chức năng để thực hiện bộ trang trí, đây là một cú đánh bổ sung trong chi phí thời gian chạy.

Mặc dù đây chỉ là khoảng 6 USEC mỗi cuộc gọi, bạn cần phải suy nghĩ về điều này trong bối cảnh. Cụ thể, nếu một trình trang trí như vậy được áp dụng cho một hàm được gọi là 1000 lần trong quá trình trao yêu cầu web, thì đó là thêm 6 ms được thêm vào đầu thời gian phản hồi cho yêu cầu web đó.

Đây là điểm mà nhiều người sẽ không nghi ngờ gì rằng việc đúng là không đáng nếu chi phí đơn giản là quá nhiều. Nhưng sau đó, nó cũng không có khả năng là chức năng được trang trí, cũng như bản thân người trang trí sẽ không làm gì cả và vì vậy chi phí bổ sung phát sinh vẫn có thể là một tỷ lệ nhỏ của chi phí thời gian chạy của những người đó và vì vậy không có gì đáng chú ý .

Tất cả đều giống nhau, việc sử dụng tiện ích mở rộng C có thể cải thiện mọi thứ không?

Đối với trường hợp proxy và trình bao bọc chức năng đối tượng được triển khai dưới dạng tiện ích mở rộng C, kết quả là:

1000000 loops, best of 3: 0.326 usec per loop
3

Vì vậy, thay vì 6 ms, đó là ít hơn 1 ms chi phí bổ sung nếu hàm được trang trí được gọi là 1000 lần.

Nó vẫn còn nhiều hơn so với khi sử dụng một bộ trang trí được thực hiện như một chức năng đóng, nhưng nhắc lại một lần nữa, việc sử dụng một chức năng đóng khi trang trí một phương pháp của một lớp bị phá vỡ về mặt kỹ thuật bởi thiết kế vì nó không tôn trọng mô hình thực hiện Python.

Ai quan tâm nếu nó không hoàn toàn đúng

Tôi có đang chia tóc và quá mức trong việc muốn mọi thứ được thực hiện đúng cách không?

Chắc chắn, đối với những gì bạn đang sử dụng các nhà trang trí cho bạn cũng có thể thoát khỏi việc sử dụng một bộ trang trí được thực hiện như một sự đóng cửa chức năng. Khi bạn bắt đầu mặc dù di chuyển vào khu vực sử dụng trình bao bọc chức năng để thực hiện việc vá khỉ của mã tùy ý, bạn không thể đủ khả năng để làm mọi thứ theo cách cẩu thả.

Nếu bạn không tôn trọng mô hình thực hiện Python khi thực hiện việc vá khỉ, bạn có thể quá dễ dàng phá vỡ theo những cách rất tinh tế và mơ hồ về mã của bên thứ ba mà bạn đang vá khỉ. Khách hàng không thực sự thích nó khi những gì bạn làm sập ứng dụng web của họ. Vì vậy, đối với những gì tôi cần làm ít nhất, nó không quan trọng và nó rất quan trọng.

Bây giờ trong bài đăng này, tôi chỉ xem xét chi phí khi trang trí các phương pháp thể hiện của một lớp. Tôi đã không bao gồm những gì trên đầu là khi trang trí các phương pháp tĩnh và phương pháp lớp. Nếu bạn tò mò về những điều đó và làm thế nào chúng có thể khác nhau, bạn có thể kiểm tra các điểm chuẩn cho toàn bộ các trường hợp trong tài liệu Wrapt.

Trong bài tiếp theo, tôi sẽ chạm một lần nữa về các vấn đề về hiệu suất, nhưng cũng có một chút về cách thực hiện thay thế để trang trí để thử và giải quyết các vấn đề được nêu trong bài viết đầu tiên của tôi. Đây sẽ là một phần của sự so sánh giữa cách tiếp cận được mô tả trong loạt bài đăng này và cách thức mà mô -đun

1000000 loops, best of 3: 0.326 usec per loop
5 có sẵn từ PYPI thực hiện biến thể của một nhà máy trang trí.