Cách kiểm tra xem thông số chức năng tùy chọn đã được đặt chưa


92

Có cách nào dễ dàng trong Python để kiểm tra xem giá trị của một tham số tùy chọn đến từ giá trị mặc định của nó hay do người dùng đã đặt nó một cách rõ ràng khi gọi hàm?


12
Bởi vì tôi muốn kiểm tra nó trong chức năng đó tất nhiên :)
Matthias

2
Chỉ cần sử dụng Nonelàm mặc định và kiểm tra điều đó. Nếu bạn thực sự có thể thiết lập thử nghiệm này, bạn cũng sẽ loại trừ mọi khả năng người dùng chuyển một cách rõ ràng giá trị gọi ra hành vi mặc định.
Michael J. Barber

3
Điều đó có thể được thực hiện theo cách có thể tái sử dụng và đẹp hơn nhiều so với câu trả lời mà bạn đã chấp nhận, ít nhất là đối với CPython. Xem câu trả lời của tôi bên dưới.
Ellioh

2
@ Tính linh hoạt: vấn đề quan trọng nếu bạn có hai bộ giá trị mặc định. Hãy xem xét một lớp đệ quy: Class My(): def __init__(self, _p=None, a=True, b=True, c=False) Người dùng gọi nó bằng x=My(b=False). Một phương thức lớp có thể gọi chính nó với x=My(_p=self, c=True)if các hàm có thể phát hiện ra rằng b không được đặt rõ ràng và các biến chưa được đặt sẽ được truyền xuống từ cấp cao nhất. Nhưng nếu họ không thể, các cuộc gọi đệ quy phải vượt qua mỗi biến một cách rõ ràng: x=My(a=self.a, b=self.b, c=True, d=self.d, ...).
Dave

@Dave nhưng đó có phải là câu hỏi về? Theo hiểu biết của tôi, câu hỏi đặt ra là làm thế nào để phân biệt x=My()x=My(a=True). Kịch bản của bạn liên quan đến việc gán các tham số tùy chọn một giá trị khác với giá trị mặc định của chúng.
Biến động

Câu trả lời:


16

Rất nhiều câu trả lời có rất ít thông tin đầy đủ, vì vậy tôi muốn tổng hợp tất cả lại với (các) mẫu yêu thích của mình.

giá trị mặc định là một mutableloại

Nếu giá trị mặc định là một đối tượng có thể thay đổi, bạn thật may mắn: bạn có thể khai thác thực tế là các đối số mặc định của Python được đánh giá một lần khi hàm được xác định (một số thông tin thêm về điều này ở cuối câu trả lời trong phần cuối cùng)

Điều này có nghĩa là bạn có thể dễ dàng so sánh một giá trị có thể thay đổi mặc định bằng cách sử dụng isđể xem liệu nó được chuyển dưới dạng đối số hay được để mặc định, như trong các ví dụ sau dưới dạng hàm hoặc phương thức:

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')

Đối số mặc định bất biến

Bây giờ, sẽ kém thanh lịch hơn một chút nếu mặc định của bạn được mong đợi là một immutablegiá trị (và hãy nhớ rằng ngay cả các chuỗi cũng không thay đổi!) Bởi vì bạn không thể khai thác thủ thuật như nó vốn có, nhưng vẫn có điều bạn có thể làm, vẫn khai thác có thể thay đổi kiểu; về cơ bản, bạn đặt mặc định "giả" có thể thay đổi trong chữ ký hàm và giá trị mặc định "thực" mong muốn trong thân hàm.

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)

Cảm giác đặc biệt buồn cười nếu mặc định thực của bạn là None, nhưng Nonekhông thay đổi được, vì vậy ... bạn vẫn cần sử dụng rõ ràng một biến có thể thay đổi làm tham số mặc định của hàm và chuyển sang Không có trong mã.

Sử dụng một Defaultlớp cho các giá trị mặc định không thể thay đổi

hoặc, tương tự như đề xuất @cz, nếu tài liệu python không đủ :-), bạn có thể thêm một đối tượng vào giữa để làm cho API rõ ràng hơn (mà không cần đọc tài liệu); cá thể lớp used_proxy_ Default có thể thay đổi được và sẽ chứa giá trị mặc định thực mà bạn muốn sử dụng.

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))

hiện nay:

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

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

Các công trình trên cũng độc đáo cho Default(None).

Các mẫu khác

Rõ ràng là các mẫu ở trên trông xấu hơn chúng nên làm vì tất cả những mẫu printđó chỉ để hiển thị cách chúng hoạt động. Nếu không, tôi thấy chúng ngắn gọn và đủ lặp lại.

Bạn có thể viết trình trang trí để thêm __call__mẫu do @dmg đề xuất theo cách hợp lý hơn, nhưng điều này vẫn bắt buộc phải sử dụng các thủ thuật kỳ lạ trong chính định nghĩa hàm - bạn sẽ cần phải tách ra valuevalue_defaultnếu mã của bạn cần phân biệt chúng, vì vậy Tôi không thấy nhiều lợi thế và tôi sẽ không viết ví dụ :-)

Các loại có thể thay đổi làm giá trị mặc định trong Python

Thông tin thêm về # 1 python gotcha! , bị lạm dụng vì niềm vui của riêng bạn ở trên. Bạn có thể thấy những gì xảy ra do đánh giá ở định nghĩa bằng cách thực hiện:

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

Bạn có thể chạy testme()bao nhiêu lần tùy thích, bạn sẽ luôn thấy một tham chiếu đến cùng một trường hợp mặc định (vì vậy về cơ bản mặc định của bạn là không thay đổi :-)).

Hãy nhớ rằng trong Python chỉ có 3 có thể thay đổi được xây dựng trong các loại : set, list, dict; mọi thứ khác - ngay cả chuỗi! - là bất biến.


Ví dụ bạn có trong "Đối số mặc định bất biến" không thực sự có đối số mặc định bất biến. Nếu nó đã làm, nó sẽ không hoạt động.
Karol

@Karol, quan tâm đến công phu? Giá trị mặc định trong ví dụ đó là 1, mà phải là bất biến ...
Stefano

Tôi thấy chữ ký của chức năng là def f(value={}).
Karol

@Karol đó là chữ ký, nhưng giá trị mặc định mong muốn1; xin lỗi nếu điều đó không rõ ràng trong phần giải thích, nhưng toàn bộ điểm của phần đó của phản hồi là có thể có mặc định không thay đổi ( 1). Nếu bạn kiểm tra ví dụ, bạn sẽ thấy nó nói print('default'); value = 1value={}
Stefano

1
Ha, tôi hiểu rồi, cảm ơn. Thật không dễ dàng theo dõi trừ khi ai đó đọc rất kỹ văn bản của bạn, điều này có thể không thường xuyên xảy ra trên SO. Cân nhắc việc ghi lại.
Karol

56

Không hẳn. Cách tiêu chuẩn là sử dụng một giá trị mặc định mà người dùng không mong đợi sẽ chuyển, ví dụ: một objectví dụ:

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

Thông thường, bạn có thể chỉ sử dụng Nonelàm giá trị mặc định, nếu nó không có ý nghĩa như một giá trị mà người dùng muốn chuyển.

Cách thay thế là sử dụng kwargs:

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

Tuy nhiên, điều này quá dài dòng và làm cho hàm của bạn khó sử dụng hơn vì tài liệu của nó sẽ không tự động bao gồm paramtham số.


9
Tôi cũng đã thấy một số người sử dụng nội trang Dấu ba chấm cho những nơi cần điều này và Không có gì được coi là đầu vào hợp lệ. Điều này về cơ bản giống như ví dụ đầu tiên.
GrandOpener

Nếu bạn muốn triển khai hành vi đặc biệt nếu Không có gì được thông qua, nhưng vẫn cần một cách để kiểm tra xem đối số có được đưa ra bởi người dùng hay không, bạn có thể sử dụng Ellipsissingleton làm mặc định, được thiết kế rõ ràng để được sử dụng để bỏ qua giá trị này . ...là một bí danh cho Ellipsis, vì vậy người dùng muốn sử dụng các đối số vị trí có thể chỉ cần gọi your_function(p1, ..., p3)điều này làm cho nó rõ ràng và dễ đọc.
Bachsau

However this is overly verbose and makes your function more difficult to use as its documentation will not automatically include the param parameter.Điều này thực sự không đúng sự thật, vì bạn có thể thiết lập mô tả của một hàm và các tham số của nó bằng cách sử dụng inspectmô-đun. Nó phụ thuộc vào IDE của bạn cho dù nó có hoạt động hay không.
EZLearner

15

Trình trang trí hàm sau đây explicit_checker, tạo một tập hợp các tên tham số của tất cả các tham số được cung cấp rõ ràng. Nó thêm kết quả như một tham số phụ ( explicit_params) vào hàm. Chỉ cần làm 'a' in explicit_paramsđể kiểm tra xem tham số acó được cung cấp rõ ràng hay không.

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)

Mã này chỉ hoạt động trong python2. Đối với python 3, hãy xem câu trả lời của tôi bên dưới: stackoverflow.com/questions/14749328/…
R. Yang,

1
Điều này khá hay, nhưng tốt hơn hết là bạn nên tránh vấn đề với thiết kế tốt hơn ngay từ đầu, nếu có thể.
Karol

@Karol, tôi đồng ý. Trong hầu hết các trường hợp, người ta không cần điều đó nếu thiết kế hợp lý.
Ellioh

4

Đôi khi tôi sử dụng một chuỗi duy nhất (như UUID).

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

Bằng cách này, không người dùng nào thậm chí có thể đoán được giá trị mặc định nếu họ đã thử, vì vậy tôi có thể rất tự tin rằng khi tôi thấy giá trị đó cho arg, nó đã không được chuyển vào.


4
Đối tượng Python là tài liệu tham khảo, bạn chỉ có thể sử dụng object()thay vì uuid4()- nó vẫn là một độc đáo ví dụ , đó là những gì iskiểm tra
cz

3

Tôi đã nhìn thấy mô hình này một vài lần (ví dụ như thư viện unittest, py-flags, jinja):

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

DEFAULT = Default()

... hoặc một lớp lót tương đương ...:

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

Không giống như DEFAULT = object(), điều này hỗ trợ kiểm tra kiểu và cung cấp thông tin khi lỗi xảy ra - thường là biểu diễn chuỗi ( "DEFAULT") hoặc tên lớp ( "Default") được sử dụng trong thông báo lỗi.


3

Câu trả lời của @ Ellioh hoạt động trong python 2. Trong python 3, mã sau sẽ hoạt động:

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

Phương thức này có thể giữ các tên đối số và giá trị mặc định (thay vì ** kwargs) với khả năng đọc tốt hơn.


3

Bạn có thể kiểm tra nó từ foo.__defaults__foo.__kwdefaults__

xem một ví dụ đơn giản dưới đây

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

sau đó bằng cách sử dụng toán tử =isbạn có thể so sánh chúng

và đối với một số trường hợp, mã dưới đây là đủ

Ví dụ: bạn cần tránh thay đổi giá trị mặc định, sau đó bạn có thể kiểm tra tính bình đẳng và sau đó sao chép nếu có

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

Ngoài ra, nó khá thuận tiện để sử dụng getcallargstừ inspectvì nó trả về các đối số thực mà hàm sẽ được gọi. Bạn truyền một hàm và args và kwargs cho nó ( inspect.getcallargs(func, /, *args, **kwds)), nó sẽ trả về các đối số của phương thức thực được sử dụng để gọi, có tính đến các giá trị mặc định và những thứ khác. Hãy xem một ví dụ dưới đây.

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

Tôi đồng ý với nhận xét của Volatile. Nhưng bạn có thể kiểm tra theo cách sau:

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

Tôi tin rằng một tham số tùy chọn là một cái gì đó giống như def func(optional=value)không**kwargs
Zaur Nasibov

Đó là một cái gì đó hơi mở để giải thích. Sự khác biệt thực tế giữa đối số có giá trị mặc định và đối số từ khóa là gì? Cả hai đều được thể hiện bằng cách sử dụng cùng một cú pháp "từ khóa = giá trị".
isedev

Tôi không đồng ý, bởi vì mục đích của các tham số tùy chọn và **kwargslà một chút khác nhau. PS không có vấn đề gì về -1 :) Và -1 của tôi cho bạn là tình cờ :)
Zaur Nasibov

2

Đây là một biến thể trong câu trả lời của stefano, nhưng tôi thấy dễ đọc hơn một chút:

not_specified = {}

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

Một ủng hộ ?? Tôi thích điều này nhất. Đơn giản, không phản ánh. Có thể đọc được.
vincent

1

Một cách tiếp cận hơi kỳ quặc sẽ là:

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()

Đầu ra nào:

passed default
z
passed different
b
not passed
z

Bây giờ, điều này, như tôi đã đề cập, khá kỳ lạ, nhưng nó thực hiện được công việc. Tuy nhiên, điều này khá khó đọc và tương tự như gợi ý của ecatmur sẽ không được tự động ghi lại.


1
Bạn có thể muốn bao gồm hành vi của check_f('z'), như bạn nói, kỳ quặc.
Michael J. Barber

@ MichaelJ.Barber Điểm tốt. Bạn cũng sẽ phải làm một số "phép thuật" với * args. Tuy nhiên, quan điểm của tôi là điều đó có thể xảy ra, nhưng cần phải bây giờ giá trị mặc định có được chuyển hay không là một thiết kế tồi.
dmg
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.