가변 함수 인수 기본값에 대한 좋은 사용?


82

파이썬에서 가변 객체를 함수의 인수 기본값으로 설정하는 것은 흔한 실수입니다. 다음 은 David Goodger의 훌륭한 글 에서 가져온 예입니다 .

>>> def bad_append(new_item, a_list=[]):
        a_list.append(new_item)
        return a_list
>>> print bad_append('one')
['one']
>>> print bad_append('two')
['one', 'two']

이것이 일어나는 이유에 대한 설명은 여기에 있습니다 .

그리고 이제 내 질문에 대해 : 이 구문에 대한 좋은 사용 사례가 있습니까?

내 말은, 모든 사람이 같은 실수를하고, 디버그하고, 문제를 이해하고, 그로부터 그것을 피하려고 시도한다면, 그러한 구문은 어떤 용도로 사용됩니까?


1
내가 아는 가장 좋은 설명은 연결된 질문에 있습니다. 함수는 클래스와 마찬가지로 일류 객체입니다. 클래스에는 변경 가능한 속성 데이터가 있습니다. 함수에는 변경 가능한 기본값이 있습니다.
Katriel 2012

10
이 동작은 "디자인 선택"이 아닙니다. 언어가 작동하는 방식의 결과입니다. 가능한 한 적은 예외를 제외하고 간단한 작동 원리에서 시작됩니다. 어느 시점에서 제가 "Python으로 생각하기"시작
하면서이

2
나는 이것도 궁금했다. 이 예제는 웹 전체에 있지만 말이되지 않습니다. 전달 된 목록을 변경하고 기본값을 갖는 것이 말이되지 않거나 새 목록을 반환하고 싶을 때 즉시 복사본을 만들어야합니다. 기능에 들어가면. 두 가지를 모두 수행하는 것이 유용한 경우를 상상할 수 없습니다.
Mark Ransom

1
FWIW 나는 루프에서 한 번만 함수를 실행하는 효율적인 방법 이라는 질문에 대한 대답 에 사용합니다 .
martineau 2014

2
위에서 불평하는 문제가없는 더 현실적인 예를 방금 발견했습니다. 기본값은 __init__인스턴스 변수로 설정되는 클래스 의 함수에 대한 인수입니다 . 이것은 완벽하게 타당한 일이고, 변경 가능한 기본값으로 인해 모든 것이 끔찍하게 잘못됩니다. stackoverflow.com/questions/43768055/…
Mark Ransom

답변:


61

이를 사용하여 함수 호출간에 값을 캐시 할 수 있습니다.

def get_from_cache(name, cache={}):
    if name in cache: return cache[name]
    cache[name] = result = expensive_calculation()
    return result

그러나 일반적으로 이러한 종류의 작업은 캐시 등을 지우는 추가 속성을 가질 수 있으므로 클래스를 사용하면 더 잘 수행됩니다.


12
... 또는 메모하는 데코레이터.
Daniel Roseman 2012

29
@functools.lru_cache(maxsize=None)
Katriel

3
@katrielalex lru_cache는 Python 3.2의 새로운 기능이므로 모든 사람이 사용할 수있는 것은 아닙니다.
Duncan

2
참고로 backports.functools_lru_cache 지금이 pypi.python.org/pypi/backports.functools_lru_cache
팬더

1
lru_cache해시 할 수없는 값이있는 경우 사용할 수 없습니다.
Synedraacus

14

정식 답변은이 페이지입니다 : http://effbot.org/zone/default-values.htm

또한 변경 가능한 기본 인수에 대한 3 가지 "좋은"사용 사례를 언급합니다.

  • 콜백에서 지역 변수를 외부 변수의 현재 값 에 바인딩
  • 캐시 / 메모리
  • 전역 이름의 로컬 리 바인딩 (고도로 최적화 된 코드 용)

12

아마도 변경 가능한 인수를 변경하지 않지만 변경 가능한 인수를 기대할 수 있습니다.

def foo(x, y, config={}):
    my_config = {'debug': True, 'verbose': False}
    my_config.update(config)
    return bar(x, my_config) + baz(y, my_config)

(예, config=()이 특별한 경우에 사용할 수 있다는 것을 알고 있지만 덜 명확하고 덜 일반적입니다.)


2
또한 당신이 있는지 확인 하는 mutate하지 작업을 수행 하고 반환하지 않습니다 , 그렇지 않으면 함수 외부 몇 가지 코드가 그것을 돌연변이 수, 기능에서 직접이 기본 값을 그리고 모든 함수 호출에 영향을 미칠 것입니다.
Andrey Semakin

10
import random

def ten_random_numbers(rng=random):
    return [rng.random() for i in xrange(10)]

random기본 난수 생성기로 효과적으로 변경 가능한 싱글 톤 인 모듈을 사용합니다 .


7
그러나 이것은 매우 중요한 사용 사례도 아닙니다.
Evgeni Sergeev

3
파이썬의 "한 번 참조 얻기"와 파이썬이 아닌 " random함수 호출 당 한 번 조회"사이에는 동작에 차이가 없다고 생각 합니다. 둘 다 동일한 객체를 사용합니다.
nyanpasu64

4

편집 (설명) : 변경 가능한 기본 인수 문제는 더 깊은 설계 선택의 증상입니다. 즉, 기본 인수 값이 함수 개체의 속성으로 저장된다는 것입니다. 이 선택이 이루어진 이유를 물어볼 수 있습니다. 언제나 그렇듯이 그러한 질문은 제대로 대답하기 어렵습니다. 그러나 확실히 좋은 용도가 있습니다.

성능 최적화 :

def foo(sin=math.sin): ...

변수 대신 클로저에서 객체 값을 가져옵니다.

callbacks = []
for i in range(10):
    def callback(i=i): ...
    callbacks.append(callback)

7
정수와 내장 함수는 변경할 수 없습니다!
Monica 복원

2
@Jonathan : 나머지 예제에는 여전히 변경 가능한 기본 인수가 없습니다. 아니면 그냥 보이지 않습니까?
Monica 복원

2
@Jonathan : 내 요점은 이것이 가변적이라는 것이 아닙니다. 파이썬이 컴파일 타임에 정의 된 함수 객체에 기본 인수를 저장하는 데 사용하는 시스템이 유용 할 수 있습니다. 이는 각 함수 호출에서 인수를 다시 평가하면 트릭이 쓸모 없게 될 것이기 때문에 변경 가능한 기본 인수 문제를 의미합니다.
Katriel

2
@katriealex : 좋습니다.하지만 답변에서 주장을 재평가해야한다고 가정하고 그게 왜 나쁜지 보여 주면 그렇게 말 해주세요. Nit-pick : 기본 인수 값은 컴파일 타임에 저장되지 않고 함수 정의 문이 실행될 때 저장됩니다.
Monica 복원

@WolframH : 사실 : P! 둘이 종종 일치하지만.
Katriel

-1

변경 가능한 기본 인수 값의 좋은 사용에 대한 질문에 대한 대답으로 다음 예제를 제공합니다.

변경 가능한 기본값은 사용하기 쉽고 직접 만든 가져 오기 가능한 명령을 프로그래밍하는 데 유용 할 수 있습니다. 변경 가능한 기본 메서드는 첫 번째 호출에서 초기화 할 수있는 (클래스와 매우 유사) 함수에 전용 정적 변수를 포함하지만 전역에 의존 할 필요가없고 래퍼를 사용하지 않고 인스턴스화 할 필요도 없습니다. 가져온 클래스 개체입니다. 당신이 동의하기를 바라는 것처럼 그것은 나름대로 우아합니다.

다음 두 가지 예를 고려하십시오.

def dittle(cache = []):

    from time import sleep # Not needed except as an example.

    # dittle's internal cache list has this format: cache[string, counter]
    # Any argument passed to dittle() that violates this format is invalid.
    # (The string is pure storage, but the counter is used by dittle.)

     # -- Error Trap --
    if type(cache) != list or cache !=[] and (len(cache) == 2 and type(cache[1]) != int):
        print(" User called dittle("+repr(cache)+").\n >> Warning: dittle() takes no arguments, so this call is ignored.\n")
        return

    # -- Initialize Function. (Executes on first call only.) --
    if not cache:
        print("\n cache =",cache)
        print(" Initializing private mutable static cache. Runs only on First Call!")
        cache.append("Hello World!")
        cache.append(0)
        print(" cache =",cache,end="\n\n")
    # -- Normal Operation --
    cache[1]+=1 # Static cycle count.
    outstr = " dittle() called "+str(cache[1])+" times."
    if cache[1] == 1:outstr=outstr.replace("s.",".")
    print(outstr)
    print(" Internal cache held string = '"+cache[0]+"'")
    print()
    if cache[1] == 3:
        print(" Let's rest for a moment.")
        sleep(2.0) # Since we imported it, we might as well use it.
        print(" Wheew! Ready to continue.\n")
        sleep(1.0)
    elif cache[1] == 4:
        cache[0] = "It's Good to be Alive!" # Let's change the private message.

# =================== MAIN ======================        
if __name__ == "__main__":

    for cnt in range(2):dittle() # Calls can be loop-driven, but they need not be.

    print(" Attempting to pass an list to dittle()")
    dittle([" BAD","Data"])
    
    print(" Attempting to pass a non-list to dittle()")
    dittle("hi")
    
    print(" Calling dittle() normally..")
    dittle()
    
    print(" Attempting to set the private mutable value from the outside.")
    # Even an insider's attempt to feed a valid format will be accepted
    # for the one call only, and is then is discarded when it goes out
    # of scope. It fails to interrupt normal operation.
    dittle([" I am a Grieffer!\n (Notice this change will not stick!)",-7]) 
    
    print(" Calling dittle() normally once again.")
    dittle()
    dittle()

이 코드를 실행하면 dittle () 함수가 첫 번째 호출에서 내부화되지만 추가 호출에서는 내부화되지 않으며 호출 사이의 내부 정적 저장소에 개인 정적 캐시 (변경 가능한 기본값)를 사용하고 하이재킹 시도를 거부합니다. 정적 저장소는 악의적 인 입력에 대해 탄력적이며 동적 조건 (여기서는 함수가 호출 된 횟수)에 따라 작동 할 수 있습니다.

가변 기본값을 사용하는 열쇠는 메모리에서 변수를 재 할당하는 작업을 수행하지 않고 항상 제자리에서 변수를 변경하는 것입니다.

이 기술의 잠재적 인 힘과 유용성을 확인하려면이 첫 번째 프로그램을 "DITTLE.py"라는 이름으로 현재 디렉토리에 저장 한 후 다음 프로그램을 실행하십시오. 기억하기위한 단계를 거치거나 점프 할 농구대를 프로그래밍하지 않고도 새로운 dittle () 명령을 가져 와서 사용합니다.

두 번째 예가 있습니다. 이것을 새 프로그램으로 컴파일하고 실행하십시오.

from DITTLE import dittle

print("\n We have emulated a new python command with 'dittle()'.\n")
# Nothing to declare, nothing to instantize, nothing to remember.

dittle()
dittle()
dittle()
dittle()
dittle()

이제 가능한 한 매끄럽고 깨끗하지 않습니까? 이러한 변경 가능한 기본값은 실제로 유용 할 수 있습니다.

========================

잠시 내 대답을 생각해 본 후, 변경 가능한 기본 방법을 사용하는 것과 동일한 작업을 수행하는 일반적인 방법의 차이를 명확하게 만들지 못했습니다.

일반적인 방법은 Class 객체를 감싸고 전역을 사용하는 임포트 가능한 함수를 사용하는 것입니다. 따라서 비교를 위해 여기에는 변경 가능한 기본 메서드와 동일한 작업을 시도하는 클래스 기반 메서드가 있습니다.

from time import sleep

class dittle_class():

    def __init__(self):
        
        self.b = 0
        self.a = " Hello World!"
        
        print("\n Initializing Class Object. Executes on First Call only.")
        print(" self.a = '"+str(self.a),"', self.b =",self.b,end="\n\n")
    
    def report(self):
        self.b  = self.b + 1
        
        if self.b == 1:
            print(" Dittle() called",self.b,"time.")
        else:
            print(" Dittle() called",self.b,"times.")
        
        if self.b == 5:
            self.a = " It's Great to be alive!"
        
        print(" Internal String =",self.a,end="\n\n")
            
        if self.b ==3:
            print(" Let's rest for a moment.")
            sleep(2.0) # Since we imported it, we might as well use it.
            print(" Wheew! Ready to continue.\n")
            sleep(1.0)

cl= dittle_class()

def dittle():
    global cl
    
    if type(cl.a) != str and type(cl.b) != int:
        print(" Class exists but does not have valid format.")
        
    cl.report()

# =================== MAIN ====================== 
if __name__ == "__main__":
    print(" We have emulated a python command with our own 'dittle()' command.\n")
    for cnt in range(2):dittle() # Call can be loop-driver, but they need not be.
    
    print(" Attempting to pass arguments to dittle()")
    try: # The user must catch the fatal error. The mutable default user did not. 
        dittle(["BAD","Data"])
    except:
        print(" This caused a fatal error that can't be caught in the function.\n")
    
    print(" Calling dittle() normally..")
    dittle()
    
    print(" Attempting to set the Class variable from the outside.")
    cl.a = " I'm a griefer. My damage sticks."
    cl.b = -7
    
    dittle()
    dittle()

이 클래스 기반 프로그램을 현재 디렉토리에 DITTLE.py로 저장 한 후 다음 코드를 실행하십시오 (이전과 동일).

from DITTLE import dittle
# Nothing to declare, nothing to instantize, nothing to remember.

dittle()
dittle()
dittle()
dittle()
dittle()

두 가지 방법을 비교하면 함수에서 가변 기본값을 사용하는 이점이 더 명확 해집니다. 변경 가능한 기본 메서드에는 전역이 필요하지 않으며 내부 변수를 직접 설정할 수 없습니다. 그리고 가변 메소드가 단일주기 동안 전달 된 인수를 받아 들인 다음이를 무시했지만, Class 메소드는 내부 변수가 외부에 직접 노출되기 때문에 영구적으로 변경되었습니다. 어떤 방법이 프로그래밍하기 더 쉬운가요? 나는 그것이 당신의 목표의 방법과 복잡성에 대한 당신의 편안함 수준에 달려 있다고 생각합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.