Русский
Русский
English
Статистика
Реклама

Библиотеки

Проверка коллекции header-only C библиотек (awesome-hpp)

22.10.2020 12:07:47 | Автор: admin
PVS-Studio и Awesome hpp

Волею судьбы мы проверили большинство библиотек, входящих в коллекцию под названием "Awesome hpp". Это небольшие проекты на языке C++, состоящие только из заголовочных файлов. Надеемся, найденные ошибки помогут сделать эти библиотеки немного лучше. Также мы будем рады, если их авторы начнут бесплатно использовать анализатор PVS-Studio на регулярной основе.

Предлагаю вашему вниманию обзор результатов проверки различных библиотек, перечисленных в списке awesome-hpp (A curated list of awesome header-only C++ libraries).

Впервые про этот список я узнал из подкаста "Cross Platform Mobile Telephony". Пользуясь случаем, рекомендую всем C++ программистам познакомиться с CppCast. CppCast is the first podcast for C++ developers by C++ developers!

Несмотря на большое количество проектов в списке, ошибок нашлось совсем немного. Тому есть три причины:

  • Это очень маленькие проекты. Многие буквально состоят из одного заголовочного файла.
  • Мы проверили не все проекты. С компиляцией некоторых из них возникли проблемы, и мы решили их пропустить.
  • Часто, чтобы понять, есть ли ошибки в шаблонных классах/функциях или нет, они должны инстанцироваться. Соответственно многие ошибки смогут быть выявлены анализатором только в настоящем проекте, когда библиотека активно используется. Мы же просто включали заголовочные файлы в пустой .cpp файл и проверяли, что делает проверку малоэффективной.

Тем не менее в процессе изучения предупреждений их набралось достаточно, чтобы написать эту статью и парочку дополнительных.

Примечание для моих коллег
Я люблю совмещать и достигать несколько полезных результатов, занимаясь каким-то делом. Призываю брать пример. Узнав про существование коллекции awesome-hpp, я смог реализовать следующие полезные дела:



Примечание для разработчиков библиотек. Желающие могут бесплатно использовать анализатор PVS-Studio для проверки открытых проектов. Для получения лицензии для вашего открытого проекта заполните, пожалуйста, эту форму.

Теперь давайте наконец посмотрим, что нашлось в некоторых библиотеках.

Найденные ошибки


Библиотека iutest


Краткое описание библиотеки iutest:
iutest is framework for writing C++ tests.
template<typename Event>pool_handler<Event> & assure() {  ....  return static_cast<pool_handler<Event> &>(it == pools.cend() ?    *pools.emplace_back(new pool_handler<Event>{}) : **it);  ....}

Предупреждение PVS-Studio: V1023 A pointer without owner is added to the 'pools' container by the 'emplace_back' method. A memory leak will occur in case of an exception. entt.hpp 17114

Этот код потенциально может привести к утечке памяти. В случае, если контейнеру понадобится реаллокация и он не сможет выделить память под новый массив, то он бросит исключение и указатель будет потерян.

Пожалуй, для тестов эта ситуация маловероятна и некритична. Однако я решил упомянуть этот недостаток в образовательных целях :).

Правильный вариант:

pools.emplace_back(std::make_unique<pool_handler<Event>>{})

Ещё одно такое же место: V1023 A pointer without owner is added to the 'pools' container by the 'emplace_back' method. A memory leak will occur in case of an exception. entt.hpp 17407

Библиотека jsoncons


Краткое описание библиотеки jsoncons:
A C++, header-only library for constructing JSON and JSON-like data formats, with JSON Pointer, JSON Patch, JSONPath, JMESPath, CSV, MessagePack, CBOR, BSON, UBJSON.
Первая ошибка

static constexpr uint64_t basic_type_bits = sizeof(uint64_t) * 8;uint64_t* data() {  return is_dynamic() ? dynamic_stor_.data_ : short_stor_.values_;}basic_bigint& operator<<=( uint64_t k ){  size_type q = (size_type)(k / basic_type_bits);  ....  if ( k )  // 0 < k < basic_type_bits:  {    uint64_t k1 = basic_type_bits - k;    uint64_t mask = (1 << k) - 1;             // <=    ....    data()[i] |= (data()[i-1] >> k1) & mask;    ....  }  reduce();  return *this;}

Предупреждение PVS-Studio: V629 Consider inspecting the '1 << k' expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64-bit type. bigint.hpp 744

Эта ошибка подробно уже рассматривалась в статье "Почему важно проводить статический анализ открытых библиотек, которые вы добавляете в свой проект". Если совсем кратко, то, чтобы получать корректные значения маски, нужно написать так:

uint64_t mask = (static_cast<uint64_t>(1) << k) - 1;

Или так:

uint64_t mask = (1ull << k) - 1;

Точно такую же ошибку, как первая, можно увидеть здесь: V629 Consider inspecting the '1 << k' expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64-bit type. bigint.hpp 779

Вторая ошибка

template <class CharT = typename std::iterator_traits<Iterator>::value_type>typename std::enable_if<sizeof(CharT) == sizeof(uint16_t)>::type next() UNICONS_NOEXCEPT{    begin_ += length_;    if (begin_ != last_)    {        if (begin_ != last_)        {  ....}

Предупреждение PVS-Studio: V571 Recurring check. The 'if (begin_ != last_)' condition was already verified in line 1138. unicode_traits.hpp 1140

Странная повторная проверка. Есть подозрение, что здесь какая-то опечатка и второе условие должно выглядеть как-то иначе.

Библиотека clipp


Краткое описание библиотеки clipp:
clipp command line interfaces for modern C++. Easy to use, powerful and expressive command line argument handling for C++11/14/17 contained in a single header file.
inline boolfwd_to_unsigned_int(const char*& s){  if(!s) return false;  for(; std::isspace(*s); ++s);  if(!s[0] || s[0] == '-') return false;  if(s[0] == '-') return false;  return true;}

Предупреждение PVS-Studio: V547 Expression 's[0] == '-'' is always false. clipp.h 303

Ну на самом деле это не ошибка, а просто избыточный код. Проверка на наличие минуса выполняется дважды.

Библиотека SimpleIni


Краткое описание библиотеки SimpleIni:
A cross-platform library that provides a simple API to read and write INI-style configuration files. It supports data files in ASCII, MBCS and Unicode.
#if defined(SI_NO_MBSTOWCS_NULL) || (!defined(_MSC_VER) && !defined(_linux))

Предупреждение PVS-Studio: V1040 Possible typo in the spelling of a pre-defined macro name. The '_linux' macro is similar to '__linux'. SimpleIni.h 2923

Скорее всего, в имени макроса _linux не хватает одного подчёркивания и должно использоваться имя __linux. Впрочем, в POSIX этот макрос объявлен устаревшим и лучше использовать __linux__.

Библиотека CSV Parser


Краткое описание библиотеки CSV Parser:
A modern C++ library for reading, writing, and analyzing CSV (and similar) files.
CSV_INLINE void CSVReader::read_csv(const size_t& bytes) {  const size_t BUFFER_UPPER_LIMIT = std::min(bytes, (size_t)1000000);  std::unique_ptr<char[]> buffer(new char[BUFFER_UPPER_LIMIT]);  auto * HEDLEY_RESTRICT line_buffer = buffer.get();  line_buffer[0] = '\0';  ....  this->feed_state->feed_buffer.push_back(    std::make_pair<>(std::move(buffer), line_buffer - buffer.get())); // <=  ....}

Предупреждение PVS-Studio: V769 The 'buffer.get()' pointer in the 'line_buffer buffer.get()' expression equals nullptr. The resulting value is senseless and it should not be used. csv.hpp 4957

Интересная ситуация, которая требует внимательного рассмотрения. Поэтому я решил, что напишу про это отдельную маленькую заметку. Плюс, ставя эксперименты с аналогичным кодом, я выявил недоработку в самом PVS-Studio :). В некоторых случаях он молчит, хотя должен выдавать предупреждения.

Если совсем кратко, работает этот код или нет, зависит от порядка вычисления аргументов. А в каком порядке вычисляются аргументы, зависит от компилятора.

Библиотека PPrint


Краткое описание библиотеки PPrint:.
Pretty Printer for Modern C++.
template <typename Container>typename std::enable_if<......>::type print_internal(......) {  ....  for (size_t i = 1; i < value.size() - 1; i++) {    print_internal(value[i], indent + indent_, "", level + 1);    if (is_container<T>::value == false)      print_internal_without_quotes(", ", 0, "\n");    else      print_internal_without_quotes(", ", 0, "\n");  }  ....}

Предупреждение PVS-Studio: V523 The 'then' statement is equivalent to the 'else' statement. pprint.hpp 715

Очень странно, что независимо от условия выполняется одно и то же действие. Нет и какого-то специального поясняющего комментария. Всё это очень похоже на Copy-Paste ошибку.

Аналогичные предупреждения:

  • V523 The 'then' statement is equivalent to the 'else' statement. pprint.hpp 780
  • V523 The 'then' statement is equivalent to the 'else' statement. pprint.hpp 851
  • V523 The 'then' statement is equivalent to the 'else' statement. pprint.hpp 927
  • V523 The 'then' statement is equivalent to the 'else' statement. pprint.hpp 1012

Библиотека Strf


Краткое описание библиотеки Strf:
A fast C++ formatting library that supports encoding conversion.
Первая ошибка

template <int Base>class numpunct: private strf::digits_grouping{  ....  constexpr STRF_HD numpunct& operator=(const numpunct& other) noexcept  {    strf::digits_grouping::operator=(other);    decimal_point_ = other.decimal_point_;    thousands_sep_ = other.thousands_sep_;  }  ....};

Предупреждение PVS-Studio: V591 Non-void function should return a value. numpunct.hpp 402

В конце функции забыли написать "return *this;".

Вторая аналогичная ошибка

template <int Base>class no_grouping final{  constexpr STRF_HD no_grouping& operator=(const no_grouping& other) noexcept  {    decimal_point_ = other.decimal_point_;  }  ....}

Предупреждение PVS-Studio: V591 Non-void function should return a value. numpunct.hpp 528.

Библиотека Indicators


Краткое описание библиотеки Indicators:
Activity Indicators for Modern C++.
static inline void move_up(int lines) { move(0, -lines); }static inline void move_down(int lines) { move(0, -lines); }   // <=static inline void move_right(int cols) { move(cols, 0); }static inline void move_left(int cols) { move(-cols, 0); }

Предупреждение PVS-Studio: V524 It is odd that the body of 'move_down' function is fully equivalent to the body of 'move_up' function. indicators.hpp 983

Я не уверен, что это ошибка. Но код очень подозрительный. Высока вероятность, что была скопирована функция move_up и заменено её имя на move_down. А вот минус удалить забыли. Стоит проверить этот код.

Примечание. Если код верен, надо понимать, что он вводит в заблуждение не только анализаторы кода, но и сторонних программистов, которые захотят использовать или развивать этот код. Полезно сопровождать такой код комментариями.

Библиотека manif


Краткое описание библиотеки manif:
manif is a header-only C++11 Lie theory library for state-estimation targeted at robotics applications.
template <typename _Derived>typename LieGroupBase<_Derived>::Scalar*LieGroupBase<_Derived>::data(){  return derived().coeffs().data();}template <typename _Derived>const typename LieGroupBase<_Derived>::Scalar*LieGroupBase<_Derived>::data() const{  derived().coeffs().data(); // <=}

Предупреждение PVS-Studio: V591 Non-void function should return a value. lie_group_base.h 347

Неконстантная функция реализована правильно, а константная нет. Интересно даже, как так получилось

Библиотека FakeIt


Краткое описание библиотеки FakeIt:
FakeIt is a simple mocking framework for C++. It supports GCC, Clang and MS Visual C++. FakeIt is written in C++11 and can be used for testing both C++11 and C++ projects.
template<typename ... arglist>struct ArgumentsMatcherInvocationMatcher :         public ActualInvocation<arglist...>::Matcher {  ....  template<typename A>  void operator()(int index, A &actualArg) {      TypedMatcher<typename naked_type<A>::type> *matcher =        dynamic_cast<TypedMatcher<typename naked_type<A>::type> *>(          _matchers[index]);      if (_matching)        _matching = matcher->matches(actualArg);  }  ....  const std::vector<Destructible *> _matchers;};

Предупреждение PVS-Studio: V522 There might be dereferencing of a potential null pointer 'matcher'. fakeit.hpp 6720

Указатель matcher инициализируется значением, которое возвращает оператор dynamic_cast. А этот оператор может возвращать nullptr, и это весьма вероятный сценарий. Иначе вместо dynamic_cast эффективнее использовать static_cast.

Есть подозрение, что в условии допущена опечатка и на самом деле должно быть написано:

if (matcher)  _matching = matcher->matches(actualArg);

Библиотека GuiLite


Краткое описание библиотеки GuiLite:
The smallest header-only GUI library(4 KLOC) for all platforms.
#define CORRECT(x, high_limit, low_limit)  {\  x = (x > high_limit) ? high_limit : x;\  x = (x < low_limit) ? low_limit : x;\}while(0)void refresh_wave(unsigned char frame){  ....  CORRECT(y_min, m_wave_bottom, m_wave_top);  ....}

Предупреждение PVS-Studio: V529 Odd semicolon ';' after 'while' operator. GuiLite.h 3413

К какой-то проблеме ошибка в макросе не приводит. Но всё равно это ошибка, поэтому я решил описать её в статье.

В макросе планировалось использовать классический паттерн do { } while(....). Это позволяет выполнить несколько действий в одном блоке и при этом иметь возможность для красоты после макроса писать точку с запятой ';', как будто это вызов функции.

Но в рассмотренном макросе случайно забыли написать ключевое слово do. В результате макрос как-бы разделился на две части. Первая это блок. Вторая пустой не выполняющийся цикл: while (0);.

А в чём, собственно, проблема?

Например, такой макрос нельзя использовать в конструкции вида:

if (A)  CORRECT(y_min, m_wave_bottom, m_wave_top);else  Foo();

Этот код не скомпилируется, так как он будет раскрыт в:

if (A)  { ..... }while(0);else  Foo();

Согласитесь, такую проблему лучше найти и исправить на этапе разработки библиотеки, а не на этапе её использования. Применяйте статический анализ кода :).


Библиотека PpluX


Краткое описание библиотеки PpluX:
Single header C++ Libraries for Thread Scheduling, Rendering, and so on...
struct DisplayList {  DisplayList& operator=(DisplayList &&d) {    data_ = d.data_;    d.data_ = nullptr;  }  ....}

Предупреждение PVS-Studio: V591 Non-void function should return a value. px_render.h 398

Библиотека Universal


Краткое описание библиотеки Universal:
The goal of Universal Numbers, or unums, is to replace IEEE floating-point with a number system that is more efficient and mathematically consistent in concurrent execution environments.
Первая ошибка

template<typename Scalar>vector<Scalar> operator*(double scalar, const vector<Scalar>& v) {  vector<Scalar> scaledVector(v);  scaledVector *= scalar;  return v;}

Предупреждение PVS-Studio: V1001 The 'scaledVector' variable is assigned but is not used by the end of the function. vector.hpp 124

Опечатка. Вместо исходного вектора v из функции нужно вернуть новый вектор scaledVector.

Аналогичную опечатку можно увидеть здесь: V1001 The 'normalizedVector' variable is assigned but is not used by the end of the function. vector.hpp 131

Вторая ошибка

template<typename Scalar>class matrix {  ....  matrix& diagonal() {  }  ....};

Предупреждение PVS-Studio: V591 Non-void function should return a value. matrix.hpp 109

Третья ошибка

template<size_t fbits, size_t abits>void module_subtract_BROKEN(  const value<fbits>& lhs, const value<fbits>& rhs, value<abits + 1>& result){  if (lhs.isinf() || rhs.isinf()) {    result.setinf();    return;  }  int lhs_scale = lhs.scale(),      rhs_scale = rhs.scale(),      scale_of_result = std::max(lhs_scale, rhs_scale);  // align the fractions  bitblock<abits> r1 =    lhs.template nshift<abits>(lhs_scale - scale_of_result + 3);  bitblock<abits> r2 =    rhs.template nshift<abits>(rhs_scale - scale_of_result + 3);  bool r1_sign = lhs.sign(), r2_sign = rhs.sign();  //bool signs_are_equal = r1_sign == r2_sign;  if (r1_sign) r1 = twos_complement(r1);  if (r1_sign) r2 = twos_complement(r2);  // <=  ....}

Предупреждение PVS-Studio: V581 The conditional expressions of the 'if' statements situated alongside each other are identical. Check lines: 789, 790. value.hpp 790

Классическая ошибка, возникшая из-за Copy-Paste. Взяли и размножили строчку:

if (r1_sign) r1 = twos_complement(r1);

Поменяли в ней r1 на r2:

if (r1_sign) r2 = twos_complement(r2);

А поменять r1_sign забыли. Правильный вариант:

if (r2_sign) r2 = twos_complement(r2);

Библиотека Chobo Single-Header Libraries


Краткое описание библиотеки Chobo Single-Header Libraries:
A collection of small single-header C++11 libraries by Chobolabs.
Первая ошибка

template <typename T, typename U, typename Alloc = std::allocator<T>>class vector_view{  ....  vector_view& operator=(vector_view&& other)  {    m_vector = std::move(other.m_vector);  }  ....}

Предупреждение PVS-Studio: V591 Non-void function should return a value. vector_view.hpp 163

Вторая ошибка

template <typename UAlloc>vector_view& operator=(const std::vector<U, UAlloc>& other){  size_type n = other.size();  resize(n);  for (size_type i = 0; i < n; ++i)  {    this->at(i) = other[i];  }}

Предупреждение PVS-Studio: V591 Non-void function should return a value. vector_view.hpp 184

Библиотека PGM-index


Краткое описание библиотеки PGM-index:
The Piecewise Geometric Model index (PGM-index) is a data structure that enables fast lookup, predecessor, range searches and updates in arrays of billions of items using orders of magnitude less space than traditional indexes while providing the same worst-case query time guarantees.
Первая ошибка

char* str_from_errno(){#ifdef MSVC_COMPILER  #pragma warning(disable:4996)  return strerror(errno);#pragma warning(default:4996)#else  return strerror(errno);#endif}

Предупреждение PVS-Studio: V665 Possibly, the usage of '#pragma warning(default: X)' is incorrect in this context. The '#pragma warning(push/pop)' should be used instead. Check lines: 9170, 9172. sdsl.hpp 9172

Неправильное временное отключение предупреждения компилятора. Подобные неаккуратности ещё как-то простительны пользовательскому коду. Но это точно недопустимо в header-only библиотеках.

Вторая ошибка

template<class t_int_vec>t_int_vec rnd_positions(uint8_t log_s, uint64_t& mask,                        uint64_t mod=0, uint64_t seed=17){  mask = (1<<log_s)-1;         // <=  t_int_vec rands(1<<log_s ,0);  set_random_bits(rands, seed);  if (mod > 0) {    util::mod(rands, mod);  }  return rands;}

Предупреждение PVS-Studio: V629 Consider inspecting the '1 << log_s' expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64-bit type. sdsl.hpp 1350

Один из правильных вариантов:

mask = ((uint64_t)(1)<<log_s)-1;

Библиотека Hnswlib


Краткое описание библиотеки Hnswlib:
Header-only C++ HNSW implementation with python bindings. Paper's code for the HNSW 200M SIFT experiment.
template<typename dist_t>class BruteforceSearch : public AlgorithmInterface<dist_t> {public:  BruteforceSearch(SpaceInterface <dist_t> *s, size_t maxElements) {    maxelements_ = maxElements;    data_size_ = s->get_data_size();    fstdistfunc_ = s->get_dist_func();    dist_func_param_ = s->get_dist_func_param();    size_per_element_ = data_size_ + sizeof(labeltype);    data_ = (char *) malloc(maxElements * size_per_element_);    if (data_ == nullptr)      std::runtime_error(        "Not enough memory: BruteforceSearch failed to allocate data");    cur_element_count = 0;  }  ....}

Предупреждение PVS-Studio: V596 The object was created but it is not being used. The 'throw' keyword could be missing: throw runtime_error(FOO); bruteforce.h 26

Забыли перед std::runtime_error написать оператор throw.

Ещё одна такая ошибка: V596 The object was created but it is not being used. The 'throw' keyword could be missing: throw runtime_error(FOO); bruteforce.h 161

Библиотека tiny-dnn


Краткое описание библиотеки tiny-dnn:
tiny-dnn is a C++14 implementation of deep learning. It is suitable for deep learning on limited computational resource, embedded systems and IoT devices.
Первая ошибка

class nn_error : public std::exception { public:  explicit nn_error(const std::string &msg) : msg_(msg) {}  const char *what() const throw() override { return msg_.c_str(); } private:  std::string msg_;};inline Device::Device(device_t type, const int platform_id, const int device_id)  : type_(type),    has_clcuda_api_(true),    platform_id_(platform_id),    device_id_(device_id) {  ....#else  nn_error("TinyDNN has not been compiled with OpenCL or CUDA support.");#endif}

Предупреждение PVS-Studio: V596 The object was created but it is not being used. The 'throw' keyword could be missing: throw nn_error(FOO); device.h 68

nn_error это не функция, генерирующая исключение, а просто класс. Поэтому правильно его использовать так:

throw nn_error("TinyDNN has not been compiled with OpenCL or CUDA support.");

Ещё одно неправильное использование этого класса: V596 The object was created but it is not being used. The 'throw' keyword could be missing: throw nn_error(FOO); conv2d_op_opencl.h 136

Вторая ошибка

inline std::string format_str(const char *fmt, ...) {  static char buf[2048];#ifdef _MSC_VER#pragma warning(disable : 4996)#endif  va_list args;  va_start(args, fmt);  vsnprintf(buf, sizeof(buf), fmt, args);  va_end(args);#ifdef _MSC_VER#pragma warning(default : 4996)#endif  return std::string(buf);}

Предупреждение PVS-Studio: V665 Possibly, the usage of '#pragma warning(default: X)' is incorrect in this context. The '#pragma warning(push/pop)' should be used instead. Check lines: 139, 146. util.h 146

Библиотека Dlib


Краткое описание библиотеки Dlib:
TDLib (Telegram Database library) is a cross-platform library for building Telegram clients. It can be easily used from almost any programming language.
Первая ошибка

Ради интереса попробуйте найти эту ошибку самостоятельно.

class bdf_parser{public:  enum bdf_enums  {    NO_KEYWORD = 0,    STARTFONT = 1,    FONTBOUNDINGBOX = 2,    DWIDTH = 4,    DEFAULT_CHAR = 8,    CHARS = 16,    STARTCHAR = 32,    ENCODING = 64,    BBX = 128,    BITMAP = 256,    ENDCHAR = 512,    ENDFONT = 1024  };  ....  bool parse_header( header_info& info )  {    ....    while ( 1 )    {      res = find_keywords( find | stop );      if ( res & FONTBOUNDINGBOX )      {          in_ >> info.FBBx >> info.FBBy >> info.Xoff >> info.Yoff;          if ( in_.fail() )              return false;    // parse_error          find &= ~FONTBOUNDINGBOX;          continue;      }      if ( res & DWIDTH )      {          in_ >> info.dwx0 >> info.dwy0;          if ( in_.fail() )              return false;    // parse_error          find &= ~DWIDTH;          info.has_global_dw = true;          continue;      }      if ( res & DEFAULT_CHAR )      {          in_ >> info.default_char;          if ( in_.fail() )              return false;    // parse_error          find &= ~DEFAULT_CHAR;          continue;      }      if ( res & NO_KEYWORD )          return false;    // parse_error: unexpected EOF      break;    }  ....};

Нашли?

Растерянный Траволта-Единорог

Она здесь:

if ( res & NO_KEYWORD )

Предупреждение PVS-Studio: V616 The 'NO_KEYWORD' named constant with the value of 0 is used in the bitwise operation. fonts.cpp 288

Именованная константа NO_KEYWORD имеет значение 0. А следовательно условие не имеет смысла. Правильно было бы написать:

if ( res == NO_KEYWORD )

Ещё одна неправильная проверка находится здесь: V616 The 'NO_KEYWORD' named constant with the value of 0 is used in the bitwise operation. fonts.cpp 334

Вторая ошибка

void set(std::vector<tensor*> items){  ....  epa.emplace_back(new enable_peer_access(*g[0], *g[i]));  ....}

Предупреждение PVS-Studio: V1023 A pointer without owner is added to the 'epa' container by the 'emplace_back' method. A memory leak will occur in case of an exception. tensor_tools.h 1665

Чтобы понять, в чём тут заковыка, предлагаю познакомиться с документацией на диагностику V1023.

Третья ошибка

template <    typename detection_type,     typename label_type     >bool is_track_association_problem (  const std::vector<    std::vector<labeled_detection<detection_type,label_type> > >& samples){  if (samples.size() == 0)    return false;  unsigned long num_nonzero_elements = 0;  for (unsigned long i = 0; i < samples.size(); ++i)  {    if (samples.size() > 0)      ++num_nonzero_elements;  }  if (num_nonzero_elements < 2)    return false;  ....}

Предупреждение PVS-Studio: V547 Expression 'samples.size() > 0' is always true. svm.h 360

Это очень, очень странный код! Если запускается цикл, то значит условие (samples.size() > 0) всегда истинно. Следовательно, цикл можно упростить:

for (unsigned long i = 0; i < samples.size(); ++i){  ++num_nonzero_elements;}

После этого становится понятно, что цикл вообще не нужен. Можно написать гораздо проще и эффективнее:

unsigned long num_nonzero_elements = samples.size();

Но это ли планировалось сделать? Код явно заслуживает внимательного изучения программистом.

Четвёртая ошибка

class console_progress_indicator{  ....  double seen_first_val;  ....};bool console_progress_indicator::print_status (  double cur, bool always_print){  ....  if (!seen_first_val)  {    start_time = cur_time;    last_time = cur_time;    first_val = cur;    seen_first_val = true;  // <=    return false;  }  ....}

Предупреждение PVS-Studio: V601 The bool type is implicitly cast to the double type. console_progress_indicator.h 136

В член класса, имеющий тип double, записывают значение true. Хм

Пятая ошибка

void file::init(const std::string& name){  ....  WIN32_FIND_DATAA data;  HANDLE ffind = FindFirstFileA(state.full_name.c_str(), &data);  if (ffind == INVALID_HANDLE_VALUE ||      (data.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY) != 0)  {    throw file_not_found("Unable to find file " + name);                  }  else  {    ....  } }

Предупреждение PVS-Studio: V773 The exception was thrown without closing the file referenced by the 'ffind' handle. A resource leak is possible. dir_nav_kernel_1.cpp 60

Если найдена директория, то генерируется исключение. Но кто будет закрывать дескриптор?

Шестая ошибка

Ещё одно очень странное место.

inline double poly_min_extrap(double f0, double d0,                              double x1, double f_x1,                              double x2, double f_x2){  ....  matrix<double,2,2> m;  matrix<double,2,1> v;  const double aa2 = x2*x2;  const double aa1 = x1*x1;  m =  aa2,       -aa1,      -aa2*x2, aa1*x1;     v = f_x1 - f0 - d0*x1,      f_x2 - f0 - d0*x2;  ....}

Предупреждение PVS-Studio: V521 Such expressions using the ',' operator are dangerous. Make sure the expression is correct. optimization_line_search.h 211

Планируется инициализировать матрицы. Но ведь все эти aa2, f_x1, d0 и так далее это просто переменные типа double. Значит, запятые не разделяют аргументы, предназначенные для создания матриц, а являются обыкновенными comma operator, которые возвращают значение правой части.

Заключение


В начале статьи я привёл пример, как можно совместить несколько полезных дел сразу. Использование статического анализатора тоже одновременно полезно по нескольким причинам:

  • Повышение квалификации. Изучая предупреждения анализатора можно узнать много нового и полезного. Примеры: memset, #pragma warning, emplace_back, strictly aligned.
  • Выявление опечаток, ошибок и потенциальных уязвимостей на ранних этапах.
  • Код постепенно становится качественнее, проще, понятней.
  • Вы можете гордиться и всем рассказывать, что используете современные технологии при разработке проектов :). И это юмор только отчасти. Это настоящее конкурентное преимущество.

Вопрос только в том, как начать, как безболезненно внедрить и как правильно использовать? С этим вам помогут следующие статьи:



Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Checking a Header-Only C++ Library Collection (awesome-hpp).
Подробнее..

Перевод Немного простого Javascript способно на многое

28.06.2020 20:20:26 | Автор: admin
Я никогда не работала профессиональным фронтенд-разработчиком, и хотя уже 15 лет пишу HTML/CSS/JS для небольших побочных проектов, но все проекты были довольно маленькими. Бывает, что я не пишу на Javascript в течение многих лет между этими проектами, и часто не уверена в том, что всё делаю правильно.

Отчасти поэтому я часто использовала библиотеки! Десять лет назад я использовал jQuery, а где-то с 2017 года для моих маленьких проектов много использую vue.js (можете посмотреть небольшую игру типа сапёр, которую я сделала в качестве вступления к Vue).

Но на прошлой неделе, впервые за долгое время, я написала простой Javascript без библиотеки, и это было весело, поэтому я хотела немного поговорить об этом!

Экспериментируя с простым Javascript


Мне очень нравится Vue. Но на прошлой неделе, когда я начала разрабатывать https://questions.wizardzines.com, у меня были немного другие ограничения, чем обычно я хотела использовать один и тот же HTML для создания PDF-файла (с Prince) и создания интерактивной версии вопросов.

Я действительно не видела, как это возможно с Vue (потому что Vue хочет создать весь HTML сам), и поскольку это был небольшой проект, то решила попробовать написать его на простом Javascript без библиотек просто написать немного HTML/CSS и добавить один <script src="js/script.js"> </script>.

Я такого не делала уже некоторое время, и по ходу узнала несколько вещей, которые сделали бы процесс проще, чем я думала, когда начинала.

Делаем почти всё, добавляя и удаляя CSS-классы


Я решила реализовать почти весь пользовательский интерфейс, просто добавояя и удаляя CSS-классы и используя CSS-переходы, если я хочу анимировать переход.

Вот небольшой пример, где нажатие кнопки next для вопроса добавляет класс done к родительскому div.

div.querySelector('.next-question').onclick = function () {    show_next_row();    this.parentElement.parentElement.classList.add('done');}

Это сработало довольно хорошо. Мой CSS, как всегда, немного хаотичный, но кажется управляемым.

Добавление/удаление CSS-классов с помощью .classList


Я начала с редактирования классов следующим образом: x.className = 'new list of classes'. Хотя это немного грязно, и я задалась вопросом, есть ли лучший способ. И он был!

CSS-классы можно добавить так:

let x = document.querySelector('div');x.classList.add('hi');x.classList.remove('hi');

element.classList.remove('hi') это намного чище, чем мой прежний метод.

Найдите элементы с помощью document.querySelectorAll


Когда я начала изучать jQuery, помню, я думала, что если нужно легко найти что-то в DOM, нужно использовать jQuery (например, $('.class')). Я только на этой неделе узнала, что вместо этого вы просто можете написать document.querySelectorAll('.some-class'), и тогда вам не нужно зависеть ни от какой библиотеки!

Мне стало любопытно, когда был представлен querySelectorAll. Я немного погуглила, и похоже, что Selectors API был создан где-то между 2008 и 2013 годами я нашла сообщение от автора jQuery, обсуждающего предлагаемую реализацию в 2008 году, и сообщение в блоге от 2011 года, в котором говорилось, что к тому времени он был во всех основных браузерах, так что, возможно, его не существовало, когда я начала использовать jQuery, но он определённо существовал уже довольно давно :)

Установка .innerHTML


В одном месте я хотела изменить HTML-содержимое кнопки. Создание элементов DOM с помощью document.createElement довольно утомительно, поэтому я постаралвсь свести работу к минимуму и вместо этого установила .innerHTML для нужной HTML-строки:

  button.innerHTML = `<i class="icon-lightbulb"></i>I learned something!    <object data="/confetti.svg" width="30" height = "30"> </object>    `;

Прокрутка страницы с помощью .scrollIntoView


Последняя забавная вещь, о которой я узнала, это .scrollIntoView. Я хотела сделать автоматическую прокрутку до следующего вопроса, когда кто-то нажимает кнопку Следующий вопрос. Оказывается, это всего лишь одна строка кода:

row.classList.add('revealed');row.scrollIntoView({behavior: 'smooth', block: 'center'});

Ещё один пример ванильного JS: peekobot


Ещё один небольшой пример простой библиотеки JS, которую я посчитала хорошей, это peekobot, который представляет собой небольшой интерфейс чат-бота, состоящий из 100 строк JS/CSS.

Как видно из его Javascript, он использует несколько подобных шаблонов очень много .classList.add, некоторые добавления элементов в DOM, некоторые .querySelectorAll.

Из исходников peekobot я узнала об элементе .closest, который находит ближайшего предка, соответствующего данному селектору. Похоже, это был бы хороший способ избавиться от некоторых .parentElement.parentElement, которые я написала в своём Javascript, что казалось немного хрупким.

Простой Javascript может очень многое!


Я была очень удивлена тем, как много получилось сделать с помощью простого JS. В итоге я написала около 50 строк JS, которые делали всё, что я хотела сделать, и ещё немного больше для сбора некоторых анонимных показателей об обучении пользователей.

Как обычно с моими фронтенд-постами, это не должно быть серьёзным советом по фронтенд-разработке. Моя цель в том, чтобы писать небольшие веб-сайты с менее чем 200 строками Javascript, которые в основном работают. Если вы также вертитесь в мире фронтенда, я надеюсь, что это немного поможет!
Подробнее..

Перевод Удобная платформа для подбора библиотек и фреймворков JavaScript openbase

15.10.2020 14:05:55 | Автор: admin
image

Что за зверь?


openbase.io

Находите и сравнивайте пакеты с открытым исходным кодом с отзывами пользователей, категоризацией и беспрецедентной информацией о популярности, надежности, активности пакетов и многом другом

Думаю, сейчас во многих языках программирования есть менеджер пакетов, который позволяет каким-то образом получать информацию о них.

Как я обычно выбираю себе библиотеку



В моем случае большинство из них для js, поэтому метод выбора библиотеки следующий.

  1. Поискать в npm
  2. Подобрать айтемы, подходящие по описанию, имеющие достаточное количество загрузок и получавшие обновления в последние несколько месяцев.
  3. Проверить доступность документации и readme на GitHub; иногда проверять наличие обновлений по ключевым вопросам.
    Кстати, я стараюсь не принимать решение только по наличию или отсутствию документов. Как правило, они могут находиться в процессе релиза или экстренных правок, о чем можно узнать на issue board, где разработчики и юзеры могут контактировать друг с другом.

После этого, я выбираю несколько вариантов, быстро проверяю их и решаю, какие буду применять.

На деле все оказывается не так просто: прочитал Readme файл, подумал: О, вот это было бы полезно, а оказывается, что функции, которые, как я думал, заработают, на деле не поддерживаются, поэтому придется прописать их самому. Всегда хотелось, чтобы были какие-нибудь ревью: ведь есть же люди, которые до меня сталкивались с подобными проблемами.

Плюсы openspace Ревью


Для моих изысканий идеально подошел сервис openbase.io

Он подходит под все пункты моего плана, плюс есть много ревью от других пользователей. Лично мне хотелось бы чуть более светлую цветовую гамму интерфейса, но это уже мелочи.

В обзоре содержатся впечатления от использования и количество звезд. Сами обзоры тоже можно оценивать по степени полезности, что, в свою очередь, помогает понять ценность самого обзора.

Например, на React оставлено более 570 отзывов.

Общая информация


image

Ревью


image

Плюсы openbase Можно сразу найти туториалы


На боковой панели есть вкладка tutorial, в которой вам все подробно разъяснят, в том числе через ролики на YouTube.

image

image

Плюсы openbase информация об альтернативных пакетах


По вкладке Alternatives там же, на сайдбаре, можно найти библиотеки, схожие с теми, на которые вы наткнулись на данный момент. На картинке ниже результаты для React, особых откровений не ожидается, но в зависимости от результатов поиска можете наткнуться на интересные открытия. Думаю, у этой функции хороший потенциал, можно делать свои проекты более оригинальными за счет новых, небанальных пакетов.

image

Спасибо за прочтение!

Мы будем очень рады, если вы расскажете нам, понравилась ли вам данная статья, понятен ли перевод, была ли она вам полезна?
Подробнее..

Перевод Устали от глупых шуток о JS? Напишите свою библиотеку

26.12.2020 18:14:42 | Автор: admin
В JavaScript есть немало моментов, вызывающих вопрос Чего???. Несмотря на то что у большинства из них есть логическое объяснение, если вы вникнете, они всё равно могут удивлять. Но JavaScript точно не заслуживает возмутительных шуток типа этой смеха. Например, иногда мы видим такие шутки:


В этом случае критика абсолютно не заслужена. Давайте разбираться почему.



JavaScript, как и какой-либо другой популярный язык программирования, представляет числа, использующие единый стандарт. Если быть точным, это стандарт IEEE 754 для чисел в 64-битном двоичном формате. Давайте попробуем проверить эту же шутку на других языках:

Как насчёт Ruby? На каком языке 0.1 + 0.2 не равно 0.3?

$ irbirb(main):001:0> 0.1 + 0.2 == 0.3=> falseirb(main):002:0> 0.1 + 0.2=> 0.30000000000000004

Ruby! Какой глупый язык.

Или Clojure? На каком языке 0.1 + 0.2 не равно 0.3?

$ cljClojure 1.10.1user=> (== (+ 0.1 0.2) 0.3)falseuser=> (+ 0.1 0.2)0.30000000000000004

Clojure! Какой глупый язык.

Или как насчёт могучего Haskell? На каком языке 0.1 + 0.2 не равно 0.3?

$ ghciGHCi, version 8.10.1: https://www.haskell.org/ghc/  :? for helpPrelude> 0.1 + 0.2 == 0.3FalsePrelude> 0.1 + 0.20.30000000000000004

Haskell! Хахаха. Какой глупый язык

Вы поняли мысль. Проблема здесь не в JavaScript. Это большая проблема представления чисел с плавающей точкой в двоичном виде. Но я не хочу пока вдаваться в подробности IEEE 754. Потому что, если нам нужны произвольные точные числа, JavaScript делает их возможными. С октября 2019 года BigInt официально входит в стандарт TC39 ECMAScript.

Зачем беспокоиться об этом?


Мы продержались с IEEE 754 целую вечность. Большую часть времени это не кажется проблемой. Правда. Почти всегда это не проблема. Но иногда это всё-таки проблема. И в такие моменты хорошо иметь варианты.

Например, в начале этого года я работал над библиотекой диаграмм. Хотел нарисовать свечные графики на SVG. А в SVG есть такая аккуратная функция, называемая transform. Вы можете применить её к группе элементов, и она изменит систему координат для этих элементов. Так что с небольшой осторожностью вы можете упростить генерацию области диаграммы. Вместо того чтобы вычислять координаты графика для каждой свечи, вы указываете одно преобразование. А затем определяете каждую свечу, используя значения сырых данных. Очень аккуратно. По крайней мере в теории.

Но в тестах свойств у меня были проблемы. Если бы график был маленьким, а значения данных были большими, я бы получил ошибки округления. И зачастую это нормально. Но на графике некоторые пиксели должны выстраиваться в линию. Иначе рисунок выглядит неправильно. Так что я начал изучать BigInt. Итогом стала библиотека, которую я назвал Ratio. И я покажу, как она пишется.

Класс дроби


Проблема с числами с плавающей точкой их двоичное представление. Компьютеры выполняют все свои вычисления в двоичном виде. И для целых чисел эта двоичность подходит. Проблема приходит, когда мы хотим представить десятичные числа. Например, в англоязычных странах, таких как Австралия, мы пишем десятичные числа следующим образом:

$3.1415926$


Часть слева от точек ( ) это целая часть, а справа от точки дробная часть. Но проблема в том, что некоторые числа имеют дробные части, которые нелегко разделить на две. Так что их трудно представить в двоичном виде. Но та же проблема возникает при основании 10. Например дробь 10/9. Можно попробовать написать что-нибудь вроде этого:

$1.111111111111111111111111111111111111.11111111111111111111111111111111111$


Однако это приближение. Чтобы представить 10/9 точно, единицы должны быть бесконечными. Поэтому мы должны использовать какую-то другую нотацию для представления повторяющихся. Например такую:

$1.\dot{1}$



Эта точка над единицей указывает на то, что единицы продолжаются. Но в большинстве языков программирования этой точки нет.

Заметьте, что 10/9 имеет идеальную точность. И всё, что нужно для точности, это два кусочка информации. Это числитель и знаменатель. С помощью одного значения BigInt мы можем представлять произвольно большие целые числа. Но если мы создадим пару из целых чисел, то сможем представлять произвольно большие или маленькие числа.

В JavaScript это может выглядеть так:

// file: ratio.jsexport default class Ratio {  // We expect n and d to be BigInt values.  constructor(n, d) {    this.numerator = n;    this.denominator = d;  }}

Итак, мы проделали самую хитрую часть. Изобрели способ представления чисел с почти бесконечной точностью. (Мы все еще ограничены объемом памяти наших устройств.) Осталось только применить математику. Так что давайте добавим функциональности.

Равенство


Первое, что хочется сделать, это сравнить две дроби. Зачем? Потому, что мне нравится сначала писать тесты. Если я могу сравнить две дроби на равенство, то писать тесты намного проще.

В простом случае написать метод равенства довольно легко:

// file: ratio.jsexport default class Ratio {  constructor(n, d) {    this.numerator = n;    this.denominator = d;  }  equals(other) {    return (      this.numerator === other.numerator &&      this.denominator === other.denominator    );  }}

Вот и хорошо. Но было бы неплохо, если бы наша библиотека могла сообщить, что 1/2 равна 2/4. Для этого нужно упростить дробь. То есть, прежде чем проверять равенство, мы хотим уменьшить числители и знаменатели обеих дробей до как можно более маленьких чисел. Итак, как мы сделаем это?

Наивный подход заключается в прогоне всех чисел от 1 до min(n,d) (где nn и dd числитель и знаменатель соответственно). И это то, что я попробовал вначале. Код выглядел как-то так:

function simplify(numerator, denominator) {    const maxfac = Math.min(numerator, denominator);    for (let i=2; i<=maxfac; i++) {      if ((numerator % i === 0) && (denominator % i === 0)) {        return simplify(numerator / i, denominator / i);      }    }    return Ratio(numerator, denominator);}

И, как и следовало ожидать, он невероятно медленный. Мои тесты заняли вечность. Так что нам нужен более эффективный подход. К счастью, греческий математик нашел его пару тысячелетий назад. Решение применение алгоритма Евклида. Это способ найти наибольший общий делитель двух целых чисел.

Рекурсивная версия алгоритма Евклида красива и элегантна:

function gcd(a, b) {    return (b === 0) ? a : gcd(b, a % b);}

Применима мемоизация, что делает алгоритм довольно привлекательным. Но, увы, у нас еще нет хвостовой рекурсии в V8 или SpiderMonkey. (По крайней мере не на момент написания статьи.) Это означает, что если мы запустим его с достаточно большими целыми числами, то получим переполнение стека. А большие целые числа это что-то вроде точки отсчёта.

Так что вместо этого воспользуемся итерационной версией:

// file: ratio.jsfunction gcd(a, b) {    let t;    while (b !== 0) {        t = b;        b = a % b;        a = t;    }    return a;}

Не так элегантно, но делает свою работу. И с этим кодом мы можем написать функцию для упрощения дробей. Пока мы это делаем, мы внесём и небольшое изменение, чтобы знаменатели всегда были положительными (т. е. для отрицательных чисел знак меняет только числитель).

// file: ratio.jsfunction sign(x) {  return x === BigInt(0) ? BigInt(0)       : x > BigInt(0)   ? BigInt(1)        /* otherwise */   : BigInt(-1);}function abs(x) {  return x < BigInt(0) ? x * BigInt(-1) : x;}function simplify(numerator, denominator) {  const sgn = sign(numerator) * sign(denominator);  const n = abs(numerator);  const d = abs(denominator);  const f = gcd(n, d);  return new Ratio((sgn * n) / f, d / f);}

И теперь мы можем написать наш метод равенства:

// file: ratio.js -- inside the class declaration  equals(other) {    const a = simplify(this);    const b = simplify(other);    return (      a.numerator === b.numerator &&      a.denominator === b.denominator    );  }

Теперь можно сравнить две дроби на равенство. Может показаться, что это не так уж и много, но это значит, что мы можем написать юнит-тесты и убедиться, что наша библиотека работает, как ожидается.

Преобразование в другие типы


Не буду утомлять, выписывая все юнит-тесты моей библиотеки. Но было бы неплохо конвертировать дроби в другие форматы. Например, мы можем захотеть представить их как строку в отладочных сообщениях. Или, возможно, мы захотим преобразовать их в числа. Итак, давайте переопределим методы .toString() и .toValue() для нашего класса.

Метод .toString() проще всего, так что давайте начнём с него.

// file: ratio.js -- inside the class declaration  toString() {    return `${this.numerator}/${this.denominator}`;  }

Достаточно просто. Но как насчёт преобразования обратно в число? Один из способов сделать это просто разделить числитель на знаменатель:

// file: ratio.js -- inside the class declaration  toValue() {    return  Number(this.numerator) / Number(this.denominator);  }

Зачастую это работает. Но, возможно, мы захотим немного подправить код. Весь смысл нашей библиотеки заключается в том, что мы используем большие целые числа для получения необходимой точности. И иногда эти целые числа будут слишком большими, чтобы их можно было преобразовать обратно к типу Number. Но мы хотим получить Number как можно ближе к истине, где это возможно. Так что мы выполняем немного арифметических действий, пока конвертируем BigInt в Number:

// file: ratio.js -- inside the class declaration  toValue() {    const intPart = this.numerator / this.denominator;    return (      Number(this.numerator - intPart * this.denominator) /        Number(this.denominator) + Number(intPart)    );  }

Извлекая целую часть, мы уменьшаем размер значений BigInt, прежде чем преобразовать их в Number. Есть и другие способы сделать это, у которых меньше проблем с диапазоном. В основном они сложнее и медленнее. Если вам интересно, я рекомендую вам заглянуть в них глубже. Но в этой статье простой подход охватывает достаточно случаев, чтобы быть полезным.

Умножение и деление


Сделаем что-нибудь с числами. Как насчёт умножения и деления? Это несложно для дробей. Умножаем числители на числители, а знаменатели на знаменатели.

// file: ratio.js -- inside the class declaration  times(x) {    return simplify(      x.numerator * this.numerator,      x.denominator * this.denominator    );  }

Деление похоже на код выше. Переворачиваем вторую дробь, затем умножаем.

// file: ratio.js -- inside the class declaration  divideBy(x) {    return simplify(      this.numerator * x.denominator,      this.denominator * x.numerator    );  }

Сложение и вычитание


Теперь у нас есть умножение и деление. Логически следующая вещь, которую нужно написать, это сложение и вычитание. Это немного сложнее, чем умножение и деление. Но не слишком.
Чтобы сложить две дроби, сначала нужно привести их к одному знаменателю, затем сложить числители. В коде это может выглядеть примерно так:

// file: ratio.js -- inside the class declaration  add(x) {    return simplify(      this.numerator * x.denominator + x.numerator * this.denominator,      this.denominator * x.denominator    );  }

Всё умножается на знаменатели. И мы используем simplify(), чтобы сохранялась дробь как можно меньше в смысле чисел числителя и знаменателя.
Вычитание похоже на сложение. Мы манипулируем двумя дробями так, чтобы одинаковые знаменатели выстраивались в ряд, как раньше. Затем не складываем, а вычитаем.

// file: ratio.js -- inside the class declaration  subtract(x) {    return simplify(      this.numerator * x.denominator - x.numerator * this.denominator,      this.denominator * x.denominator    );  }

Итак, у нас есть основные операторы. Можно складывать, вычитать, умножать и делить. Но нам всё ещё нужно несколько других методов. В частности, цифры имеют важное свойство: мы можем сравнивать их друг с другом.

Сравнения


Мы уже обсуждали .equals(). Но нам нужно нечто большее, чем просто равенство. Мы также хотели бы иметь возможность определить отношения дробей больше, меньше. Поэтому создадим метод .lte(), который расскажет нам, является ли одна дробь меньшей или равной другой дроби. Как и в случае .equals(), не очевидно, какая из двух дробей меньше. Чтобы сравнить их, нам нужно преобразовать обе к одному знаменателю, затем сравнить числители. С небольшим упрощением это может выглядеть так:

// file: ratio.js -- inside the class declaration  lte(other) {    const { numerator: thisN, denominator: thisD } = simplify(      this.numerator,      this.denominator    );    const { numerator: otherN, denominator: otherD } = simplify(      other.numerator,      other.denominator    );    return thisN * otherD <= otherN * thisD;  }

Как только мы получим .lte() и .equals(), то сможем вывести остальные сравнения. Можно выбрать любой оператор сравнения. Но если у нас есть equals() и >, <, или , то мы сможем выводить остальные с помощью булевой логики. В данном случае мы выбрали lte(), потому что его использует стандарт FantasyLand. Вот как могут выглядеть другие операторы:

// file: ratio.js -- inside the class declaration  lt(other) {    return this.lte(other) && !this.equals(other);  }  gt(other) {    return !this.lte(other);  }  gte(other) {    return this.gt(other) || this.equals(other);  }

Округление


Теперь мы можем сравнить дроби. А ещё можем умножать и делить, складывать и вычитать. Но если мы собираемся делать больше интересного с нашей библиотекой, нам нужно больше инструментов. Удобные объекты JavaScript Math содержат методы .floor() и .ceil().
Начнём с .floor(). Floor принимает значение и округляет его вниз. При положительных числах это означает, что мы просто сохраняем целую часть и отбрасываем оставшуюся часть. Но для отрицательных чисел мы округляем вверх от нуля, так что отрицательным числам нужно уделить немного больше внимания.

// file: ratio.js -- inside the class declaration  floor() {    const one = new Ratio(BigInt(1), BigInt(0));    const trunc = simplify(this.numerator / this.denominator, BigInt(1));    if (this.gte(one) || trunc.equals(this)) {      return trunc;    }    return trunc.minus(one);  }

Теперь можно использовать код выше, чтобы рассчитать округленные вверх значения.

// file: ratio.js -- inside the class declaration  ceil() {    const one = new Ratio(BigInt(1), BigInt(0));    return this.equals(this.floor()) ? this : this.floor().add(one);  }

Сейчас у нас есть большая часть необходимого для многих математических операций. А с помощью .toValue() мы можем легко преобразовать вычисления обратно в десятичные числа. Но что, если мы хотим преобразовать число с плавающей точкой в дробь?

Число в дробь


Преобразование чисел в дроби сложнее, чем может показаться на первый взгляд. И есть много разных способов проделать это преобразование. Мой способ реализации не самый точный, но он достаточно хорош. Чтобы он сработал, сначала конвертируем число в строку, которая, как мы знаем, приобретёт формат последовательности. Для этого JavaScript предоставляет нам метод .toExponential(). Метод возвращает число в экспоненциальной нотации. Вот несколько примеров для понимания идеи:

let x = 12.345;console.log(x.toExponential(5));//  '1.23450e+1''x = 0.000000000042;console.log(x.toExponential(3));//  '4.200e-11'x = 123456789;console.log(x.toExponential(4));//  '1.2346e+8'

Код работает, представляя число в виде нормализованного десятичного значения и множителя. Нормализованный десятичный бит называется мантиссой, а множитель экспонентой. Здесь нормализованный означает, что абсолютное значение мантиссы всегда меньше 10. А экспонента всегда теперь 10. Мы указываем начало множителя с буквой 'e' (сокращение от 'exponent').

Преимущество этой нотации в том, что она последовательна. Всегда есть ровно одна цифра слева от десятичной точки. А .toExponential() позволяет указать, сколько значащих цифр мы хотим. Затем идет 'e' и экспонента всегда целое число. Поскольку значение последовательно, мы можем использовать наглое регулярное выражение, чтобы разобрать его.

Процесс идёт примерно так. Как уже упоминалось, .toExponential() принимает параметр для указания количества значащих цифр. Нам нужен максимум цифр. Итак, мы установили точность на 100 (столько позволит большинство JavaScript-движков). В этом примере, однако, мы будем придерживаться точности 10. Теперь представьте, что у нас есть число 0.987654321e0. Мы хотим перенести десятичную точку на 10 цифр вправо. Это дало бы нам 9876543210. Затем делим на 10^10, и получаем 9876543210/100000000. Это, в свою очередь, упрощает до 987654321/100000000.

Но мы должны обратить внимание на эту экспоненту. Если у нас есть число вроде 0.987654321e9, то мы всё равно сдвинем десятичную точку на 10 цифр вправо. Но мы делим на десять, к степени 10-9=1.

$0.98765432110^9 = 9876543210/ 10^1=$


$987654321/1$


Чтобы всё было именно так, мы определили пару вспомогательных функций:

// Transform a + or - character to +1 or -1function pm(c) {  return parseFloat(c + "1");}// Create a new bigint of 10^n. This turns out to be a bit// faster than multiplying.function exp10(n) {  return BigInt(`1${[...new Array(n)].map(() => 0).join("")}`);}

С их помощью мы можем собрать всю функцию fromNumber() воедино.

// file: ratio.js -- inside the class declaration  static fromNumber(x) {    const expParse = /(-?\d)\.(\d+)e([-+])(\d+)/;    const [, n, decimals, sgn, pow] =      x.toExponential(PRECISION).match(expParse) || [];    const exp = PRECISION - pm(sgn) * +pow;    return exp < 0      ? simplify(BigInt(`${n}${decimals}`) * exp10(-1 * exp), BigInt(1))      : simplify(BigInt(`${n}${decimals}`), exp10(exp));  }

Охвачено большинство основных функций. Мы можем перейти от чисел к дробям и обратно. Но для моего конкретного приложения мне нужно было большее. В частности нужно было найти возведение в степень и логарифмы.

Возведение в степень


Возведение в степень это когда число многократно умножается само на себя. Например, 2^3=222=8. Для простых случаев, когда степень целое число, есть встроенный оператор BigInt: **. Так что, если мы возводим в степень дробь, это хороший вариант. Вот так дробь возводится в степень:

$\left(\frac{x}{y}\right)^{n} = \frac{x^n}{y^n}$


Следовательно, первый отрезок нашего метода возведения в степень может выглядеть примерно так:

// file: ratio.js -- inside the class declaration  pow(exponent) {    if (exponent.denominator === BigInt(1)) {        return simplify(            this.numerator ** exponent.numerator,            this.denominator ** exponent.numerator        );    }  }

Прекрасно работает. Ну в основном хорошо. Теперь всё становится сложнее. Из-за пределов ограничений обеспечения и математики мы должны пойти на некоторые компромиссы. Возможно, нам придётся пожертвовать точностью ради получения ответа в разумные сроки.

Возведение в степень легко порождает большие числа. И когда числа становятся большими, всё замедляется. Пока я писал эту статью, я также написал вычисления, которые, не завершаясь, выполнялись в течение дней. Так что нужно соблюдать осторожность. Но ничего страшного. Все поставляется для BigInt.

Но есть и другая проблема. Что делать, если знаменатель степени не единица? Например, что, если бы мы хотели рассчитать 8^(2/3)?

К счастью, мы можем разделить эту проблему на две проблемы поменьше. Мы хотим привести одну дробь к степени другой. Например, мы можем отнести x/y к a/b. Законы возведения в степень гласят, что следующее эквивалентно:

$\left(\frac{x}{y}\right)^\frac{a}{b} = \left(\left(\frac{x}{y}\right)^\frac{1}{b}\right)^a = \left(\frac{x^\frac{1}{b}}{y^\frac{1}{b}}\right)^a$


Мы уже знаем, как привести одно число BigInt к степени другого числа BigInt. Но как насчёт дробной степени? Ну, есть ещё один эквивалент:

$x^\frac{1}{n} = \sqrt[n]{x}$


То есть приведение xx к степени 1n1n эквивалентно нахождению n-го корня из xx. Это означает, что если мы найдем способ вычислить n-й корень BigInt, то мы сможем вычислить любую степень.

При хорошо продуманном поиске в вебе, нахождение алгоритма оценки n-го корня не займёт много времени. Наиболее распространённый метод метод Ньютона. Он работает начиная с оценки, rr. Затем делается такой расчёт, чтобы получить лучшую оценку:

$\begin{align} r &\approx x^{\frac{1}{n}} \\ r^{\prime} &= \frac{1}{n}\left((n-1)r + \left(\frac{x}{r^{n-1}}\right)\right) \end{align}$


Мы продолжаем повторять эти расчёты до тех пор, пока не достигнем желаемой точности. К сожалению, есть некоторые корни, которые не могут быть представлены в виде конечной дроби. Другими словами, чтобы получить идеальную точность, нам понадобятся бесконечно длинные значения BigInt. На практике это означает, что мы должны выбрать произвольное ограничение итераций.

Мы вернемся к этому моменту. А пока давайте разберёмся, как вычислить достаточно точный корень n-й степени. Так как оценка rr будет дробью, мы можем записать её как:

$r = \frac{a}{b}.$


И это позволяет нам переписать расчёты так:

$\frac{a^{\prime}}{b^{\prime}} = \frac{(n - 1)a^{n} + x b^{n}}{n b a^{n - 1}}$


Теперь всё в терминах целочисленных вычислений, подходящих для использования с BigInt. Не стесняйтесь вставлять abab в уравнение для rr выше и проверьте мои выводы. В JavaScript это выглядит вот так:

const estimate = [...new Array(NUM_ITERATIONS)].reduce(r => {  return simplify(    (n - BigInt(1)) * r.numerator ** n + x * r.denominator ** n,    n * r.denominator * r.numerator ** (n - BigInt(1))  );}, INITIAL_ESTIMATE);

Мы просто повторяем это вычисление до тех пор, пока не достигнем подходящей точности для нашей оценки корня n-й степени. Проблема в том, что нам нужно придумать подходящие значения для наших констант. То есть NUM_ITERATIONS и INITIAL_ESTIMATE.
Многие алгоритмы начинаются с INITIAL_ESTIMATE в единицу. Это разумный выбор. Зачастую у нас нет хорошего способа предположить, каким может быть корень n-й степени. Но напишем обманку. Предположим (пока), что наш числитель и знаменатель находятся в диапазоне Number. Затем мы можем использовать Math.pow() для получения начальной оценки. Это может выглядеть так:

// Get an initial estimate using floating point math// Recall that x is a bigint value and n is the desired root.const initialEstimate = Ratio.fromNumber(    Math.pow(Number(x), 1 / Number(n)));

Итак, у нас есть значение для нашей первоначальной оценки. А как же NUM_ITERATION? Ну, на практике, то, что я делал, начиналось с предположения в 10. А потом я проводил тесты свойств. Я продолжал наращивать число до тех пор, пока вычисления укладывались в разумные сроки. И цифра, которая, наконец, сработала 1. Одна итерация. Это меня немного огорчает, но мы немного более точны, чем при вычислениях с плавающей точкой. На практике вы можете увеличивать это число, если не вычисляете много дробных степеней.

Для простоты мы извлечём вычисление n-го корня в отдельную функцию. Если сложить всё вместе, код может выглядеть так:

// file: ratio.js -- inside the class declaration  static nthRoot(x, n) {    // Handle special cases    if (x === BigInt(1)) return new Ratio(BigInt(1), BigInt(1));    if (x === BigInt(0)) return new Ratio(BigInt(0), BigInt(1));    if (x < 0) return new Ratio(BigInt(1), BigInt(0)); // Infinity    // Get an initial estimate using floating point math    const initialEstimate = Ratio.fromNumber(      Math.pow(Number(x), 1 / Number(n))    );    const NUM_ITERATIONS = 1;    return [...new Array(NUM_ITERATIONS)].reduce((r) => {      return simplify(        n -          BigInt(1) * (r.numerator ** n) +          x * (r.denominator ** n),        n * r.denominator * r.numerator ** (n - BigInt(1))      );    }, initialEstimate);  }  pow(n) {    const { numerator: nNumerator, denominator: nDenominator } = n.simplify();    const { numerator, denominator } = this.simplify();    if (nNumerator < 0) return this.invert().pow(n.abs());    if (nNumerator === BigInt(0)) return Ratio.one;    if (nDenominator === BigInt(1)) {      return new Ratio(numerator ** nNumerator, denominator ** nNumerator);    }    if (numerator < 0 && nDenominator !== BigInt(1)) {      return Ratio.infinity;    }    const { numerator: newN, denominator: newD } = Ratio.nthRoot(      numerator,      nDenominator    ).divideBy(Ratio.nthRoot(denominator, nDenominator));    return new Ratio(newN ** nNumerator, newD ** nNumerator);  }

Неидеально и медленно. Но задача стала в основном выполнимой. Остаётся вопрос, как получить оценку, если у нас целые числа больше Number.MAX_VALUE. Однако я оставлю это как упражнение для читателя; эта статья и так уже слишком длинная.

Логарифмы


Должен признать, логарифмы озадачили меня на несколько недель. Для моей разработки мне нужно вычислить логарифмы по основанию 10. Поэтому я пошёл искать алгоритмы для вычисления логарифмов. И их много. Но я не смог найти такой, который работал бы достаточно хорошо, чтобы включить его в математическую библиотеку.

Почему это так сложно? Моей целью было вычислить логарифмы ради точности большей, чем точность чисел с плавающей точкой. Иначе зачем всё это? Функция вычисления логарифма с типом числа с плавающей точкой, Math.log10(), быстрая и встроенная. Итак, я посмотрел на алгоритмы, которые дают способы итеративного вычисления логарифмов. И они работают. Но они медленны в получении точности выше точности числа с плавающей точкой. Не просто немного медленнее. Намного медленнее.

По мере того как мы проходим через итерации, дробь, которую мы строим, становится всё точнее. Но за эту точность приходится платить. Значения BigInt в дроби становятся всё больше и больше. И по мере того как они становятся больше, их умножение начинает занимать много времени. В какой-то момент я оставил вычисление на три дня. Но пока шли расчёты, я кое-что вспомнил.

Я вспомнил, что мне нужен метод log10() для того, чтобы можно было вычислять красивые масштабированные значения для графиков. И для этих вычислений каждый раз, когда я вызывал .log10(), я сразу же вызывал .floor(). Это означает, что мне нужна только целочисленная часть логарифма. Расчёт логарифма до 100 знаков после запятой был просто пустой тратой времени и мощностей.

Более того, есть простой способ вычислить целую часть логарифма по основанию 10. Всё, что нам нужно, это посчитать цифры. Наивная попытка может выглядеть так:

// file: ratio.js -- inside the class declaration  floorLog10() {    return simplify(BigInt((this.numerator / this.denominator).toString().length - 1), BigInt(1));  }

К сожалению, это не работает для значений меньше 1. Но даже в этом случае мы можем использовать некоторые логарифмические законы для работы с таким значением.

$\begin{align} \log_{10}\left(\frac{a}{b}\right) &= \log_{10}(a) - \log_{10}(b) \\ \log_{10}\left(\frac{1}{x}\right) &= \log_{10}(1) - \log_{10}(x) \\ &= -\log_{10}(x) \end{align}$


Поэтому:

$\log_{10}\left(\frac{b}{a}\right) = -\log_{10}\left(\frac{a}{b}\right)$


Собрав всё воедино, мы получаем более надежный метод floorLog10():

// file: ratio.js -- inside the class declaration  invert() {    return simplify(this.denominator, this.numerator);  }  floorLog10() {    if (this.equals(simplify(BigInt(0), BigInt(1)))) {      return new Ratio(BigInt(-1), BigInt(0));    }    return this.numerator >= this.denominator      ? simplify((this.numerator / this.denominator).toString().length - 1, 1)      : simplify(BigInt(-1), BigInt(1)).subtract(this.invert().floorLog10());  }

Опять. Зачем мучиться?


На данный момент библиотека имеет все необходимые функции для моего приложения, для работы с графиками. Но всё равно может быть интересно, зачем все эти неприятности? Уже есть несколько библиотек произвольной точности. Почему бы просто не использовать одну из них и не покончить с этим?

Честно говоря, я бы использовал существующую библиотеку в большинстве случаев. Особенно, если я тороплюсь. Нет смысла делать всё это, если кто-то уже сделал превосходящую работу.

Ключевое слово здесь превосходящую. И именно здесь в игру вступают мои мотивы желания написать свою собственную библиотеку. Метод floorLog10() выше идеальный пример. Он обеспечивает точный расчёт, который мне нужен для того, что хочу сделать я. Он делает это эффективно, примерно в шести строках кода.

Если бы я воспользовался чужой библиотекой, я бы столкнулся с одним из двух:

  1. Разработчики не реализовали log10() или любые другие логарифмические методы.

или

  1. Разработчики реализовали метод log10() (или его эквивалент).

В первом сценарии мне всё равно пришлось бы написать floorLog10(). Во второй ситуации я бы, вероятно, использовал их логарифмический метод. И мой код был бы медленнее и сложнее, чем должен быть.

Написание собственной библиотеки позволяет мне адаптировать её к приложению. Конечно, другие люди могут найти код полезным, но я не в ответе за их потребности. Чтобы моему приложению не приходилось таскать за собой сложный код, который оно никогда не использует.

Кроме всего этого, я многому научился, создавая свою собственную библиотеку. Теперь я на практике понимаю ограничения BigInt намного лучше, чем раньше. Я знаю, что могу настроить производительность метода корня n-й степени. Я могу настроить его в зависимости от того, сколько вычислений выполняю и какая точность мне нужна.

Иногда стоит написать свою собственную библиотеку общего назначения. Даже если ты не планируешь открывать код. Даже если никто больше ею не воспользуется. Ты можешь многому научиться, и, кроме того, это может принести радость.

Если вы хотите узнать больше о проблемах с числами с плавающей точкой, обратитесь к 0.30000000000000004.com. А если вы хотите посмотреть библиотеку целиком и сделать некоторые вычисления, то можете посмотреть эту песочницу с кодом.

image



Подробнее..

Перевод Система рекомендаций фильмов с GUI на Python

14.10.2020 18:14:52 | Автор: admin

Без опыта я никому не нужен! Где взять опыт? часто думают люди, осваивающие новую для себя сферу или изучающие новый язык программирования. Решение есть делать пет-проекты. Представленный под катом проект системы рекомендации фильмов не претендует на сложность и точность аналогичных систем от энтерпрайз-контор, но может стать практическим стартом для новичка, которому интересны системы рекомендации в целом. Этот пост также подойдет для демонстрации как использовать Python-библиотеку EasyGUI на практике.

Важное предупреждение: если вы крепкий миддл либо сеньор, то проект может показаться вам простым. Однако не стоит спешить опускать палец вниз и забывать про тех, кто не так опытен, и кому пост может быть полезен, ведь все мы когда-то были джунами.



Начало работы


В этой статье я расскажу, как создать базовую систему рекомендаций фильмов со встроенным графическим пользовательским интерфейсом. Прежде всего нам нужны данные. Чтобы получить хорошее представление о том, насколько хорошо система рекомендаций работает на самом деле, нам понадобится довольно большой набор данных. Используем MovieLens на 25M, который вы можете скачать здесь. Набор данных состоит из шести файлов .csv и файла readme, объясняющих набор данных. Не стесняйтесь взглянуть на него, если хотите. Мы будем использовать только эти три файла:

movies.csv; ratings.csv; tags.csv

Также потребуется несколько библиотек Python:

  1. NumPy
  2. Pandas
  3. Progress (pip install progress)
  4. Fuzzywuzzy (pip install fuzzywuzzy & pip install python-Levenshtein)
  5. EasyGUI

Вероятно, все они могут быть установлены через pip. Точные команды будут зависеть от ОС. Кроме того, должна работать любая IDE Python (см. материал по ним вот тут). Я пользуюсь Geany, легкой IDE для Raspbian. Посмотрим на набор данных:


movies.csv

Выше показан файл movies.csv с тремя столбцами данных, а именно: movieId, title и genres идентификатор фильма, название и жанр. Все очень удобно и просто. Будем работать со всеми тремя.

Ниже мы видим tags.csv. Здесь используются только столбцы movieId и tag, связывающие тег со столбцом movieId, которые также есть в файлах movies.csv и rating.csv.


tags.csv

И последний, но не менее важный файл: rating.csv. От этого парня мы возьмем столбцы movieId и rating.


ratings.csv

Отлично, теперь давайте запустим IDE и начнем. Импортируем библиотеки, как показано ниже. Pandas и NumPy хорошо известны в области Data Science. Fuzzywuzzy, EasyGUI и библиотека Progress менее известны^ судя по тому, что мне удалось собрать, однако вы, возможно, знакомы с ними. Я добавлю в код много комментариев, чтобы все было понятно. Посмотрите:



Программа в основном состоит из набора функций, кроме начальной загрузки набора данных и настройки дисплея для gui, в который будет подаваться массив NumPy. Без настройки отображения в строке 12, когда список рекомендуемых фильмов из системы слишком длинный, мы получим урезанное и бесполезное изображение.

Есть разные способы сортировки значений на дисплее, например, цикл по массиву и добавление каждой строки к переменной, инициализированной в пустой список, однако этот метод мы будем использовать именно в строке 12.

Строки 16, 17, 33 и 35 это, в основном, индикатор прогресса из библиотеки Progress. Цикл выполняется только один раз. Загрузка набора данных в мою систему занимает около 30 секунд, поэтому мы используем индикатор прогресса, чтобы показать, что набор данных загружается после запуска программы, как показано ниже. После этого нам не придется загружать его снова во время навигации по графическому интерфейсу.



Загрузка набора данных и главное меню графического интерфейса пользователя.

Внутри цикла, набор данных загружается во фреймы Pandas и данные немного изменяются для удобства. Начнем со слияния фильма и рейтингового фрейма в один фрейм. В столбце рейтинга есть несколько значений NaN, и мы будем иметь дело с этим, заполняя его средним рейтингом, вычисленным во всей колонке рейтингов. Поскольку некоторые фильмы имеют оценки с сотен площадок, мы хотим получить средний рейтинг каждого фильма и сгруппировать фильмы по movieId, который связан с названием фильма. Теперь, когда с данными проще работать, пришло время создавать функции.

Первая функция называется which_way(). Эта функция вызывает главное меню графического интерфейса пользователя и предлагает сделать выбор. Ищите фильмы по жанру или по тегу. Пользователь, конечно, тоже может нажать кнопку отмены и вообще не искать фильмы, это полностью его дело.


which_way()

Внутри функции мы определяем параметры для EasyGUI chiocebox, который отображается пользователю. Строка параметра, которую вводит пользователь, возвращается и сохраняется в переменной fieldValues. После условный оператор направляет пользователя к следующей функции или окну на основе выбора.

Если пользователь нажимает отмену, программа завершается. Но если пользователь нажимает поиск фильмов по жанру или поиск фильмов по тегу, то будут вызваны функции genre_entry() или tag_entry() и появится что-то, известное как EasyGUI multenterbox.



Из названия понятно, что это поле ввода может принимать несколько входных значений, когда необходимо. Обе функции genre_entry() и tag_entry() очень похожи, поэтому я объясню только одну из них, но в исходный код включу обе. Давай посмотрим на tag_entry().



После передачи параметров отображения multenterbox вызывается другая функция, которая проверяет ввод данных пользователем. Пользовательский ввод возвращается функцией field_check. Давайте взглянем на нее:



Иногда пользователи оставляют значение в поле пустым, возможно, по ошибке. Чтобы контролировать такое поведение, а также сделать графический интерфейс более надежным, нам нужны такие функции. Я нашел эту конкретную функцию на странице документации EasyGUI. По сути, пока вы вводите пустое поле, вам сообщат, что это поле является обязательным, и вы застрянете в цикле.



Как только пользователь что-то вводит, текст (строка) сохраняется в переменной fieldValues, а код работает со строки 114 в функции tag_entry. Пользовательский ввод из поля EasyGUI multenter возвращается в виде списка. При нажатии кнопки отмены возвращается Нет. Чтобы использовать этот ввод и в других функциях, нам нужно объявить переменную как глобальную.

Теперь мы нарезаем список пользовательского ввода из multenterbox, и сохраняем как переменную user_input_2. Нас интересует только возвращаемый текст, если пользователь не нажимает кнопку отмены, отсюда и условный оператор:

if fieldValues != None:

Если пользователь был достаточно любезен, чтобы ввести какой-то текст, мы переходим к функции Similarity_test2(), которая в основном содержит базовые аспекты этой системы рекомендаций, в качестве альтернативы пользователь возвращается в главное меню. Similarity_test1() и Similarity_test2() очень похожи, так же как genre_entry и tag_entry. Я буду рассматривать здесь только Similarity_test2(). Давайте посмотрим на это:



Как мы видим, она принимает один параметр, переменнаую user_input_2 из функции tag_entry(). Помните тот фрейм данных, который мы создали в строке 19 из файла tags.csv? Сначала мы хотим собрать все уникальные теги из столбца тегов и сохранить их в переменной.

Существует множество способов применения библиотеки Fuzzywuzzy в зависимости от ваших задач. Мы будем работать так: output = process.extract(query, choices)

Можно передать дополнительный параметр тип счетчика. Мы просто будем использовать синтаксис по умолчанию. По сути Fuzzywuzzy работает с расстоянием Левенштейна для вычисления различий между последовательностями и возвращает оценку в пределах 100%.

Функция process.extract(query, choices) возвращает список оценок, где каждая строка и ее оценка заключена в скобки, например (строка 95) в качестве элементов списка.

После перебора всего списка тегов и поиска совпадений с переменной user_input_2, мы перебираем список оценок и вырезаем только совпадения выше 90% и сохраняем их в переменной final_2. Мы объявляем его глобальным, чтобы использовать в следующей функции. Если fuzzywuzzy не нашел для нас соответствия, вернется []. Если совпадение по условию не найдено, просим пользователя повторить попытку, вернувшись к функции tag_entry(). В качестве альтернативы, когда у нас есть совпадения более чем на 90%, мы можем использовать их в функции tag(), как показано ниже:



tag()

Теперь, когда у нас есть совпадения более 90%, перебираем их в цикле и просматриваем каждую строку столбца tag фрейма данных df_tags, чтобы увидеть, какие теги соответствуют строкам из Fuzzywuzzy. Теперь сохраняем все совпадения тегов вместе с идентификатором movieId в переменной final_1. Чтобы очистить добавленные данные, мы отрезаем первый элемент и сбрасываем индекс фрейма данных. Теперь можно удалить столбец с именем index и все дубликаты из столбца movieId. Чтобы фильмы с наивысшим рейтингом отображались первыми в порядке убывания, отсортируем фрейм данных и удалим фильмы с рейтингом меньше 2,5/5,0.

Теперь мы можем вставить новую строку во фрейм данных прямо вверху и дублировать имена столбцов над ними. Это делается только для отображения EasyGUI. Элементу codebox не очень нравится фрейм данных pandas, поэтому нужно изменить формат фрейма.

Преобразуем каждый столбец фрейма в список, а затем повторно соберем его с помощью numpy. Да, мы просто убираем скобки и переходим к окну codebox для отображения списка. Это всё! Давайте найдем фильм по тегу: hacker и посмотрим, что покажут рекомендации.



Программа работает. Не стесняйтесь экспериментировать с ней и, пожалуйста, дайте мне знать, если я где-то ошибся!

Исходный код проекта
# импорт библиотекfrom fuzzywuzzy import fuzz from fuzzywuzzy import processfrom progress.bar import IncrementalBarfrom easygui import *import easygui as guiimport pandas as pdimport numpy as npimport sys# максимальное увеличение размера массива отображения numpy для отображения easygui np.set_printoptions(threshold=sys.maxsize)# фрейм данных относительно большой, начальная загрузка займет около 30 секунд# в зависимости от вашего компьютера, поэтому здесь уместна индикация загрузкиprogress_bar = IncrementalBar('Loading Movie Database...', max=1)for i in range(1):    # чтение файлов csv    df_tags = pd.read_csv("tags.csv", usecols = [1,2])    df_movies = pd.read_csv("movies.csv")    df_ratings = pd.read_csv("ratings.csv", usecols = [1,2])        # объединение столбцов из отдельных фреймов данных в новый фрейм    df_1 = pd.merge(df_movies ,df_ratings, on='movieId', how='outer')    # заполнение значений NaN средним рейтингом    df_1['rating'] = df_1['rating'].fillna(df_1['rating'].mean())     # группирование строк df по среднему рейтингу фильма    df_1 = pd.DataFrame(df_1.groupby('movieId')['rating'].mean().reset_index().round(1))    # добавление столбцов title и genres в df    df_1['title'] = df_movies['title']    df_1['genres'] = df_movies['genres']        progress_bar.next()    # заполнение индикатора загрузки при успешной загрузке progress_bar.finish()def which_way():    '''    Эта функция, которая выполняется при запуске программы.     Работает как перекресток, вы выбираете поиск фильмов по    тегу или по жанру. По выбору пользователь переходит к следующему окну.    '''    # определение параметров easygui choicebox    msg = "Choose an option:"    title = "Main Menu"    choices = ["Search recommended movies by genre:","Search recommended movies by tag:"]    fieldValues = choicebox(msg,title, choices)        # переменная fieldValues - это пользовательский ввод, который возвращается из графического интерфейса    # условный оператор, направляющий пользователя к следующему интерфейсу на основе ввода    if fieldValues == "Search recommended movies by genre:":        genre_entry()        elif fieldValues == "Search recommended movies by tag:":        tag_entry()def field_check(msg, title, fieldNames):    '''    Эта функция проверяет отсутствие вводимых пользователем значений в multenterbox    и возвращает пользовательский ввод как переменную fieldValues.        Параметры:        msg, title и fieldnames графического интерфейса multienterbox        '''        fieldValues = multenterbox(msg, title, fieldNames)        # Цикл с условием, чтобы проверить,    # что поля ввода не пусты    while 1:        if fieldValues is None: break        errmsg = ""        for i in range(len(fieldNames)):            if fieldValues[i].strip() == "":                errmsg += ('"%s" is a required field.\n\n' % fieldNames[i])        if errmsg == "":            break # если пустых полей не найдено, перейти к следующему блоку кода        # cохранить пользовательский ввода в виде списка в переменной fieldValues        fieldValues = multenterbox(errmsg, title, fieldNames, fieldValues)        return fieldValuesdef tag_entry():    '''     Эта функция определяет параметры easygui multenterbox и вызывает    field_check, если пользователь вводил значнеие,    вызывает тест на подобие; если совпадение не найдено, пользователь возвращается    в окно ввода    '''        # определение параметров easygui multenterbox    msg = "Enter movie tag for example: world war 2 | brad pitt | documentary \nIf tag not found you will be returned to this window"    title = 'Search by tag'                            fieldNames = ["Tag"]        # вызов field_check() для проверки отсутствия пользовательского ввода и    # сохранения вода как переменной fieldValues    fieldValues = field_check(msg, title, fieldNames)        # Если пользователь ввел значение, сохраняем его в fieldValues[0]    if fieldValues != None:        global user_input_2        user_input_2 = fieldValues[0]                # здесь мы вызываем функцию, которая в основном проверяет строку        # на схожесть с другими строками. Когда пользователь нажимает кнопку отмены, он возвращается в главное меню         similarity_test2(user_input_2)    else:        which_way()def tag():    '''    Эта функция добавляет все совпадающие по тегам фильмы во фрейм данных pandas,    изменяет фрейм данных для правильного отображения easygui, отбросив некоторые    столбцы, сбрасывая индекс df, объединяя фреймы и сортируя элементы так,    чтобы показывались фильмы с рейтингом >= 2.5. Она также преобразует столбцы df в списки    и приводит их в порядок в массиве numpy для отображения easygui.      '''        # добавление тегов найденных фильмов как объекта фрейма    final_1 = []    for i in final_2:        final_1.append(df_tags.loc[df_tags['tag'].isin(i)])        # сброс индекса df, удаление столбца индекса, а также повторяющихся записей    lst = final_1[0]    lst = lst.reset_index()    lst.drop('index', axis=1, inplace=True)    lst = lst.drop_duplicates(subset='movieId')# слияние movieId с названиями и жанрами + удаление тега и идентификатора фильма    df = pd.merge(lst, df_1, on='movieId', how='left')    df.drop('tag', axis=1, inplace=True)    df.drop('movieId', axis=1, inplace=True)# сортировка фильмов по рейтингам, отображение только фильмов с рейтингом выше или равным 2,5    data = df.sort_values(by='rating', ascending=False)    data = data[data['rating'] >= 2.5]    heading = [] # добавление названий столбцов как первой строки фрейма данных для отображения easygui    heading.insert(0, {'rating': 'Rating', 'title': '----------Title',     'genres': '----------Genre'})    data = pd.concat([pd.DataFrame(heading), data], ignore_index=True, sort=True)        # преобразование столбцов фрейма данных в списки    rating = data['rating'].tolist()    title = data['title'].tolist()    genres = data['genres'].tolist()        # составление массива numpy из списков столбцов dataframe для отображения easygui    data = np.concatenate([np.array(i)[:,None] for i in [rating,title,genres]], axis=1)    data = str(data).replace('[','').replace(']','')        # отображение фильмов пользователю    gui.codebox(msg='Movies filtered by tag returned from database:',    text=(data),title='Movies')        which_way()def genre_entry():    '''     Эта функция определяет параметры easygui multenterbox    и вызывает field_check, если пользователь что-то вводил,    вызывается тест на подобие. Если совпадение не найдено, пользователь возвращается    в то же окно      '''    # определение параметров easygui multenterbox    msg = "Enter movie genre for example: mystery | action comedy | war \nIf genre not found you will be returned to this window"    title = "Search by genre"    fieldNames = ["Genre"]        # вызов field_check() для проверки отсутствия пользовательского ввода и    # сохранения ввода в fieldValues.    fieldValues = field_check(msg, title, fieldNames)        # Если пользовательский ввод не пуст, сохраняет его в переменной user_input    if fieldValues != None:        global user_input        user_input = fieldValues[0]            # здесь мы вызываем функцию, которая в основном проверяет строку    # на подобие с другими строками. Если пользователь нажмет кнопку отмена, то он вернется в главное меню         similarity_test1(user_input)    else:        which_way()def genre():    '''    Эта функция добавляет все соответствующие жанру фильмы во фрейм pandas,    изменяет фрейм для правильного отображения easygui, отбросив некоторые    столбцы, сбрасывает индекс df, объединеняет фреймы и сортирует фильмы для отображения    только фильмов с рейтингом >= 2.5. Она также преобразует столбцы конечного df в списки    и приводит их в порядок в массиве numpy для отображения easygui.    '''        # добавление соответствующих жанру фильмов во фрейм.    final_1 = []    for i in final:        final_1.append(df_movies.loc[df_movies['genres'].isin(i)])        # сброс индекса df, удаление индекса столбцов и дубликатов записей    lst = final_1[0]    lst = lst.reset_index()    lst.drop('index', axis=1, inplace=True)    lst.drop('title', axis=1, inplace=True)    lst.drop('genres', axis=1, inplace=True)    lst = lst.drop_duplicates(subset='movieId')        # объединение идентификатора фильма с названием, рейтингом и жанром + удаление индекса, названия и жанра    df = pd.merge(lst, df_1, on='movieId', how='left')        # сортировка по рейтингу, отображение только фильмов с рейтингом выше или равным 2,5    data = df.sort_values(by='rating', ascending=False)    data.drop('movieId', axis=1, inplace=True)    data = data[data['rating'] >= 2.5]    heading = [] # add column names as first dataframe row for easygui display    heading.insert(0, {'rating': 'Rating', 'title': '----------Title',     'genres': '----------Genre'})    data = pd.concat([pd.DataFrame(heading), data], ignore_index=True, sort=True)        # преобразование столбцов фрейма данных в списки    rating = data['rating'].tolist()    title = data['title'].tolist()    genres = data['genres'].tolist()        # составление массива numpy из списков столбцов фрейма для отображения easygui    data = np.concatenate([np.array(i)[:,None] for i in [rating,title,genres]], axis=1)    data = str(data).replace('[','').replace(']','')        # отображение фильмов пользователю    gui.codebox(msg='Movies filtered by genre returned from database:',    text=(data),title='Movies')        which_way()def similarity_test1(user_input):    '''    Эта функция проверяет схожесть строк путем сопоставления пользовательского ввода    для жанров фильмов, совпадения > 90% сохраняется в переменной, которая    затем передается функции жанра для сопоставления с базой данных и    возврата в окно ввода, если совпадение не найдено    '''    # сохранение жанров фильмов в качестве тестовой базы и пользовательского ввода для тестирования     genre_list = df_movies['genres'].unique()    query = user_input    choices = genre_list     # here fuzzywuzzy does its magic to test for similarity    output = process.extract(query, choices)        # сохранение совпадений в переменной и их передача следующей функции    global final    final = [i for i in output if i[1] > 90]        # если совпадений > 90%  не найдено, вернуть пользователя в окно жанра    if final == []:        genre_entry()    else:        genre()def similarity_test2(user_input_2):    '''    Эта функция проверяет схожесть строк путем сопоставления пользовательского ввода    в теги фильмов, совпадение > 90% сохраняется в переменной, которая    затем передается в функцию тега для сопоставления базы данных и    возврата в окно ввода, если совпадение не найдено    '''    # сохранение тега фильма в качестве тестовой базы и пользовательского ввода для тестирования    tag_list = df_tags['tag'].unique()    query = user_input_2    choices = tag_list     # here fuzzywuzzy does its magic to test for similarity    output = process.extract(query, choices)        # сохранение возвращенных совпадений в переменной и их передача следующей функции    global final_2    final_2 = [i for i in output if i[1] > 90]        #если совпадение> 90% не найдено, возврат в окно ввода    if final_2 == []:        tag_entry()    else:        tag()if __name__ == '__main__':    which_way()



image

Получить востребованную профессию с нуля или Level Up по навыкам и зарплате, можно, пройдя наши онлайн-курсы.



Читать еще


Подробнее..

Вышли ожидаемые релизы от Google Flutter 2 и язык Dart 2.12

08.03.2021 18:14:03 | Автор: admin

Корпорация Google продолжает развивать свои продукты. Несколько дней назад была представлена новая версия построения интерфейса пользователя Flutter 2. Она, по словам разработчиков, стала универсальным фреймворком, который позволяет разрабатывать любые типы программ. Это могут быть приложения как для десктопов, так и web-сервисы.

Flutter в процессе эволюции стал неплохой альтернативой React Native. Его главное достоинство возможность на основе одной кодовой базы создавать приложения для разных платформ, включая iOS, Android, Windows, macOS и Linux. Плюс можно разрабатывать и чисто браузерные приложения. И да, обновление до 2 версии не влияет на поддержку продуктов, написанных на Flutter 1. Они без дополнительного редактирования кода могут быть адаптированы для работы на рабочем столе и в Web.

Новшества во Flutter 2


Большая часть кода Flutter реализована на языке Dart. Что касается runtime-движка, то он написан на С++. При разработке приложений можно использовать не только Dart, но и интерфейс Dart Foreign Function для вызова кода на C/C++. Благодаря компиляции приложений в машинный код для целевых платформ разработчики добились высокой производительности выполнения. И при этом программу не требуется перекомпилировать после каждого изменения. У Flutter есть режим горячей перезагрузки, который дает возможность вносить любые изменения в работающее приложения, сразу оценивая результат.

Во второй версии появилась полноценная поддержка создания приложений для Web, которая пригодна для рабочих внедрений. Есть три главных сценария применения Flutter для Web:

  • Разработка обособленных web-приложений (PWA, Progressive Web Apps).
  • Создание одностраничных web-приложений (SPA, Single page apps).
  • Преобразование мобильных приложений в web-приложения.

В описании второй версии упоминается также использование механизмов ускорения отрисовки двухмерной и трехмерной графики, плюс гибкая компоновка элементов на экране и компилируемый в WebAssembly движок отрисовки CanvasKit.


Поддержка приложений для рабочего стола уже реализована, но она находится на стадии бета-тестирования. Разработчики обещают запустить финальный вариант в одном из следующих выпусков. Flutter уже используют Canonical, Microsoft и Toyota. Так, компания Canonical использует Flutter в качестве главного фреймворка для разработки своих приложений. Например, фреймворк используется в новом инсталляторе для Ubuntu. Microsoft адаптировала Flutter для складных устройств с несколькими экранами, включая Surface Duo. Ну а Toyota будет использовать Flutter для создания автомобильных информационно-развлекательных систем. На основе Flutter построена еще и пользовательская оболочка ОС Fuchsia, о которой мы несколько раз писали.

Язык программирования Dart 2.12


Эта версия продолжение радикально преобразованной ветки Dart 2. Отличия от первой версии использование сильной статической типизации, где типы могут выводиться автоматически. Динамическая типизация больше не используется, так что вычисленный изначально тип закреплен за переменной, так что в дальнейшем применяется строгая проверка типа.

В этом выпуске есть одно значительное дополнение стабилизация режима безопасного использования значения Null (null safety). Теперь, после выхода из бета-версии, режим дает возможность избежать проблем, вызванных попытками использования не определенных и выставленных в Null переменных. В новой версии по умолчанию определено, что переменные не могут иметь неопределенные значения, если им не присваивается значение null. В этом режиме учитываются типы переменных, что дает возможность компилятору применять дополнительные оптимизации. Ну а соответствие типов проверяется еще на этапе компиляции. Пример попытка присвоения значения Null переменной с типом, который не подразумевает неопределенное состояние.


Еще одно улучшение выход стабильной версии библиотеки FFI, позволяющей создавать высокопроизводительный код, из которого можно обращаться к API на языке Си. Разработчики оптимизировали ее производительность и размер. Кроме того, они добавили инструменты для разработчиков и систему профилирования кода, которые написаны с использованием Flutter. Плюс добавлены новые плагины для разработки приложений на Dart и Flutter для Android Studio/IntelliJ и VS Code.

Подробнее..

Перевод Создание PDF-документа на Python с помощью pText

19.05.2021 18:19:31 | Автор: admin

Один из самых гибких и привычных способов сгенерировать pdf написать код на LaTeX и воспользоваться соответствующей программой. Но есть и другие способы, которые могут оказаться проще и понятнее, чем LaTeX. Специально к старту курса Fullstack-разработчик на Python представляем перевод статьи о том, как для генерации PDF можно воспользоваться библиотекой pText; эта статья написана Йорисом Схеллекенсом разработчиком pText.


В этом руководстве мы будем использовать pText библиотеку Python, предназначенную для чтения, обработки и создания PDF-документов. Он предлагает как низкоуровневую (позволяющую получить доступ к точным координатам и макету, если вы решите их использовать), так и высокоуровневую модель (где вы можете делегировать точные расчёты полей, позиций и т. д.). Мы рассмотрим, как создавать и проверять PDF-документ в Python, используя pText, а также как использовать некоторые LayoutElement [элементы макета] для добавления штрих-кодов и таблиц.

Portable Document Format (PDF) не является форматом WYSIWYG (что видишь, то и получаешь). Он был разработан как платформенно-независимый, не зависящий от базовой операционной системы и механизмов рендеринга.

Чтобы добиться этого, PDF был создан взаимодействовать с помощью чего-то, больше похожего на язык программирования, и полагается на серию инструкций и операций для достижения результата. Фактически PDF основан на языке сценариев PostScript, который был первым независимым от устройства языком описания страниц. В нём есть операторы, которые изменяют графические состояния, на высоком уровне они выглядят примерно так:

  • Установить шрифт Helvetica.

  • Установить чёрный цвет обводки.

  • Перейти к (60,700).

  • Нарисовать глиф "H".

Это объясняет несколько вещей:

  • Почему так сложно точно извлечь текст из PDF.

  • Почему сложно редактировать PDF-документ.

  • Почему большинство библиотек PDF применяют очень низкоуровневый подход к созданию контента (вы должны указать координаты, в которых следует отображать текст, поля и т. д.).

Установка pText

pText можно загрузить c GitHub или установить через pip:

$ pip install ptext-joris-schellekens

Примечание. На момент написания статьи в версии 1.8.6 по умолчанию не устанавливаются внешние зависимости, такие как python-barcode и qrcode. Если появится сообщение об ошибке, установите их вручную:

$ pip install qrcode python-barcode requests

Создание PDF-документа на Python с помощью pText

pText имеет два интуитивно понятных ключевых класса Document и Page, которые представляют документ и страницы в нём. Это основная структура для создания PDF-документов. Кроме того, класс PDF представляет собой API для загрузки и сохранения создаваемых нами документов. Имея это в виду, давайте создадим пустой файл PDF:

from ptext.pdf.document import Documentfrom ptext.pdf.page.page import Pagefrom ptext.pdf.pdf import PDF# Create an empty Documentdocument = Document()# Create an empty pagepage = Page()# Add the Page to the Documentdocument.append_page(page)# Write the Document to a filewith open("output.pdf", "wb") as pdf_file_handle:    PDF.dumps(pdf_file_handle, document)

Большая часть кода здесь говорит сама за себя. Мы начинаем с создания пустого документа, затем добавляем пустую страницу в документ с помощью функции append() и, наконец, сохраняем файл с помощью PDF.dumps().

Стоит отметить, что мы использовали флаг "wb" для записи в двоичном режиме, поскольку мы не хотим, чтобы Python кодировал этот текст. Это даёт нам пустой PDF-файл с названием output.pdf в вашей файловой системе:

Создание документа Hello World с помощью pText

Конечно, пустые PDF-документы не содержат много информации. Давайте добавим контент на страницу, прежде чем добавлять его в экземпляр документа.

Подобно двум классам, описанным ранее, чтобы добавить контент на страницу, мы добавим PageLayout, указывающий на тип макета, который мы хотели бы видеть, и добавим один или несколько абзацев в этот макет.

С этой целью Document является экземпляром самого низкого уровня в иерархии объектов, а Paragraph экземпляром самого высокого уровня, размещённым поверх PageLayout и, следовательно, страницы. Давайте добавим абзац на нашу страницу:

from ptext.pdf.document import Documentfrom ptext.pdf.page.page import Pagefrom ptext.pdf.pdf import PDFfrom ptext.pdf.canvas.layout.paragraph import Paragraphfrom ptext.pdf.canvas.layout.page_layout import SingleColumnLayoutfrom ptext.io.read.types import Decimaldocument = Document()page = Page()# Setting a layout manager on the Pagelayout = SingleColumnLayout(page)# Adding a Paragraph to the Pagelayout.add(Paragraph("Hello World", font_size=Decimal(20), font="Helvetica"))document.append_page(page)with open("output.pdf", "wb") as pdf_file_handle:    PDF.dumps(pdf_file_handle, document)

Вы заметите, что мы добавили 2 дополнительных объекта:

  • Экземпляр PageLayout, более конкретный через его подкласс SingleColumnLayout: этот класс отслеживает, где контент добавляется на страницу, какие области доступны для будущего контента, каковы поля страницы и какие ведущие (пространство между объектами Paragraph) должно быть.

Поскольку здесь мы работаем только с одним столбцом, мы используем SingleColumnLayout. В качестве альтернативы мы можем использовать MultiColumnLayout.

  • Экземпляр Paragraph: этот класс представляет блок текста. Вы можете установить такие свойства, как шрифт, font_size, font_color, и многие другие. Дополнительные примеры вы можете найти в документации.

Код генерирует файл output.pdf, содержащий наш абзац:

Проверка созданного PDF с помощью pText.

Примечание: этот раздел является необязательным, если вас не интересует внутренняя работа PDF-документа.

Но может быть очень полезно немного узнать о формате (например, при отладке классической проблемы почему мой контент теперь отображается на этой странице). Обычно программа для чтения PDF читает документ, начиная с последних байтов:

xref0 110000000000 00000 f0000000015 00000 n0000002169 00000 n0000000048 00000 n0000000105 00000 n0000000258 00000 n0000000413 00000 n0000000445 00000 n0000000475 00000 n0000000653 00000 n0000001938 00000 ntrailer<</Root 1 0 R /Info 2 0 R /Size 11 /ID [<61e6d144af4b84e0e0aa52deab87cfe9><61e6d144af4b84e0e0aa52deab87cfe9>]>>startxref2274%%EOF

Здесь мы видим маркер конца файла (%% EOF) и таблицу перекрестных ссылок (обычно сокращённо xref).

Внешняя ссылка ограничена токенами startxref и xref.

Внешняя ссылка (в документе может быть несколько) действует как справочная таблица для программы чтения PDF-файлов.

Он содержит байтовое смещение (начиная с верхней части файла) каждого объекта в PDF. Первая строка внешней ссылки (0 11) говорит, что в этой внешней ссылке 11 объектов и что первый объект начинается с номера 0.

Каждая последующая строка состоит из байтового смещения, за которым следует так называемый номер поколения и буква f или n:

  • Объекты, отмеченные буквой f, являются свободными, их рендеринг не ожидается.

  • Объекты, отмеченные буквой n, используются.

Внизу xref мы находим словарь трейлеров. Словари в синтаксисе PDF разделяются символами << и >>. В этом словаре есть следующие пары:

  • /Root 1 0 R

  • /Info 2 0 R

  • /Size 11

  • /ID [<61e6d144af4b84e0e0aa52deab87cfe9> <61e6d144af4b84e0e0aa52deab87cfe9>]

Словарь трейлеров является отправной точкой для программы чтения PDF-файлов и содержит ссылки на все другие данные. В этом случае:

  • /Root: это ещё один словарь, который ссылается на фактическое содержание документа.

  • /Info: это словарь, содержащий метаинформацию документа (автор, название и так далее).

Строки типа 1 0 R в синтаксисе PDF называются ссылками. И здесь нам пригодится таблица xref. Чтобы найти объект, связанный с 1 0 R, мы смотрим на объект 1 (номер поколения 0). Таблица поиска xref сообщает нам, что мы можем ожидать найти этот объект в 15-м байте документа. Если проверить это, то обнаружим:

1 0 obj<</Pages 3 0 R>>endobj

Обратите внимание, что тот объект начинается с 1 0 obj и заканчивается endobj. Это ещё одно подтверждение того, что мы на самом деле имеем дело с объектом 1. Этот словарь говорит нам, что мы можем найти страницы документа в объекте 3:

3 0 obj<</Count 1 /Kids [4 0 R] /Type /Pages>>endobj

Это словарь /Pages, и он сообщает нам, что в этом документе одна страница (запись /Count). Запись для /Kids обычно представляет собой массив с одной ссылкой-объектом на страницу. Мы можем ожидать найти первую страницу в объекте 4:

4 0 obj<</Type /Page /MediaBox [0 0 595 842] /Contents 5 0 R /Resources 6 0 R /Parent 3 0 R>>endobj

Этот словарь содержит несколько интересных записей:

  • /MediaBox: физические размеры страницы (в данном случае страница формата A4).

  • /Contents: ссылка на (обычно сжатый) поток операторов содержимого PDF.

  • /Resources: ссылка на словарь, содержащий все ресурсы (шрифты, изображения и так далее), используемые для рендеринга этой страницы.

Давайте проверим объект 5, чтобы узнать, что на самом деле отображается на этой странице:

5 0 obj<</Filter /FlateDecode /Length 85>>streamxR@\<`aw3T0!K3Benl7'999E9)!Y(!8yT*endstreamendobj

Как упоминалось ранее, этот поток содержимого сжимается. Вы можете определить, какой метод сжатия использовался, с помощью записи /Filter. Если мы применим распаковку (unzip) к объекту 5, то мы должны получить фактические операторы содержимого:

5 0 obj<</Filter /FlateDecode /Length 85>>stream            q            BT            0.000000 0.000000 0.000000 rg            /F1 1.000000 Tf                        20.000000 0 0 20.000000 60.000000 738.000000 Tm                        (Hello world) Tj            ET                        Qendstreamendobj

Наконец, мы находимся на уровне, где можем декодировать контент. Каждая строка состоит из аргументов, за которыми следует их оператор. Быстро пройдёмся по операторам:

  • q: сохранить текущее графическое состояние (помещая его в стек);

  • BT: начать текст;

  • 0 0 0 rg: установить текущий цвет обводки на (0,0,0) rgb. Это чёрный;

  • /F1 1 Tf: установить текущий шрифт на /F1 (это запись в словаре ресурсов, упомянутом ранее) и размер шрифта на 1.

  • 20.000000 0 0 20.000000 60.000000 738.000000 Tm: установить текстовую матрицу, что требует отдельного руководства. Достаточно сказать, что эта матрица регулирует размер шрифта и положение текста. Здесь мы масштабируем шрифт до размера 20 и устанавливаем курсор для рисования текста на 60 738. Система координат PDF начинается в нижнем левом углу страницы. Итак, 60 738 находится где-то рядом с левым верхом страницы (с учётом того, что высота страницы составляет 842 единицы).

  • (Hello world) Tj: строки в синтаксисе PDF разделяются ( и ). Эта команда указывает программе чтения PDF-файлов отобразить строку Hello world в позиции, которую мы указали ранее с помощью текстовой матрицы, в шрифте, размере и цвете, которые мы указали в командах перед этим.

  • ET: конец текста.

  • Q: извлечь состояние графики из стека (таким образом восстанавливая состояние графики).

Добавление других элементов макета pText на страницы

pText поставляется с широким спектром объектов LayoutElement. В предыдущем примере мы кратко исследовали Paragraph. Но есть и другие элементы, такие как UnorderedList, OrderedList, Image, Shape, Barcode и Table. Давайте создадим чуть более сложный пример с таблицей и штрих-кодом. Таблицы состоят из TableCells, которые мы добавляем в экземпляр Table. Штрих-код может быть одним из многих типов штрих-кода мы будем использовать QR-код:

from ptext.pdf.document import Documentfrom ptext.pdf.page.page import Pagefrom ptext.pdf.pdf import PDFfrom ptext.pdf.canvas.layout.paragraph import Paragraphfrom ptext.pdf.canvas.layout.page_layout import SingleColumnLayoutfrom ptext.io.read.types import Decimalfrom ptext.pdf.canvas.layout.table import Table, TableCellfrom ptext.pdf.canvas.layout.barcode import Barcode, BarcodeTypefrom ptext.pdf.canvas.color.color import X11Colordocument = Document()page = Page()# Layoutlayout = SingleColumnLayout(page)# Create and add headinglayout.add(Paragraph("DefaultCorp Invoice", font="Helvetica", font_size=Decimal(20)))# Create and add barcodelayout.add(Barcode(data="0123456789", type=BarcodeType.QR, width=Decimal(64), height=Decimal(64)))# Create and add tabletable = Table(number_of_rows=5, number_of_columns=4)# Header rowtable.add(TableCell(Paragraph("Item", font_color=X11Color("White")), background_color=X11Color("SlateGray")))table.add(TableCell(Paragraph("Unit Price", font_color=X11Color("White")), background_color=X11Color("SlateGray")))table.add(TableCell(Paragraph("Amount", font_color=X11Color("White")), background_color=X11Color("SlateGray")))table.add(TableCell(Paragraph("Price", font_color=X11Color("White")), background_color=X11Color("SlateGray")))# Data rowsfor n in [("Lorem", 4.99, 1), ("Ipsum", 9.99, 2), ("Dolor", 1.99, 3), ("Sit", 1.99, 1)]:    table.add(Paragraph(n[0]))    table.add(Paragraph(str(n[1])))    table.add(Paragraph(str(n[2])))    table.add(Paragraph(str(n[1] * n[2])))# Set paddingtable.set_padding_on_all_cells(Decimal(5), Decimal(5), Decimal(5), Decimal(5))layout.add(table)# Append pagedocument.append_page(page)# Persist PDF to filewith open("output4.pdf", "wb") as pdf_file_handle:    PDF.dumps(pdf_file_handle, document)

Некоторые детали реализации:

  • pText поддерживает различные цветовые модели, в том числе RGBColor, HexColor, X11Color и HSVColor.

  • Вы можете добавлять объекты LayoutElement непосредственно в объект Table, но вы также можете обернуть их объектом TableCell, это даёт вам некоторые дополнительные параметры, такие как col_span и row_span или, в данном случае, background_color.

  • Если font, font_size или font_color не указаны, Paragraph примет значение по умолчанию Helvetica, размер 12, чёрный.

Код сгенерирует такой документ:

Заключение

В этом руководстве мы рассмотрели pText библиотеку для чтения, записи и управления файлами PDF. Мы рассмотрели ключевые классы, такие как Document и Page, а также некоторые элементы, такие как Paragraph, Barcode и PageLayout. Наконец, мы создали несколько PDF-файлов с различным содержимым, а также проверили, как PDF-файлы хранят данные под капотом.

Документ PDF приятен глазу и достаточно удобен для того, чтобы использовать его в электронном документообороте, для генерации разнообразных счетов и отчётов, особенно актуальных в крупных организациях и в их внутренней сети, поэтому разработка крупных и сложных веб-проектов часто сопряжена с генерацией PDF. Если вам интересна работа со сложными веб-проектами и не хочется выбирать между бэком или фронтом, то вы можете присмотреться к курсу Fullstack-разработчик на Python.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Перевод 5 разных библиотек Python, которые сэкономят ваше время

12.06.2021 18:20:44 | Автор: admin

В этой подборке, переводом которой мы решили поделиться к старту курса о машинном и глубоком обучении, по мнению автора, каждая библиотека заслуживает отдельной статьи. Всё начинается с самого начала: предлагается библиотека, которая сокращает шаблонный код импортирования; заканчивается статья пакетом удобной визуализации данных для исследовательского анализа. Автор также касается работы с картами Google, ускорения и упрощения работы с моделями ML и библиотеки, которая может повысить качество вашего проекта в области обработки естественного языка. Посвящённый подборке блокнот Jupyter вы найдёте в конце.


PyForest

Когда вы начинаете писать код для проекта, каков ваш первый шаг? Наверное, вы импортируете нужные библиотеки. Проблема в том, что заранее неизвестно, сколько библиотек нужно импортировать, пока они вам не понадобятся, то есть пока вы не получите ошибку.

Вот почему PyForest это одна из самых удобных библиотек, которые я знаю. С её помощью в ваш блокнот Jupyter можно импортировать более 40 популярнейших библиотек (Pandas, Matplotlib, Seaborn, Tensorflow, Sklearn, NLTK, XGBoost, Plotly, Keras, Numpy и другие) при помощи всего одной строки кода.

Выполните pip install pyforest. Для импорта библиотек в ваш блокнот введите команду from pyforest import *, и можно начинать. Чтобы узнать, какие библиотеки импортированы, выполните lazy_imports().

При этом с библиотеками удобно работать. Технически они импортируются только тогда, когда вы упоминаете их в коде. Если библиотека не упоминается, она не импортируется.

Emot

Эта библиотека может повысить качество вашего проекта по обработке естественного языка. Она преобразует эмотиконы в их описание. Представьте, например, что кто-то оставил в Твиттере сообщение I [здесь в оригинале эмодзи "красное сердце", новый редактор Хабра вырезает его] Python. Человек не написал слово люблю, вместо него вставив эмодзи. Если твит задействован в проекте, придётся удалить эмодзи, а значит, потерять часть информации.

Вот здесь и пригодится пакет emot, преобразующий эмодзи в слова. Для тех, кто не совсем понял, о чём речь, эмотиконы это способ выражения через символы. Например, :) означает улыбку, а :( выражает грусть. Как же работать с библиотекой?

Чтобы установить Emot, выполните команду pip install emot, а затем командой import emot импортируйте её в свой блокнот. Нужно решить, с чем вы хотите работать, то есть с эмотиконами или с эмодзи. В случае эмодзи код будет таким: emot.emoji(your_text). Посмотрим на emot в деле.

Выше видно предложение I [эмодзи "красное сердце"] Python, обёрнутое в метод Emot, чтобы разобраться со значениями. Код выводит словарь со значением, описанием и расположением символов. Как всегда, из словаря можно получить слайс и сосредоточиться на необходимой информации, например, если я напишу ans['mean'], вернётся только описание эмодзи.

Geemap

Говоря коротко, с её помощью можно интерактивно отображать данные Google Earth Engine. Наверное, вы знакомы с Google Earth Engine и всей его мощью, так почему не задействовать его в вашем проекте? За следующие несколько недель я хочу создать проект, раскрывающий всю функциональность пакета geemap, а ниже расскажу, как можно начать с ним работать.

Установите geemap командой pip install geemap из терминала, затем импортируйте в блокнот командой import geemap. Для демонстрации я создам интерактивную карту на основе folium:

import geemap.eefolium as geemapMap = geemap.Map(center=[40,-100], zoom=4)Map

Как я уже сказал, я не изучил эту библиотеку настолько, насколько она того заслуживает. Но у неё есть исчерпывающий Readme о том, как она работает и что можно делать с её помощью.

Dabl

Позвольте мне рассказать об основах. Dabl создан, чтобы упростить работу с моделями ML для новичков. Чтобы установить её, выполните pip install dabl, импортируйте пакет командой import dabl и можно начинать. Выполните также строчку dabl.clean(data), чтобы получить информацию о признаках, например о том, есть ли какие-то бесполезные признаки. Она также показывает непрерывные, категориальные признаки и признаки с высокой кардинальностью.

Чтобы визуализировать конкретный признак, можно выполнить dabl.plot(data).

Наконец, одной строчкой кода вы можете создать несколько моделей вот так: dabl.AnyClassifier, или так: dabl.Simplefier(), как это делается в scikit-learn. Но на этом шаге придётся предпринять некоторые обычные шаги, такие как создание тренировочного и тестового набора данных, вызов, обучение модели и вывод её прогноза.

# Setting X and y variablesX, y = load_digits(return_X_y=True)# Splitting the dataset into train and test setsX_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1)# Calling the modelsc = dabl.SimpleClassifier().fit(X_train, y_train)# Evaluating accuracy scoreprint(Accuracy score, sc.score(X_test, y_test))

Как видите, Dabl итеративно проходит через множество моделей, включая Dummy Classifier (фиктивный классификатор), GaussianNB (гауссовский наивный Байес), деревья решений различной глубины и логистическую регрессию. В конце библиотека показывает лучшую модель. Все модели отрабатывают примерно за 10 секунд. Круто, правда? Я решил протестировать последнюю модель при помощи scikit-learn, чтобы больше доверять результату:

Я получил точность 0,968 с обычным подходом к прогнозированию и 0,971 с помощью Dabl. Для меня это достаточно близко! Обратите внимание, что я не импортировал модель логистической регрессии из scikit-learn, поскольку это уже сделано через PyForest. Должен признаться, что предпочитаю LazyPredict, но Dabl стоит попробовать.

SweetViz

Это low-code библиотека, которая генерирует прекрасные визуализации, чтобы вывести ваш исследовательский анализ данных на новый уровень при помощи всего двух строк кода. Вывод библиотеки интерактивный файл HTML. Давайте посмотрим на неё в общем и целом. Установить её можно так: pip install sweetviz, а импортировать в блокнот строкой import sweetviz as sv. И вот пример кода:

my_report = sv.analyze(dataframe)my_report.show_html()

Вы видите это? Библиотека создаёт HTML-файл с исследовательским анализом данных на весь набор данных и разбивает его таким образом, что каждый признак вы можете проанализировать отдельно. Возможно также получить численные или категориальные ассоциации с другими признаками; малые, большие и часто встречающиеся значения. Также визуализация изменяется в зависимости от типа данных. При помощи SweetViz можно сделать так много, что я даже напишу о ней отдельный пост, а пока настоятельно рекомендую попробовать её.

Заключение

Все эти библиотеки заслуживают отдельной статьи и того, чтобы вы узнали о них, потому что они превращают сложные задачи в прямолинейно простые. Работая с этими библиотеками, вы сохраняете драгоценное время для действительно важных задач. Я рекомендую попробовать их, а также исследовать не упомянутую здесь функциональность. На Github вы найдёте блокнот Jupyter, который я написал, чтобы посмотреть на эти библиотеки в деле.

Этот материал не только даёт представление о полезных пакетах экосистемы Python, но и напоминает о широте и разнообразии проектов, в которых можно работать на этом языке. Python предельно лаконичен, он позволяет экономить время и в процессе написания кода, выражать идеи максимально быстро и эффективно, то есть беречь силы, чтобы придумывать новые подходы и решения задач, в том числе в области искусственного интеллекта, получить широкое и глубокое представление о котором вы можете на нашем курсе "Machine Learning и Deep Learning".

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Создание SDK под Android в стиле Single-Activity

18.10.2020 04:15:28 | Автор: admin

Single activity подходом при создании конечного приложения под Android никого не удивишь. Но мы пошли дальше и использовали No-Activity при разработке SDK. Сейчас разберемся для чего это понадобилось, возникшие сложности и как их решали.

Стандартные 3rd party SDK в Android

Как обычно работают внешние SDK в Android? Открывается Activity библиотеки, выполняется некая работа, при необходимости возвращается результат в onActivityResult.

Стандартная схема работы SDK.Стандартная схема работы SDK.

Обычно такого подхода хватает, когда SDK выполняет целиком инкапсулированную функцию и не зависит от внешнего приложения. Но иногда необходимо обеспечить более тесное взаимодействие между приложением и SDK, например:

Желаемый стек экранов приложения и SDKЖелаемый стек экранов приложения и SDK

Получается, что экраны нашего SDK должны быть частью внешнего приложения. Точно также, как вы можете использовать, например, MapFragment от Google. Итого, при стандартном подходе, мы сталкиваемся с рядом трудностей и ограничений.

Проблемы при стандартном подходе к SDK

  • Если вам нужно несколько взаимодействий между SDK и приложением, то придется открывать-закрывать Activity от SDK и аккуратно обрабатывать передачу данных туда-обратно.

  • Сложно поддержать такой логический порядок экранов, когда элементы приложения чередуются с SDK. (Спойлер: это можно понадобится, но редко).

  • При относительно долгом возможном нахождении в SDK внешнее приложение может уйти в Lock Screen. Такое может случиться, если Lock реализован на колбеках жизненного цикла Activity.

No-Activity подход при разработке SDK

Итак, мы решили, что основная проблема в том, что контекст (Activity) внешнего приложения и SDK разные. Отсюда следует резонное решение - отказаться от контекста SDK и во внешнее приложение поставлять только фрагменты. В таком случае разработчик сможет сам управлять стеком экранов.

No-Activity SDK на ФрагментахNo-Activity SDK на Фрагментах

Данный подход имеет как ряд плюсов, так и значительные минусы. Какие же?

Плюсы No-Acitivty SDK

  • Приложение и SDK имеют общий контекст, т.е. для пользователя это выглядит как абсолютно единое приложение.

  • Основное приложение имеет свой стек фрагментов, а SDK - свой через childFragmentManager.

  • Можно организовать любой порядок экранов и наложений элементов, т.к. навигация доступна и для внешнего приложения.

Минусы No-Acitivty SDK

  • Внешнее приложение должно изначально работать с фрагментами, желательно вообще быть Single-Activity.

  • У SDK нет своего контекста, если хотите использовать dagger - придется исхитриться (но это все же возможно).

  • SDK может влиять на внешнее Acitivty, т.к. requireActivity вернет именно его. Надо полностью доверять SDK.

  • Activity будет получать onActivityResult, и, вероятно, придется его прокидывать во фрагменты.

  • Разработчику внешнего приложения сложнее интегрировать SDK, т.к. простой вызов Activity уже не сработает.

Использование 3rd party библиотек внутри SDK

При любом подходе так или иначе придется использовать библиотеки внутри SDK. Это в свою очередь может привести к коллизии версий с внешним приложением. А части библиотек, например dagger2 нужен будет выделенный контекст.

Dagger2 внутри SDK

Для использования dagger зачастую в приложении используется класс Application. В случае с SDK так сделать не получится, потому что Application, вероятно, будет перетерт со стороны внешнего приложения.

Нужен отдельный класс, который заведомо не будет испорчен внешним приложением.

internal object ComponentHolder {    lateinit var appComponent: SdkAppComponent        private set    @Synchronized    fun init(ctx: Context) {        if (this::appComponent.isInitialized) return        appComponent = DaggerSdkAppComponent            .builder()            .sdkAppModule(SdkAppModule(ctx))            .build()    }}

Остается только лишь понять, откуда вызвать init, да так, чтобы в процессе жизни SDK быть уверенным, что инициализация выполнилась до любой другой работы. Для этого можно использовать одну точку входа в SDK. Назовем ее EntryPointFragment. Данный фрагмент и будет виден внешнему приложению как единственная точка входа в SDK. Вся дальнейшая навигация внутри SDK будет происходить уже в нем через childFragmentManager.

Как раз при создании EntryPointFragment можно и инициализировать ComponentHolder для Dagger.

override fun onCreate(savedInstanceState: Bundle?) {        ComponentHolder.init(requireActivity())        ComponentHolder.appComponent.inject(this)        super.onCreate(savedInstanceState)    }

Итого, на выходе мы получили ComponentHolder, который можно использовать внутри SDK для инъекции нужных компонент.

Устранение коллизии в версиях

С данной проблемой столкнулись при обновлении версии okhttp3 до новой major версии 4.+. В ней добавили улучшенную поддержку Kotlin, в том числе, например, доступ к коду ошибки через code() теперь стало ошибкой. Клиенты SDK, используя либо 3, либо 4 версию должны получать ту же внутри SDK, иначе все сломается.

Это реально сделать, вынеся код с коллизиями в отдельный модуль. В нем будут 2 flavor:

    flavorDimensions("okhttpVersion")    productFlavors {        v3 {            dimension = "okhttpVersion"        }        v4 {            dimension = "okhttpVersion"        }    }        dependencies {        v3Api okhttp3.core        v3Api okhttp3.logging        v4Api okhttp4.core        v4Api okhttp4.logging}

В двух разных папках, отвечающих за каждый flavor будут одинаковые классы, один из которых будет использовать code() а другой code.

// Code in v3 folderclass ResponseWrapper(private val response: Response) {    val code : Int        get() = response.code()}
// Code in v4 folderclass ResponseWrapper(private val response: Response) {    val code : Int        get() = response.code}

Остается только на уровне приложения выбрать необходимый конфиг и дальше правильная версия приедет в финальный проект.

Подсказка: если вы сами используете свой же модуль в проекте и подключаете как исходники, не забудьте следующее:

defaultConfig {...missingDimensionStrategy 'okhttpVersion', 'v4'}

В таком случае вы избавитесь от конфликта при сборке. Иначе просто версия не найдется.

Заключение

Разработка SDK, если сравнивать с просто Android приложением, намного сложнее, но порой интереснее. Также требования к качеству конечного продукта выше - если что-то упадет, то упадет не у вас, а у вашего клиента, что прямо очень плохо.

Подробнее..

Библиотека дляработы сiOS-пермишенами, отидеи дорелиза (часть1)

07.12.2020 20:14:08 | Автор: admin

Привет! Из этого мини-цикла статей ты узнаешь:

  • Как унаследовать Swift-класс не целиком, а лишь то в нём, что тебе нужно?

  • Как позволить юзеру твоей CocoaPods- или Carthage-библиотеки компилировать лишь те её части, что он действительно использует?

  • Как раздербанить ресурсы iOS, чтобы достать оттуда конкретные системные иконки и локализованные строки?

  • Как поддержать completion blocks даже там, где это не предусмотрено дефолтным API системных разрешений?

А вообще, здесь о том, как я попытался написать ультимативную библиотеку для работы с пермишенами в iOS с какими неожиданностями столкнулся и какие неочевидные решения нашёл для некоторых проблем. Буду рад, если окажется интересно и полезно!

Немного о самой либе

Однажды мне пришла в голову следующая мысль всем мобильным разработчикам то и дело приходится работать с системными разрешениями. Хочешь использовать камеру? Получи у юзера пермишен. Решил присылать ему уведомления? Получи разрешение.

В каждой подобной ситуации идеальный разработчик ходит изучать документацию на сайте Apple, но чаще мы экономим время и просто гуглим готовое решение на Stack Overflow. Не сказал бы, что это всегда плохо, но так оказывается слишком легко упустить какой-нибудь важный нюанс.

Быстрый поиск по GitHub показывает, что либы для работы с пермишенами уже давно существуют, причём их не одна и не две. Но какую ни возьми, везде одно и то же либо перестала обновляться, либо что-то не поддерживает, либо документация на китайском.

В итоге я решил написать собственную библиотеку. Попытаться сделать идеально. Буду благодарен, если напишете в комментариях, получилось ли. А вообще, PermissionWizard...

  • Поддерживает новейшие фичи iOS 14 и macOS 11 Big Sur

  • Отлично работает с Mac Catalyst

  • Поддерживает все существующие типы системных разрешений

  • Валидирует твой Info.plist и защищает от падений, если с ним что-то не так

  • Поддерживает коллбэки даже там, где этого нет в дефолтном системном API

  • Позволяет не париться о том, что ответ на запрос какого-нибудь разрешения вернётся вневедомом потоке, пока ты его ждёшь, например, в DispatchQueue.main

  • Полностью написан на чистом Swift

  • Обеспечивает унифицированное API вне зависимости от типа разрешения, с которым ты прямо сейчас работаешь

  • Опционально включает нативные иконки и локализованные строки для твоего UI

  • Модульный, подключай лишь те компоненты, что тебе нужны

Но перейдём наконец-то к действительно интересному...

Как унаследовать класс не целиком, а лишь то в нём, что тебе нужно?

Начав работать над PermissionWizard, я довольно быстро понял, что для большинства поддерживаемых типов разрешений мне нужны одни и те же элементы:

  • Свойство usageDescriptionPlistKey

  • Методы checkStatus и requestAccess

Было бы странно не унаследовать каждый класс, отвечающий за тот или иной тип пермишена, от универсального родительского класса, где всё это уже объявлено и частично имплементировано.

Кроме того, я собирался задокументировать каждый метод, каждое свойство в библиотеке, а поскольку Swift и Xcode не позволяют переиспользовать комментарии к коду, подобное наследование убивает сразу двух зайцев мне не нужно из класса в класс копировать одни итеже комментарии.

Только вот оказалось всё сложнее, чем можно было ожидать:

  • Некоторые типы пермишенов (например, дом и локальная сеть) не позволяют проверить текущий статус разрешения, не выполнив собственно запрос на доступ к нему, и унаследованное объявление checkStatus оказывается в таком случае неуместным. Оно лишь сбивает с толку торчит в автоподстановке, хотя не имеет имплементации.

  • Для работы с пермишеном геолокации не годится стандартное объявление requestAccess(completion:), поскольку для запроса на доступ необходимо определиться, нужен он нам всегда, или только когда юзер активно пользуется приложением. Здесь подходит requestAccess(whenInUseOnly:completion:), но тогда опять-таки выходит, что унаследованная перегрузка метода болтается не в тему.

  • Пермишен на доступ к фотографиям использует сразу два разных plist-ключа один на полный доступ (NSPhotoLibraryUsageDescription) и один, чтобы только добавлять новые фото и видео (NSPhotoLibraryAddUsageDescription). Видим, что опять-таки наследуемое свойство usageDescriptionPlistKey получается лишним логичнее иметь два отдельных и с более говорящими названиями.

Я привёл лишь несколько примеров возникших проблем. Однако подобных исключений потребовали только некоторые типы пермишенов. Большинство, а всего их целых 18 штук, строится по одному и тому же неизменному скелету, отказываться от наследования которого совершенно не хочется.

Решить подобную ситуацию можно по-разному. Например, раскидать все эти объявления свойств и методов по разным протоколам и в каждом отдельном случае наследовать лишь нужные. Но это громоздко и неудобно, в данном случае нашёлся способ изящнее атрибут.

class SupportedType {    func requestAccess(completion: (Status) -> Void) { }}final class Bluetooth: SupportedType { ... }final class Location: SupportedType {    @available(*, unavailable)    override func requestAccess(completion: (Status) -> Void) { }        func requestAccess(whenInUseOnly: Bool, completion: (Status) -> Void) { ... }}

Переопределение метода, помеченное атрибутом @available(*, unavailable), не только делает его вызов невозможным, возвращая при сборке ошибку, но и полностью скрывает его из автоподстановки в Xcode, то есть фактически как будто исключает метод из наследования.

Разумеется, я не открыл здесь никакой Америки, однако решение оказалось не слишком широко известным, поэтому решил им поделиться.

Как позволить юзеру твоей CocoaPods- или Carthage-библиотеки компилировать лишь те её части, что он действительно использует?

PermissionWizard поддерживает 18 видов системных разрешений от фото и контактов до Siri и появившегося в iOS 14 трекинга. Это в свою очередь означает, что библиотека импортирует и использует AVKit, CoreBluetooth, CoreLocation, CoreMotion, EventKit, HealthKit, HomeKit и ещё много разных системных фреймворков.

Нетрудно догадаться, что если ты подключишь такую либу к своему проекту целиком, пусть даже будешь использовать её исключительно для работы с каким-то одним пермишеном, Apple не пропустит твоё приложение в App Store, поскольку увидит, что оно использует подозрительно много разных API конфиденциальности. Да и собираться такой проект будет чуть дольше, а готовое приложение весить чуть больше. Требуется какой-то выход.

CocoaPods

В случае с этим менеджером зависимостей решение найти относительно просто. Разбиваем библиотеку на независимые компоненты, позволяя выборочно устанавливать лишь те, что нужны разработчику. Заодно отделяем компонент с иконками и локализованными строками, поскольку нужны они далеко не всем.

pod 'PermissionWizard/Assets' # Icons and localized stringspod 'PermissionWizard/Bluetooth'pod 'PermissionWizard/Calendars'pod 'PermissionWizard/Camera'pod 'PermissionWizard/Contacts'pod 'PermissionWizard/FaceID'pod 'PermissionWizard/Health'pod 'PermissionWizard/Home'pod 'PermissionWizard/LocalNetwork'pod 'PermissionWizard/Location'pod 'PermissionWizard/Microphone'pod 'PermissionWizard/Motion'pod 'PermissionWizard/Music'pod 'PermissionWizard/Notifications'pod 'PermissionWizard/Photos'pod 'PermissionWizard/Reminders'pod 'PermissionWizard/Siri'pod 'PermissionWizard/SpeechRecognition'pod 'PermissionWizard/Tracking'

В свою очередь, Podspec нашей библиотеки (файл, описывающий её для CocoaPods) выглядит примерно следующим образом:

Pod::Spec.new do |spec|    ...    spec.subspec 'Core' do |core|    core.source_files = 'Source/Permission.swift', 'Source/Framework'  end    spec.subspec 'Assets' do |assets|    assets.dependency 'PermissionWizard/Core'    assets.pod_target_xcconfig = { 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => 'ASSETS' }        assets.resource_bundles = {      'Icons' => 'Source/Icons.xcassets',      'Localizations' => 'Source/Localizations/*.lproj'    }  end    spec.subspec 'Bluetooth' do |bluetooth|    bluetooth.dependency 'PermissionWizard/Core'    bluetooth.pod_target_xcconfig = { 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => 'BLUETOOTH' }    bluetooth.source_files = 'Source/Supported Types/Bluetooth*.swift'  end    ...    spec.default_subspec = 'Assets', 'Bluetooth', 'Calendars', 'Camera', 'Contacts', 'FaceID', 'Health', 'Home', 'LocalNetwork', 'Location', 'Microphone', 'Motion', 'Music', 'Notifications', 'Photos', 'Reminders', 'Siri', 'SpeechRecognition', 'Tracking'  end

Подключение каждого нового компонента не только добавляет в устанавливаемый дистрибутив библиотеки новые файлы с кодом, но и проставляет в настройках проекта флаги, на основе которых мы можем исключать из сборки те или иные участки кода.

#if BLUETOOTH    final class Bluetooth { ... }#endif

Carthage

Здесь всё оказывается немного сложнее. Этот менеджер зависимостей не поддерживает дробление библиотек, если только не разделить их по-настоящему на разные репозитории, к примеру. Выходит, нужен какой-то обходной путь.

В корне нашей либы создаём файл Settings.xcconfig и пишем в нём следующее:

#include? "../../../../PermissionWizard.xcconfig"

По умолчанию Carthage устанавливает зависимости в директорию Carthage/Build/iOS, так что вышеприведённая инструкция ссылается на некий файл PermissionWizard.xcconfig, который может быть расположен юзером нашей библиотеки в корневой папке своего проекта.

Очертим и его примерное содержимое:

ENABLED_FEATURES = ASSETS BLUETOOTH ...SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) $(ENABLED_FEATURES) CUSTOM_SETTINGS

Наконец, необходимо указать нашей либе, что она должна ссылаться на Settings.xcconfig как на дополнительный источник настроек для сборки. Чтобы это сделать, добавляем в проект библиотеки ссылку на указанный файл, а затем открываем project.pbxproj любым удобным текстовым редактором. Здесь ищем идентификатор, присвоенный только что добавленному в проект файлу, как на примере ниже.

A53DFF50255AAB8200995A85 /* Settings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Settings.xcconfig; sourceTree = "<group>"; };

Теперь для каждого имеющегося у нас блока XCBuildConfiguration добавляем строку с базовыми настройками по следующему образцу (строка 3):

B6DAF0412528D771002483A6 /* Release */ = {isa = XCBuildConfiguration;baseConfigurationReference = A53DFF50255AAB8200995A85 /* Settings.xcconfig */;buildSettings = {...};name = Release;};

Ты можешь спросить, зачем помимо флагов с нужными компонентами мы также проставляем некое CUSTOM_SETTINGS. Всё просто в отсутствие этого флага мы считаем, что юзер библиотеки не попытался её настроить, то есть не создал PermissionWizard.xcconfig в корне своего проекта, и включаем сразу все поддерживаемые либой компоненты.

#if BLUETOOTH || !CUSTOM_SETTINGS    final class Bluetooth { ... }#endif

На этом пока всё

В следующей части поговорим о том, как я среди 5 гигабайт прошивки iOS 14 нашёл нужные мне локализованные строки и как добыл иконки всех системных пермишенов. А ещё расскажу, как мне удалось запилить requestAccess(completion:) даже там, где дефолтное системное API разрешений не поддерживает коллбэки.

Спасибо за внимание!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru