การรักษาลายเซ็นของฟังก์ชันตกแต่ง


111

สมมติว่าฉันเขียนมัณฑนากรที่ทำอะไรธรรมดา ๆ ตัวอย่างเช่นอาจแปลงอาร์กิวเมนต์ทั้งหมดเป็นประเภทเฉพาะดำเนินการบันทึกใช้การบันทึก ฯลฯ

นี่คือตัวอย่าง:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

>>> funny_function("3", 4.0, z="5")
22

ทุกอย่างดีจนถึงตอนนี้ อย่างไรก็ตามมีปัญหาอย่างหนึ่ง ฟังก์ชั่นตกแต่งไม่ได้เก็บเอกสารของฟังก์ชันดั้งเดิมไว้:

>>> help(funny_function)
Help on function g in module __main__:

g(*args, **kwargs)

โชคดีที่มีวิธีแก้ปัญหา:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

คราวนี้ชื่อฟังก์ชันและเอกสารประกอบถูกต้อง:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z

แต่ยังมีปัญหาอยู่: ลายเซ็นฟังก์ชันผิด ข้อมูล "* args, ** kwargs" นั้นไร้ประโยชน์

จะทำอย่างไร? ฉันนึกถึงวิธีแก้ปัญหาง่ายๆ แต่มีข้อบกพร่องสองวิธี:

1 - รวมลายเซ็นที่ถูกต้องใน docstring:

def funny_function(x, y, z=3):
    """funny_function(x, y, z=3) -- computes x*y + 2*z"""
    return x*y + 2*z

สิ่งนี้ไม่ดีเนื่องจากการทำซ้ำ ลายเซ็นจะยังแสดงไม่ถูกต้องในเอกสารที่สร้างขึ้นโดยอัตโนมัติ ง่ายต่อการอัปเดตฟังก์ชันและลืมเกี่ยวกับการเปลี่ยน docstring หรือพิมพ์ผิด [ และใช่ฉันตระหนักดีว่า docstring ซ้ำซ้อนกับเนื้อความของฟังก์ชันแล้ว โปรดเพิกเฉยต่อสิ่งนี้ funny_function เป็นเพียงตัวอย่างแบบสุ่ม ]

2 - ไม่ใช้มัณฑนากรหรือใช้มัณฑนากรแบบพิเศษสำหรับลายเซ็นเฉพาะทุกรายการ:

def funny_functions_decorator(f):
    def g(x, y, z=3):
        return f(int(x), int(y), z=int(z))
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

วิธีนี้ใช้ได้ดีกับชุดฟังก์ชันที่มีลายเซ็นเหมือนกัน แต่โดยทั่วไปแล้วไม่มีประโยชน์ ดังที่ได้กล่าวไปแล้วในตอนต้นว่าฉันต้องการใช้มัณฑนากรแบบทั่วไปทั้งหมด

ฉันกำลังมองหาวิธีแก้ปัญหาที่เป็นแบบทั่วไปและอัตโนมัติ

คำถามคือ: มีวิธีแก้ไขลายเซ็นฟังก์ชันตกแต่งหลังจากสร้างแล้วหรือไม่?

มิฉะนั้นฉันสามารถเขียนมัณฑนากรที่แยกลายเซ็นฟังก์ชันและใช้ข้อมูลนั้นแทน "* kwargs, ** kwargs" เมื่อสร้างฟังก์ชันตกแต่งได้หรือไม่ ฉันจะดึงข้อมูลนั้นได้อย่างไร ฉันจะสร้างฟังก์ชันตกแต่ง - กับผู้บริหารได้อย่างไร?

แนวทางอื่น ๆ หรือไม่?


1
ไม่เคยพูดว่า "ล้าสมัย" ฉันสงสัยไม่มากก็น้อยว่ามีอะไรinspect.Signatureเพิ่มในการจัดการกับฟังก์ชันตกแต่ง
NightShadeQueen

คำตอบ:


79
  1. ติดตั้งโมดูลมัณฑนากร :

    $ pip install decorator
  2. ปรับคำจำกัดความของargs_as_ints():

    import decorator
    
    @decorator.decorator
    def args_as_ints(f, *args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    
    @args_as_ints
    def funny_function(x, y, z=3):
        """Computes x*y + 2*z"""
        return x*y + 2*z
    
    print funny_function("3", 4.0, z="5")
    # 22
    help(funny_function)
    # Help on function funny_function in module __main__:
    # 
    # funny_function(x, y, z=3)
    #     Computes x*y + 2*z

Python 3.4+

functools.wraps()จาก stdlibรักษาลายเซ็นตั้งแต่ Python 3.4:

import functools


def args_as_ints(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# 22
help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

functools.wraps()มีให้ใช้ตั้งแต่ Python 2.5เป็นอย่างน้อยแต่ไม่ได้เก็บรักษาลายเซ็นไว้ที่นั่น:

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(*args, **kwargs)
#    Computes x*y + 2*z

หมายเหตุ: แทน*args, **kwargsx, y, z=3


คุณไม่ใช่คำตอบแรก แต่เป็นคำตอบที่ครอบคลุมที่สุด :-) จริงๆแล้วฉันต้องการโซลูชันที่ไม่เกี่ยวข้องกับโมดูลของบุคคลที่สาม แต่ดูที่แหล่งที่มาของโมดูลมัณฑนากรมันง่ายพอที่ฉันจะทำได้ เพียงแค่คัดลอก
Fredrik Johansson

1
@MarkLodato: functools.wraps()รักษาลายเซ็นใน Python 3.4+ ไว้แล้ว (ตามที่กล่าวไว้ในคำตอบ) คุณหมายถึงการตั้งค่าความwrapper.__signature__ช่วยเหลือในเวอร์ชันก่อนหน้าหรือไม่? (คุณได้ทดสอบรุ่นใดบ้าง)
jfs

1
@MarkLodato: help()แสดงลายเซ็นที่ถูกต้องบน Python 3.4 ทำไมคุณถึงคิดว่าfunctools.wraps()เสียและไม่ใช่ IPython?
jfs

1
@MarkLodato: มันเสียถ้าเราต้องเขียนโค้ดเพื่อแก้ไข เนื่องจากให้help()ผลลัพธ์ที่ถูกต้องคำถามคือซอฟต์แวร์ควรได้รับการแก้ไข: functools.wraps()หรือ IPython? ไม่ว่าในกรณีใดการมอบหมายงานด้วยตนเอง__signature__เป็นวิธีแก้ปัญหาที่ดีที่สุด - ไม่ใช่วิธีแก้ปัญหาในระยะยาว
jfs

1
ดูเหมือนว่าinspect.getfullargspec()ยังไม่ส่งคืนลายเซ็นที่เหมาะสมสำหรับfunctools.wrapsใน python 3.4 และคุณต้องใช้inspect.signature()แทน
Tuukka Mustonen

16

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

from functools import wraps

def args_as_ints(f):
    @wraps(f) 
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

เมื่อดำเนินการใน Python 3 สิ่งนี้จะทำให้เกิดสิ่งต่อไปนี้:

>>> funny_function("3", 4.0, z="5")
22
>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

ข้อเสียเปรียบประการเดียวคือใน Python 2 อย่างไรก็ตามจะไม่อัปเดตรายการอาร์กิวเมนต์ของฟังก์ชัน เมื่อดำเนินการใน Python 2 มันจะสร้าง:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z

ไม่แน่ใจว่าเป็นสฟิงซ์หรือเปล่า แต่ดูเหมือนจะใช้ไม่ได้เมื่อฟังก์ชันที่รวมเป็นเมธอดของคลาส สฟิงซ์ยังคงรายงานลายเซ็นการโทรของมัณฑนากร
alphabetasoup

9

มีโมดูลdecoratorมัณฑนากรพร้อมมัณฑนากรที่คุณสามารถใช้:

@decorator
def args_as_ints(f, *args, **kwargs):
    args = [int(x) for x in args]
    kwargs = dict((k, int(v)) for k, v in kwargs.items())
    return f(*args, **kwargs)

จากนั้นลายเซ็นและความช่วยเหลือของวิธีการจะถูกเก็บรักษาไว้:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

แก้ไข: JF Sebastian ชี้ว่าฉันไม่ได้แก้ไขargs_as_intsฟังก์ชัน - ตอนนี้ได้รับการแก้ไขแล้ว



6

ตัวเลือกที่สอง:

  1. ติดตั้งโมดูลห่อ:

$ easy_install wrapt

wrapt มีโบนัสรักษาลายเซ็นคลาส


import wrapt
import inspect

@wrapt.decorator def args_as_ints(wrapped, instance, args, kwargs): if instance is None: if inspect.isclass(wrapped): # Decorator was applied to a class. return wrapped(*args, **kwargs) else: # Decorator was applied to a function or staticmethod. return wrapped(*args, **kwargs) else: if inspect.isclass(instance): # Decorator was applied to a classmethod. return wrapped(*args, **kwargs) else: # Decorator was applied to an instancemethod. return wrapped(*args, **kwargs) @args_as_ints def funny_function(x, y, z=3): """Computes x*y + 2*z""" return x * y + 2 * z >>> funny_function(3, 4, z=5)) # 22 >>> help(funny_function) Help on function funny_function in module __main__: funny_function(x, y, z=3) Computes x*y + 2*z

2

ตามความเห็นข้างบนในคำตอบของ jfs ; หากคุณกังวลเกี่ยวกับลายเซ็นในแง่ของรูปลักษณ์ ( helpและinspect.signature) การใช้งานfunctools.wrapsก็ทำได้ดี

หากคุณกังวลกับลายเซ็นในแง่ของพฤติกรรม (โดยเฉพาะอย่างยิ่งTypeErrorในกรณีที่อาร์กิวเมนต์ไม่ตรงกัน) functools.wrapsอย่าเก็บรักษาลายเซ็นไว้ คุณค่อนข้างจะใช้สำหรับการนั้นหรือลักษณะทั่วไปของฉันของเครื่องยนต์หลักของชื่อdecoratormakefun

from makefun import wraps

def args_as_ints(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("wrapper executes")
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# wrapper executes
# 22

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

funny_function(0)  
# observe: no "wrapper executes" is printed! (with functools it would)
# TypeError: funny_function() takes at least 2 arguments (1 given)

ดูโพสต์นี้เกี่ยวกับfunctools.wraps .


1
นอกจากนี้ผลจากการไม่ได้เก็บไว้โดยโทรinspect.getfullargspec functools.wraps
laike9m

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