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:

  1. Xác định giá trị mặc định là None ở câu lệnh def
  2. 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()