Funcools.wraps làm gì?


650

Trong một bình luận về câu trả lời này cho một câu hỏi khác , có người nói rằng họ không chắc chắn những gì functools.wrapsđang làm. Vì vậy, tôi đang hỏi câu hỏi này để có một bản ghi về nó trên StackOverflow để tham khảo trong tương lai: functools.wrapschính xác thì nó làm gì?

Câu trả lời:


1069

Khi bạn sử dụng một trình trang trí, bạn đang thay thế một chức năng bằng một chức năng khác. Nói cách khác, nếu bạn có một người trang trí

def logged(func):
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

sau đó khi bạn nói

@logged
def f(x):
   """does some math"""
   return x + x * x

nó giống hệt như nói

def f(x):
    """does some math"""
    return x + x * x
f = logged(f)

và chức năng của bạn fđược thay thế bằng chức năng with_logging. Thật không may, điều này có nghĩa là nếu sau đó bạn nói

print(f.__name__)

nó sẽ in with_loggingvì đó là tên của chức năng mới của bạn. Trong thực tế, nếu bạn nhìn vào chuỗi doc f, nó sẽ trống vì with_loggingkhông có chuỗi doc, và do đó, chuỗi doc bạn đã viết sẽ không còn ở đó nữa. Ngoài ra, nếu bạn nhìn vào kết quả pydoc cho hàm đó, nó sẽ không được liệt kê là lấy một đối số x; thay vào đó, nó sẽ được liệt kê là lấy *args**kwargsbởi vì đó là những gì with_logging mất.

Nếu sử dụng một trình trang trí luôn có nghĩa là mất thông tin này về một chức năng, nó sẽ là một vấn đề nghiêm trọng. Đó là lý do tại sao chúng ta có functools.wraps. Điều này có một chức năng được sử dụng trong một trình trang trí và thêm chức năng sao chép tên hàm, chuỗi, danh sách đối số, v.v. Và vì wrapschính nó là một trình trang trí, mã sau đây thực hiện đúng:

from functools import wraps
def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   """does some math"""
   return x + x * x

print(f.__name__)  # prints 'f'
print(f.__doc__)   # prints 'does some math'

7
Đúng, tôi muốn tránh mô-đun trang trí vì funcools.wraps là một phần của thư viện tiêu chuẩn và do đó không giới thiệu một phụ thuộc bên ngoài khác. Nhưng mô-đun trang trí thực sự giải quyết vấn đề trợ giúp, hy vọng funcools.wraps một ngày nào đó cũng sẽ như vậy.
Eli Courtwright

6
đây là một ví dụ về những gì có thể xảy ra nếu bạn không sử dụng kết thúc tốt đẹp: các bài kiểm tra doctools có thể đột nhiên biến mất. đó là bởi vì doctools không thể tìm thấy các bài kiểm tra trong các chức năng được trang trí trừ khi một cái gì đó như kết thúc tốt đẹp () đã sao chép chúng qua.
andrew cooke

88
Tại sao chúng ta cần functools.wrapscho công việc này, không phải nó chỉ là một phần của mẫu trang trí ở nơi đầu tiên? khi nào bạn không muốn sử dụng @wraps?
wim

56
@wim: Tôi đã viết một số trang trí làm phiên bản riêng của họ @wrapsđể thực hiện các loại sửa đổi hoặc chú thích khác nhau trên các giá trị được sao chép qua. Về cơ bản, đó là một phần mở rộng của triết lý Python rõ ràng tốt hơn các trường hợp ngầm và đặc biệt không đủ đặc biệt để phá vỡ các quy tắc. (Mã đơn giản hơn nhiều và ngôn ngữ dễ hiểu hơn nếu @wrapsphải được cung cấp thủ công, thay vì sử dụng một số loại cơ chế từ
chối

35
@LucasMalor Không phải tất cả các trang trí bao bọc các chức năng họ trang trí. Một số áp dụng tác dụng phụ, chẳng hạn như đăng ký chúng trong một số loại hệ thống tra cứu.
ssokolow

22

Tôi rất thường sử dụng các lớp, thay vì các hàm, cho các trang trí của tôi. Tôi đã gặp một số rắc rối với điều này bởi vì một đối tượng sẽ không có tất cả các thuộc tính giống như được mong đợi của một hàm. Ví dụ, một đối tượng sẽ không có thuộc tính __name__. Tôi đã có một vấn đề cụ thể với vấn đề này khá khó để theo dõi Django đã báo cáo lỗi "đối tượng không có thuộc tính ' __name__'". Thật không may, đối với những người trang trí theo phong cách đẳng cấp, tôi không tin rằng @wrap sẽ thực hiện công việc. Thay vào đó tôi đã tạo ra một lớp trang trí cơ sở như vậy:

class DecBase(object):
    func = None

    def __init__(self, func):
        self.__func = func

    def __getattribute__(self, name):
        if name == "func":
            return super(DecBase, self).__getattribute__(name)

        return self.func.__getattribute__(name)

    def __setattr__(self, name, value):
        if name == "func":
            return super(DecBase, self).__setattr__(name, value)

        return self.func.__setattr__(name, value)

Lớp này ủy nhiệm tất cả các thuộc tính gọi đến hàm đang được trang trí. Vì vậy, bây giờ bạn có thể tạo một trình trang trí đơn giản để kiểm tra 2 đối số được chỉ định như vậy:

class process_login(DecBase):
    def __call__(self, *args):
        if len(args) != 2:
            raise Exception("You can only specify two arguments")

        return self.func(*args)

7
Như các tài liệu từ @wrapsnói, @wrapschỉ là một chức năng tiện lợi functools.update_wrapper(). Trong trường hợp trang trí lớp, bạn có thể gọi update_wrapper()trực tiếp từ __init__()phương thức của bạn . Vì vậy, bạn không cần phải tạo ra DecBaseở tất cả, bạn chỉ có thể bao gồm trên __init__()các process_logindòng: update_wrapper(self, func). Đó là tất cả.
Fabiano

14

Kể từ con trăn 3,5+:

@functools.wraps(f)
def g():
    pass

Là một bí danh cho g = functools.update_wrapper(g, f). Nó thực hiện chính xác ba điều:

  • nó sao chép __module__, __name__, __qualname__, __doc__, và __annotations__thuộc tính của ftrên g. Danh sách mặc định này là trong WRAPPER_ASSIGNMENTS, bạn có thể nhìn thấy nó trong nguồn funcools .
  • nó cập nhật __dict__của gtất cả các yếu tố từ f.__dict__. (xem WRAPPER_UPDATEStrong nguồn)
  • nó đặt một __wrapped__=fthuộc tính mới trêng

Hậu quả là gdường như có cùng tên, chuỗi, tên mô-đun và chữ ký hơn f. Vấn đề duy nhất là liên quan đến chữ ký, điều này không thực sự đúng: nó chỉ inspect.signaturetheo sau các chuỗi trình bao bọc theo mặc định. Bạn có thể kiểm tra nó bằng cách sử dụng inspect.signature(g, follow_wrapped=False)như được giải thích trong tài liệu . Điều này có hậu quả gây phiền nhiễu:

  • mã trình bao bọc sẽ thực thi ngay cả khi các đối số được cung cấp không hợp lệ.
  • mã trình bao bọc không thể dễ dàng truy cập một đối số bằng tên của nó, từ các đối số * args, ** kwargs nhận được. Thật vậy, người ta sẽ phải xử lý tất cả các trường hợp (vị trí, từ khóa, mặc định) và do đó để sử dụng một cái gì đó như Signature.bind().

Bây giờ có một chút nhầm lẫn giữa functools.wrapsvà trang trí, bởi vì một trường hợp sử dụng rất thường xuyên để phát triển trang trí là để bọc các chức năng. Nhưng cả hai đều là những khái niệm hoàn toàn độc lập. Nếu bạn quan tâm đến việc tìm hiểu sự khác biệt, tôi đã triển khai các thư viện trợ giúp cho cả hai: decopatch để viết trang trí dễ dàng và makefun để cung cấp thay thế bảo tồn chữ ký cho @wraps. Lưu ý rằng makefundựa trên cùng một thủ thuật đã được chứng minh so với decoratorthư viện nổi tiếng .


3

đây là mã nguồn về kết thúc tốt đẹp:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__')

WRAPPER_UPDATES = ('__dict__',)

def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):

    """Update a wrapper function to look like the wrapped function

       wrapper is the function to be updated
       wrapped is the original function
       assigned is a tuple naming the attributes assigned directly
       from the wrapped function to the wrapper function (defaults to
       functools.WRAPPER_ASSIGNMENTS)
       updated is a tuple naming the attributes of the wrapper that
       are updated with the corresponding attribute from the wrapped
       function (defaults to functools.WRAPPER_UPDATES)
    """
    for attr in assigned:
        setattr(wrapper, attr, getattr(wrapped, attr))
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

   Returns a decorator that invokes update_wrapper() with the decorated
   function as the wrapper argument and the arguments to wraps() as the
   remaining arguments. Default arguments are as for update_wrapper().
   This is a convenience function to simplify applying partial() to
   update_wrapper().
    """
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

2
  1. Điều kiện tiên quyết: Bạn phải biết cách sử dụng trang trí và đặc biệt với kết thúc tốt đẹp. Nhận xét này giải thích một chút rõ ràng hoặc liên kết này cũng giải thích nó khá tốt.

  2. Bất cứ khi nào chúng tôi sử dụng Ví dụ: @wraps theo sau là hàm bao bọc riêng của chúng tôi. Theo các chi tiết được đưa ra trong này liên kết , nó nói rằng

funcools.wraps là hàm tiện lợi để gọi update_wrapper () làm công cụ trang trí hàm, khi xác định hàm bao bọc.

Nó tương đương với một phần (update_wrapper, quấn = bọc, gán = gán, cập nhật = cập nhật).

Vì vậy, trang trí @wraps thực sự đưa ra một cuộc gọi đến funcools.partial (func [, * args] [, ** Keywords]).

Định nghĩa funcools.partial () nói rằng

Một phần () được sử dụng cho ứng dụng chức năng một phần mà mà Freeze đóng băng một số phần của các đối số và / hoặc từ khóa của hàm dẫn đến một đối tượng mới có chữ ký đơn giản hóa. Ví dụ, một phần () có thể được sử dụng để tạo một hàm có thể gọi hoạt động giống như hàm int () trong đó đối số cơ sở mặc định là hai:

>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18

Điều này đưa tôi đến kết luận rằng, @wraps đưa ra một cuộc gọi đến một phần () và nó chuyển chức năng trình bao bọc của bạn làm tham số cho nó. Một phần () cuối cùng trả về phiên bản đơn giản hóa, tức là đối tượng của những gì bên trong hàm bao bọc chứ không phải chính hàm bao bọc.


-4

Nói tóm lại, funcools.wraps chỉ là một chức năng thông thường. Hãy xem xét ví dụ chính thức này . Với sự trợ giúp của mã nguồn , chúng ta có thể xem thêm chi tiết về việc triển khai và các bước chạy như sau:

  1. kết thúc tốt đẹp (f) trả về một đối tượng, nói O1 . Nó là một đối tượng của lớp Một phần
  2. Bước tiếp theo là @ O1 ... đó là ký hiệu trang trí trong python. Nó có nghĩa là

trình bao bọc = O1 .__ gọi __ (trình bao bọc)

Kiểm tra việc triển khai __call__ , chúng tôi thấy rằng sau bước này, trình bao bọc (phía bên trái) trở thành đối tượng do self.func (* self.args, * args, ** newkeywords) Kiểm tra việc tạo O1 trong __new__ , chúng tôi biết self.func là hàm update_wrapper . Nó sử dụng tham số * args , trình bao bọc phía bên phải , làm tham số đầu tiên. Kiểm tra bước cuối cùng của update_wrapper , người ta có thể thấy trình bao bọc phía bên phải được trả về, với một số thuộc tính được sửa đổi khi cần.

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.