Bắt buộc đặt tên cho các tham số trong Python


111

Trong Python, bạn có thể có một định nghĩa hàm:

def info(object, spacing=10, collapse=1)

có thể được gọi theo bất kỳ cách nào sau đây:

info(odbchelper)                    
info(odbchelper, 12)                
info(odbchelper, collapse=0)        
info(spacing=15, object=odbchelper)

nhờ Python cho phép các đối số theo thứ tự bất kỳ, miễn là chúng được đặt tên.

Vấn đề chúng tôi đang gặp phải là khi một số hàm lớn hơn của chúng tôi phát triển, mọi người có thể thêm các tham số vào giữa spacingcollapse, có nghĩa là các giá trị sai có thể chuyển đến các tham số không được đặt tên. Ngoài ra, đôi khi không phải lúc nào cũng rõ ràng về những gì cần phải nhập. Chúng ta đang tìm cách buộc mọi người đặt tên cho các thông số nhất định - không chỉ là một tiêu chuẩn mã hóa, mà lý tưởng là một cờ hoặc plugin pydev?

để trong 4 ví dụ trên, chỉ cái cuối cùng vượt qua kiểm tra vì tất cả các tham số đều được đặt tên.

Tỷ lệ cược là chúng tôi sẽ chỉ bật nó cho một số chức năng nhất định, nhưng bất kỳ đề xuất nào về cách thực hiện điều này - hoặc nếu nó thậm chí có thể sẽ được đánh giá cao.

Câu trả lời:


214

Trong Python 3 - Có, bạn có thể chỉ định *trong danh sách đối số.

Từ tài liệu :

Các tham số sau “*” hoặc “* số nhận dạng” là các tham số chỉ từ khóa và chỉ có thể được chuyển qua các đối số từ khóa đã sử dụng.

>>> def foo(pos, *, forcenamed):
...   print(pos, forcenamed)
... 
>>> foo(pos=10, forcenamed=20)
10 20
>>> foo(10, forcenamed=20)
10 20
>>> foo(10, 20)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() takes exactly 1 positional argument (2 given)

Điều này cũng có thể được kết hợp với **kwargs:

def foo(pos, *, forcenamed, **kwargs):

32

Bạn có thể buộc mọi người sử dụng các đối số từ khóa trong Python3 bằng cách xác định một hàm theo cách sau.

def foo(*, arg0="default0", arg1="default1", arg2="default2"):
    pass

Bằng cách đặt đối số đầu tiên là đối số vị trí không có tên, bạn buộc tất cả những người gọi hàm phải sử dụng đối số từ khóa, đó là điều tôi nghĩ bạn đang hỏi. Trong Python2, cách duy nhất để làm điều này là xác định một hàm như thế này

def foo(**kwargs):
    pass

Điều đó sẽ buộc người gọi sử dụng kwargs nhưng đây không phải là một giải pháp tuyệt vời vì sau đó bạn phải đặt một séc để chỉ chấp nhận đối số mà bạn cần.


11

Đúng, hầu hết các ngôn ngữ lập trình đều biến thứ tự tham số trở thành một phần của hợp đồng gọi hàm, nhưng điều này không cần phải như vậy. Tại sao nó? Sự hiểu biết của tôi về câu hỏi là, liệu Python có gì khác so với các ngôn ngữ lập trình khác về mặt này hay không. Ngoài các câu trả lời hay khác cho Python 2, vui lòng xem xét những điều sau:

__named_only_start = object()

def info(param1,param2,param3,_p=__named_only_start,spacing=10,collapse=1):
    if _p is not __named_only_start:
        raise TypeError("info() takes at most 3 positional arguments")
    return str(param1+param2+param3) +"-"+ str(spacing) +"-"+ str(collapse)

Cách duy nhất mà người gọi có thể cung cấp các đối số spacingvà theo collapsevị trí (không có ngoại lệ) sẽ là:

info(arg1, arg2, arg3, module.__named_only_start, 11, 2)

Quy ước không sử dụng các phần tử private thuộc các mô-đun khác đã rất cơ bản trong Python. Cũng như với chính Python, quy ước này cho các tham số sẽ chỉ được thực thi một phần.

Nếu không, các cuộc gọi sẽ cần phải có dạng:

info(arg1, arg2, arg3, spacing=11, collapse=2)

Một cuộc gọi

info(arg1, arg2, arg3, 11, 2)

sẽ gán giá trị 11 cho tham số _pvà một ngoại lệ được tăng lên bởi lệnh đầu tiên của hàm.

Nét đặc trưng:

  • Các thông số trước đây _p=__named_only_startđược thừa nhận theo vị trí (hoặc theo tên).
  • Các thông số sau chỉ _p=__named_only_startphải được cung cấp theo tên (trừ khi __named_only_startcó và sử dụng kiến thức về đối tượng lính canh đặc biệt ).

Ưu điểm:

  • Các thông số được thể hiện rõ ràng về số lượng và ý nghĩa (tất nhiên càng về sau nếu tên hay cũng được chọn).
  • Nếu sentinel được chỉ định làm tham số đầu tiên, thì tất cả các đối số cần được chỉ định theo tên.
  • Khi gọi hàm, có thể chuyển sang chế độ định vị bằng cách sử dụng đối tượng sentinel __named_only_startở vị trí tương ứng.
  • Một hiệu suất tốt hơn so với các lựa chọn thay thế khác có thể được dự đoán trước.

Nhược điểm:

  • Kiểm tra xảy ra trong thời gian chạy, không phải thời gian biên dịch.
  • Sử dụng một tham số bổ sung (mặc dù không phải là đối số) và một kiểm tra bổ sung. Sự suy giảm hiệu suất nhỏ đối với các chức năng thông thường.
  • Chức năng là một bản hack mà không có sự hỗ trợ trực tiếp của ngôn ngữ (xem ghi chú bên dưới).
  • Khi gọi hàm, có thể chuyển sang chế độ định vị bằng cách sử dụng đối tượng sentinel __named_only_startở đúng vị trí. Vâng, đây cũng có thể được coi là một người chuyên nghiệp.

Xin lưu ý rằng câu trả lời này chỉ hợp lệ cho Python 2. Python 3 triển khai cơ chế hỗ trợ ngôn ngữ tương tự nhưng rất thanh lịch được mô tả trong các câu trả lời khác.

Tôi thấy rằng khi tôi mở rộng tâm trí và suy nghĩ về nó, không có câu hỏi nào hoặc quyết định của người khác có vẻ ngu ngốc, ngớ ngẩn, hoặc ngớ ngẩn. Hoàn toàn ngược lại: Tôi thường học được rất nhiều.


"Kiểm tra xảy ra trong thời gian chạy, không phải thời gian biên dịch." - Tôi nghĩ điều đó đúng với tất cả việc kiểm tra đối số của hàm. Cho đến khi bạn thực sự thực hiện dòng lệnh gọi hàm, bạn không phải lúc nào cũng biết hàm nào đang được thực thi. Ngoài ra, +1 - điều này thật thông minh.
Eric

@Eric: Chỉ là tôi thích kiểm tra tĩnh hơn. Nhưng bạn nói đúng: đó hoàn toàn không phải là Python. Mặc dù không phải là điểm quyết định, nhưng cấu trúc "*" của Python 3 cũng được kiểm tra động. Cám ơn bạn đã góp ý.
Mario Rossi,

Ngoài ra, nếu bạn đặt tên cho biến mô-đun _named_only_start, bạn sẽ không thể tham chiếu nó từ một mô-đun bên ngoài, điều này sẽ lấy ra một chuyên gia và một kẻ lừa đảo. (dấu gạch dưới hàng đầu duy nhất ở phạm vi mô-đun là riêng tư, IIRC)
Eric

Liên quan đến việc đặt tên cho khu giám sát, chúng tôi cũng có thể có cả a __named_only_startvà a named_only_start(không có gạch dưới đầu tiên), thứ hai để chỉ ra rằng chế độ được đặt tên là "được khuyến nghị", nhưng không đến mức được "quảng bá tích cực" (vì một chế độ là công khai và khác không). Về "tính riêng tư" của việc _namesbắt đầu bằng dấu gạch dưới, nó không được thực thi mạnh mẽ bởi ngôn ngữ: nó có thể dễ dàng bị phá vỡ bằng cách sử dụng các tên nhập khẩu hoặc đủ điều kiện (không phải *) cụ thể. Đây là lý do tại sao một số tài liệu Python thích sử dụng thuật ngữ "không công khai" thay vì "riêng tư".
Mario Rossi

6

Bạn có thể làm điều đó theo cách hoạt động trong cả Python 2 và Python 3 , bằng cách tạo đối số từ khóa đầu tiên "không có thật" với giá trị mặc định sẽ không xảy ra "tự nhiên". Đối số từ khóa đó có thể được đặt trước một hoặc nhiều đối số không có giá trị:

_dummy = object()

def info(object, _kw=_dummy, spacing=10, collapse=1):
    if _kw is not _dummy:
        raise TypeError("info() takes 1 positional argument but at least 2 were given")

Điều này sẽ cho phép:

info(odbchelper)        
info(odbchelper, collapse=0)        
info(spacing=15, object=odbchelper)

nhưng không:

info(odbchelper, 12)                

Nếu bạn thay đổi chức năng thành:

def info(_kw=_dummy, spacing=10, collapse=1):

thì tất cả các đối số phải có từ khóa và info(odbchelper)sẽ không hoạt động nữa.

Điều này sẽ cho phép bạn đặt các đối số từ khóa bổ sung vào bất kỳ vị trí nào sau đó _kwmà không buộc bạn phải đặt chúng sau mục nhập cuối cùng. Điều này thường có ý nghĩa, ví dụ như nhóm mọi thứ một cách hợp lý hoặc sắp xếp các từ khóa theo thứ tự bảng chữ cái có thể giúp duy trì và phát triển.

Vì vậy, không cần phải quay lại sử dụng def(**kwargs)và mất thông tin chữ ký trong trình soạn thảo thông minh của bạn. Hợp đồng xã hội của bạn là cung cấp một số thông tin nhất định, bằng cách buộc (một số người trong số họ) yêu cầu các từ khóa, thứ tự mà chúng được trình bày, đã trở nên không liên quan.


2

Cập nhật:

Tôi nhận ra rằng việc sử dụng **kwargssẽ không giải quyết được vấn đề. Nếu các lập trình viên của bạn thay đổi các đối số của hàm như họ muốn, chẳng hạn, người ta có thể thay đổi hàm thành thế này:

def info(foo, **kwargs):

và mã cũ sẽ lại bị hỏng (bởi vì bây giờ mọi lệnh gọi hàm phải bao gồm đối số đầu tiên).

Nó thực sự đi xuống những gì Bryan nói.


(...) mọi người có thể đang thêm các tham số giữa spacingcollapse(...)

Nói chung, khi thay đổi các hàm, các đối số mới phải luôn đi cuối cùng. Nếu không, nó sẽ phá vỡ mã. Nên rõ ràng.
Nếu ai đó thay đổi chức năng để mã bị hỏng, thay đổi này phải bị từ chối.
(Như Bryan nói, nó giống như một hợp đồng)

(...) đôi khi không phải lúc nào cũng rõ ràng những gì cần phải đi vào.

Bằng cách nhìn vào chữ ký của hàm (tức là def info(object, spacing=10, collapse=1)), người ta sẽ thấy ngay rằng mọi đối số không có giá trị mặc định, đều là bắt buộc.
Có gì các đối số là cho, nên đi vào docstring.


Câu trả lời cũ (được giữ lại để hoàn thiện) :

Đây có lẽ không phải là một giải pháp tốt:

Bạn có thể xác định các hàm theo cách này:

def info(**kwargs):
    ''' Some docstring here describing possible and mandatory arguments. '''
    spacing = kwargs.get('spacing', 15)
    obj = kwargs.get('object', None)
    if not obj:
       raise ValueError('object is needed')

kwargslà một từ điển chứa bất kỳ đối số từ khóa nào. Bạn có thể kiểm tra xem có một đối số bắt buộc hay không và nếu không, hãy nêu ra một ngoại lệ.

Nhược điểm là, nó có thể không còn rõ ràng nữa, đối số nào là có thể, nhưng với một chuỗi docstring thích hợp, nó sẽ ổn.


3
Tôi thích câu trả lời cũ của bạn hơn. Chỉ cần đặt một nhận xét tại sao bạn chỉ chấp nhận ** kwargs trong hàm. Rốt cuộc, bất kỳ ai cũng có thể thay đổi bất kỳ thứ gì trong mã nguồn - bạn cần tài liệu để mô tả ý định và mục đích đằng sau các quyết định của mình.
Brandon

Không có câu trả lời thực tế trong câu trả lời này!
Phil

2

Các đối số chỉ từ khóa python3 ( *) có thể được mô phỏng trong python2.x với**kwargs

Hãy xem xét mã python3 sau:

def f(pos_arg, *, no_default, has_default='default'):
    print(pos_arg, no_default, has_default)

và hành vi của nó:

>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes 1 positional argument but 3 were given
>>> f(1, no_default='hi')
1 hi default
>>> f(1, no_default='hi', has_default='hello')
1 hi hello
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() missing 1 required keyword-only argument: 'no_default'
>>> f(1, no_default=1, wat='wat')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() got an unexpected keyword argument 'wat'

Điều này có thể được mô phỏng bằng cách sử dụng như sau, lưu ý rằng tôi đã tự do chuyển TypeErrorsang KeyErrortrong trường hợp "đối số được đặt tên bắt buộc", sẽ không quá nhiều việc để tạo cùng một loại ngoại lệ

def f(pos_arg, **kwargs):
    no_default = kwargs.pop('no_default')
    has_default = kwargs.pop('has_default', 'default')
    if kwargs:
        raise TypeError('unexpected keyword argument(s) {}'.format(', '.join(sorted(kwargs))))

    print(pos_arg, no_default, has_default)

Và hành vi:

>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes exactly 1 argument (3 given)
>>> f(1, no_default='hi')
(1, 'hi', 'default')
>>> f(1, no_default='hi', has_default='hello')
(1, 'hi', 'hello')
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
KeyError: 'no_default'
>>> f(1, no_default=1, wat='wat')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in f
TypeError: unexpected keyword argument(s) wat

Công thức hoạt động tương tự như trong python3.x, nhưng nên tránh nếu bạn chỉ dùng python3.x


Ah, kwargs.pop('foo')một thành ngữ Python 2 cũng vậy? Tôi cần cập nhật phong cách viết mã của mình. Tôi vẫn đang sử dụng phương pháp này trong Python 3 🤔
Neil

0

Bạn có thể khai báo các chức năng của mình là **argschỉ nhận . Điều đó sẽ bắt buộc các đối số từ khóa nhưng bạn sẽ phải làm thêm một số việc để đảm bảo rằng chỉ những tên hợp lệ mới được chuyển vào.

def foo(**args):
   print args

foo(1,2) # Raises TypeError: foo() takes exactly 0 arguments (2 given)
foo(hello = 1, goodbye = 2) # Works fine.

1
Bạn không chỉ phải thêm kiểm tra từ khóa, mà hãy nghĩ về một người tiêu dùng biết rằng họ phải gọi một phương thức có chữ ký foo(**kwargs). Tôi vượt qua điều đó để làm gì? foo(killme=True, when="rightnowplease")
Dagroom 19/1217

Nó thực sự phụ thuộc. Hãy cân nhắc dict.
Noufal Ibrahim

0

Như các câu trả lời khác nói, thay đổi chữ ký chức năng là một ý tưởng tồi. Thêm các tham số mới vào cuối hoặc sửa mọi người gọi nếu các đối số được chèn vào.

Nếu bạn vẫn muốn làm điều đó, hãy sử dụng một trình trang trí hàm và hàm Kiểm tra.getargspec . Nó sẽ được sử dụng như thế này:

@require_named_args
def info(object, spacing=10, collapse=1):
    ....

Thực hiện require_named_argscòn lại như một bài tập cho người đọc.

Tôi sẽ không bận tâm. Nó sẽ chậm mỗi khi hàm được gọi, và bạn sẽ nhận được kết quả tốt hơn từ việc viết mã cẩn thận hơn.


-1

Bạn có thể sử dụng **toán tử:

def info(**kwargs):

theo cách này mọi người buộc phải sử dụng các tham số được đặt tên.


2
Và không biết làm thế nào để gọi phương thức của bạn mà không cần đọc mã của bạn, tăng tải nhận thức lên người tiêu dùng của bạn :(
Dagrooms 19/1217

Bởi vì lý do đã đề cập, đây là thực hành thực sự xấu và nên tránh.
David S.

-1
def cheeseshop(kind, *arguments, **keywords):

trong python nếu sử dụng * args có nghĩa là bạn có thể truyền n-số đối số cho tham số này - đó sẽ là danh sách bên trong hàm để truy cập

và nếu sử dụng ** kw có nghĩa là các đối số từ khóa của nó, có thể được truy cập dưới dạng dict - bạn có thể chuyển n-số kw args và nếu bạn muốn hạn chế người dùng đó phải nhập chuỗi và đối số theo thứ tự thì không sử dụng * và ** - (cách tuyệt vời của nó để cung cấp các giải pháp chung cho các kiến ​​trúc lớn ...)

nếu bạn muốn hạn chế chức năng của mình bằng các giá trị mặc định thì bạn có thể kiểm tra bên trong nó

def info(object, spacing, collapse)
  spacing = spacing or 10
  collapse = collapse or 1

Điều gì xảy ra nếu khoảng cách được mong muốn là 0? (trả lời, bạn nhận được 10). Câu trả lời này sai giống như tất cả các câu trả lời ** kwargs khác vì tất cả các lý do giống nhau.
Phil

-2

Tôi không hiểu tại sao một lập trình viên sẽ thêm một tham số vào giữa hai người khác ngay từ đầu.

Nếu bạn muốn các tham số hàm được sử dụng với tên (ví dụ: info(spacing=15, object=odbchelper) ) thì không quan trọng thứ tự chúng được xác định, vì vậy bạn cũng có thể đặt các tham số mới ở cuối.

Nếu bạn muốn đơn đặt hàng có vấn đề thì không thể mong đợi bất cứ điều gì hoạt động nếu bạn thay đổi nó!


2
Điều này không trả lời câu hỏi. Cho dù đó là một ý tưởng tốt hay không thì không liên quan - ai đó có thể sẽ làm điều đó.
Graeme Perrow

1
Như Graeme đã đề cập, ai đó sẽ làm điều đó. Ngoài ra, nếu bạn đang viết một thư viện để người khác sử dụng, việc buộc (chỉ dành cho python 3) chuyển đối số chỉ từ khóa cho phép bạn linh hoạt hơn khi bạn phải cấu trúc lại API của mình.
s0undt3ch
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.