functools.wraps ทำอะไร


650

ในความคิดเห็นเกี่ยวกับคำตอบของคำถามนี้มีคนพูดว่าพวกเขาไม่แน่ใจว่าfunctools.wrapsกำลังทำอะไรอยู่ ดังนั้นฉันถามคำถามนี้เพื่อให้มีการบันทึกไว้ใน StackOverflow สำหรับการอ้างอิงในอนาคต: สิ่งที่จะfunctools.wrapsทำอย่างไร

คำตอบ:


1069

เมื่อคุณใช้มัณฑนากรคุณจะแทนที่ฟังก์ชันหนึ่งด้วยฟังก์ชันอื่น กล่าวอีกนัยหนึ่งถ้าคุณมีมัณฑนากร

def logged(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

มันเหมือนกับที่พูด

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

และการทำงานของคุณจะถูกแทนที่ด้วยฟังก์ชั่นf with_loggingน่าเสียดายที่นี่หมายความว่าถ้าคุณพูดแล้ว

print(f.__name__)

มันจะพิมพ์with_loggingเพราะนั่นคือชื่อของฟังก์ชั่นใหม่ของคุณ ในความเป็นจริงถ้าคุณดูที่ docstring fมันจะว่างเปล่าเพราะwith_loggingไม่มี docstring ดังนั้น docstring ที่คุณเขียนจะไม่อยู่ที่นั่นอีกต่อไป นอกจากนี้ถ้าคุณมองไปที่ผล pydoc สำหรับฟังก์ชั่นที่มันจะไม่ถูกระบุว่าเป็นการหนึ่งอาร์กิวเมนต์x; มันจะถูกระบุว่าเป็นการถ่าย*argsและ**kwargsเพราะนั่นคือสิ่งที่ with_logging ใช้

หากใช้มัณฑนากรหมายถึงการสูญเสียข้อมูลนี้เกี่ยวกับฟังก์ชั่นมันจะเป็นปัญหาร้ายแรง functools.wrapsนั่นเป็นเหตุผลที่เรามี สิ่งนี้จะใช้ฟังก์ชั่นที่ใช้ในมัณฑนากรและเพิ่มฟังก์ชั่นในการคัดลอกชื่อฟังก์ชั่น, docstring, รายการข้อโต้แย้ง ฯลฯ และเนื่องจากwrapsตัวมันเองเป็นมัณฑนากรรหัสต่อไปนี้ทำสิ่งที่ถูกต้อง:

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
ใช่ฉันชอบที่จะหลีกเลี่ยงโมดูลมัณฑนากรเนื่องจาก functools.wraps เป็นส่วนหนึ่งของไลบรารีมาตรฐานและดังนั้นจึงไม่แนะนำการพึ่งพาภายนอกอื่น แต่โมดูลมัณฑนากรช่วยแก้ปัญหาความช่วยเหลือได้อย่างแน่นอนซึ่งหวังว่าจะทำงานได้ทุกวัน
Eli Courtwright

6
นี่คือตัวอย่างของสิ่งที่สามารถเกิดขึ้นได้หากคุณไม่ใช้ wraps: การทดสอบ doctools อาจหายไปในทันที นั่นเป็นเพราะ Doctools ไม่สามารถหาการทดสอบในฟังก์ชั่นการตกแต่งได้ยกเว้นบางอย่างที่ wraps () คัดลอกมา
andrew cooke

88
ทำไมเราถึงต้องการfunctools.wrapsงานนี้ไม่ควรเป็นเพียงส่วนหนึ่งของลวดลายตกแต่งภายในในตอนแรก คุณไม่ต้องการใช้ @wraps เมื่อใด
Wim

56
@wim: ฉันได้เขียนมัณฑนากรบางคนที่ทำเวอร์ชั่นของตัวเอง@wrapsเพื่อที่จะทำการดัดแปลงหรือใส่หมายเหตุประกอบต่าง ๆ ในค่าที่คัดลอกมา โดยพื้นฐานแล้วมันเป็นส่วนเสริมของปรัชญา Python ที่ชัดเจนดีกว่าโดยปริยายและกรณีพิเศษไม่ได้พิเศษพอที่จะทำลายกฎ (รหัสนั้นง่ายกว่ามากและภาษาจะเข้าใจง่ายขึ้นหาก@wrapsต้องจัดเตรียมด้วยตนเองแทนที่จะใช้กลไกการเลือกไม่ใช้บางประเภท)
ssokolow

35
@LucasMalor มัณฑนากรทุกคนไม่ห่อฟังก์ชั่นที่พวกเขาตกแต่ง บางคนใช้ผลข้างเคียงเช่นการลงทะเบียนในระบบค้นหาบางประเภท
ssokolow

22

ฉันมักจะใช้ชั้นเรียนมากกว่าฟังก์ชั่นสำหรับนักตกแต่งของฉัน ฉันมีปัญหาบางอย่างกับสิ่งนี้เพราะวัตถุจะไม่มีคุณลักษณะเดียวกันทั้งหมดที่คาดว่าจะมีฟังก์ชั่น __name__ยกตัวอย่างเช่นวัตถุจะไม่ได้มีแอตทริบิวต์ ฉันมีปัญหาเฉพาะเกี่ยวกับสิ่งนี้ซึ่งค่อนข้างยากในการติดตามที่ Django รายงานข้อผิดพลาด "วัตถุไม่มีแอตทริบิวต์ ' __name__'" น่าเสียดายสำหรับผู้ตกแต่งในชั้นเรียนฉันไม่เชื่อว่า @wrap จะทำงานนี้ได้ ฉันได้สร้างคลาสมัณฑนากรฐานแทนเช่น:

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)

ชั้นนี้ผู้รับมอบฉันทะคุณลักษณะทั้งหมดเรียกผ่านไปยังฟังก์ชั่นที่ถูกตกแต่ง ดังนั้นตอนนี้คุณสามารถสร้างมัณฑนากรง่ายๆที่ตรวจสอบว่ามีการระบุอาร์กิวเมนต์ 2 ข้อดังนี้:

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
ในฐานะที่เป็นเอกสารจาก@wrapsกล่าวว่าเป็นเพียงฟังก์ชั่นอำนวยความสะดวกให้@wraps functools.update_wrapper()ในกรณีของมัณฑนากรชั้นคุณสามารถโทรupdate_wrapper()โดยตรงจาก__init__()วิธีการของคุณ ดังนั้นคุณไม่จำเป็นต้องสร้างDecBaseที่ทุกท่านก็สามารถรวมอยู่ใน__init__()ของเส้น:process_login update_wrapper(self, func)นั่นคือทั้งหมดที่
Fabiano

14

ตั้งแต่ python 3.5+:

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

g = functools.update_wrapper(g, f)เป็นนามแฝงสำหรับ มันทำสามสิ่ง:

  • มันสำเนา__module__, __name__, __qualname__, __doc__และ__annotations__คุณลักษณะของบนf gรายการเริ่มต้นนี้อยู่ในWRAPPER_ASSIGNMENTSคุณสามารถเห็นมันในแหล่ง functools
  • มันปรับปรุง__dict__ของกับองค์ประกอบทั้งหมดจากg f.__dict__(ดูWRAPPER_UPDATESในแหล่งที่มา)
  • มันตั้งค่า__wrapped__=fคุณลักษณะใหม่ในg

ผลที่ตามมาคือการที่gปรากฏว่ามีชื่อเดียวกัน docstring fชื่อโมดูลและลายเซ็นกว่า ปัญหาเดียวคือว่าเกี่ยวกับลายเซ็นนี้ไม่เป็นความจริง: มันเป็นเพียงที่inspect.signatureตามโซ่เสื้อคลุมโดยค่าเริ่มต้น คุณสามารถตรวจสอบได้โดยใช้inspect.signature(g, follow_wrapped=False)ตามที่อธิบายไว้ในเอกสาร สิ่งนี้มีผลที่น่ารำคาญ:

  • รหัสของ wrapper จะทำงานแม้เมื่ออาร์กิวเมนต์ที่ระบุนั้นไม่ถูกต้อง
  • รหัสของ wrapper ไม่สามารถเข้าถึงอาร์กิวเมนต์ได้ง่ายโดยใช้ชื่อจาก * args ที่ได้รับ ** kwargs อันที่จริงใครจะต้องจัดการทุกกรณี (ตำแหน่งคำหลักเริ่มต้น) Signature.bind()และดังนั้นจึงใช้สิ่งที่ต้องการ

ขณะนี้มีความสับสนเล็กน้อยfunctools.wrapsและการตกแต่งเนื่องจากกรณีที่ใช้บ่อยมากสำหรับการพัฒนานักตกแต่งคือการห่อฟังก์ชั่น แต่ทั้งคู่เป็นแนวคิดที่เป็นอิสระอย่างสมบูรณ์ หากคุณสนใจที่จะเข้าใจความแตกต่างฉันได้ติดตั้งไลบรารีผู้ช่วยเหลือสำหรับทั้งสอง: decopatchเพื่อเขียนตัวตกแต่งได้อย่างง่ายดายและสร้างเพื่อให้ทดแทนลายเซ็นที่เก็บรักษา@wrapsไว้ โปรดทราบว่าmakefunอาศัยเคล็ดลับที่พิสูจน์แล้วเช่นเดียวกันกับdecoratorห้องสมุดที่มีชื่อเสียง


3

นี่คือซอร์สโค้ดเกี่ยวกับ wraps:

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. วิชาบังคับก่อน: คุณต้องรู้จักวิธีใช้มัณฑนากรและมีการห่อ ความคิดเห็นนี้จะอธิบายได้อย่างชัดเจนหรือลิงก์นี้ยังอธิบายได้ค่อนข้างดี

  2. เมื่อใดก็ตามที่เราใช้ For เช่น: @wraps ตามด้วยฟังก์ชั่น wrapper ของเรา ตามรายละเอียดที่ให้ไว้ในลิงค์นี้มันบอกว่า

functools.wraps เป็นฟังก์ชั่นอำนวยความสะดวกสำหรับเรียกใช้ update_wrapper () เป็นมัณฑนากรฟังก์ชันเมื่อกำหนดฟังก์ชั่น wrapper

มันเทียบเท่ากับบางส่วน (update_wrapper, wra = wray, มอบหมาย = มอบหมาย, ปรับปรุง = ปรับปรุง)

ดังนั้น @wraps มัณฑนากรจึงให้เรียกไปยัง functools.partial (func [, * args] [, ** คำหลัก])

นิยาม functools.partial () บอกว่า

บางส่วน () ใช้สำหรับแอปพลิเคชั่นฟังก์ชั่นบางส่วนซึ่ง“ หยุด” บางส่วนของอาร์กิวเมนต์และ / หรือคำหลักของฟังก์ชันทำให้เกิดวัตถุใหม่ที่มีลายเซ็นที่เรียบง่าย ตัวอย่างเช่นบางส่วน () สามารถใช้ในการสร้าง callable ที่ทำหน้าที่เหมือนฟังก์ชั่น int () ที่อาร์กิวเมนต์ฐานเริ่มต้นที่สอง:

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

ซึ่งนำฉันไปสู่ข้อสรุปว่า @wraps ให้การเรียกบางส่วน () และผ่านฟังก์ชัน wrapper ของคุณเป็นพารามิเตอร์ บางส่วน () ในที่สุดส่งกลับรุ่นที่ง่ายเช่นวัตถุของสิ่งที่อยู่ในฟังก์ชั่นการห่อหุ้มและไม่ฟังก์ชั่นการห่อหุ้มตัวเอง


-4

ในระยะสั้นfunctools.wrapsเป็นเพียงฟังก์ชั่นปกติ ลองพิจารณาตัวอย่างที่เป็นทางการนี้ ด้วยความช่วยเหลือของซอร์สโค้ดเราสามารถดูรายละเอียดเพิ่มเติมเกี่ยวกับการใช้งานและขั้นตอนการทำงานดังนี้:

  1. ตัด (ฉ)ผลตอบแทนวัตถุพูดO1 มันเป็นวัตถุของคลาสบางส่วน
  2. ขั้นตอนต่อไปคือ@ O1 ...ซึ่งเป็นเครื่องหมายของมัณฑนากรในหลาม มันหมายถึง

เสื้อคลุม = O1 .__ โทร __ (เสื้อคลุม)

การตรวจสอบการใช้งาน__call__เราจะเห็นว่าหลังจากขั้นตอนนี้wrapper (ด้านซ้ายมือ) กลายเป็นวัตถุที่เกิดจากself.func (* self.args, * args, newkeywords **) การตรวจสอบการสร้างO1ใน__new__เรา รู้self.funcเป็นฟังก์ชั่นupdate_wrapper มันใช้พารามิเตอร์* args , wrapperด้านขวาเป็นพารามิเตอร์ที่ 1 เมื่อตรวจสอบขั้นตอนสุดท้ายของupdate_wrapperเราจะเห็นว่ามีการส่งคืนwrapperทางด้านขวาด้วยคุณลักษณะบางอย่างที่ปรับเปลี่ยนตามต้องการ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.