วิธีตรวจสอบว่ามีการตั้งค่าพารามิเตอร์ฟังก์ชันเสริมหรือไม่


93

มีวิธีง่ายๆใน Python ในการตรวจสอบว่าค่าของพารามิเตอร์ที่เป็นทางเลือกนั้นมาจากค่าเริ่มต้นหรือเนื่องจากผู้ใช้ตั้งค่าไว้อย่างชัดเจนในการเรียกใช้ฟังก์ชันหรือไม่


12
เพราะฉันต้องการตรวจสอบในฟังก์ชันนั้นแน่นอน :)
Matthias

2
เพียงใช้Noneเป็นค่าเริ่มต้นและตรวจสอบว่า หากคุณสามารถตั้งค่าการทดสอบนี้ได้จริงคุณจะยกเว้นความเป็นไปได้ที่ผู้ใช้จะส่งผ่านค่าที่เรียกใช้พฤติกรรมเริ่มต้นอย่างชัดเจน
Michael J. Barber

3
ซึ่งสามารถทำได้ด้วยวิธีที่ใช้ซ้ำได้และสวยงามกว่าคำตอบที่คุณยอมรับอย่างน้อยก็สำหรับ CPython ดูคำตอบของฉันด้านล่าง
Ellioh

2
@Volatility: มันสำคัญถ้าคุณมีค่าเริ่มต้นสองชุด พิจารณาระดับ recursive: ผู้ใช้เรียกมันว่าด้วยClass My(): def __init__(self, _p=None, a=True, b=True, c=False) x=My(b=False)เมธอดคลาสสามารถเรียกตัวเองx=My(_p=self, c=True)ว่าฟังก์ชันสามารถตรวจพบว่า b ไม่ได้ตั้งค่าไว้อย่างชัดเจนและตัวแปรที่ไม่ได้ตั้งค่านั้นจะถูกส่งต่อจากระดับบนสุด แต่ถ้าพวกเขาไม่สามารถที่โทร recursive x=My(a=self.a, b=self.b, c=True, d=self.d, ...)ต้องผ่านตัวแปรทุกอย่างชัดเจน:
Dave

@ เดฟ แต่นั่นคือคำถามเกี่ยวกับอะไร? ในความเข้าใจของฉันเป็นคำถามที่ถามถึงวิธีการแยกความแตกต่างและx=My() x=My(a=True)สถานการณ์ของคุณเกี่ยวข้องกับการกำหนดค่าพารามิเตอร์ที่เป็นทางเลือกนอกเหนือจากค่าเริ่มต้น
ความผันผวน

คำตอบ:


19

คำตอบจำนวนมากมีข้อมูลทั้งหมดเพียงเล็กน้อยดังนั้นฉันจึงต้องการนำมารวมกับรูปแบบที่ฉันชอบ

ค่าเริ่มต้นคือmutableประเภท

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

ซึ่งหมายความว่าคุณสามารถเปรียบเทียบค่าเริ่มต้นที่เปลี่ยนแปลงได้อย่างง่ายดายโดยใช้isเพื่อดูว่ามีการส่งผ่านเป็นอาร์กิวเมนต์หรือปล่อยให้เป็นค่าเริ่มต้นดังตัวอย่างต่อไปนี้เป็นฟังก์ชันหรือวิธีการ:

def f(value={}):
    if value is f.__defaults__[0]:
        print('default')
    else:
        print('passed in the call')

และ

class A:
    def f(self, value={}):
        if value is self.f.__defaults__[0]:
            print('default')
        else:
            print('passed in the call')

อาร์กิวเมนต์เริ่มต้นที่ไม่เปลี่ยนรูป

ตอนนี้มันดูหรูหราน้อยลงเล็กน้อยหากค่าเริ่มต้นของคุณคาดว่าจะเป็นไฟล์ immutableค่า (และจำไว้ว่าแม้แต่สตริงก็ไม่เปลี่ยนรูป!) เพราะคุณไม่สามารถใช้ประโยชน์จากกลลวงได้เหมือนเดิม แต่ยังมีบางอย่างที่คุณสามารถทำได้ ประเภท; โดยพื้นฐานแล้วคุณใส่ค่าเริ่มต้น "ปลอม" ที่ไม่แน่นอนในลายเซ็นของฟังก์ชันและค่าเริ่มต้น "จริง" ที่ต้องการในเนื้อหาของฟังก์ชัน

def f(value={}):
    """
    my function
    :param value: value for my function; default is 1
    """
    if value is f.__defaults__[0]:
        print('default')
        value = 1
    else:
        print('passed in the call')
    # whatever I want to do with the value
    print(value)

รู้สึกตลกเป็นอย่างยิ่งถ้าคุณเป็นค่าเริ่มต้นที่แท้จริงNoneแต่Noneไม่สามารถเปลี่ยนรูปได้ดังนั้น ... คุณยังคงต้องใช้ตัวแปรเริ่มต้นอย่างชัดเจนเป็นพารามิเตอร์เริ่มต้นของฟังก์ชันและเปลี่ยนเป็นไม่มีในโค้ด

การใช้Defaultคลาสสำหรับค่าเริ่มต้นที่ไม่เปลี่ยนรูป

หรือคล้ายกับคำแนะนำ @cz หากเอกสาร python ไม่เพียงพอ :-) คุณสามารถเพิ่มวัตถุในระหว่างนั้นเพื่อทำให้ API ชัดเจนยิ่งขึ้น (โดยไม่ต้องอ่านเอกสาร) อินสแตนซ์คลาส used_proxy_ เริ่มต้นไม่แน่นอนและจะมีค่าเริ่มต้นจริงที่คุณต้องการใช้

class Default:
    def __repr__(self):
        return "Default Value: {} ({})".format(self.value, type(self.value))

    def __init__(self, value):
        self.value = value

def f(default=Default(1)):
    if default is f.__defaults__[0]:
        print('default')
        print(default)
        default = default.value
    else:
        print('passed in the call')
    print("argument is: {}".format(default))

ตอนนี้:

>>> f()
default
Default Value: 1 (<class 'int'>)
argument is: 1

>>> f(2)
passed in the call
argument is: 2

ข้างต้นใช้งานได้ดีเช่นDefault(None)กัน

รูปแบบอื่น ๆ

เห็นได้ชัดว่ารูปแบบข้างต้นดูน่าเกลียดกว่าที่ควรเพราะทั้งหมดprintนี้มีไว้เพื่อแสดงวิธีการทำงานเท่านั้น มิฉะนั้นฉันจะพบว่ามันสั้นและทำซ้ำได้มากพอ

คุณสามารถเขียนมัณฑนากรเพื่อเพิ่ม__call__รูปแบบที่แนะนำโดย @dmg ได้อย่างคล่องตัวมากขึ้น แต่สิ่งนี้จะยังคงต้องใช้เทคนิคแปลก ๆ ในการกำหนดฟังก์ชันเอง - คุณจะต้องแยกออกvalueและvalue_defaultหากโค้ดของคุณต้องการแยกความแตกต่างดังนั้น ฉันไม่เห็นประโยชน์มากนักและฉันจะไม่เขียนตัวอย่าง :-)

ประเภทที่เปลี่ยนแปลงได้เป็นค่าเริ่มต้นใน Python

ข้อมูลเพิ่มเติมเกี่ยวกับ# 1 python gotcha! ทารุณกรรมเพื่อความสุขของคุณเองข้างต้น คุณสามารถดูว่าเกิดอะไรขึ้นเนื่องจากการประเมินตามคำจำกัดความโดยทำดังนี้

def testme(default=[]):
    print(id(default))

คุณสามารถเรียกใช้testme()กี่ครั้งก็ได้ตามที่คุณต้องการคุณจะเห็นการอ้างอิงถึงอินสแตนซ์เริ่มต้นเดียวกันเสมอ (ดังนั้นโดยพื้นฐานแล้วค่าเริ่มต้นของคุณจะไม่เปลี่ยนรูป :-))

โปรดจำไว้ว่าในหลามมีเพียง 3 ไม่แน่นอนในตัวชนิด : set, list, dict; อย่างอื่น - แม้แต่สตริง! - ไม่เปลี่ยนรูป


ตัวอย่างที่คุณมีใน "อาร์กิวเมนต์เริ่มต้นที่ไม่เปลี่ยนรูป" ไม่มีอาร์กิวเมนต์เริ่มต้นที่ไม่เปลี่ยนรูป ถ้าเป็นเช่นนั้นก็จะไม่ได้ผล
Karol

@ คุณ Karol ใส่ใจบรรจง? ค่าดีฟอลต์ในตัวอย่างนั้น1ซึ่งควรจะไม่เปลี่ยนรูป ...
Stefano

def f(value={})ผมเห็นลายเซ็นของฟังก์ชั่นเป็น
Karol

1
ฮ่าฉันเข้าใจแล้วขอบคุณ ไม่ใช่เรื่องง่ายที่จะติดตามเว้นแต่จะมีคนอ่านข้อความของคุณอย่างระมัดระวังซึ่งอาจไม่เกิดขึ้นบ่อยนักใน SO พิจารณาการเขียนซ้ำ
Karol

1
ใน "ถ้าค่าเริ่มต้นคือค่าเริ่มต้น f .__ __ [0]:" คุณต้องฮาร์ดโค้ดซึ่งจะใช้หมายเลขพารามิเตอร์เริ่มต้นซึ่งอาจเปราะบางหากลายเซ็นของฟังก์ชันเปลี่ยนไป อีกทางเลือกหนึ่งคือ "if default in f .__ defaults__:" สมมติว่าคุณใช้อินสแตนซ์เริ่มต้นที่แตกต่างกันสำหรับอาร์กิวเมนต์แต่ละตัว "in" ควรทำงานเช่นเดียวกับ "is"
Stephen Warren

57

ไม่จริง วิธีมาตรฐานคือใช้ค่าดีฟอลต์ที่ผู้ใช้คาดว่าจะไม่ผ่านเช่นobjectอินสแตนซ์:

DEFAULT = object()
def foo(param=DEFAULT):
    if param is DEFAULT:
        ...

โดยปกติคุณสามารถใช้Noneเป็นค่าเริ่มต้นได้หากไม่เหมาะสมกับค่าที่ผู้ใช้ต้องการส่งผ่าน

ทางเลือกคือการใช้kwargs:

def foo(**kwargs):
    if 'param' in kwargs:
        param = kwargs['param']
    else:
        ...

อย่างไรก็ตามนี่เป็นคำที่ละเอียดเกินไปและทำให้ฟังก์ชันของคุณใช้งานได้ยากขึ้นเนื่องจากเอกสารประกอบจะไม่รวมparamพารามิเตอร์โดยอัตโนมัติ


9
ฉันยังเห็นหลายคนใช้ Ellipsis builtin สำหรับสถานที่ที่จำเป็นและไม่มีถือว่าเป็นอินพุตที่ถูกต้อง โดยพื้นฐานแล้วจะเหมือนกับตัวอย่างแรก
GrandOpener

หากคุณต้องการใช้พฤติกรรมพิเศษหากไม่มีการส่งผ่าน แต่ยังต้องการวิธีทดสอบว่าอาร์กิวเมนต์ได้รับจากผู้ใช้หรือไม่คุณสามารถใช้Ellipsisซิงเกิลตันเป็นค่าเริ่มต้นซึ่งได้รับการออกแบบมาอย่างชัดเจนเพื่อใช้เป็นการข้ามค่านี้ ...เป็นนามแฝงสำหรับEllipsisดังนั้นผู้ใช้ที่ต้องการใช้อาร์กิวเมนต์ตำแหน่งสามารถเรียกได้your_function(p1, ..., p3)ซึ่งทำให้อ่านได้ชัดเจนและดี
Bachsau

However this is overly verbose and makes your function more difficult to use as its documentation will not automatically include the param parameter.สิ่งนี้ไม่เป็นความจริงเนื่องจากคุณสามารถตั้งค่าคำอธิบายของฟังก์ชันและพารามิเตอร์โดยใช้inspectโมดูล ขึ้นอยู่กับ IDE ของคุณว่าจะใช้งานได้หรือไม่
EZLearner

15

มัณฑนากรฟังก์ชันต่อไปนี้explicit_checkerสร้างชุดชื่อพารามิเตอร์ของพารามิเตอร์ทั้งหมดที่กำหนดไว้อย่างชัดเจน เพิ่มผลลัพธ์เป็นพารามิเตอร์พิเศษ ( explicit_params) ให้กับฟังก์ชัน ทำ'a' in explicit_paramsเพื่อตรวจสอบว่าaมีการกำหนดพารามิเตอร์อย่างชัดเจนหรือไม่

def explicit_checker(f):
    varnames = f.func_code.co_varnames
    def wrapper(*a, **kw):
        kw['explicit_params'] = set(list(varnames[:len(a)]) + kw.keys())
        return f(*a, **kw)
    return wrapper

@explicit_checker
def my_function(a, b=0, c=1, explicit_params=None):
    print a, b, c, explicit_params
    if 'b' in explicit_params:
        pass # Do whatever you want


my_function(1)
my_function(1, 0)
my_function(1, c=1)

รหัสนี้ใช้ได้เฉพาะใน python2 สำหรับ python 3 ดูคำตอบของฉันด้านล่าง: stackoverflow.com/questions/14749328/…
R. Yang

1
นี่เป็นเรื่องที่ค่อนข้างดี แต่ถ้าเป็นไปได้ควรหลีกเลี่ยงปัญหาด้วยการออกแบบที่ดีกว่าในตอนแรก
Karol

@ Karol ฉันเห็นด้วย ในกรณีส่วนใหญ่เราไม่ควรต้องการสิ่งนั้นหากการออกแบบนั้นสมเหตุสมผล
Ellioh

4

บางครั้งฉันใช้สตริงที่ไม่ซ้ำกัน (เช่น UUID)

import uuid
DEFAULT = uuid.uuid4()
def foo(arg=DEFAULT):
  if arg is DEFAULT:
    # it was not passed in
  else:
    # it was passed in

ด้วยวิธีนี้ผู้ใช้ไม่สามารถคาดเดาค่าเริ่มต้นได้หากพวกเขาพยายามดังนั้นฉันจึงมั่นใจได้มากว่าเมื่อฉันเห็นค่าargนั้นจะไม่มีการส่งผ่าน


4
วัตถุ Python เป็นข้อมูลอ้างอิงคุณสามารถใช้object()แทนได้uuid4()- มันยังคงเป็นอินสแตนซ์ที่ไม่ซ้ำกันซึ่งเป็นสิ่งที่isตรวจสอบ
cz

3

ผมเคยเห็นรูปแบบนี้ไม่กี่ครั้ง (ห้องสมุดเช่นunittest, py-flags, jinja):

class Default:
    def __repr__( self ):
        return "DEFAULT"

DEFAULT = Default()

... หรือซับเดียวที่เทียบเท่า ... :

DEFAULT = type( 'Default', (), { '__repr__': lambda x: 'DEFAULT' } )()

ซึ่งแตกต่างจากDEFAULT = object()สิ่งนี้ช่วยในการตรวจสอบประเภทและให้ข้อมูลเมื่อเกิดข้อผิดพลาด - มักใช้การแสดงสตริง ( "DEFAULT") หรือชื่อคลาส ( "Default") ในข้อความแสดงข้อผิดพลาด


3

คำตอบของ @ Ellioh ทำงานใน python 2 ใน python 3 รหัสต่อไปนี้ควรใช้งานได้:

import inspect
def explicit_checker(f):
    varnames = inspect.getfullargspec(f)[0]
    def wrapper(*a, **kw):
        kw['explicit_params'] = set(list(varnames[:len(a)]) + list(kw.keys()))
        return f(*a, **kw)
    return wrapper

@explicit_checker
def my_function(a, b=0, c=1, explicit_params=None):
    print a, b, c, explicit_params
    if 'b' in explicit_params:
        pass # Do whatever you want

วิธีนี้สามารถเก็บชื่ออาร์กิวเมนต์และค่าเริ่มต้น (แทน ** kwargs) ที่มีความสามารถในการอ่านได้ดีขึ้น


3

คุณสามารถตรวจสอบได้จากfoo.__defaults__และfoo.__kwdefaults__

ดูตัวอย่างง่ายๆ

def foo(a, b, c=123, d=456, *, e=789, f=100):
    print(foo.__defaults__)
    # (123, 456) 
    print(foo.__kwdefaults__)
    # {'e': 789, 'f': 100}
    print(a, b, c, d, e, f)

#and these variables are also accessible out of function body
print(foo.__defaults__)    
# (123, 456)  
print(foo.__kwdefaults__)  
# {'e': 789, 'f': 100}

foo.__kwdefaults__['e'] = 100500

foo(1, 2) 
#(123, 456)
#{'f': 100, 'e': 100500}
#1 2 123 456 100500 100

จากนั้นใช้ตัวดำเนินการ=และisคุณสามารถเปรียบเทียบได้

และสำหรับบางกรณีการร้องรหัสก็เพียงพอแล้ว

ตัวอย่างเช่นคุณต้องหลีกเลี่ยงการเปลี่ยนค่าเริ่มต้นจากนั้นคุณสามารถตรวจสอบความเท่าเทียมกันแล้วคัดลอกถ้าเป็นเช่นนั้น

    def update_and_show(data=Example):
        if data is Example:
            data = copy.deepcopy(data)
        update_inplace(data) #some operation
        print(data)

นอกจากนี้ก็ค่อนข้างสะดวกต่อการใช้งานgetcallargsจากการinspectเป็นมันจะกลับข้อโต้แย้งจริงที่มีฟังก์ชั่นซึ่งจะถูกเรียก คุณส่งฟังก์ชันและ args และ kwargs ไปยังมัน ( inspect.getcallargs(func, /, *args, **kwds)) มันจะส่งคืนอาร์กิวเมนต์ของเมธอดจริงที่ใช้สำหรับการเรียกใช้โดยคำนึงถึงค่าเริ่มต้นและสิ่งอื่น ๆ ดูตัวอย่างด้านล่าง

from inspect import getcallargs

# we have a function with such signature
def show_params(first, second, third=3):
    pass

# if you wanted to invoke it with such params (you could get them from a decorator as example)
args = [1, 2, 5]
kwargs = {}
print(getcallargs(show_params, *args, **kwargs))
#{'first': 1, 'second': 2, 'third': 5}

# here we didn't specify value for d
args = [1, 2, 3, 4]
kwargs = {}

# ----------------------------------------------------------
# but d has default value =7
def show_params1(first, *second, d = 7):
    pass


print(getcallargs(show_params1, *args, **kwargs))
# it will consider b to be equal to default value 7 as it is in real method invocation
# {'first': 1, 'second': (2, 3, 4), 'd': 7}

# ----------------------------------------------------------
args = [1]
kwargs = {"d": 4}

def show_params2(first, d=3):
    pass


print(getcallargs(show_params2, *args, **kwargs))
#{'first': 1, 'd': 4}

https://docs.python.org/3/library/inspect.html


2

ฉันเห็นด้วยกับความคิดเห็นของ Volatility แต่คุณสามารถตรวจสอบในลักษณะต่อไปนี้:

def function(arg1,...,**optional):
    if 'optional_arg' in optional:
        # user has set 'optional_arg'
    else:
        # user has not set 'optional_arg'
        optional['optional_arg'] = optional_arg_default_value # set default

ฉันเชื่อว่าพารามิเตอร์ที่เป็นทางเลือกเป็นสิ่งที่def func(optional=value)ไม่เหมือน**kwargs
Zaur Nasibov

นั่นเป็นสิ่งที่ค่อนข้างเปิดกว้างสำหรับการตีความ อะไรคือความแตกต่างที่แท้จริงระหว่างอาร์กิวเมนต์ที่มีค่าเริ่มต้นและอาร์กิวเมนต์คำหลัก ทั้งคู่แสดงโดยใช้ไวยากรณ์เดียวกัน "keyword = value"
isedev

ฉันไม่เห็นด้วยเพราะจุดประสงค์ของพารามิเตอร์เสริมและ**kwargsแตกต่างกันเล็กน้อย ป.ล. ไม่มีปัญหาเกี่ยวกับ -1 :) และ -1 ของฉันสำหรับคุณเป็นเรื่องบังเอิญ :)
Zaur Nasibov

2

นี่เป็นรูปแบบของคำตอบของ stefano แต่ฉันพบว่าอ่านได้มากกว่านี้เล็กน้อย:

not_specified = {}

def foo(x=not_specified):
    if x is not_specified:
            print("not specified")
    else:
            print("specified")

หนึ่งโหวตขึ้น ?? ฉันชอบสิ่งนี้ที่สุด เรียบง่ายไม่มีเงาสะท้อน อ่านได้.
vincent

1

แนวทางที่แปลกประหลาดเล็กน้อยคือ:

class CheckerFunction(object):
    def __init__(self, function, **defaults):
        self.function = function
        self.defaults = defaults

    def __call__(self, **kwargs):
        for key in self.defaults:
            if(key in kwargs):
                if(kwargs[key] == self.defaults[key]):
                    print 'passed default'
                else:
                    print 'passed different'
            else:
                print 'not passed'
                kwargs[key] = self.defaults[key]

        return self.function(**kwargs)

def f(a):
    print a

check_f = CheckerFunction(f, a='z')
check_f(a='z')
check_f(a='b')
check_f()

ผลลัพธ์ใด:

passed default
z
passed different
b
not passed
z

ตอนนี้ดังที่ฉันพูดถึงค่อนข้างแปลก แต่ก็ทำงานได้ดี อย่างไรก็ตามสิ่งนี้ค่อนข้างอ่านไม่ได้และในทำนองเดียวกันกับคำแนะนำของecatmurจะไม่ได้รับการจัดทำเป็นเอกสารโดยอัตโนมัติ


1
คุณอาจต้องการรวมพฤติกรรมcheck_f('z')ซึ่งก็คืออย่างที่คุณพูดว่าประหลาด
Michael J. Barber

@ MichaelJ.Barber จุดดี. คุณจะต้องทำ "เวทมนตร์" บางอย่างด้วย * args ด้วย อย่างไรก็ตามประเด็นของฉันคือมันเป็นไปได้ แต่ตอนนี้ต้องว่าค่าเริ่มต้นถูกส่งผ่านหรือไม่นั้นเป็นการออกแบบที่ไม่ดี
dmg
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.