Tham số mặc định
Python cho phép chúng ta khai báo hàm với tham số mặc định như sau:
def function(arg_1, arg_2={}):
arg_2[arg_1] = True
print (arg_2)
Với khai báo hàm trên, tham số arg_2* trở thành một tham số mặc định và sẽ nhận giá trị là một từ điển rỗng. Khi gọi hàm function, chúng ta có thể không cung cấp giá trị cho tham số arg_2. Ví dụ khi thực hiện lệnh gọi sau, trên màn hình sẽ xuất hiện chuỗi {1: True}:
function(1) # in ra '{1: True}'
Nếu tiếp tục gọi một lần tương tự, chúng ta nhận được một kết quả ngoài mong đợi.
function(2) # in ra '{1: True, 2: True}'
Lần này, thay vì chỉ in ra từ điển với một khóa {2: True}, ta nhận được cả hai giá trị.
Ý nghĩa của tham số mặc định
Lý giải điều này không khó. Trong mục 4.7.1 của Bài chỉ dẫn Python có nêu:
Giá trị mặc định được định giá tại nơi hàm được định nghĩa.
Điều này dẫn đến hệ quả là:
Giá trị mặc định chỉ được định giá một lần. Điểm này quan trọng khi *giá trị* mặc định là một giá trị khả biến như danh sách, từ điển hoặc các đối tượng của hầu hết mọi lớp.
Do đó, với ví dụ của hàm function ở trên, tham số arg_2 sẽ nhận giá trị mặc định là từ điển được tạo ra ngay khi dòng lệnh def function(...) được dịch và thực thi. Trong các lần gọi lệnh function sau đó, nếu không xác định tham số cho arg_2, thì arg_2 sẽ chỉ đến cùng một từ điển này. Và vì thế mọi tác động đến arg_2 đều tác động đến cùng một đối tượng từ điển.
Thông thường, cách giải quyết vấn đề này sẽ bao gồm:
- Xác định giá trị mặc định là None ở câu lệnh def
- Khởi tạo giá trị mặc định mới và gán nó vào biến ở trong thân hàm nếu giá trị hiện tại là None
Hàm function sẽ được sửa lại như sau:
def function(arg_1, arg_2=None):
if arg_2 is None: # kiểm tra arg_2 có phải None không
arg_2 = {} # gán đối tượng từ điển mới vào arg_2
arg_2[arg_1] = True
print(arg_2)
Mô-đun auto_argument
Để đơn giản hóa và tránh lập đi lập lại dòng lệnh if, tôi đã phát triển một thư viện nhỏ đặt tên là auto_argument (thông số tự động).
Với thư viện này, chúng ta có thể viết lại hàm function như sau:
@callable_auto_argument([('arg_2', None, dict)])
def function(arg_1, arg_2=None):
arg_2[arg_1] = True
print (arg_2)
Điểm khác biệt chính là ở việc sử dụng bộ trang hoàng callable_auto_argument để tự động thay thế tham số arg_2 với giá trị trả về từ lệnh gọi dict().
Bộ trang hoàng auto_argument (lớp cha của callable_auto_argument) nhận một dãy các bộ 3 (tên tham số, giá trị dấu, giá trị thay thế) (argument name, marker, value). Khi tham số có giá trị là giá trị dấu thì giá trị của tham số sẽ được thay bằng giá trị tạo ra từ giá trị thay thế. callable_auto_argument tạo ra giá trị thay thế bằng cách gọi hàm giá trị thay thế. Người dùng cũng có thể tạo lớp con của các bộ trang hoàng này để tùy chỉnh giá trị thay thế riêng hoặc tránh phải lập lại giá trị dấu. Xem qua hàm test_subclass và lớp auto_dict_argument trong mã nguồn.
Mã nguồn của các bộ trang hoàng này được liệt kê ở ngay dưới. Mã nguồn này được cung cấp theo điều khoản bản quyền MIT. Người dùng có thể tùy ý sử dụng, hay sửa đổi mã nguồn cho hợp với nhu cầu.
'''Automatically replace a default argument with some other (potentially
dynamic) value.
The default argument is usually guarded like this::
def func(arg=None):
if arg is None:
arg = dict()
// use arg
With decorators provided in this module, one can write::
__marker = object()
@callable_auto_argument([('arg', __marker, dict)])
def func(arg=__marker):
// use arg
See class:`callable_auto_argument`.
Also, the standard Python behavior could be thought as::
__marker = object()
@passthru_auto_argument([('arg', __marker, {})])
def func(arg=__marker):
// ...
See class:`passthru_auto_argument`.
These classes can be used by themselves or serve as base classes for more
customizations. For example, to eliminate repeated typings, one can subclass
``callable_auto_argument`` like this::
class auto_dict_argument(callable_auto_argument):
def __init__(self, *names):
names_markers_values = []
for name in names:
names_markers_values.append((name, None, dict))
super(auto_dict_argument, self).__init__(names_markers_values)
And then apply this on methods like this::
@auto_dict_argument('arg_1', 'arg_2', 'arg_3')
def method(arg_1=None, arg_2=None, arg_3=None):
# arg_1, arg_2, arg_3 are new dicts unless specified otherwise
# and these lines are no longer required
# if arg_1 is None: arg_1 = {}
# if arg_2 is None: arg_2 = {}
# if arg_3 is None: arg_3 = {}
'''
import unittest
class auto_argument(object):
'''The base class for automatic argument.
Subclasses must implement method:`create` to create appropriate value for
the argument.
'''
def __init__(self, names_markers_values):
'''Construct an auto argument decorator with a collection of variable
names, their markers, and their supposed values.
The __supposed__ value objects are used in method:`create` to produce
final value.
Args:
names_markers_values (collection of 3-tuples): A collection of
(string, object, object) tuples specifying (in that order) the
names, the marker objects, and the supposed value objects
'''
self.names_markers_values = names_markers_values
def create(self, name, current_value, value):
'''Return a value based for the named argument and its current value.
This final value will be used to replace what is currently passed in
the invocation.
Subclasses MUST override this method to provide more meaningful
behavior.
Args:
name (string): The argument's name
current_value: Its current value in this invocation
value: The supposed value passed in during construction time
Returns:
Final value for this argument
'''
raise NotImplementedError()
def __call__(self, orig_func):
def new_func(*args, **kw_args):
for name, marker, value in self.names_markers_values:
# check kw_args first
try:
if kw_args[name] is marker:
kw_args[name] = self.create(name, kw_args[name], value)
except KeyError:
# ignored
pass
else:
continue
# then check args
# we need to instropect the arg names from orig_func
co_obj = orig_func.func_code
# number of required arguments
nr_required = (co_obj.co_argcount -
len(orig_func.func_defaults))
for i in range(nr_required, co_obj.co_argcount):
if co_obj.co_varnames[i] != name:
continue
# is it supplied in args?
if i < len(args):
if args[i] is marker:
if isinstance(args, tuple):
args = list(args)
args[i] = self.create(name, args[i], value)
# it is not, so, check defaults
else:
default = orig_func.func_defaults[i - nr_required]
if default is marker:
kw_args[name] = self.create(name, default, value)
# invoke orig_func with new args
return orig_func(*args, **kw_args)
return new_func
class callable_auto_argument(auto_argument):
def create(self, name, current_value, value):
# call on value
return value()
class passthru_auto_argument(auto_argument):
def create(self, name, current_value, value):
# just return it directly
return value
class AutoArgumentTest(unittest.TestCase):
def test_keyword_1(self):
marker = 'replace_me'
@passthru_auto_argument([('arg_name', marker, 'done')])
def orig_func_0(arg_name=marker):
return arg_name
self.assertEqual('done', orig_func_0())
self.assertEqual('done', orig_func_0(marker))
self.assertEqual('not_replace', orig_func_0('not_replace'))
self.assertEqual('done', orig_func_0(arg_name=marker))
self.assertEqual('not_replace', orig_func_0(arg_name='not_replace'))
@passthru_auto_argument([('arg_name', 'replace_me', 'done')])
def orig_func_1(junk, arg_name='replace_me'):
return junk, arg_name
self.assertEqual(('ignore', 'done'), orig_func_1('ignore'))
self.assertEqual(('ignore', 'done'),
orig_func_1('ignore', marker))
self.assertEqual(('ignore', 'not_replace'),
orig_func_1('ignore', 'not_replace'))
self.assertEqual(('ignore', 'done'),
orig_func_1('ignore', arg_name=marker))
self.assertEqual(('ignore', 'not_replace'),
orig_func_1('ignore', arg_name='not_replace'))
def test_keyword_2(self):
marker_1 = 'replace_me'
marker_2 = 'replace_too'
@passthru_auto_argument([('arg_1', marker_1, 'done'),
('arg_2', marker_2, 'enod')])
def orig_func_0(arg_1=marker_1, arg_2=marker_2):
return arg_1, arg_2
self.assertEqual(('done', 'enod'), orig_func_0())
self.assertEqual(('not_replace', 'enod'), orig_func_0('not_replace'))
self.assertEqual(('done', 'not'), orig_func_0(marker_1, 'not'))
self.assertEqual(('done', 'enod'), orig_func_0(marker_1, marker_2))
self.assertEqual(('not_1', 'not_2'), orig_func_0('not_1', 'not_2'))
@passthru_auto_argument([('arg_1', marker_1, 'done'),
('arg_2', marker_2, 'enod')])
def orig_func_1(junk, arg_1=marker_1, arg_2=marker_2):
return junk, arg_1, arg_2
self.assertEqual(('.', 'done', 'enod'), orig_func_1('.'))
self.assertEqual(('.', 'not_replace', 'enod'),
orig_func_1('.', 'not_replace'))
self.assertEqual(('.', 'done', 'not'),
orig_func_1('.', marker_1, 'not'))
self.assertEqual(('.', 'done', 'enod'),
orig_func_1('.', marker_1, marker_2))
self.assertEqual(('.', 'not_1', 'not_2'),
orig_func_1('.', 'not_1', 'not_2'))
def test_subclass(self):
class auto_dict_argument(callable_auto_argument):
def __init__(self, *names):
names_markers_values = []
for name in names:
names_markers_values.append((name, None, dict))
super(auto_dict_argument, self).__init__(names_markers_values)
@auto_dict_argument('arg_1', 'arg_2')
def test_func(arg_1=None, arg_2=None):
arg_1['1'] = 'arg_1'
arg_2['2'] = 'arg_2'
return (arg_1, arg_2)
self.assertEqual(({'1': 'arg_1'}, {'2': 'arg_2'}), test_func())
arg_1, arg_2 = test_func()
self.assertNotEqual(id(arg_1), id(arg_2))
if __name__ == '__main__':
unittest.main()